Skip to content
Open
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
1 change: 1 addition & 0 deletions news/changelog-1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ All changes included in 1.9:

- ([#4426](https://github.com/quarto-dev/quarto-cli/issues/4426)): New `quarto install verapdf` command installs [veraPDF](https://verapdf.org/) for PDF/A and PDF/UA validation. When verapdf is available, PDFs created with the `pdf-standard` option are automatically validated for compliance. Also supports `quarto uninstall verapdf`, `quarto update verapdf`, and `quarto tools`.
- ([#11877](https://github.com/quarto-dev/quarto-cli/issues/11877), [#10961](https://github.com/quarto-dev/quarto-cli/issues/10961), [#6821](https://github.com/quarto-dev/quarto-cli/issues/6821), [#13704](https://github.com/quarto-dev/quarto-cli/issues/13704)): New `quarto install chrome-headless-shell` command downloads [Chrome Headless Shell](https://developer.chrome.com/blog/chrome-headless-shell) from Google's Chrome for Testing API. This is the recommended headless browser for diagram rendering (Mermaid, Graphviz) to non-HTML formats. Smaller and lighter than full Chrome, with fewer system dependencies.
- ([#12124](https://github.com/quarto-dev/quarto-cli/issues/12124)): Support `quarto install tinytex` on ARM64 Linux and adopt new TinyTeX naming scheme with `.tar.xz` compression.

### `preview`

Expand Down
18 changes: 16 additions & 2 deletions src/core/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,21 @@ import { safeWindowsExec } from "./windows.ts";
export function unzip(file: string, dir?: string) {
if (!dir) dir = dirname(file);

if (file.endsWith("zip")) {
if (isWindows && file.endsWith(".exe")) {
// Self-extracting 7z archive (e.g., TinyTeX-windows.exe)
return safeWindowsExec(
file,
["-y"],
(cmd: string[]) => {
return execProcess({
cmd: cmd[0],
args: cmd.slice(1),
cwd: dir,
stdout: "piped",
});
},
);
} else if (file.endsWith("zip")) {
// It's a zip file
if (isWindows) {
const args = [
Expand Down Expand Up @@ -51,7 +65,7 @@ export function unzip(file: string, dir?: string) {
// Otherwise fall back to "tar" in PATH
}
return execProcess(
{ cmd: tarCmd, args: ["xfz", file], cwd: dir, stdout: "piped" },
{ cmd: tarCmd, args: ["xf", file], cwd: dir, stdout: "piped" },
);
}
}
Expand Down
89 changes: 53 additions & 36 deletions src/tools/impl/tinytex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { suggestUserBinPaths } from "../../core/path.ts";

import { ensureDirSync, walkSync } from "../../deno_ral/fs.ts";
import {
arch,
isLinux,
isMac,
isWindows,
Expand Down Expand Up @@ -65,15 +66,6 @@ export const tinyTexInstallable: InstallableTool = {
},
os: ["darwin"],
message: "The directory /usr/local/bin is not writable.",
}, {
check: () => {
// Can't be a linux non-x86 platform
const needsSource = needsSourceInstall();
return Promise.resolve(!needsSource);
},
os: ["linux"],
message:
"This platform doesn't support installation at this time. Please install manually instead. See https://yihui.org/tinytex/#installation.",
}],
installed,
installDir,
Expand Down Expand Up @@ -161,17 +153,17 @@ async function preparePackage(
const version = pkgInfo.version;

// target package information
const pkgName = tinyTexPkgName(kPackageMaximal, version);
const filePath = join(context.workingDir, pkgName);

// Download the package
const url = tinyTexUrl(pkgName, pkgInfo);
if (url) {
// Download the package
await context.download(`TinyTex ${version}`, url, filePath);
const candidates = tinyTexPkgName(kPackageMaximal, version);
const result = tinyTexUrl(candidates, pkgInfo);
if (result) {
const filePath = join(context.workingDir, result.name);
await context.download(`TinyTex ${version}`, result.url, filePath);
return { filePath, version };
} else {
context.error("Couldn't determine what URL to use to download");
context.error(
`Couldn't determine what URL to use to download TinyTeX. ` +
`Tried: ${candidates.join(", ")}`,
);
return Promise.reject();
}
}
Expand Down Expand Up @@ -502,22 +494,55 @@ async function textLiveRepo() {
return autoUrl;
}

function tinyTexPkgName(base?: string, ver?: string) {
const ext = isWindows ? "zip" : isLinux ? "tar.gz" : "tgz";
export function tinyTexPkgName(
base?: string,
ver?: string,
options?: { os?: string; arch?: string },
): string[] {
const effectiveOs = options?.os ??
(isWindows ? "windows" : isLinux ? "linux" : "darwin");
const effectiveArch = options?.arch ?? arch;

base = base || "TinyTeX";
if (ver) {
return `${base}-${ver}.${ext}`;

if (!ver) {
const ext = effectiveOs === "windows"
? "zip"
: effectiveOs === "linux"
? "tar.gz"
: "tgz";
return [`${base}.${ext}`];
}

const candidates: string[] = [];

if (effectiveOs === "windows") {
candidates.push(`${base}-windows-${ver}.exe`);
candidates.push(`${base}-${ver}.zip`);
} else if (effectiveOs === "linux") {
if (effectiveArch === "aarch64") {
candidates.push(`${base}-linux-arm64-${ver}.tar.xz`);
candidates.push(`${base}-arm64-${ver}.tar.gz`);
} else {
candidates.push(`${base}-linux-x86_64-${ver}.tar.xz`);
candidates.push(`${base}-${ver}.tar.gz`);
}
} else {
return `${base}.${ext}`;
candidates.push(`${base}-darwin-${ver}.tar.xz`);
candidates.push(`${base}-${ver}.tgz`);
}

return candidates;
}

function tinyTexUrl(pkg: string, remotePkgInfo: RemotePackageInfo) {
const asset = remotePkgInfo.assets.find((asset) => {
return asset.name === pkg;
});
return asset?.url;
function tinyTexUrl(candidates: string[], remotePkgInfo: RemotePackageInfo) {
for (const pkg of candidates) {
const asset = remotePkgInfo.assets.find((asset) => asset.name === pkg);
if (asset) {
return { url: asset.url, name: pkg };
}
}
return undefined;
}

async function remotePackageInfo(): Promise<RemotePackageInfo> {
Expand All @@ -537,14 +562,6 @@ async function isWritable(path: string) {
return status.state === "granted";
}

function needsSourceInstall() {
if (isLinux && Deno.build.arch !== "x86_64") {
return true;
} else {
return false;
}
}

async function isTinyTex() {
const root = await texLiveRoot();
if (root) {
Expand Down
160 changes: 160 additions & 0 deletions tests/unit/tools/tinytex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* tinytex.test.ts
*
* Copyright (C) 2026 Posit Software, PBC
*/

import { unitTest } from "../../test.ts";
import { assert, assertEquals } from "testing/asserts";
import { tinyTexPkgName } from "../../../src/tools/impl/tinytex.ts";
import { getLatestRelease } from "../../../src/tools/github.ts";
import { GitHubRelease } from "../../../src/tools/types.ts";

// ---- Pure logic tests for tinyTexPkgName ----

unitTest("tinyTexPkgName - Linux aarch64 with version", async () => {
assertEquals(
tinyTexPkgName("TinyTeX", "v2026.04", { os: "linux", arch: "aarch64" }),
[
"TinyTeX-linux-arm64-v2026.04.tar.xz",
"TinyTeX-arm64-v2026.04.tar.gz",
],
);
});

unitTest("tinyTexPkgName - Linux x86_64 with version", async () => {
assertEquals(
tinyTexPkgName("TinyTeX", "v2026.04", { os: "linux", arch: "x86_64" }),
[
"TinyTeX-linux-x86_64-v2026.04.tar.xz",
"TinyTeX-v2026.04.tar.gz",
],
);
});

unitTest("tinyTexPkgName - macOS with version", async () => {
assertEquals(
tinyTexPkgName("TinyTeX", "v2026.04", { os: "darwin", arch: "aarch64" }),
[
"TinyTeX-darwin-v2026.04.tar.xz",
"TinyTeX-v2026.04.tgz",
],
);
});

unitTest("tinyTexPkgName - Windows with version", async () => {
assertEquals(
tinyTexPkgName("TinyTeX", "v2026.04", { os: "windows", arch: "x86_64" }),
[
"TinyTeX-windows-v2026.04.exe",
"TinyTeX-v2026.04.zip",
],
);
});

unitTest("tinyTexPkgName - versionless Linux aarch64", async () => {
assertEquals(
tinyTexPkgName("TinyTeX", undefined, { os: "linux", arch: "aarch64" }),
["TinyTeX.tar.gz"],
);
});

unitTest("tinyTexPkgName - TinyTeX-1 ARM64 Linux", async () => {
assertEquals(
tinyTexPkgName("TinyTeX-1", "v2026.04", {
os: "linux",
arch: "aarch64",
}),
[
"TinyTeX-1-linux-arm64-v2026.04.tar.xz",
"TinyTeX-1-arm64-v2026.04.tar.gz",
],
);
});

unitTest("tinyTexPkgName - default base", async () => {
assertEquals(
tinyTexPkgName(undefined, "v2026.04", { os: "linux", arch: "x86_64" }),
[
"TinyTeX-linux-x86_64-v2026.04.tar.xz",
"TinyTeX-v2026.04.tar.gz",
],
);
});

// ---- Asset-existence tests (network, verify against latest release) ----

const kTinyTexRepo = "rstudio/tinytex-releases";

let cachedRelease: GitHubRelease | undefined;
async function getRelease() {
if (!cachedRelease) {
cachedRelease = await getLatestRelease(kTinyTexRepo);
}
return cachedRelease;
}

function assertAssetExists(
candidates: string[],
assetNames: string[],
label: string,
) {
const found = candidates.some((c) => assetNames.includes(c));
assert(
found,
`No matching asset for ${label}. Candidates: ${candidates.join(", ")}. ` +
`Available TinyTeX assets: ${assetNames.filter((a) => a.startsWith("TinyTeX")).join(", ")}`,
);
}

unitTest(
"tinyTexPkgName - Linux x86_64 candidates match latest release",
async () => {
const release = await getRelease();
const assetNames = release.assets.map((a) => a.name);
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
os: "linux",
arch: "x86_64",
});
assertAssetExists(candidates, assetNames, "Linux x86_64");
},
);

unitTest(
"tinyTexPkgName - Linux aarch64 candidates match latest release",
async () => {
const release = await getRelease();
const assetNames = release.assets.map((a) => a.name);
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
os: "linux",
arch: "aarch64",
});
assertAssetExists(candidates, assetNames, "Linux aarch64");
},
);

unitTest(
"tinyTexPkgName - macOS candidates match latest release",
async () => {
const release = await getRelease();
const assetNames = release.assets.map((a) => a.name);
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
os: "darwin",
arch: "aarch64",
});
assertAssetExists(candidates, assetNames, "macOS");
},
);

unitTest(
"tinyTexPkgName - Windows candidates match latest release",
async () => {
const release = await getRelease();
const assetNames = release.assets.map((a) => a.name);
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
os: "windows",
arch: "x86_64",
});
assertAssetExists(candidates, assetNames, "Windows");
},
);
Loading