Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 (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
84 changes: 49 additions & 35 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,14 +153,11 @@ 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");
Expand Down Expand Up @@ -502,22 +491,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 +559,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