Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 99 additions & 35 deletions package/src/util/tar.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,80 @@
/*
* tar.ts
*
* Copyright (C) 2020-2022 Posit Software, PBC
*
*/
* tar.ts
*
* Copyright (C) 2020-2026 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";

export function windowsSystemTar(systemRoot?: string): string {
return `${systemRoot || "C:\\Windows"}\\System32\\tar.exe`;
}

// 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 | undefined,
): string {
if (os !== "windows") {
return "tar";
}
return systemTarPath ?? "tar";
}

// ZIP archives need no compression flag; bsdtar detects their format.
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";
}

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;
}

function currentTarBinary(): string {
const systemTarPath = windowsSystemTar(Deno.env.get("WINDIR"));
return resolveTarBinary(
os,
existsSync(systemTarPath) ? systemTarPath : undefined,
);
}

export async function makeTarball(
input: string,
Expand All @@ -16,25 +84,25 @@ 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);
// @ts-expect-error `Deno.run()` is soft-removed as of Deno 2; the runtime keeps it.
const p = Deno.run({
cmd: tarCmd,
});
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(" ")}`,
);
}
}

Expand All @@ -45,30 +113,26 @@ 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);
// @ts-expect-error `Deno.run()` is soft-removed as of Deno 2; the runtime keeps it.
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 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}).${fallbackNote} Command was: ${
tarCmd.join(" ")
}`,
);
}
}
162 changes: 162 additions & 0 deletions tests/unit/tar-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* tar-command.test.ts
*
* 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"),
"C:\\WINDOWS\\System32\\tar.exe",
);
},
);

unitTest(
"resolveTarBinary - windows falls back to PATH when System32 tar is absent",
async () => {
assertEquals(resolveTarBinary("windows", undefined), "tar");
},
);

unitTest(
"resolveTarBinary - other platforms keep the bare binary",
async () => {
assertEquals(resolveTarBinary("linux", "irrelevant"), "tar");
assertEquals(resolveTarBinary("linux", undefined), "tar");
assertEquals(resolveTarBinary("darwin", "irrelevant"), "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"],
);
},
);
Loading