Skip to content

Commit dfd864a

Browse files
authored
Support ARM64 Linux and new TinyTeX naming scheme (#14181)
* Support tar.xz and self-extracting exe in unzip() Change `tar xfz` to `tar xf` for automatic compression detection, handling .tar.gz, .tgz, and .tar.xz files. Add .exe branch for self-extracting 7z archives used by TinyTeX's new Windows naming. * Add unit tests for TinyTeX package name generation Pure logic tests for all platform/arch combos via options parameter. Asset-existence tests verify candidates match actual release assets, catching naming drift in CI. * Support ARM64 Linux and adopt new TinyTeX naming scheme Remove the needsSourceInstall() prereq gate that blocks ARM64 Linux. Update tinyTexPkgName() to generate candidate filenames for both new (TinyTeX-{os}[-{arch}]-{ver}.{ext}) and old naming schemes, with new preferred and old as fallback. Update tinyTexUrl() to pick the first matching asset from release. Handles the transition seamlessly as releases adopt new naming. Fixes #12124 * Add changelog entry for ARM64 Linux TinyTeX support * Guard .exe extraction to Windows and improve error diagnostics Only attempt self-extracting .exe on Windows in unzip(). Include candidate filenames in TinyTeX download error for easier debugging when naming drift occurs.
1 parent a976a5c commit dfd864a

4 files changed

Lines changed: 230 additions & 38 deletions

File tree

news/changelog-1.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ All changes included in 1.9:
181181

182182
- ([#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`.
183183
- ([#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.
184+
- ([#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.
184185

185186
### `preview`
186187

src/core/zip.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,21 @@ import { safeWindowsExec } from "./windows.ts";
1212
export function unzip(file: string, dir?: string) {
1313
if (!dir) dir = dirname(file);
1414

15-
if (file.endsWith("zip")) {
15+
if (isWindows && file.endsWith(".exe")) {
16+
// Self-extracting 7z archive (e.g., TinyTeX-windows.exe)
17+
return safeWindowsExec(
18+
file,
19+
["-y"],
20+
(cmd: string[]) => {
21+
return execProcess({
22+
cmd: cmd[0],
23+
args: cmd.slice(1),
24+
cwd: dir,
25+
stdout: "piped",
26+
});
27+
},
28+
);
29+
} else if (file.endsWith("zip")) {
1630
// It's a zip file
1731
if (isWindows) {
1832
const args = [
@@ -51,7 +65,7 @@ export function unzip(file: string, dir?: string) {
5165
// Otherwise fall back to "tar" in PATH
5266
}
5367
return execProcess(
54-
{ cmd: tarCmd, args: ["xfz", file], cwd: dir, stdout: "piped" },
68+
{ cmd: tarCmd, args: ["xf", file], cwd: dir, stdout: "piped" },
5569
);
5670
}
5771
}

src/tools/impl/tinytex.ts

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { suggestUserBinPaths } from "../../core/path.ts";
3333

3434
import { ensureDirSync, walkSync } from "../../deno_ral/fs.ts";
3535
import {
36+
arch,
3637
isLinux,
3738
isMac,
3839
isWindows,
@@ -65,15 +66,6 @@ export const tinyTexInstallable: InstallableTool = {
6566
},
6667
os: ["darwin"],
6768
message: "The directory /usr/local/bin is not writable.",
68-
}, {
69-
check: () => {
70-
// Can't be a linux non-x86 platform
71-
const needsSource = needsSourceInstall();
72-
return Promise.resolve(!needsSource);
73-
},
74-
os: ["linux"],
75-
message:
76-
"This platform doesn't support installation at this time. Please install manually instead. See https://yihui.org/tinytex/#installation.",
7769
}],
7870
installed,
7971
installDir,
@@ -161,17 +153,17 @@ async function preparePackage(
161153
const version = pkgInfo.version;
162154

163155
// target package information
164-
const pkgName = tinyTexPkgName(kPackageMaximal, version);
165-
const filePath = join(context.workingDir, pkgName);
166-
167-
// Download the package
168-
const url = tinyTexUrl(pkgName, pkgInfo);
169-
if (url) {
170-
// Download the package
171-
await context.download(`TinyTex ${version}`, url, filePath);
156+
const candidates = tinyTexPkgName(kPackageMaximal, version);
157+
const result = tinyTexUrl(candidates, pkgInfo);
158+
if (result) {
159+
const filePath = join(context.workingDir, result.name);
160+
await context.download(`TinyTex ${version}`, result.url, filePath);
172161
return { filePath, version };
173162
} else {
174-
context.error("Couldn't determine what URL to use to download");
163+
context.error(
164+
`Couldn't determine what URL to use to download TinyTeX. ` +
165+
`Tried: ${candidates.join(", ")}`,
166+
);
175167
return Promise.reject();
176168
}
177169
}
@@ -502,22 +494,55 @@ async function textLiveRepo() {
502494
return autoUrl;
503495
}
504496

505-
function tinyTexPkgName(base?: string, ver?: string) {
506-
const ext = isWindows ? "zip" : isLinux ? "tar.gz" : "tgz";
497+
export function tinyTexPkgName(
498+
base?: string,
499+
ver?: string,
500+
options?: { os?: string; arch?: string },
501+
): string[] {
502+
const effectiveOs = options?.os ??
503+
(isWindows ? "windows" : isLinux ? "linux" : "darwin");
504+
const effectiveArch = options?.arch ?? arch;
507505

508506
base = base || "TinyTeX";
509-
if (ver) {
510-
return `${base}-${ver}.${ext}`;
507+
508+
if (!ver) {
509+
const ext = effectiveOs === "windows"
510+
? "zip"
511+
: effectiveOs === "linux"
512+
? "tar.gz"
513+
: "tgz";
514+
return [`${base}.${ext}`];
515+
}
516+
517+
const candidates: string[] = [];
518+
519+
if (effectiveOs === "windows") {
520+
candidates.push(`${base}-windows-${ver}.exe`);
521+
candidates.push(`${base}-${ver}.zip`);
522+
} else if (effectiveOs === "linux") {
523+
if (effectiveArch === "aarch64") {
524+
candidates.push(`${base}-linux-arm64-${ver}.tar.xz`);
525+
candidates.push(`${base}-arm64-${ver}.tar.gz`);
526+
} else {
527+
candidates.push(`${base}-linux-x86_64-${ver}.tar.xz`);
528+
candidates.push(`${base}-${ver}.tar.gz`);
529+
}
511530
} else {
512-
return `${base}.${ext}`;
531+
candidates.push(`${base}-darwin-${ver}.tar.xz`);
532+
candidates.push(`${base}-${ver}.tgz`);
513533
}
534+
535+
return candidates;
514536
}
515537

516-
function tinyTexUrl(pkg: string, remotePkgInfo: RemotePackageInfo) {
517-
const asset = remotePkgInfo.assets.find((asset) => {
518-
return asset.name === pkg;
519-
});
520-
return asset?.url;
538+
function tinyTexUrl(candidates: string[], remotePkgInfo: RemotePackageInfo) {
539+
for (const pkg of candidates) {
540+
const asset = remotePkgInfo.assets.find((asset) => asset.name === pkg);
541+
if (asset) {
542+
return { url: asset.url, name: pkg };
543+
}
544+
}
545+
return undefined;
521546
}
522547

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

540-
function needsSourceInstall() {
541-
if (isLinux && Deno.build.arch !== "x86_64") {
542-
return true;
543-
} else {
544-
return false;
545-
}
546-
}
547-
548565
async function isTinyTex() {
549566
const root = await texLiveRoot();
550567
if (root) {

tests/unit/tools/tinytex.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* tinytex.test.ts
3+
*
4+
* Copyright (C) 2026 Posit Software, PBC
5+
*/
6+
7+
import { unitTest } from "../../test.ts";
8+
import { assert, assertEquals } from "testing/asserts";
9+
import { tinyTexPkgName } from "../../../src/tools/impl/tinytex.ts";
10+
import { getLatestRelease } from "../../../src/tools/github.ts";
11+
import { GitHubRelease } from "../../../src/tools/types.ts";
12+
13+
// ---- Pure logic tests for tinyTexPkgName ----
14+
15+
unitTest("tinyTexPkgName - Linux aarch64 with version", async () => {
16+
assertEquals(
17+
tinyTexPkgName("TinyTeX", "v2026.04", { os: "linux", arch: "aarch64" }),
18+
[
19+
"TinyTeX-linux-arm64-v2026.04.tar.xz",
20+
"TinyTeX-arm64-v2026.04.tar.gz",
21+
],
22+
);
23+
});
24+
25+
unitTest("tinyTexPkgName - Linux x86_64 with version", async () => {
26+
assertEquals(
27+
tinyTexPkgName("TinyTeX", "v2026.04", { os: "linux", arch: "x86_64" }),
28+
[
29+
"TinyTeX-linux-x86_64-v2026.04.tar.xz",
30+
"TinyTeX-v2026.04.tar.gz",
31+
],
32+
);
33+
});
34+
35+
unitTest("tinyTexPkgName - macOS with version", async () => {
36+
assertEquals(
37+
tinyTexPkgName("TinyTeX", "v2026.04", { os: "darwin", arch: "aarch64" }),
38+
[
39+
"TinyTeX-darwin-v2026.04.tar.xz",
40+
"TinyTeX-v2026.04.tgz",
41+
],
42+
);
43+
});
44+
45+
unitTest("tinyTexPkgName - Windows with version", async () => {
46+
assertEquals(
47+
tinyTexPkgName("TinyTeX", "v2026.04", { os: "windows", arch: "x86_64" }),
48+
[
49+
"TinyTeX-windows-v2026.04.exe",
50+
"TinyTeX-v2026.04.zip",
51+
],
52+
);
53+
});
54+
55+
unitTest("tinyTexPkgName - versionless Linux aarch64", async () => {
56+
assertEquals(
57+
tinyTexPkgName("TinyTeX", undefined, { os: "linux", arch: "aarch64" }),
58+
["TinyTeX.tar.gz"],
59+
);
60+
});
61+
62+
unitTest("tinyTexPkgName - TinyTeX-1 ARM64 Linux", async () => {
63+
assertEquals(
64+
tinyTexPkgName("TinyTeX-1", "v2026.04", {
65+
os: "linux",
66+
arch: "aarch64",
67+
}),
68+
[
69+
"TinyTeX-1-linux-arm64-v2026.04.tar.xz",
70+
"TinyTeX-1-arm64-v2026.04.tar.gz",
71+
],
72+
);
73+
});
74+
75+
unitTest("tinyTexPkgName - default base", async () => {
76+
assertEquals(
77+
tinyTexPkgName(undefined, "v2026.04", { os: "linux", arch: "x86_64" }),
78+
[
79+
"TinyTeX-linux-x86_64-v2026.04.tar.xz",
80+
"TinyTeX-v2026.04.tar.gz",
81+
],
82+
);
83+
});
84+
85+
// ---- Asset-existence tests (network, verify against latest release) ----
86+
87+
const kTinyTexRepo = "rstudio/tinytex-releases";
88+
89+
let cachedRelease: GitHubRelease | undefined;
90+
async function getRelease() {
91+
if (!cachedRelease) {
92+
cachedRelease = await getLatestRelease(kTinyTexRepo);
93+
}
94+
return cachedRelease;
95+
}
96+
97+
function assertAssetExists(
98+
candidates: string[],
99+
assetNames: string[],
100+
label: string,
101+
) {
102+
const found = candidates.some((c) => assetNames.includes(c));
103+
assert(
104+
found,
105+
`No matching asset for ${label}. Candidates: ${candidates.join(", ")}. ` +
106+
`Available TinyTeX assets: ${assetNames.filter((a) => a.startsWith("TinyTeX")).join(", ")}`,
107+
);
108+
}
109+
110+
unitTest(
111+
"tinyTexPkgName - Linux x86_64 candidates match latest release",
112+
async () => {
113+
const release = await getRelease();
114+
const assetNames = release.assets.map((a) => a.name);
115+
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
116+
os: "linux",
117+
arch: "x86_64",
118+
});
119+
assertAssetExists(candidates, assetNames, "Linux x86_64");
120+
},
121+
);
122+
123+
unitTest(
124+
"tinyTexPkgName - Linux aarch64 candidates match latest release",
125+
async () => {
126+
const release = await getRelease();
127+
const assetNames = release.assets.map((a) => a.name);
128+
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
129+
os: "linux",
130+
arch: "aarch64",
131+
});
132+
assertAssetExists(candidates, assetNames, "Linux aarch64");
133+
},
134+
);
135+
136+
unitTest(
137+
"tinyTexPkgName - macOS candidates match latest release",
138+
async () => {
139+
const release = await getRelease();
140+
const assetNames = release.assets.map((a) => a.name);
141+
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
142+
os: "darwin",
143+
arch: "aarch64",
144+
});
145+
assertAssetExists(candidates, assetNames, "macOS");
146+
},
147+
);
148+
149+
unitTest(
150+
"tinyTexPkgName - Windows candidates match latest release",
151+
async () => {
152+
const release = await getRelease();
153+
const assetNames = release.assets.map((a) => a.name);
154+
const candidates = tinyTexPkgName("TinyTeX", release.tag_name, {
155+
os: "windows",
156+
arch: "x86_64",
157+
});
158+
assertAssetExists(candidates, assetNames, "Windows");
159+
},
160+
);

0 commit comments

Comments
 (0)