Describe your issue
Thank you for your hard work, I LOVE doom emacs. I used vim and nvim for a few years, had zero emacs experience, but it only took a few days to became fully productive in emacs thanks to Doom ❤️
Disclosure: opus 4.8 investigated and wrote this. But I can confirm that after adding this code block
(after! ob-tangle
(defun my/org-babel-tangle-skip-store-link-a (orig &rest args)
(cl-letf (((symbol-function 'org-store-link) #'ignore))
(apply orig args)))
(advice-add 'org-babel-tangle :around #'my/org-babel-tangle-skip-store-link-a))
my tangle time went from a few seconds to under 1 second in a file with 297 tangle blocks
Summary
When :tools pdf is enabled but the epdfinfo binary has never been built, Doom's lazy
org-pdftools link handler (+org--pdftools-link-handler in modules/lang/org/config.el) calls
(require 'org-pdftools nil t) on every org-store-link invocation. Because loading
org-pdftools.el aborts — via a load-time file-executable-p check on the missing epdfinfo,
raised inside its dependency pdf-annot.el — before it reaches (provide 'org-pdftools), the
feature is never registered and each require re-loads the whole file instead of being a cheap
no-op. Org's tangler calls org-store-link once per qualifying source block, so tangling a large
literate config performs hundreds of silent failed reloads. The same per-call cost applies to
org-capture, manual link storing (C-c l), and link export — tangle is simply the most visible
trigger because it multiplies the cost by one call per block.
Environment
- OS: macOS (darwin), Apple Silicon
- Emacs: GNU Emacs 30.2 (
636f166cf)
- Doom: v2.2.0 (
ef473206, master, 2026-06-23); doom+ v26.06.0 (5a44cf7bd)
- Modules:
:lang org and :tools pdf enabled
- Org mode: 9.8.4 (straight build at
~/.config/emacs/.local/straight/build-30.2/org/)
- org-pdftools: pinned by Doom to
5613b7ae561e0af199f25aacc0a9c34c16638408
epdfinfo: not built — pdf-info-epdfinfo-program points at a non-executable path
- State:
(featurep 'org-pdftools) => nil
Profiler output
Root cause
The shim
Doom registers org-pdftools' link parameters through a wrapper that lazily requires the package on
each call. From modules/lang/org/config.el (use-package! org-pdftools), introduced in
doomemacs/core@bf8495b "fix(org): epdfinfo error when storing/exporting links" and still present
on master:
(defun +org--pdftools-link-handler (fn &rest args)
"Produces a link handler for org-pdftools that suppresses missing-epdfinfo
errors whenever storing or exporting links."
(lambda (&rest args)
(and (ignore-errors (require 'org-pdftools nil t))
(file-executable-p pdf-info-epdfinfo-program)
(apply fn args))))
(org-link-set-parameters (or (bound-and-true-p org-pdftools-link-prefix) "pdf")
:follow (+org--pdftools-link-handler #'org-pdftools-open)
:complete (+org--pdftools-link-handler #'org-pdftools-complete-link)
:store (+org--pdftools-link-handler #'org-pdftools-store-link)
:export (+org--pdftools-link-handler #'org-pdftools-export))
The :store closure is installed into Org's link-store machinery, so it is invoked by
org-link--try-link-store-functions on every org-store-link.
The require/reload mechanism
Standard require semantics: a file is loaded only when its feature is absent from features, and
the feature is added only when (provide 'FEATURE) runs. (provide 'org-pdftools) is the final
top-level form of org-pdftools.el. When epdfinfo is not built, the load aborts before reaching
it — and the error originates not in org-pdftools.el itself but in a dependency it requires
unconditionally: loading pdf-annot.el evaluates the defcustom pdf-annot-list-listed-types, whose
default value form calls (pdf-info-markup-annotations-p) at load time, which runs
pdf-info-check-epdfinfo → file-executable-p → signals
"pdf-info-epdfinfo-program is not executable".
The error is swallowed by the shim's ignore-errors, org-pdftools never gets onto features,
(featurep 'org-pdftools) stays nil, and each subsequent (require 'org-pdftools nil t)
re-loads the entire file (load-with-code-conversion). The shim's file-executable-p guard then
short-circuits, so (apply fn args) is never reached — but the full reload cost has already been
paid. The commit's intent was solely UX (suppress the loud error); it suppresses the error
correctly but does not prevent the repeated reload.
The tangle call chain (Org >= 9.8)
org-babel-tangle
-> org-babel-tangle-collect-blocks
-> org-babel-tangle-single-block (once per qualifying src block)
-> org-babel-tangle--unbracketed-link
-> org-store-link (for blocks whose :comments is not "no")
-> org-link--try-link-store-functions (calls EVERY registered :store fn)
-> +org--pdftools-link-handler closure
-> (require 'org-pdftools nil t)
-> load-with-code-conversion (org-pdftools.el re-loaded, every call)
Org-version nuance
Both Org 9.7.11 (bundled with Emacs 30) and Org 9.8.4 keep the early-return
(unless (string= "no" (cdr (assq :comments params))) ...) in org-babel-tangle--unbracketed-link.
The relevant difference is that 9.7.x additionally wrapped the call in
(cl-letf (((symbol-function 'org-store-link-functions) (lambda () nil))) (org-store-link nil)),
which made tangling skip all third-party :store handlers. Org 9.8 removed that cl-letf binding
(commit f6a0f151b1a2, "ob-tangle: Fix regression after 95554543b9" — a deliberate
accuracy-over-performance change after id links began flowing through org-store-link-functions).
So on Org 9.7.x the shim is never reached during tangle; on Org 9.8.x it is reached once per block
whose :comments does not resolve to "no". (This is upstream behavior, not something Doom controls
— but it is what newly exposes the shim to the tangle hot path; the shim defect itself is
independent and affects every org-store-link regardless of tangling.)
Impact
From a representative CPU profile (3596 total samples, tangling a ~295-block literate config):
org-babel-tangle subtree: 2695 samples (~74.9%)
org-store-link: 2116 (~58.8%)
org-link--try-link-store-functions: 2086 (~58.0%)
- the org-pdftools shim closure: 2085 (~58.0%)
- the two dominant leaf frames are pure
require+load work (proving a reload, not a cheap feature
check): [require load-with-code-conversion require progn condition-case and <shim> ...] = 1046
samples and [require progn condition-case and <shim> ...] = 989 samples — together
~2035 (~56.6%).
- the smoking-gun frame for why the load aborts before
provide:
[file-executable-p pdf-info-check-epdfinfo pdf-info-process-assert-running pdf-info-query pdf-info-features pdf-info-markup-annotations-p ... custom-declare-variable byte-code require load-with-code-conversion ...].
Breadth. This is not machine-specific: the shim lives in shared Doom source and is enabled for
any user with :lang org + :tools pdf and no built epdfinfo (the common default, since
:tools pdf never builds it). The per-call cost is a full file reload, not single-digit ms, and
is environment-dependent (it can be dominated by eval-buffer / char-displayable-p font scanning).
The closely related prior report #3979 shows a single interactive org-capture taking ~20 s to
~2 min on a different machine — so single interactive actions can also be severely affected; tangle
just makes it unmissable.
Evidence
The :store handler, as printed from org-store-link-functions:
#[(&rest args)
((and (condition-case nil (progn (require 'org-pdftools nil t)) (error nil))
(file-executable-p pdf-info-epdfinfo-program)
(apply fn args)))
((fn . org-pdftools-store-link))]
This is exactly Doom's +org--pdftools-link-handler closure: ignore-errors macroexpands to
(condition-case nil (progn ...) (error nil)), and ((fn . org-pdftools-store-link)) is the
lexical capture of the fn argument.
Forcing the require surfaces the swallowed error:
(require 'org-pdftools) => (error "pdf-info-epdfinfo-program is not executable")
(featurep 'org-pdftools) => nil
Suggested fixes (options for the maintainer to weigh)
-
Make the lazy require idempotent / don't retry a known failure. Check
(file-executable-p pdf-info-epdfinfo-program) before attempting the require, and/or
short-circuit when the feature is already loaded, e.g.
(or (featurep 'org-pdftools) (and (file-executable-p pdf-info-epdfinfo-program) (ignore-errors (require 'org-pdftools nil t)))).
Smallest change; preserves the error-suppression intent. Care needed so that building
epdfinfo later in the same session is still picked up (re-check file-executable-p cheaply
rather than caching a permanent negative).
-
Only register the :store/:follow/:complete/:export handlers once epdfinfo is
available. Defer org-link-set-parameters behind an executable check (e.g. after a successful
pdf-tools-install) instead of at :init. Cleanest separation; pdf links simply aren't
registered until the binary exists — acceptable, since they can't work without it anyway.
-
Load org-pdftools eagerly (or provide a stub) when the module is on, so the feature is
genuinely present and require becomes a real no-op. Removes the laziness benefit; only sensible
if the package can load cleanly without epdfinfo.
-
(Context, not a Doom fix.) The tangle exposure stems from Org 9.8 removing the cl-letf
no-op binding of org-store-link-functions during tangle — a deliberate upstream
accuracy-over-performance decision, so the actionable fix likely belongs in Doom/org-pdftools
rather than asking Org to restore the guard.
Options 1 or 2 look like the lowest-risk, most targeted fixes.
Workaround (today, for affected users)
- Build the server:
M-x pdf-tools-install (install build deps first if needed). Once
pdf-info-epdfinfo-program is executable and org-pdftools actually (provide)s, require
becomes a no-op and the reloads stop.
- Or disable
:tools pdf (or the org-pdftools integration) if PDF links aren't needed, which
removes the shim entirely.
- For tangling specifically, ensure
:comments no is genuinely in effect for your blocks (so
org-babel-tangle--unbracketed-link early-returns and never calls org-store-link), or advise
org-babel-tangle to bind org-store-link to #'ignore for the duration of the tangle.
References
Steps to reproduce
- Enable both
:lang org and :tools pdf in init.el (the latter satisfies the
:when (modulep! :tools pdf) gate on the org-pdftools block).
- Do not build
epdfinfo (the default state — :tools pdf uses pdf-tools-install-noverify,
which does not build it).
- Confirm the precondition:
(featurep 'org-pdftools) => nil and
(file-executable-p pdf-info-epdfinfo-program) => nil.
- Exercise the hot path. Either:
M-: (benchmark-run 10 (org-store-link nil)) RET with point in a normal Org buffer — each call
re-loads org-pdftools.el; or
- tangle a large literate Org config (
C-c C-v t) on Org >= 9.8 where the blocks' :comments
does not resolve to "no" (see "Org-version nuance").
- Profile it:
M-x profiler-start RET cpu RET, run the action, then M-x profiler-report.
Observed: the CPU profile is dominated by repeated load-with-code-conversion of org-pdftools.el
underneath org-store-link.
System information
env.txt
Disclosures
Describe your issue
Thank you for your hard work, I LOVE doom emacs. I used vim and nvim for a few years, had zero emacs experience, but it only took a few days to became fully productive in emacs thanks to Doom ❤️
Disclosure: opus 4.8 investigated and wrote this. But I can confirm that after adding this code block
my tangle time went from a few seconds to under 1 second in a file with 297 tangle blocks
Summary
When
:tools pdfis enabled but theepdfinfobinary has never been built, Doom's lazyorg-pdftools link handler (
+org--pdftools-link-handlerinmodules/lang/org/config.el) calls(require 'org-pdftools nil t)on everyorg-store-linkinvocation. Because loadingorg-pdftools.elaborts — via a load-timefile-executable-pcheck on the missingepdfinfo,raised inside its dependency
pdf-annot.el— before it reaches(provide 'org-pdftools), thefeature is never registered and each
requirere-loads the whole file instead of being a cheapno-op. Org's tangler calls
org-store-linkonce per qualifying source block, so tangling a largeliterate config performs hundreds of silent failed reloads. The same per-call cost applies to
org-capture, manual link storing (C-c l), and link export — tangle is simply the most visibletrigger because it multiplies the cost by one call per block.
Environment
636f166cf)ef473206, master, 2026-06-23); doom+ v26.06.0 (5a44cf7bd):lang organd:tools pdfenabled~/.config/emacs/.local/straight/build-30.2/org/)5613b7ae561e0af199f25aacc0a9c34c16638408epdfinfo: not built —pdf-info-epdfinfo-programpoints at a non-executable path(featurep 'org-pdftools)=>nilProfiler output
Root cause
The shim
Doom registers org-pdftools' link parameters through a wrapper that lazily requires the package on
each call. From
modules/lang/org/config.el(use-package! org-pdftools), introduced indoomemacs/core@bf8495b"fix(org): epdfinfo error when storing/exporting links" and still presenton master:
The
:storeclosure is installed into Org's link-store machinery, so it is invoked byorg-link--try-link-store-functionson everyorg-store-link.The require/reload mechanism
Standard
requiresemantics: a file is loaded only when its feature is absent fromfeatures, andthe feature is added only when
(provide 'FEATURE)runs.(provide 'org-pdftools)is the finaltop-level form of
org-pdftools.el. Whenepdfinfois not built, the load aborts before reachingit — and the error originates not in
org-pdftools.elitself but in a dependency it requiresunconditionally: loading
pdf-annot.elevaluates the defcustompdf-annot-list-listed-types, whosedefault value form calls
(pdf-info-markup-annotations-p)at load time, which runspdf-info-check-epdfinfo→file-executable-p→ signals"pdf-info-epdfinfo-program is not executable".The error is swallowed by the shim's
ignore-errors,org-pdftoolsnever gets ontofeatures,(featurep 'org-pdftools)staysnil, and each subsequent(require 'org-pdftools nil t)re-loads the entire file (
load-with-code-conversion). The shim'sfile-executable-pguard thenshort-circuits, so
(apply fn args)is never reached — but the full reload cost has already beenpaid. The commit's intent was solely UX (suppress the loud error); it suppresses the error
correctly but does not prevent the repeated reload.
The tangle call chain (Org >= 9.8)
Org-version nuance
Both Org 9.7.11 (bundled with Emacs 30) and Org 9.8.4 keep the early-return
(unless (string= "no" (cdr (assq :comments params))) ...)inorg-babel-tangle--unbracketed-link.The relevant difference is that 9.7.x additionally wrapped the call in
(cl-letf (((symbol-function 'org-store-link-functions) (lambda () nil))) (org-store-link nil)),which made tangling skip all third-party
:storehandlers. Org 9.8 removed thatcl-letfbinding(commit
f6a0f151b1a2, "ob-tangle: Fix regression after 95554543b9" — a deliberateaccuracy-over-performance change after id links began flowing through
org-store-link-functions).So on Org 9.7.x the shim is never reached during tangle; on Org 9.8.x it is reached once per block
whose
:commentsdoes not resolve to"no". (This is upstream behavior, not something Doom controls— but it is what newly exposes the shim to the tangle hot path; the shim defect itself is
independent and affects every
org-store-linkregardless of tangling.)Impact
From a representative CPU profile (3596 total samples, tangling a ~295-block literate config):
org-babel-tanglesubtree: 2695 samples (~74.9%)org-store-link: 2116 (~58.8%)org-link--try-link-store-functions: 2086 (~58.0%)require+load work (proving a reload, not a cheap featurecheck):
[require load-with-code-conversion require progn condition-case and <shim> ...]= 1046samples and
[require progn condition-case and <shim> ...]= 989 samples — together~2035 (~56.6%).
provide:[file-executable-p pdf-info-check-epdfinfo pdf-info-process-assert-running pdf-info-query pdf-info-features pdf-info-markup-annotations-p ... custom-declare-variable byte-code require load-with-code-conversion ...].Breadth. This is not machine-specific: the shim lives in shared Doom source and is enabled for
any user with
:lang org+:tools pdfand no builtepdfinfo(the common default, since:tools pdfnever builds it). The per-call cost is a full file reload, not single-digit ms, andis environment-dependent (it can be dominated by
eval-buffer/char-displayable-pfont scanning).The closely related prior report #3979 shows a single interactive
org-capturetaking ~20 s to~2 min on a different machine — so single interactive actions can also be severely affected; tangle
just makes it unmissable.
Evidence
The
:storehandler, as printed fromorg-store-link-functions:This is exactly Doom's
+org--pdftools-link-handlerclosure:ignore-errorsmacroexpands to(condition-case nil (progn ...) (error nil)), and((fn . org-pdftools-store-link))is thelexical capture of the
fnargument.Forcing the require surfaces the swallowed error:
Suggested fixes (options for the maintainer to weigh)
Make the lazy require idempotent / don't retry a known failure. Check
(file-executable-p pdf-info-epdfinfo-program)before attempting the require, and/orshort-circuit when the feature is already loaded, e.g.
(or (featurep 'org-pdftools) (and (file-executable-p pdf-info-epdfinfo-program) (ignore-errors (require 'org-pdftools nil t)))).Smallest change; preserves the error-suppression intent. Care needed so that building
epdfinfolater in the same session is still picked up (re-checkfile-executable-pcheaplyrather than caching a permanent negative).
Only register the
:store/:follow/:complete/:exporthandlers onceepdfinfoisavailable. Defer
org-link-set-parametersbehind an executable check (e.g. after a successfulpdf-tools-install) instead of at:init. Cleanest separation; pdf links simply aren'tregistered until the binary exists — acceptable, since they can't work without it anyway.
Load org-pdftools eagerly (or
providea stub) when the module is on, so the feature isgenuinely present and
requirebecomes a real no-op. Removes the laziness benefit; only sensibleif the package can load cleanly without
epdfinfo.(Context, not a Doom fix.) The tangle exposure stems from Org 9.8 removing the
cl-letfno-op binding of
org-store-link-functionsduring tangle — a deliberate upstreamaccuracy-over-performance decision, so the actionable fix likely belongs in Doom/org-pdftools
rather than asking Org to restore the guard.
Options 1 or 2 look like the lowest-risk, most targeted fixes.
Workaround (today, for affected users)
M-x pdf-tools-install(install build deps first if needed). Oncepdf-info-epdfinfo-programis executable andorg-pdftoolsactually(provide)s,requirebecomes a no-op and the reloads stop.
:tools pdf(or the org-pdftools integration) if PDF links aren't needed, whichremoves the shim entirely.
:comments nois genuinely in effect for your blocks (soorg-babel-tangle--unbracketed-linkearly-returns and never callsorg-store-link), or adviseorg-babel-tangleto bindorg-store-linkto#'ignorefor the duration of the tangle.References
bf8495b"fix(org): epdfinfo error when storing/exporting links": bf8495b:tools pdfconfig (usespdf-tools-install-noverify, does not build epdfinfo): https://github.com/doomemacs/doomemacs/blob/master/modules/tools/pdf/config.elob-tangle.el@ release_9.8.4 (bareorg-store-link, nocl-letf): https://raw.githubusercontent.com/bzg/org-mode/release_9.8.4/lisp/ob-tangle.elob-tangle.el@ release_9.7.11 (has thecl-letfno-op binding): https://raw.githubusercontent.com/bzg/org-mode/release_9.7.11/lisp/ob-tangle.elf6a0f151b1a2"ob-tangle: Fix regression after 95554543b9": bzg/org-mode@f6a0f15pdf-info.el(pdf-info-check-epdfinfo/ the "is not executable" error): https://raw.githubusercontent.com/vedang/pdf-tools/master/lisp/pdf-info.elprovide): https://github.com/fuxialexander/org-pdftoolsSteps to reproduce
:lang organd:tools pdfininit.el(the latter satisfies the:when (modulep! :tools pdf)gate on the org-pdftools block).epdfinfo(the default state —:tools pdfusespdf-tools-install-noverify,which does not build it).
(featurep 'org-pdftools)=>niland(file-executable-p pdf-info-epdfinfo-program)=>nil.M-: (benchmark-run 10 (org-store-link nil)) RETwith point in a normal Org buffer — each callre-loads
org-pdftools.el; orC-c C-v t) on Org >= 9.8 where the blocks':commentsdoes not resolve to
"no"(see "Org-version nuance").M-x profiler-start RET cpu RET, run the action, thenM-x profiler-report.Observed: the CPU profile is dominated by repeated
load-with-code-conversionoforg-pdftools.elunderneath
org-store-link.System information
env.txt
Disclosures