From 0b18e1a507338893e7353e69fd937af3334c8196 Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Wed, 6 May 2026 13:24:54 +0200 Subject: [PATCH 1/7] Add `fmtutilFailureMessage` helper for non-fatal fmtutil-sys handling Returns a warning message when `fmtutil-sys --all` exits non-zero, or undefined on success. Lets callers log a warning and continue rather than aborting. Matches the upstream tinytex R package pattern, which ignores the `fmtutil` exit code. Refs: #14461 --- src/command/render/latexmk/texlive.ts | 20 ++++++++++ tests/unit/latexmk/fmtutil-handler.test.ts | 45 ++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/unit/latexmk/fmtutil-handler.test.ts diff --git a/src/command/render/latexmk/texlive.ts b/src/command/render/latexmk/texlive.ts index 923e6421966..4175b678a44 100644 --- a/src/command/render/latexmk/texlive.ts +++ b/src/command/render/latexmk/texlive.ts @@ -6,6 +6,7 @@ import * as ld from "../../../core/lodash.ts"; import { execProcess } from "../../../core/process.ts"; +import { ProcessResult } from "../../../core/process-types.ts"; import { lines } from "../../../core/text.ts"; import { requireQuoting, safeWindowsExec } from "../../../core/windows.ts"; import { hasTinyTex, tinyTexBinDir } from "../../../tools/impl/tinytex-info.ts"; @@ -453,6 +454,25 @@ function tlmgrCommand( } } +// Returns a warning message when `fmtutil-sys --all` failed, or `undefined` +// when it succeeded. fmtutil failure is treated as non-fatal: package install +// already succeeded by the time we reach the recovery branches in +// `installPackage`, and the format-tree rebuild is best-effort housekeeping +// to mitigate l3kernel version-mismatch issues (#7252). +// +// Upstream tinytex R package follows the same pattern (R/tlmgr.R discards +// the `system2('fmtutil', ...)` exit code). +export function fmtutilFailureMessage( + result: ProcessResult, +): string | undefined { + if (result.code === 0) { + return undefined; + } + const stderr = result.stderr?.trim() ?? ""; + const detail = stderr.length > 0 ? `\n${stderr}` : ""; + return `Failed to rebuild format tree (\`fmtutil-sys --all\` exited ${result.code}). This is non-fatal — package installation will continue.${detail}`; +} + // Execute fmtutil // https://tug.org/texlive/doc/fmtutil.html function fmtutilCommand(context: TexLiveContext) { diff --git a/tests/unit/latexmk/fmtutil-handler.test.ts b/tests/unit/latexmk/fmtutil-handler.test.ts new file mode 100644 index 00000000000..71fc690cb03 --- /dev/null +++ b/tests/unit/latexmk/fmtutil-handler.test.ts @@ -0,0 +1,45 @@ +/* +* fmtutil-handler.test.ts +* +* Copyright (C) 2026 Posit Software, PBC +* +*/ + +import { fmtutilFailureMessage } from "../../../src/command/render/latexmk/texlive.ts"; +import { unitTest } from "../../test.ts"; +import { assert, assertEquals } from "testing/asserts"; + +// deno-lint-ignore require-await +unitTest("fmtutilFailureMessage - undefined when fmtutil-sys succeeded", async () => { + assertEquals( + fmtutilFailureMessage({ code: 0, success: true, stdout: "", stderr: "" }), + undefined, + ); +}); + +// deno-lint-ignore require-await +unitTest("fmtutilFailureMessage - warning text when exit code is non-zero", async () => { + const msg = fmtutilFailureMessage({ + code: 1, + success: false, + stdout: "", + stderr: "fmtutil: error rebuilding format luatex", + }); + assert(msg, "expected a warning message"); + assert(msg.includes("fmtutil-sys --all"), "should mention the command"); + assert(msg.includes("non-fatal"), "should signal non-fatal"); + assert(msg.includes("fmtutil: error rebuilding format luatex"), "should include stderr"); +}); + +// deno-lint-ignore require-await +unitTest("fmtutilFailureMessage - omits stderr suffix when stderr is empty/whitespace", async () => { + const msg = fmtutilFailureMessage({ + code: 1, + success: false, + stdout: "", + stderr: " \n", + }); + const baseMsg = + `Failed to rebuild format tree (\`fmtutil-sys --all\` exited 1). This is non-fatal — package installation will continue.`; + assertEquals(msg, baseMsg); +}); From 1e55893a4ee6b7acbdebab9ca30d66e23a1591e4 Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Wed, 6 May 2026 13:34:22 +0200 Subject: [PATCH 2/7] Demote fmtutil-sys failure to warning during package install Both recovery branches in `installPackage` previously aborted the render with `Problem running fmtutil-sys --all to rebuild format tree.` whenever fmtutil exited non-zero. Format-tree rebuild is best-effort housekeeping after `tlmgr update`; the package install itself has already succeeded by that point. Log a warning and continue, mirroring upstream tinytex R behavior. Fixes #14461 --- src/command/render/latexmk/texlive.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/command/render/latexmk/texlive.ts b/src/command/render/latexmk/texlive.ts index 4175b678a44..d2c34249a89 100644 --- a/src/command/render/latexmk/texlive.ts +++ b/src/command/render/latexmk/texlive.ts @@ -13,6 +13,7 @@ import { hasTinyTex, tinyTexBinDir } from "../../../tools/impl/tinytex-info.ts"; import { join } from "../../../deno_ral/path.ts"; import { logProgress } from "../../../core/log.ts"; import { isWindows } from "../../../deno_ral/platform.ts"; +import { warning } from "../../../deno_ral/log.ts"; export interface TexLiveContext { preferTinyTex: boolean; @@ -243,12 +244,12 @@ async function installPackage( return Promise.reject("Problem running `tlmgr update`."); } - // Rebuild format tree + // Rebuild format tree (best-effort; failure is non-fatal — see + // fmtutilFailureMessage doc). const fmtutilResult = await fmtutilCommand(context); - if (fmtutilResult.code !== 0) { - return Promise.reject( - "Problem running `fmtutil-sys --all` to rebuild format tree.", - ); + const fmtutilWarn = fmtutilFailureMessage(fmtutilResult); + if (fmtutilWarn) { + warning(fmtutilWarn); } } @@ -282,12 +283,12 @@ async function installPackage( return Promise.reject("Problem running `tlmgr update`."); } - // Rebuild format tree + // Rebuild format tree (best-effort; failure is non-fatal — see + // fmtutilFailureMessage doc). const fmtutilResult = await fmtutilCommand(context); - if (fmtutilResult.code !== 0) { - return Promise.reject( - "Problem running `fmtutil-sys --all` to rebuild format tree.", - ); + const fmtutilWarn = fmtutilFailureMessage(fmtutilResult); + if (fmtutilWarn) { + warning(fmtutilWarn); } // Rerun the install command From 4a750c7a6179755a7f8209ff1b013f63311b7ac4 Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Wed, 6 May 2026 13:40:19 +0200 Subject: [PATCH 3/7] Wrap fmtutil-sys via safeWindowsExec on Windows Mirrors the `tlmgrCommand` pattern: invoke through a temp .bat with `cmd /c`. Defends against 8.3 short-path resolution failures inside TeX Live's `runscript.tlu` (rstudio/tinytex#427) when Deno spawns the process directly. Refs: #14461 --- src/command/render/latexmk/texlive.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/command/render/latexmk/texlive.ts b/src/command/render/latexmk/texlive.ts index d2c34249a89..88cef5243a6 100644 --- a/src/command/render/latexmk/texlive.ts +++ b/src/command/render/latexmk/texlive.ts @@ -476,14 +476,23 @@ export function fmtutilFailureMessage( // Execute fmtutil // https://tug.org/texlive/doc/fmtutil.html +// +// On Windows, route through `safeWindowsExec` (mirrors `tlmgrCommand`). +// This wraps the call in a temp `.bat` invoked via `cmd /c`, which +// avoids 8.3 short-path resolution failures inside TeX Live's +// `runscript.tlu` (rstudio/tinytex#427). function fmtutilCommand(context: TexLiveContext) { const fmtutil = texLiveCmd("fmtutil-sys", context); - return execProcess( - { - cmd: fmtutil.fullPath, - args: ["--all"], + const execFmtutil = (cmd: string[]) => { + return execProcess({ + cmd: cmd[0], + args: cmd.slice(1), stdout: "piped", stderr: "piped", - }, - ); + }); + }; + if (isWindows) { + return safeWindowsExec(fmtutil.fullPath, ["--all"], execFmtutil); + } + return execFmtutil([fmtutil.fullPath, "--all"]); } From 89bbba11e97d6bad4d6c1323b71568593ad9db37 Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Wed, 6 May 2026 14:07:41 +0200 Subject: [PATCH 4/7] news: add changelog entry for #14461 --- news/changelog-1.10.md | 1 + 1 file changed, 1 insertion(+) diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index 93f30f3ff46..d8ccd2d1505 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -70,4 +70,5 @@ All changes included in 1.10: - ([#14342](https://github.com/quarto-dev/quarto-cli/issues/14342)): Work around TOCTOU race in Deno's `expandGlobSync` that can cause unexpected exceptions to be raised while traversing directories during project initialization. - ([#14445](https://github.com/quarto-dev/quarto-cli/issues/14445)): Fix intermittent `Uncaught (in promise) TypeError: Writable stream is closed or errored.` aborting renders on Linux. `execProcess` now awaits and swallows the rejection from `process.stdin.close()` when the child closes its stdin first. The captured stderr is now also surfaced when `typst-gather analyze` falls back to staging all packages, so failures are diagnosable without bypassing `quarto`. - ([#14359](https://github.com/quarto-dev/quarto-cli/issues/14359)): Fix intermediate `.quarto_ipynb` file not being deleted after rendering a `.qmd` with Jupyter engine, causing numbered variants (`_1`, `_2`, ...) to accumulate on disk across renders. +- ([#14461](https://github.com/quarto-dev/quarto-cli/issues/14461)): Fix `quarto render --to pdf` aborting with `ERROR: Problem running 'fmtutil-sys --all' to rebuild format tree.` when an automatically-installed LaTeX package's post-update format rebuild fails. Format-tree rebuild is now treated as best-effort housekeeping (matching upstream `tinytex` R behavior) — the failure is logged as a warning and the package install completes. - ([#14472](https://github.com/quarto-dev/quarto-cli/issues/14472)): Add support for Kotlin in code annotations and YAML cell options. (author: @barendgehrels) \ No newline at end of file From e945d9dda68357d2934206e9bc2822008a4f4b56 Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Wed, 6 May 2026 14:19:18 +0200 Subject: [PATCH 5/7] Quote fmtutil-sys path before safeWindowsExec on Windows `safeWindowsExec` joins program + args into a `.bat` command line with literal spaces, so paths containing spaces would be tokenized incorrectly (e.g. `C:\Users\Jane Doe\...`). Mirrors the documented pattern from the `safeWindowsExec - handles program path with spaces` unit test (#13997). Refs: #14461 --- src/command/render/latexmk/texlive.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/command/render/latexmk/texlive.ts b/src/command/render/latexmk/texlive.ts index 88cef5243a6..70d11fa39af 100644 --- a/src/command/render/latexmk/texlive.ts +++ b/src/command/render/latexmk/texlive.ts @@ -492,7 +492,13 @@ function fmtutilCommand(context: TexLiveContext) { }); }; if (isWindows) { - return safeWindowsExec(fmtutil.fullPath, ["--all"], execFmtutil); + // Quote both program and args before handing them to `safeWindowsExec` + // — `safeWindowsExec` joins them into a `.bat` line with a literal space + // separator, so paths containing spaces (e.g. `C:\Users\Jane Doe\...`) + // would otherwise be tokenized incorrectly. See issue #13997 and the + // `safeWindowsExec - handles program path with spaces` unit test. + const quoted = requireQuoting([fmtutil.fullPath, "--all"]); + return safeWindowsExec(quoted.args[0], quoted.args.slice(1), execFmtutil); } return execFmtutil([fmtutil.fullPath, "--all"]); } From a879d60b854d133e2e5f4e959283abbca16306fd Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Wed, 6 May 2026 14:19:44 +0200 Subject: [PATCH 6/7] rules: document safeWindowsExec quoting requirement Captures the pattern hit while wiring fmtutil-sys: `safeWindowsExec` needs `requireQuoting` on both program path and args, otherwise the generated .bat tokenizes incorrectly when paths contain spaces. Points to the #13997 test and existing call sites as authoritative refs. --- .claude/rules/typescript/windows-safe-exec.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .claude/rules/typescript/windows-safe-exec.md diff --git a/.claude/rules/typescript/windows-safe-exec.md b/.claude/rules/typescript/windows-safe-exec.md new file mode 100644 index 00000000000..1fcc3ff4501 --- /dev/null +++ b/.claude/rules/typescript/windows-safe-exec.md @@ -0,0 +1,14 @@ +--- +paths: + - "src/**/*.ts" +--- + +# Windows: `safeWindowsExec` quoting + +`safeWindowsExec` (defined at `src/core/windows.ts`) is the right tool whenever a Windows process needs to be spawned through `cmd /c` instead of being launched directly by Deno — `.bat` wrappers (`tlmgr.bat`), TeX Live binaries that use `runscript.tlu` for path resolution (`fmtutil-sys.exe`, see `rstudio/tinytex#427`), and registry queries via `reg.exe` are the existing call sites. + +## Quote both program and args before calling + +`safeWindowsExec` writes a temp `.bat` whose contents are `[program, ...args].join(" ")`. Any space in the program path or any arg tokenizes the line incorrectly, and the spawn either fails or runs the wrong binary. Pass the program path and the args through `requireQuoting` together, then split the returned array — the unit test `safeWindowsExec - handles program path with spaces (issue #13997)` in `tests/unit/windows-exec.test.ts` is the authoritative usage example, and `fmtutilCommand` in `src/command/render/latexmk/texlive.ts` is the in-tree consumer. + +`tlmgrCommand` quotes only the user-supplied subcommand args, not the program path, because `tlmgr.bat` historically lives at a stable TinyTeX path. New call sites should default to quoting both — paths under `C:\Users\\…` are common enough that this isn't theoretical. From f731c610f012f7ec942f81267f2566fb36628aea Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Wed, 6 May 2026 14:29:10 +0200 Subject: [PATCH 7/7] rules: broaden safeWindowsExec call-site list Earlier wording implied tlmgr/fmtutil/reg.exe were the full set; `zip.ts` and `shell.ts` are also callers. Reframe as examples + tell readers to grep when they need the current list. --- .claude/rules/typescript/windows-safe-exec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/rules/typescript/windows-safe-exec.md b/.claude/rules/typescript/windows-safe-exec.md index 1fcc3ff4501..f2663079dff 100644 --- a/.claude/rules/typescript/windows-safe-exec.md +++ b/.claude/rules/typescript/windows-safe-exec.md @@ -5,7 +5,7 @@ paths: # Windows: `safeWindowsExec` quoting -`safeWindowsExec` (defined at `src/core/windows.ts`) is the right tool whenever a Windows process needs to be spawned through `cmd /c` instead of being launched directly by Deno — `.bat` wrappers (`tlmgr.bat`), TeX Live binaries that use `runscript.tlu` for path resolution (`fmtutil-sys.exe`, see `rstudio/tinytex#427`), and registry queries via `reg.exe` are the existing call sites. +`safeWindowsExec` (defined at `src/core/windows.ts`) is the right tool whenever a Windows process needs to be spawned through `cmd /c` instead of being launched directly by Deno. Examples include `.bat` wrappers (`tlmgr.bat`), TeX Live binaries that use `runscript.tlu` for path resolution (`fmtutil-sys.exe`, see `rstudio/tinytex#427`), registry queries via `reg.exe`, and other shell-sensitive callouts elsewhere under `src/core/` (`zip.ts`, `shell.ts`). Grep `safeWindowsExec` for the current set of call sites. ## Quote both program and args before calling