From ff6c9a6403e16463587725bd7826e0265cdaaa5c Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 9 Mar 2026 17:28:04 +0100 Subject: [PATCH 1/5] 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. --- src/core/zip.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/core/zip.ts b/src/core/zip.ts index b9bc8170c91..1677fd28c4c 100644 --- a/src/core/zip.ts +++ b/src/core/zip.ts @@ -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 = [ @@ -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" }, ); } } From 52ac492ce3e1c20a5d86171eed2b5e2f366945b9 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 9 Mar 2026 17:28:27 +0100 Subject: [PATCH 2/5] 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. --- tests/unit/tools/tinytex.test.ts | 160 +++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/unit/tools/tinytex.test.ts diff --git a/tests/unit/tools/tinytex.test.ts b/tests/unit/tools/tinytex.test.ts new file mode 100644 index 00000000000..5522c1bc7d7 --- /dev/null +++ b/tests/unit/tools/tinytex.test.ts @@ -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"); + }, +); From df6156358f821224a3288e3ac035d870be971e20 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 9 Mar 2026 17:28:51 +0100 Subject: [PATCH 3/5] 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 --- src/tools/impl/tinytex.ts | 84 +++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/src/tools/impl/tinytex.ts b/src/tools/impl/tinytex.ts index ec638401020..0f631632bbf 100644 --- a/src/tools/impl/tinytex.ts +++ b/src/tools/impl/tinytex.ts @@ -33,6 +33,7 @@ import { suggestUserBinPaths } from "../../core/path.ts"; import { ensureDirSync, walkSync } from "../../deno_ral/fs.ts"; import { + arch, isLinux, isMac, isWindows, @@ -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, @@ -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"); @@ -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 { @@ -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) { From 63abd37d235d3fba6d9a14b23c54c9354a75a7c3 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 9 Mar 2026 17:30:14 +0100 Subject: [PATCH 4/5] Add changelog entry for ARM64 Linux TinyTeX support --- news/changelog-1.9.md | 1 + 1 file changed, 1 insertion(+) diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index ecc77efc655..51f9f904b87 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -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` From 28703d27a985950d300e636af6bc15b53753ffeb Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 9 Mar 2026 18:57:36 +0100 Subject: [PATCH 5/5] 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. --- src/core/zip.ts | 2 +- src/tools/impl/tinytex.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/core/zip.ts b/src/core/zip.ts index 1677fd28c4c..8ed23ddde57 100644 --- a/src/core/zip.ts +++ b/src/core/zip.ts @@ -12,7 +12,7 @@ import { safeWindowsExec } from "./windows.ts"; export function unzip(file: string, dir?: string) { if (!dir) dir = dirname(file); - if (file.endsWith(".exe")) { + if (isWindows && file.endsWith(".exe")) { // Self-extracting 7z archive (e.g., TinyTeX-windows.exe) return safeWindowsExec( file, diff --git a/src/tools/impl/tinytex.ts b/src/tools/impl/tinytex.ts index 0f631632bbf..cb07917406b 100644 --- a/src/tools/impl/tinytex.ts +++ b/src/tools/impl/tinytex.ts @@ -160,7 +160,10 @@ async function preparePackage( 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(); } }