From 0b8038f069234e39727c3ac2e1e3cf73e848454e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Aug 2026 18:31:27 +0200 Subject: [PATCH 1/6] Extract tar command construction for explicit Windows tar resolution --- package/src/util/tar.ts | 96 +++++++++++++++++++++ tests/unit/tar-command.test.ts | 153 +++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 tests/unit/tar-command.test.ts diff --git a/package/src/util/tar.ts b/package/src/util/tar.ts index dcb50d9ca70..7a9c55c16ec 100644 --- a/package/src/util/tar.ts +++ b/package/src/util/tar.ts @@ -8,6 +8,102 @@ import { info } from "../../../src/deno_ral/log.ts"; import { dirname, extname } from "../../../src/deno_ral/path.ts"; +// Deno 2 dropped Deno.run from its type definitions, but the runtime still +// implements it (verified: typeof Deno.run === "function" on Deno 2.7.14). +// Declaring its actual shape here fixes the TS2339 errors this file's tests +// would otherwise hit under `deno test --check`, instead of suppressing them; +// migrating off Deno.run to Deno.Command is out of scope. The shape below is +// deliberately minimal - just what this file's two call sites use, not the +// full Deno 1 RunOptions/Process API. Other files calling Deno.run (git.ts, +// configure.ts, import-report/*) have their own untested latent TS2339s; +// if a future change needs to type Deno.run more broadly across the +// codebase, that belongs in src/deno_ral/process.ts (which already wraps +// Deno.Command, the migration target), not a wider version of this block. +declare global { + namespace Deno { + function run(options: { cmd: string[]; cwd?: string }): { + status(): Promise<{ code: number }>; + }; + } +} + +// Windows ships bsdtar as System32\tar.exe, which reads ZIP archives. +export function windowsSystemTar(systemRoot?: string): string { + return `${systemRoot || "C:\\Windows"}\\System32\\tar.exe`; +} + +// Resolve which tar to run. On Windows prefer the absolute System32 bsdtar: a +// GNU tar earlier on PATH - Git for Windows installs one in usr\bin, and +// anything launched from a Git Bash shell inherits that PATH - cannot read ZIP +// and reads a Windows absolute path as a remote host spec, so it fails every +// dependency extraction. configure.cmd calls the absolute System32 path for the +// Deno bootstrap for this same reason. +// +// Fall back to PATH when that file is not there. That does not make such a host +// work, since its PATH tar is the very binary that cannot read ZIP; it keeps +// this change from regressing a host we have not tested, and it means a wrong +// systemRoot degrades to today's behaviour instead of failing outright. +// Callers pass the existence result so this stays pure and both branches are +// testable. +export function resolveTarBinary( + os: string, + systemTarPath: string, + systemTarExists: boolean, +): string { + if (os !== "windows") { + return "tar"; + } + return systemTarExists ? systemTarPath : "tar"; +} + +// tar's compression flag for an archive. A .zip gets none: the format is not +// gzip, and bsdtar detects it unaided. Passing z for a zip happens to work +// under bsdtar's lenient format detection, which is what has kept the mistake +// invisible and made the GNU tar failure look like archive corruption. +export function tarCompressFlag(input: string): string { + const ext = extname(input).toLowerCase(); + if (ext === ".xz") { + return "J"; + } else if (ext === ".bz2") { + return "j"; + } else if (ext === ".zip") { + return ""; + } + return "z"; +} + +// The command builders take an already-resolved binary, so the filesystem +// check stays out of them and a test can pass any binary string it likes. +export function unTarCommand( + tarBin: string, + input: string, + directory?: string, +): string[] { + const cmd = [tarBin, `-xv${tarCompressFlag(input)}f`, input]; + if (directory) { + cmd.push("--directory"); + cmd.push(directory); + } + return cmd; +} + +export function makeTarballCommand( + tarBin: string, + input: string, + output: string, + changewd: boolean, +): string[] { + const cmd = [tarBin, "czvf", output]; + if (changewd) { + cmd.push("-C"); + } + cmd.push(input); + if (changewd) { + cmd.push("."); + } + return cmd; +} + export async function makeTarball( input: string, output: string, diff --git a/tests/unit/tar-command.test.ts b/tests/unit/tar-command.test.ts new file mode 100644 index 00000000000..db4ce35521a --- /dev/null +++ b/tests/unit/tar-command.test.ts @@ -0,0 +1,153 @@ +/* + * tar-command.test.ts + * + * Tests how quarto-bld builds its tar invocations. On Windows the binary must + * be the absolute System32 bsdtar, because a GNU tar earlier on PATH cannot + * read ZIP and treats a Windows absolute path as a remote host. Archive names + * here are the real ones from package/src/common/dependencies. + * + * Copyright (C) 2026 Posit Software, PBC + * + */ + +import { unitTest } from "../test.ts"; +import { assertEquals } from "testing/asserts"; +import { + makeTarballCommand, + resolveTarBinary, + tarCompressFlag, + unTarCommand, + windowsSystemTar, +} from "../../package/src/util/tar.ts"; + +unitTest("windowsSystemTar - builds the System32 path", async () => { + assertEquals( + windowsSystemTar("C:\\WINDOWS"), + "C:\\WINDOWS\\System32\\tar.exe", + ); +}); + +unitTest("windowsSystemTar - falls back to C:\\Windows when unset or empty", async () => { + assertEquals(windowsSystemTar(), "C:\\Windows\\System32\\tar.exe"); + assertEquals(windowsSystemTar(""), "C:\\Windows\\System32\\tar.exe"); +}); + +unitTest("windowsSystemTar - honours a relocated system root", async () => { + assertEquals(windowsSystemTar("D:\\Win"), "D:\\Win\\System32\\tar.exe"); +}); + +unitTest("resolveTarBinary - windows prefers System32 bsdtar when present", async () => { + assertEquals( + resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", true), + "C:\\WINDOWS\\System32\\tar.exe", + ); +}); + +// No System32 bsdtar means a pre-17063 Windows. PATH tar there is very likely +// GNU tar, which cannot read ZIP, so this does not rescue such a host - it +// keeps behaviour identical to before this change rather than failing outright. +unitTest("resolveTarBinary - windows falls back to PATH when System32 tar is absent", async () => { + assertEquals( + resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", false), + "tar", + ); +}); + +unitTest("resolveTarBinary - other platforms keep the bare binary", async () => { + assertEquals(resolveTarBinary("linux", "irrelevant", true), "tar"); + assertEquals(resolveTarBinary("linux", "irrelevant", false), "tar"); + assertEquals(resolveTarBinary("darwin", "irrelevant", true), "tar"); +}); + +unitTest("tarCompressFlag - zip gets no compression flag", async () => { + assertEquals(tarCompressFlag("dart-sass-1.101.0-windows-x64.zip"), ""); + assertEquals(tarCompressFlag("typst-x86_64-pc-windows-msvc.zip"), ""); + assertEquals( + tarCompressFlag("typst-gather-x86_64-pc-windows-msvc.zip"), + "", + ); +}); + +unitTest("tarCompressFlag - zip detection is case insensitive", async () => { + assertEquals(tarCompressFlag("ARCHIVE.ZIP"), ""); +}); + +unitTest("tarCompressFlag - xz and bz2 keep their own flags", async () => { + assertEquals(tarCompressFlag("typst-x86_64-unknown-linux-musl.tar.xz"), "J"); + assertEquals(tarCompressFlag("something.tar.bz2"), "j"); +}); + +unitTest("tarCompressFlag - gzip forms stay gzip", async () => { + assertEquals(tarCompressFlag("dart-sass-1.101.0-linux-x64.tar.gz"), "z"); + assertEquals(tarCompressFlag("esbuild-win32-x64.tgz"), "z"); + assertEquals( + tarCompressFlag("typst-gather-x86_64-unknown-linux-gnu.tar.gz"), + "z", + ); +}); + +const kSystemTar = "C:\\WINDOWS\\System32\\tar.exe"; + +unitTest("unTarCommand - windows zip gets no gzip flag", async () => { + assertEquals( + unTarCommand(kSystemTar, "C:\\tools\\typst-x86_64-pc-windows-msvc.zip"), + [kSystemTar, "-xvf", "C:\\tools\\typst-x86_64-pc-windows-msvc.zip"], + ); +}); + +unitTest("unTarCommand - linux tar.gz is unchanged from today", async () => { + assertEquals( + unTarCommand("tar", "/tools/dart-sass-1.101.0-linux-x64.tar.gz"), + ["tar", "-xvzf", "/tools/dart-sass-1.101.0-linux-x64.tar.gz"], + ); +}); + +unitTest("unTarCommand - darwin tar.xz keeps the J flag", async () => { + assertEquals( + unTarCommand("tar", "/tools/typst-x86_64-apple-darwin.tar.xz"), + ["tar", "-xvJf", "/tools/typst-x86_64-apple-darwin.tar.xz"], + ); +}); + +unitTest("unTarCommand - a directory appends --directory", async () => { + assertEquals( + unTarCommand( + kSystemTar, + "C:\\tools\\dart-sass-1.101.0-windows-x64.zip", + "C:\\tools\\x86_64", + ), + [ + kSystemTar, + "-xvf", + "C:\\tools\\dart-sass-1.101.0-windows-x64.zip", + "--directory", + "C:\\tools\\x86_64", + ], + ); +}); + +unitTest("makeTarballCommand - darwin form is unchanged from today", async () => { + assertEquals( + makeTarballCommand("tar", "/src/payload", "/out/bundle.tar.gz", false), + ["tar", "czvf", "/out/bundle.tar.gz", "/src/payload"], + ); +}); + +unitTest("makeTarballCommand - changewd wraps the input with -C and dot", async () => { + assertEquals( + makeTarballCommand("tar", "/src/payload", "/out/bundle.tar.gz", true), + ["tar", "czvf", "/out/bundle.tar.gz", "-C", "/src/payload", "."], + ); +}); + +unitTest("makeTarballCommand - carries whatever binary it is handed", async () => { + assertEquals( + makeTarballCommand( + kSystemTar, + "C:\\src\\payload", + "C:\\out\\bundle.tar.gz", + false, + ), + [kSystemTar, "czvf", "C:\\out\\bundle.tar.gz", "C:\\src\\payload"], + ); +}); From 1b4bf232b6a6526cbb18f00bf3acd8b97596e031 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Aug 2026 18:33:26 +0200 Subject: [PATCH 2/6] Resolve the Windows tar binary explicitly when extracting dependencies --- package/src/util/tar.ts | 57 +++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/package/src/util/tar.ts b/package/src/util/tar.ts index 7a9c55c16ec..e7b4aecab39 100644 --- a/package/src/util/tar.ts +++ b/package/src/util/tar.ts @@ -7,6 +7,8 @@ import { info } from "../../../src/deno_ral/log.ts"; import { dirname, extname } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { os } from "../../../src/deno_ral/platform.ts"; // Deno 2 dropped Deno.run from its type definitions, but the runtime still // implements it (verified: typeof Deno.run === "function" on Deno 2.7.14). @@ -104,6 +106,14 @@ export function makeTarballCommand( return cmd; } +// The one impure step: read the environment and stat the candidate, then let +// the pure resolver decide. Not exported - the pure functions are what the +// tests cover. +function currentTarBinary(): string { + const systemTarPath = windowsSystemTar(Deno.env.get("WINDIR")); + return resolveTarBinary(os, systemTarPath, existsSync(systemTarPath)); +} + export async function makeTarball( input: string, output: string, @@ -112,17 +122,7 @@ export async function makeTarball( info("Make Tarball"); info(`Input: ${input}`); info(`Output: ${output}\n`); - const tarCmd: string[] = []; - tarCmd.push("tar"); - tarCmd.push("czvf"); - tarCmd.push(output); - if (changewd) { - tarCmd.push("-C"); - } - tarCmd.push(input); - if (changewd) { - tarCmd.push("."); - } + const tarCmd = makeTarballCommand(currentTarBinary(), input, output, changewd); info(tarCmd); const p = Deno.run({ @@ -130,7 +130,11 @@ export async function makeTarball( }); const status = await p.status(); if (status.code !== 0) { - throw Error("Failure to make tarball"); + throw Error( + `Failure to make tarball ${output} using ${tarCmd[0]} (exit ${status.code}). Command was: ${ + tarCmd.join(" ") + }`, + ); } } @@ -141,30 +145,23 @@ export async function unTar(input: string, directory?: string) { const cwd = dirname(input); info(`Cwd: ${cwd}`); - // Properly process the compressions - let compressFlag = "z"; // zip by default - const ext = extname(input); - if (ext === ".xz") { - compressFlag = "J"; - } else if (ext === ".bz2") { - compressFlag = "j"; - } - - const tarCmd: string[] = []; - tarCmd.push("tar"); - tarCmd.push(`-xv${compressFlag}f`); - tarCmd.push(input); - if (directory) { - tarCmd.push("--directory"); - tarCmd.push(directory); - } + const tarCmd = unTarCommand(currentTarBinary(), input, directory); + info(tarCmd); const p = Deno.run({ cmd: tarCmd, cwd, }); const status = await p.status(); if (status.code !== 0) { - throw Error("Failure to untar"); + const systemTarPath = windowsSystemTar(Deno.env.get("WINDIR")); + const fellBack = os === "windows" && tarCmd[0] === "tar" + ? ` ${systemTarPath} was not found, so tar was resolved from PATH.` + : ""; + throw Error( + `Failure to untar ${input} using ${tarCmd[0]} (exit ${status.code}).${fellBack} Command was: ${ + tarCmd.join(" ") + }`, + ); } } From fa590c17d6211e762c9c30faa9868345cf4d5433 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Aug 2026 18:50:14 +0200 Subject: [PATCH 3/6] Trim tar implementation comments --- package/src/util/tar.ts | 67 ++++++------------ tests/unit/tar-command.test.ts | 126 ++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 103 deletions(-) diff --git a/package/src/util/tar.ts b/package/src/util/tar.ts index e7b4aecab39..d60dd6565bf 100644 --- a/package/src/util/tar.ts +++ b/package/src/util/tar.ts @@ -1,26 +1,16 @@ /* -* tar.ts -* -* Copyright (C) 2020-2022 Posit Software, PBC -* -*/ + * tar.ts + * + * Copyright (C) 2020-2022 Posit Software, PBC + */ import { info } from "../../../src/deno_ral/log.ts"; import { dirname, extname } from "../../../src/deno_ral/path.ts"; import { existsSync } from "../../../src/deno_ral/fs.ts"; import { os } from "../../../src/deno_ral/platform.ts"; -// Deno 2 dropped Deno.run from its type definitions, but the runtime still -// implements it (verified: typeof Deno.run === "function" on Deno 2.7.14). -// Declaring its actual shape here fixes the TS2339 errors this file's tests -// would otherwise hit under `deno test --check`, instead of suppressing them; -// migrating off Deno.run to Deno.Command is out of scope. The shape below is -// deliberately minimal - just what this file's two call sites use, not the -// full Deno 1 RunOptions/Process API. Other files calling Deno.run (git.ts, -// configure.ts, import-report/*) have their own untested latent TS2339s; -// if a future change needs to type Deno.run more broadly across the -// codebase, that belongs in src/deno_ral/process.ts (which already wraps -// Deno.Command, the migration target), not a wider version of this block. +// Deno 2 omits Deno.run from its types, but Quarto's runtime still provides it. +// Declare only the API used here until these calls migrate to Deno.Command. declare global { namespace Deno { function run(options: { cmd: string[]; cwd?: string }): { @@ -29,24 +19,12 @@ declare global { } } -// Windows ships bsdtar as System32\tar.exe, which reads ZIP archives. export function windowsSystemTar(systemRoot?: string): string { return `${systemRoot || "C:\\Windows"}\\System32\\tar.exe`; } -// Resolve which tar to run. On Windows prefer the absolute System32 bsdtar: a -// GNU tar earlier on PATH - Git for Windows installs one in usr\bin, and -// anything launched from a Git Bash shell inherits that PATH - cannot read ZIP -// and reads a Windows absolute path as a remote host spec, so it fails every -// dependency extraction. configure.cmd calls the absolute System32 path for the -// Deno bootstrap for this same reason. -// -// Fall back to PATH when that file is not there. That does not make such a host -// work, since its PATH tar is the very binary that cannot read ZIP; it keeps -// this change from regressing a host we have not tested, and it means a wrong -// systemRoot degrades to today's behaviour instead of failing outright. -// Callers pass the existence result so this stays pure and both branches are -// testable. +// Git for Windows may put GNU tar on PATH, where it cannot extract ZIP files +// from Windows paths. Prefer the system bsdtar and preserve PATH as a fallback. export function resolveTarBinary( os: string, systemTarPath: string, @@ -58,10 +36,7 @@ export function resolveTarBinary( return systemTarExists ? systemTarPath : "tar"; } -// tar's compression flag for an archive. A .zip gets none: the format is not -// gzip, and bsdtar detects it unaided. Passing z for a zip happens to work -// under bsdtar's lenient format detection, which is what has kept the mistake -// invisible and made the GNU tar failure look like archive corruption. +// ZIP archives need no compression flag; bsdtar detects their format. export function tarCompressFlag(input: string): string { const ext = extname(input).toLowerCase(); if (ext === ".xz") { @@ -74,8 +49,6 @@ export function tarCompressFlag(input: string): string { return "z"; } -// The command builders take an already-resolved binary, so the filesystem -// check stays out of them and a test can pass any binary string it likes. export function unTarCommand( tarBin: string, input: string, @@ -106,9 +79,6 @@ export function makeTarballCommand( return cmd; } -// The one impure step: read the environment and stat the candidate, then let -// the pure resolver decide. Not exported - the pure functions are what the -// tests cover. function currentTarBinary(): string { const systemTarPath = windowsSystemTar(Deno.env.get("WINDIR")); return resolveTarBinary(os, systemTarPath, existsSync(systemTarPath)); @@ -122,7 +92,12 @@ export async function makeTarball( info("Make Tarball"); info(`Input: ${input}`); info(`Output: ${output}\n`); - const tarCmd = makeTarballCommand(currentTarBinary(), input, output, changewd); + const tarCmd = makeTarballCommand( + currentTarBinary(), + input, + output, + changewd, + ); info(tarCmd); const p = Deno.run({ @@ -131,9 +106,9 @@ export async function makeTarball( const status = await p.status(); if (status.code !== 0) { throw Error( - `Failure to make tarball ${output} using ${tarCmd[0]} (exit ${status.code}). Command was: ${ - tarCmd.join(" ") - }`, + `Failure to make tarball ${output} using ${ + tarCmd[0] + } (exit ${status.code}). Command was: ${tarCmd.join(" ")}`, ); } } @@ -159,9 +134,9 @@ export async function unTar(input: string, directory?: string) { ? ` ${systemTarPath} was not found, so tar was resolved from PATH.` : ""; throw Error( - `Failure to untar ${input} using ${tarCmd[0]} (exit ${status.code}).${fellBack} Command was: ${ - tarCmd.join(" ") - }`, + `Failure to untar ${input} using ${ + tarCmd[0] + } (exit ${status.code}).${fellBack} Command was: ${tarCmd.join(" ")}`, ); } } diff --git a/tests/unit/tar-command.test.ts b/tests/unit/tar-command.test.ts index db4ce35521a..31cc683561f 100644 --- a/tests/unit/tar-command.test.ts +++ b/tests/unit/tar-command.test.ts @@ -1,13 +1,7 @@ /* * tar-command.test.ts * - * Tests how quarto-bld builds its tar invocations. On Windows the binary must - * be the absolute System32 bsdtar, because a GNU tar earlier on PATH cannot - * read ZIP and treats a Windows absolute path as a remote host. Archive names - * here are the real ones from package/src/common/dependencies. - * * Copyright (C) 2026 Posit Software, PBC - * */ import { unitTest } from "../test.ts"; @@ -27,37 +21,46 @@ unitTest("windowsSystemTar - builds the System32 path", async () => { ); }); -unitTest("windowsSystemTar - falls back to C:\\Windows when unset or empty", async () => { - assertEquals(windowsSystemTar(), "C:\\Windows\\System32\\tar.exe"); - assertEquals(windowsSystemTar(""), "C:\\Windows\\System32\\tar.exe"); -}); +unitTest( + "windowsSystemTar - falls back to C:\\Windows when unset or empty", + async () => { + assertEquals(windowsSystemTar(), "C:\\Windows\\System32\\tar.exe"); + assertEquals(windowsSystemTar(""), "C:\\Windows\\System32\\tar.exe"); + }, +); unitTest("windowsSystemTar - honours a relocated system root", async () => { assertEquals(windowsSystemTar("D:\\Win"), "D:\\Win\\System32\\tar.exe"); }); -unitTest("resolveTarBinary - windows prefers System32 bsdtar when present", async () => { - assertEquals( - resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", true), - "C:\\WINDOWS\\System32\\tar.exe", - ); -}); - -// No System32 bsdtar means a pre-17063 Windows. PATH tar there is very likely -// GNU tar, which cannot read ZIP, so this does not rescue such a host - it -// keeps behaviour identical to before this change rather than failing outright. -unitTest("resolveTarBinary - windows falls back to PATH when System32 tar is absent", async () => { - assertEquals( - resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", false), - "tar", - ); -}); - -unitTest("resolveTarBinary - other platforms keep the bare binary", async () => { - assertEquals(resolveTarBinary("linux", "irrelevant", true), "tar"); - assertEquals(resolveTarBinary("linux", "irrelevant", false), "tar"); - assertEquals(resolveTarBinary("darwin", "irrelevant", true), "tar"); -}); +unitTest( + "resolveTarBinary - windows prefers System32 bsdtar when present", + async () => { + assertEquals( + resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", true), + "C:\\WINDOWS\\System32\\tar.exe", + ); + }, +); + +unitTest( + "resolveTarBinary - windows falls back to PATH when System32 tar is absent", + async () => { + assertEquals( + resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", false), + "tar", + ); + }, +); + +unitTest( + "resolveTarBinary - other platforms keep the bare binary", + async () => { + assertEquals(resolveTarBinary("linux", "irrelevant", true), "tar"); + assertEquals(resolveTarBinary("linux", "irrelevant", false), "tar"); + assertEquals(resolveTarBinary("darwin", "irrelevant", true), "tar"); + }, +); unitTest("tarCompressFlag - zip gets no compression flag", async () => { assertEquals(tarCompressFlag("dart-sass-1.101.0-windows-x64.zip"), ""); @@ -126,28 +129,37 @@ unitTest("unTarCommand - a directory appends --directory", async () => { ); }); -unitTest("makeTarballCommand - darwin form is unchanged from today", async () => { - assertEquals( - makeTarballCommand("tar", "/src/payload", "/out/bundle.tar.gz", false), - ["tar", "czvf", "/out/bundle.tar.gz", "/src/payload"], - ); -}); - -unitTest("makeTarballCommand - changewd wraps the input with -C and dot", async () => { - assertEquals( - makeTarballCommand("tar", "/src/payload", "/out/bundle.tar.gz", true), - ["tar", "czvf", "/out/bundle.tar.gz", "-C", "/src/payload", "."], - ); -}); - -unitTest("makeTarballCommand - carries whatever binary it is handed", async () => { - assertEquals( - makeTarballCommand( - kSystemTar, - "C:\\src\\payload", - "C:\\out\\bundle.tar.gz", - false, - ), - [kSystemTar, "czvf", "C:\\out\\bundle.tar.gz", "C:\\src\\payload"], - ); -}); +unitTest( + "makeTarballCommand - darwin form is unchanged from today", + async () => { + assertEquals( + makeTarballCommand("tar", "/src/payload", "/out/bundle.tar.gz", false), + ["tar", "czvf", "/out/bundle.tar.gz", "/src/payload"], + ); + }, +); + +unitTest( + "makeTarballCommand - changewd wraps the input with -C and dot", + async () => { + assertEquals( + makeTarballCommand("tar", "/src/payload", "/out/bundle.tar.gz", true), + ["tar", "czvf", "/out/bundle.tar.gz", "-C", "/src/payload", "."], + ); + }, +); + +unitTest( + "makeTarballCommand - carries whatever binary it is handed", + async () => { + assertEquals( + makeTarballCommand( + kSystemTar, + "C:\\src\\payload", + "C:\\out\\bundle.tar.gz", + false, + ), + [kSystemTar, "czvf", "C:\\out\\bundle.tar.gz", "C:\\src\\payload"], + ); + }, +); From e72daa171804c34d4062935a0ec53b0eb4ab2ea0 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Aug 2026 18:58:44 +0200 Subject: [PATCH 4/6] Suppress Deno.run TS2339 per call site instead of a global type patch --- package/src/util/tar.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/package/src/util/tar.ts b/package/src/util/tar.ts index d60dd6565bf..0c10c6994e4 100644 --- a/package/src/util/tar.ts +++ b/package/src/util/tar.ts @@ -9,16 +9,6 @@ import { dirname, extname } from "../../../src/deno_ral/path.ts"; import { existsSync } from "../../../src/deno_ral/fs.ts"; import { os } from "../../../src/deno_ral/platform.ts"; -// Deno 2 omits Deno.run from its types, but Quarto's runtime still provides it. -// Declare only the API used here until these calls migrate to Deno.Command. -declare global { - namespace Deno { - function run(options: { cmd: string[]; cwd?: string }): { - status(): Promise<{ code: number }>; - }; - } -} - export function windowsSystemTar(systemRoot?: string): string { return `${systemRoot || "C:\\Windows"}\\System32\\tar.exe`; } @@ -100,6 +90,7 @@ export async function makeTarball( ); info(tarCmd); + // @ts-expect-error `Deno.run()` is soft-removed as of Deno 2; the runtime keeps it. const p = Deno.run({ cmd: tarCmd, }); @@ -123,6 +114,7 @@ export async function unTar(input: string, directory?: string) { const tarCmd = unTarCommand(currentTarBinary(), input, directory); info(tarCmd); + // @ts-expect-error `Deno.run()` is soft-removed as of Deno 2; the runtime keeps it. const p = Deno.run({ cmd: tarCmd, cwd, From f5110ed69ac11b2b05bbdb7e43358d5f9e2d7a2c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 26 Aug 2026 19:05:44 +0200 Subject: [PATCH 5/6] Collapse resolveTarBinary to one path-or-undefined argument, rename fellBack --- package/src/util/tar.ts | 16 ++++++++++------ tests/unit/tar-command.test.ts | 13 +++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/package/src/util/tar.ts b/package/src/util/tar.ts index 0c10c6994e4..47c8a5286f4 100644 --- a/package/src/util/tar.ts +++ b/package/src/util/tar.ts @@ -17,13 +17,12 @@ export function windowsSystemTar(systemRoot?: string): string { // from Windows paths. Prefer the system bsdtar and preserve PATH as a fallback. export function resolveTarBinary( os: string, - systemTarPath: string, - systemTarExists: boolean, + systemTarPath: string | undefined, ): string { if (os !== "windows") { return "tar"; } - return systemTarExists ? systemTarPath : "tar"; + return systemTarPath ?? "tar"; } // ZIP archives need no compression flag; bsdtar detects their format. @@ -71,7 +70,10 @@ export function makeTarballCommand( function currentTarBinary(): string { const systemTarPath = windowsSystemTar(Deno.env.get("WINDIR")); - return resolveTarBinary(os, systemTarPath, existsSync(systemTarPath)); + return resolveTarBinary( + os, + existsSync(systemTarPath) ? systemTarPath : undefined, + ); } export async function makeTarball( @@ -122,13 +124,15 @@ export async function unTar(input: string, directory?: string) { const status = await p.status(); if (status.code !== 0) { const systemTarPath = windowsSystemTar(Deno.env.get("WINDIR")); - const fellBack = os === "windows" && tarCmd[0] === "tar" + const fallbackNote = os === "windows" && tarCmd[0] === "tar" ? ` ${systemTarPath} was not found, so tar was resolved from PATH.` : ""; throw Error( `Failure to untar ${input} using ${ tarCmd[0] - } (exit ${status.code}).${fellBack} Command was: ${tarCmd.join(" ")}`, + } (exit ${status.code}).${fallbackNote} Command was: ${ + tarCmd.join(" ") + }`, ); } } diff --git a/tests/unit/tar-command.test.ts b/tests/unit/tar-command.test.ts index 31cc683561f..daaf197b402 100644 --- a/tests/unit/tar-command.test.ts +++ b/tests/unit/tar-command.test.ts @@ -37,7 +37,7 @@ unitTest( "resolveTarBinary - windows prefers System32 bsdtar when present", async () => { assertEquals( - resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", true), + resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe"), "C:\\WINDOWS\\System32\\tar.exe", ); }, @@ -46,19 +46,16 @@ unitTest( unitTest( "resolveTarBinary - windows falls back to PATH when System32 tar is absent", async () => { - assertEquals( - resolveTarBinary("windows", "C:\\WINDOWS\\System32\\tar.exe", false), - "tar", - ); + assertEquals(resolveTarBinary("windows", undefined), "tar"); }, ); unitTest( "resolveTarBinary - other platforms keep the bare binary", async () => { - assertEquals(resolveTarBinary("linux", "irrelevant", true), "tar"); - assertEquals(resolveTarBinary("linux", "irrelevant", false), "tar"); - assertEquals(resolveTarBinary("darwin", "irrelevant", true), "tar"); + assertEquals(resolveTarBinary("linux", "irrelevant"), "tar"); + assertEquals(resolveTarBinary("linux", undefined), "tar"); + assertEquals(resolveTarBinary("darwin", "irrelevant"), "tar"); }, ); From f9433c24802609ccf32c7de41c8610b9aac54bd8 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Thu, 27 Aug 2026 11:17:51 +0200 Subject: [PATCH 6/6] change copyright year --- package/src/util/tar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/src/util/tar.ts b/package/src/util/tar.ts index 47c8a5286f4..2a7524c173c 100644 --- a/package/src/util/tar.ts +++ b/package/src/util/tar.ts @@ -1,7 +1,7 @@ /* * tar.ts * - * Copyright (C) 2020-2022 Posit Software, PBC + * Copyright (C) 2020-2026 Posit Software, PBC */ import { info } from "../../../src/deno_ral/log.ts";