Skip to content

Commit 9db6863

Browse files
authored
[backport] Add arm64 Linux support for chrome-headless-shell and deprecate chromium installer (#14335)
Add arm64 Linux support for `quarto install chrome-headless-shell` using Microsoft's Playwright CDN as the download source, since Chrome for Testing has no arm64 Linux builds. Version metadata comes from Playwright's browsers.json, and the arm64 binary name difference (`headless_shell` vs `chrome-headless-shell`) is abstracted by a platform-aware helper. Add deprecation warnings guiding users from `chromium` to `chrome-headless-shell`: - `quarto install chromium` and `quarto update chromium` show deprecation before proceeding (including on WSL) - `quarto check install` shows migration guidance when legacy Chromium is detected in the installed tools list Add CI workflow (`test-install.yml`) covering arm64 Linux and macOS tool installation, and chromium deprecation warnings across all platforms. Partial backport from #14334. Fixes #1187
1 parent cff3319 commit 9db6863

File tree

11 files changed

+359
-21
lines changed

11 files changed

+359
-21
lines changed

.github/workflows/test-ff-matrix.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ on:
2020
- ".github/workflows/stale-needs-repro.yml"
2121
- ".github/workflows/test-bundle.yml"
2222
- ".github/workflows/test-smokes-parallel.yml"
23+
- ".github/workflows/test-install.yml"
2324
- ".github/workflows/test-quarto-latexmk.yml"
2425
- ".github/workflows/update-test-timing.yml"
2526
pull_request:
@@ -31,6 +32,7 @@ on:
3132
- ".github/workflows/performance-check.yml"
3233
- ".github/workflows/stale-needs-repro.yml"
3334
- ".github/workflows/test-bundle.yml"
35+
- ".github/workflows/test-install.yml"
3436
- ".github/workflows/test-smokes-parallel.yml"
3537
- ".github/workflows/test-quarto-latexmk.yml"
3638
- ".github/workflows/update-test-timing.yml"

.github/workflows/test-install.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Integration test for `quarto install` on platforms not covered by smoke tests.
2+
# Smoke tests (test-smokes.yml) cover x86_64 Linux and Windows.
3+
# This workflow fills the gap for arm64 Linux and macOS.
4+
name: Test Tool Install
5+
on:
6+
workflow_dispatch:
7+
push:
8+
branches:
9+
- main
10+
- "v1.*"
11+
paths:
12+
- "src/tools/**"
13+
- ".github/workflows/test-install.yml"
14+
pull_request:
15+
paths:
16+
- "src/tools/**"
17+
- ".github/workflows/test-install.yml"
18+
schedule:
19+
# Weekly Monday 9am UTC — detect upstream CDN/API breakage
20+
- cron: "0 9 * * 1"
21+
22+
permissions:
23+
contents: read
24+
25+
jobs:
26+
test-install:
27+
name: Install tools (${{ matrix.os }})
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
os: [ubuntu-24.04-arm, macos-latest]
32+
runs-on: ${{ matrix.os }}
33+
steps:
34+
- name: Checkout Repo
35+
uses: actions/checkout@v6
36+
37+
- uses: ./.github/workflows/actions/quarto-dev
38+
39+
- name: Install TinyTeX
40+
env:
41+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42+
run: |
43+
quarto install tinytex
44+
45+
- name: Install Chrome Headless Shell
46+
run: |
47+
quarto install chrome-headless-shell --no-prompt
48+
49+
- name: Verify tools with quarto check
50+
run: |
51+
quarto check install
52+
53+
test-chromium-deprecation:
54+
name: Chromium deprecation warning (${{ matrix.os }})
55+
strategy:
56+
fail-fast: false
57+
matrix:
58+
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
59+
runs-on: ${{ matrix.os }}
60+
steps:
61+
- name: Checkout Repo
62+
uses: actions/checkout@v6
63+
64+
- uses: ./.github/workflows/actions/quarto-dev
65+
66+
- name: Make quarto available in bash (Windows)
67+
if: runner.os == 'Windows'
68+
shell: bash
69+
run: |
70+
quarto_cmd=$(command -v quarto.cmd)
71+
dir=$(dirname "$quarto_cmd")
72+
printf '#!/bin/bash\nexec "%s" "$@"\n' "$quarto_cmd" > "$dir/quarto"
73+
chmod +x "$dir/quarto"
74+
75+
- name: Install chromium and capture result
76+
id: install-chromium
77+
shell: bash
78+
run: |
79+
set +e
80+
output=$(quarto install chromium --no-prompt 2>&1)
81+
exit_code=$?
82+
set -e
83+
echo "$output"
84+
if echo "$output" | grep -Fq "is deprecated"; then
85+
echo "deprecation-warning=true" >> "$GITHUB_OUTPUT"
86+
fi
87+
if [ "$exit_code" -eq 0 ]; then
88+
echo "chromium-installed=true" >> "$GITHUB_OUTPUT"
89+
fi
90+
91+
- name: Assert install deprecation warning was shown
92+
shell: bash
93+
run: |
94+
if [ "${{ steps.install-chromium.outputs.deprecation-warning }}" != "true" ]; then
95+
echo "::error::Deprecation warning missing from quarto install chromium output"
96+
exit 1
97+
fi
98+
echo "Install deprecation warning found"
99+
100+
- name: Update chromium and capture result
101+
id: update-chromium
102+
shell: bash
103+
run: |
104+
set +e
105+
output=$(quarto update chromium --no-prompt 2>&1)
106+
set -e
107+
echo "$output"
108+
if echo "$output" | grep -Fq "is deprecated"; then
109+
echo "deprecation-warning=true" >> "$GITHUB_OUTPUT"
110+
fi
111+
112+
- name: Assert update deprecation warning was shown
113+
shell: bash
114+
run: |
115+
if [ "${{ steps.update-chromium.outputs.deprecation-warning }}" != "true" ]; then
116+
echo "::error::Deprecation warning missing from quarto update chromium output"
117+
exit 1
118+
fi
119+
echo "Update deprecation warning found"
120+
121+
- name: Verify quarto check warns about outdated Chromium
122+
shell: bash
123+
run: |
124+
if [ "${{ steps.install-chromium.outputs.chromium-installed }}" != "true" ]; then
125+
echo "Chromium install did not succeed on this platform, skipping check"
126+
exit 0
127+
fi
128+
output=$(quarto check install 2>&1)
129+
echo "$output"
130+
if ! echo "$output" | grep -Fq "Chromium is outdated"; then
131+
echo "::error::Outdated Chromium warning missing from quarto check"
132+
exit 1
133+
fi
134+
echo "Outdated Chromium warning found in quarto check"

.github/workflows/test-smokes-parallel.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ on:
2929
- ".github/workflows/stale-needs-repro.yml"
3030
- ".github/workflows/test-bundle.yml"
3131
- ".github/workflows/test-ff-matrix.yml"
32+
- ".github/workflows/test-install.yml"
3233
- ".github/workflows/test-quarto-latexmk.yml"
3334
- ".github/workflows/update-test-timing.yml"
3435
push:

news/changelog-1.9.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- ([#14281](https://github.com/quarto-dev/quarto-cli/issues/14281)): Fix transient `.quarto_ipynb` files accumulating during `quarto preview` with Jupyter engine.
77
- ([#14298](https://github.com/quarto-dev/quarto-cli/issues/14298)): Fix `quarto preview` browse URL including output filename (e.g., `hello.html`) for single-file documents, breaking Posit Workbench proxied server access.
88
- ([rstudio/rstudio#17333](https://github.com/rstudio/rstudio/issues/17333)): Fix `quarto inspect` on standalone files emitting project metadata that breaks RStudio's publishing wizard.
9+
- ([#14334](https://github.com/quarto-dev/quarto-cli/pull/14334), [#9710](https://github.com/quarto-dev/quarto-cli/issues/9710)): Add arm64 Linux support for `quarto install chrome-headless-shell` using Playwright CDN as download source. `quarto install chromium` and `quarto update chromium` now show a deprecation warning — use `chrome-headless-shell` instead, which always installs the latest stable Chrome (the legacy `chromium` installer pins an outdated Puppeteer revision that cannot receive security updates). `quarto check install` also warns when legacy Chromium is detected.
910

1011
## In previous releases
1112

src/command/check/check.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ async function checkInstall(conf: CheckConfiguration) {
364364
toolsJson[tool.name] = {
365365
version,
366366
};
367+
if (tool.name === "Chromium") {
368+
toolsOutput.push(
369+
`${kIndent} (Chromium is outdated. Run "quarto uninstall chromium" then "quarto install chrome-headless-shell")`,
370+
);
371+
}
367372
}
368373
for (const tool of tools.notInstalled) {
369374
toolsOutput.push(`${kIndent}${tool.name}: (not installed)`);

src/tools/impl/chrome-for-testing.ts

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { InstallContext } from "../types.ts";
1818
/** CfT platform identifiers matching the Google Chrome for Testing API. */
1919
export type CftPlatform =
2020
| "linux64"
21+
| "linux-arm64"
2122
| "mac-arm64"
2223
| "mac-x64"
2324
| "win32"
@@ -32,11 +33,12 @@ export interface PlatformInfo {
3233

3334
/**
3435
* Map os + arch to a CfT platform string.
35-
* Throws on unsupported platforms (e.g., linux aarch64 — to be handled by Playwright CDN).
36+
* Throws on unsupported platforms.
3637
*/
3738
export function detectCftPlatform(): PlatformInfo {
3839
const platformMap: Record<string, CftPlatform> = {
3940
"linux-x86_64": "linux64",
41+
"linux-aarch64": "linux-arm64",
4042
"darwin-aarch64": "mac-arm64",
4143
"darwin-x86_64": "mac-x64",
4244
"windows-x86_64": "win64",
@@ -47,14 +49,8 @@ export function detectCftPlatform(): PlatformInfo {
4749
const platform = platformMap[key];
4850

4951
if (!platform) {
50-
if (os === "linux" && arch === "aarch64") {
51-
throw new Error(
52-
"linux-arm64 is not supported by Chrome for Testing. " +
53-
"Use 'quarto install chromium' for arm64 support.",
54-
);
55-
}
5652
throw new Error(
57-
`Unsupported platform for Chrome for Testing: ${os} ${arch}`,
53+
`Unsupported platform for chrome-headless-shell: ${os} ${arch}`,
5854
);
5955
}
6056

@@ -122,6 +118,79 @@ export async function fetchLatestCftRelease(): Promise<CftStableRelease> {
122118
};
123119
}
124120

121+
/** Parsed entry from Playwright's browsers.json for chromium-headless-shell. */
122+
export interface PlaywrightBrowserEntry {
123+
revision: string;
124+
browserVersion: string;
125+
}
126+
127+
const kPlaywrightBrowsersJsonUrl =
128+
"https://raw.githubusercontent.com/microsoft/playwright/main/packages/playwright-core/browsers.json";
129+
130+
/** Check if the current platform requires Playwright CDN (arm64 Linux). */
131+
export function isPlaywrightCdnPlatform(info?: PlatformInfo): boolean {
132+
const p = info ?? detectCftPlatform();
133+
return p.platform === "linux-arm64";
134+
}
135+
136+
/**
137+
* Fetch Playwright's browsers.json and extract the chromium-headless-shell entry.
138+
* Used as the version/revision source for arm64 Linux where CfT has no builds.
139+
*/
140+
export async function fetchPlaywrightBrowsersJson(): Promise<PlaywrightBrowserEntry> {
141+
let response: Response;
142+
const fallbackHint = "\nIf this persists, install a system Chrome/Chromium instead " +
143+
"(Quarto will detect it automatically).";
144+
try {
145+
response = await fetch(kPlaywrightBrowsersJsonUrl);
146+
} catch (e) {
147+
throw new Error(
148+
`Failed to fetch Playwright browsers.json: ${
149+
e instanceof Error ? e.message : String(e)
150+
}${fallbackHint}`,
151+
);
152+
}
153+
154+
if (!response.ok) {
155+
throw new Error(
156+
`Playwright browsers.json returned ${response.status}: ${response.statusText}${fallbackHint}`,
157+
);
158+
}
159+
160+
// deno-lint-ignore no-explicit-any
161+
let data: any;
162+
try {
163+
data = await response.json();
164+
} catch {
165+
throw new Error("Playwright browsers.json returned invalid JSON");
166+
}
167+
168+
const browsers = data?.browsers;
169+
if (!Array.isArray(browsers)) {
170+
throw new Error("Playwright browsers.json missing 'browsers' array");
171+
}
172+
173+
// deno-lint-ignore no-explicit-any
174+
const entry = browsers.find((b: any) => b.name === "chromium-headless-shell");
175+
if (!entry || !entry.revision || !entry.browserVersion) {
176+
throw new Error(
177+
"Playwright browsers.json has no 'chromium-headless-shell' entry with revision and browserVersion",
178+
);
179+
}
180+
181+
return {
182+
revision: entry.revision,
183+
browserVersion: entry.browserVersion,
184+
};
185+
}
186+
187+
/**
188+
* Construct the Playwright CDN download URL for chrome-headless-shell on linux arm64.
189+
*/
190+
export function playwrightCdnDownloadUrl(revision: string): string {
191+
return `https://cdn.playwright.dev/builds/chromium/${revision}/chromium-headless-shell-linux-arm64.zip`;
192+
}
193+
125194
/**
126195
* Find a named executable inside an extracted CfT directory.
127196
* Handles platform-specific naming (.exe on Windows) and nested directory structures.

src/tools/impl/chrome-headless-shell.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
detectCftPlatform,
2121
downloadAndExtractCft,
2222
fetchLatestCftRelease,
23+
fetchPlaywrightBrowsersJson,
2324
findCftExecutable,
25+
isPlaywrightCdnPlatform,
26+
playwrightCdnDownloadUrl,
2427
} from "./chrome-for-testing.ts";
2528

2629
const kVersionFileName = "version";
@@ -32,6 +35,18 @@ export function chromeHeadlessShellInstallDir(): string {
3235
return quartoDataDir("chrome-headless-shell");
3336
}
3437

38+
/**
39+
* The executable name for chrome-headless-shell on the current platform.
40+
* CfT builds use "chrome-headless-shell", Playwright arm64 builds use "headless_shell".
41+
*/
42+
export function chromeHeadlessShellBinaryName(): string {
43+
try {
44+
return isPlaywrightCdnPlatform() ? "headless_shell" : "chrome-headless-shell";
45+
} catch {
46+
return "chrome-headless-shell";
47+
}
48+
}
49+
3550
/**
3651
* Find the chrome-headless-shell executable in the install directory.
3752
* Returns the absolute path if installed, undefined otherwise.
@@ -41,7 +56,7 @@ export function chromeHeadlessShellExecutablePath(): string | undefined {
4156
if (!existsSync(dir)) {
4257
return undefined;
4358
}
44-
return findCftExecutable(dir, "chrome-headless-shell");
59+
return findCftExecutable(dir, chromeHeadlessShellBinaryName());
4560
}
4661

4762
/** Record the installed version as a plain text file. */
@@ -62,7 +77,7 @@ export function readInstalledVersion(dir: string): string | undefined {
6277
/** Check if chrome-headless-shell is installed in the given directory. */
6378
export function isInstalled(dir: string): boolean {
6479
return existsSync(join(dir, kVersionFileName)) &&
65-
findCftExecutable(dir, "chrome-headless-shell") !== undefined;
80+
findCftExecutable(dir, chromeHeadlessShellBinaryName()) !== undefined;
6681
}
6782

6883
// -- InstallableTool methods --
@@ -84,8 +99,22 @@ async function installedVersion(): Promise<string | undefined> {
8499
}
85100

86101
async function latestRelease(): Promise<RemotePackageInfo> {
102+
const platformInfo = detectCftPlatform();
103+
104+
if (isPlaywrightCdnPlatform(platformInfo)) {
105+
// arm64 Linux: use Playwright CDN
106+
const entry = await fetchPlaywrightBrowsersJson();
107+
const url = playwrightCdnDownloadUrl(entry.revision);
108+
return {
109+
url,
110+
version: entry.browserVersion,
111+
assets: [{ name: "chrome-headless-shell", url }],
112+
};
113+
}
114+
115+
// All other platforms: use CfT API
87116
const release = await fetchLatestCftRelease();
88-
const { platform } = detectCftPlatform();
117+
const { platform } = platformInfo;
89118

90119
const downloads = release.downloads["chrome-headless-shell"];
91120
if (!downloads) {
@@ -110,13 +139,15 @@ async function preparePackage(ctx: InstallContext): Promise<PackageInfo> {
110139
const release = await latestRelease();
111140
const workingDir = Deno.makeTempDirSync({ prefix: "quarto-chrome-hs-" });
112141

142+
const binaryName = chromeHeadlessShellBinaryName();
143+
113144
try {
114145
await downloadAndExtractCft(
115146
"Chrome Headless Shell",
116147
release.url,
117148
workingDir,
118149
ctx,
119-
"chrome-headless-shell",
150+
binaryName,
120151
);
121152
} catch (e) {
122153
safeRemoveSync(workingDir, { recursive: true });

0 commit comments

Comments
 (0)