{{ message }}
Repository navigation
fix(desktop): find pandoc the same way the uploader finds picgo (#2751) - #5525
Merged
Merged
Conversation
A command can live somewhere PATH does not reach for reasons the resolver has no business knowing: an installer's own folder, an explicit user override. Passing those in keeps one resolver instead of one per tool, and they are held to the same runnable bar as everything else it answers with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pandoc kept its own resolver — own location list, own existence check, own weaker predicate — so #5518's fix stopped at picgo. A pandoc under nvm/asdf, which only the login shell knows about, still reported missing, and an override pointing at a file without the executable bit reported present and then failed at spawn. It goes through resolveCommand now, with pandocLocations and MARKTEXT_PANDOC passed as preferred locations: the Windows installer folders stay pandoc's business, the PATH and login-shell half is shared. pandoc.exists() and the spawn sites become async, which the three call sites already could be — the menu never gated on it at build time, contrary to what the last PR assumed. The dead converter.stream goes too: it had no callers and was the only thing forcing the command to be resolved synchronously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
Build artifacts for PR #5525: Run: https://github.com/marktext/marktext/actions/runs/35846520397 |
…guess MARKTEXT_PANDOC is documented as overwriting the pandoc path, but passing it at the head of a candidate list demoted it to a hint: a typo'd or non-executable value was skipped without a word and some other copy ran, or the user was told to install a tool while staring at the path they had set. An override is now its own option, and a set-but-unusable one resolves to missing rather than falling through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ution getCommand() and exists() each spelled out the same resolveCommand call, which re-opens the seam this branch exists to close — add a location to one and the other keeps the old answer. They share a findCommand() now. exists() cannot simply call getCommand(): a pandoc found on PATH resolves to the bare name, which is also getCommand's miss fallback, so the shared point has to be the string-or-null one. The converter thunk goes with it. It existed to carry .stream, which had no callers and was deleted, leaving a two-stage call whose only consumer immediately invoked it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chocolatey, scoop and winget put their shims in folders that are on no PATH — the Windows twin of the pnpm/volta/bun dirs extraPathDirs already carries on POSIX. They were filed under pandocLocations, so on Windows they were searched for pandoc and for nothing else: picgo and a cliScript got none of them. extraPathDirs returned [] on win32 because a GUI app there does inherit the user's PATH, which is true of PATH and irrelevant to a dir that was never on it. pandocLocations keeps the three folders the pandoc installer itself writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolution runs a blocking command -v on the main process, and one LaTeX export now asks three times: the menu gate, the media listing, the conversion. Measured at ~7ms each on macOS with pandoc on PATH, and 30-50ms per miss on Windows, all of it freezing the event loop while the pandoc runs themselves are async. A hit is remembered. A miss is not: the notice tells the user to install pandoc, and a cached no would make that unanswerable without a restart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vi.waitFor's first check runs synchronously, while toFile is still suspended on the command resolution, so every call fell through to the default 50ms interval and observed the result one tick later. Ten call sites, ~52ms each: the file went from ~5ms of test time to 535ms. The stub resolves a promise from inside its own mockImplementation now — 6ms, and the hand-kept spawn counters go with it. The discovery spec's six cases each repeated the same five-line baseline with one line different; that moves to beforeEach. The narrower of the two override cases goes: the other one covers it with a pandoc on PATH as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two places answer "can this be run" and answer it differently. The shared one uses access(X_OK) rather than the mode bits, which is the question the caller actually has: only the owner bits count for the owner, so a file carrying just the "other" bit passes a bit test and gets EACCES at spawn. Verified against a real spawn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mt::fs::is-executable tested the mode bits while the uploader spawns through execFile, so the preferences panel could green-tick a script that then failed with EACCES — the check and the spawn disagreeing, which is the whole thing this branch has been closing. Both now call isExecutableFile. resolveCommand's private isRunnable was the second copy of that predicate; it goes with them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both failures CI caught are real consequences of this branch that a macOS run could not see. patchEnvPath is no longer a no-op on win32 now that the shim dirs live there, and the case pinning the old behaviour only passed locally because ProgramData does not exist on a mac. It sets the anchor itself now, and a second case covers the untouched path when no anchor can be found. Windows has no execute bit, so X_OK degrades to "exists" and a file without the bit is executable there — what the mode-bit check this replaced also did. The assertion says so rather than expecting POSIX everywhere. Drive letters are avoided throughout: ':' is the POSIX path delimiter, so 'C:\\Windows' splits into two entries on a mac runner. Verified the arithmetic under path.win32 as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Follows #5519, which left this behind: pandoc kept a second copy of the command-resolution machinery, so the fix stopped at picgo.
The duplication
src/main/utils/pandoc.tshad its own location list, its own existence check, and its own (weaker) file predicate, parallel to theresolveCommandthat #5519 introduced:resolveCommandgetCommand()resolveCommand(...) !== nullcommand !== 'pandoc' || commandExists.sync('pandoc')statSync().isFile()+X_OKisFile2()— any fileTwo user-visible consequences:
pandoc.exists()was synchronous and never awaitedensureShellEnvPath(), so a pandoc under nvm/fnm/asdf — a version-numbered dir no static list can guess — stayed invisible. That is Pandoc error on file import/drag to #2751, still open for exactly the installs [Bug] picgo installed but not detected and uploads fail: a GUI-launched app never sees the login-shell PATH #5518 was about.MARKTEXT_PANDOCpointing at a non-executable file reported present, then failed at spawn.isFile2checks that a file exists, not that it can run.The change
resolveCommandgainspreferred— absolute paths to try before PATH, held to the same runnable bar as everything else it returns. pandoc passesMARKTEXT_PANDOCandpandocLocations()through it:pandocLocations()stays pandoc-specific — it encodes where the Windows installer writes, which is a different question from #5518's — and so does the.bat/.cmdfilter (CVE-2024-27980). Everything else is now shared.pandoc.exists()and the spawn sites become async. The previous PR assumed they could not be; checking the call sites shows otherwise: two of the three (menu/actions/file.ts:580,:801) were already insideasyncfunctions, and the third is reached from a menuclickhandler whose return value is discarded. Nopandoc.exists()call happens while the menu is built.converter.streamis removed: it had no callers anywhere in the repo, and it was the only thing requiring the command to be resolved synchronously.Tests
test/unit/specs/pandoc-discovery.spec.ts(new) — a pandoc reachable only through a fake login shell is found; one that is nowhere is not; an override without the executable bit is refused. Verified red against the old mechanisms: weakening the runnable check and dropping the shell import turns the first and third red.test/unit/specs/resolve-command.spec.ts—preferredwins over PATH, an unrunnable preferred entry is skipped, and a miss falls through to PATH.test/unit/specs/pandoc-export.spec.ts— reworked for the async resolution: the spawn now lands a microtask later, so the stub process has to be awaited into place rather than driven synchronously. The suite also pinsMARKTEXT_PANDOCto a real executable, becausechild_processis mocked in that file and a fall-through to the login shell would hang on the mock.Full desktop suite: 931 passing.
🤖 Generated with Claude Code