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
7 changes: 5 additions & 2 deletions .github/workflows/test-smokes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,11 @@ jobs:
id: cache-typst
uses: ./.github/actions/cache-typst

- name: Install Chrome
uses: browser-actions/setup-chrome@latest
- name: Install Chrome Headless Shell
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
quarto install chrome-headless-shell --no-prompt

- name: Setup Julia
uses: julia-actions/setup-julia@v2
Expand Down
3 changes: 2 additions & 1 deletion news/changelog-1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ All changes included in 1.9:

- (): New `quarto call build-ts-extension` command builds a TypeScript extension, such as an engine extension, and places the artifacts in the `_extensions` directory. See the [engine extension pre-release documentation](https://prerelease.quarto.org/docs/extensions/engine.html) for details.

### `install verapdf`
### `install`

- ([#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.

### `preview`

Expand Down
132 changes: 104 additions & 28 deletions src/command/check/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { pandocBinaryPath } from "../../core/resources.ts";
import { lines } from "../../core/text.ts";
import { satisfies } from "semver/mod.ts";
import { dartCommand } from "../../core/dart-sass.ts";
import { allTools } from "../../tools/tools.ts";
import { allTools, installableTool } from "../../tools/tools.ts";
import { texLiveContext, tlVersion } from "../render/latexmk/texlive.ts";
import { which } from "../../core/path.ts";
import { dirname } from "../../deno_ral/path.ts";
Expand All @@ -32,6 +32,12 @@ import { quartoCacheDir } from "../../core/appdirs.ts";
import { isWindows } from "../../deno_ral/platform.ts";
import { makeStringEnumTypeEnforcer } from "../../typing/dynamic.ts";
import { findChrome } from "../../core/puppeteer.ts";
import { safeExistsSync } from "../../core/path.ts";
import {
chromeHeadlessShellExecutablePath,
chromeHeadlessShellInstallDir,
readInstalledVersion,
} from "../../tools/impl/chrome-headless-shell.ts";
import { executionEngines } from "../../execute/engine.ts";

export function getTargets(): readonly string[] {
Expand Down Expand Up @@ -436,35 +442,28 @@ async function checkInstall(conf: CheckConfiguration) {
conf.jsonResult.chrome = chromeJson;
}
const chromeCb = async () => {
const chromeDetected = await findChrome();
const chromiumQuarto = tools.installed.find((tool) =>
tool.name === "chromium"
);
if (chromeDetected.path !== undefined) {
chromeHeadlessOutput.push(`${kIndent}Using: Chrome found on system`);
chromeHeadlessOutput.push(
`${kIndent}Path: ${chromeDetected.path}`,
);
if (chromeDetected.source) {
chromeHeadlessOutput.push(`${kIndent}Source: ${chromeDetected.source}`);
const check = await detectChromeForCheck();

if (check.warning) {
chromeHeadlessOutput.push(`${kIndent}NOTE: ${check.warning}`);
chromeJson["warning"] = check.warning;
}

if (check.detected) {
const { label, path, source, displaySource, version } = check.detected;
chromeHeadlessOutput.push(`${kIndent}Using: ${label}`);
if (path) {
chromeHeadlessOutput.push(`${kIndent}Path: ${path}`);
chromeJson["path"] = path;
}
chromeJson["path"] = chromeDetected.path;
chromeJson["source"] = chromeDetected.source;
} else if (chromiumQuarto !== undefined) {
chromeJson["source"] = "quarto";
chromeHeadlessOutput.push(
`${kIndent}Using: Chromium installed by Quarto`,
);
if (chromiumQuarto?.binDir) {
chromeHeadlessOutput.push(
`${kIndent}Path: ${chromiumQuarto?.binDir}`,
);
chromeJson["path"] = chromiumQuarto?.binDir;
chromeJson["source"] = source;
if (displaySource) {
chromeHeadlessOutput.push(`${kIndent}Source: ${displaySource}`);
}
if (version) {
chromeHeadlessOutput.push(`${kIndent}Version: ${version}`);
chromeJson["version"] = version;
}
chromeHeadlessOutput.push(
`${kIndent}Version: ${chromiumQuarto.installedVersion}`,
);
chromeJson["version"] = chromiumQuarto.installedVersion;
} else {
chromeHeadlessOutput.push(`${kIndent}Chrome: (not detected)`);
chromeJson["installed"] = false;
Expand Down Expand Up @@ -524,3 +523,80 @@ title: "Title"
}, markdownRenderCb);
}
}

interface ChromeDetectionResult {
label: string;
path?: string;
source: string;
version?: string;
displaySource?: string;
}

interface ChromeCheckInfo {
warning?: string;
detected?: ChromeDetectionResult;
}

async function detectChromeForCheck(): Promise<ChromeCheckInfo> {
const result: ChromeCheckInfo = {};

// 1. QUARTO_CHROMIUM environment variable
const envPath = Deno.env.get("QUARTO_CHROMIUM");
if (envPath) {
if (safeExistsSync(envPath)) {
result.detected = {
label: "Chrome from QUARTO_CHROMIUM",
path: envPath,
source: "QUARTO_CHROMIUM",
};
return result;
}
result.warning =
`QUARTO_CHROMIUM is set to ${envPath} but the path does not exist.`;
}

// 2. Chrome headless shell installed by Quarto
const chromeHsPath = chromeHeadlessShellExecutablePath();
if (chromeHsPath !== undefined) {
const version = readInstalledVersion(chromeHeadlessShellInstallDir());
result.detected = {
label: "Chrome Headless Shell installed by Quarto",
path: chromeHsPath,
source: "quarto-chrome-headless-shell",
version,
};
return result;
}

// 3. System Chrome
const chromeDetected = await findChrome();
if (chromeDetected.path !== undefined) {
result.detected = {
label: "Chrome found on system",
path: chromeDetected.path,
source: chromeDetected.source ?? "system",
displaySource: chromeDetected.source,
};
return result;
}

// 4. Legacy chromium installed by Quarto
const chromiumTool = installableTool("chromium");
if (chromiumTool && await chromiumTool.installed()) {
let path: string | undefined;
if (chromiumTool.binDir) {
path = await chromiumTool.binDir();
}
const version = await chromiumTool.installedVersion();
result.detected = {
label: "Chromium installed by Quarto",
path,
source: "quarto",
version,
};
return result;
}

// 5. Not found
return result;
}
46 changes: 30 additions & 16 deletions src/core/puppeteer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UnreachableError } from "./lib/error.ts";
import { quartoDataDir } from "./appdirs.ts";
import { isMac, isWindows } from "../deno_ral/platform.ts";
import puppeteer from "puppeteer";
import { chromeHeadlessShellExecutablePath } from "../tools/impl/chrome-headless-shell.ts";

// deno-lint-ignore no-explicit-any
// let puppeteerImport: any = undefined;
Expand Down Expand Up @@ -212,20 +213,8 @@ interface ChromeInfo {
export async function findChrome(): Promise<ChromeInfo> {
let path;
let source;
// First check env var and use this path if specified
const envPath = Deno.env.get("QUARTO_CHROMIUM");
if (envPath) {
debug("[CHROMIUM] Using path specified in QUARTO_CHROMIUM");
if (safeExistsSync(envPath)) {
debug(`[CHROMIUM] Found at ${envPath}, and will be used.`);
return { path: envPath, source: "QUARTO_CHROMIUM" };
} else {
debug(
`[CHROMIUM] Not found at ${envPath}. Check your environment variable valye. Searching now for another binary.`,
);
}
}
// Otherwise, try to find the path based on OS.
// Find Chrome/Edge from OS-specific known locations.
// QUARTO_CHROMIUM env var is handled by callers before calling this function.
if (isMac) {
const programs = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
Expand Down Expand Up @@ -291,10 +280,35 @@ export async function getBrowserExecutablePath() {

let executablePath: string | undefined = undefined;

// Priority 1: QUARTO_CHROMIUM env var
const envPath = Deno.env.get("QUARTO_CHROMIUM");
if (envPath) {
if (safeExistsSync(envPath)) {
debug(`[CHROMIUM] Using QUARTO_CHROMIUM: ${envPath}`);
executablePath = envPath;
} else {
debug(`[CHROMIUM] QUARTO_CHROMIUM is set to ${envPath} but the path does not exist. Searching for another browser.`);
}
}

// Priority 2: Quarto-installed chrome-headless-shell
if (executablePath === undefined) {
executablePath = (await findChrome()).path;
executablePath = chromeHeadlessShellExecutablePath();
if (executablePath) {
debug(`[CHROMIUM] Using chrome-headless-shell: ${executablePath}`);
}
}

// Priority 3: System Chrome/Edge
if (executablePath === undefined) {
const chromeInfo = await findChrome();
if (chromeInfo.path) {
debug(`[CHROMIUM] Using system Chrome from ${chromeInfo.source}: ${chromeInfo.path}`);
executablePath = chromeInfo.path;
}
}

// Priority 4: Legacy puppeteer-managed Chromium revisions
if (executablePath === undefined && availableRevisions.length > 0) {
// get the latest available revision
availableRevisions.sort((a: string, b: string) => Number(b) - Number(a));
Expand All @@ -306,7 +320,7 @@ export async function getBrowserExecutablePath() {
if (executablePath === undefined) {
error("Chrome not found");
info(
"\nNo Chrome or Chromium installation was detected.\n\nPlease run 'quarto install chromium' to install Chromium.\n",
"\nNo Chrome or Chromium installation was detected.\n\nPlease run 'quarto install chrome-headless-shell' to install a headless browser.\n",
);
throw new Error();
}
Expand Down
16 changes: 16 additions & 0 deletions src/deno_ral/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { resolve, SEP as SEPARATOR } from "./path.ts";
import { copySync } from "fs/copy";
import { existsSync } from "fs/exists";
import { originalRealPathSync } from "./original-real-path.ts";
import { debug } from "./log.ts";

export { ensureDir, ensureDirSync } from "fs/ensure-dir";
export { existsSync } from "fs/exists";
Expand Down Expand Up @@ -175,3 +176,18 @@ export function safeModeFromFile(path: string): number | undefined {
}
}
}

/**
* Set file mode in a platform-safe way. No-op on Windows (where chmod
* is not supported). Swallows errors on other platforms since permission
* changes are often non-fatal (e.g., on filesystems that don't support it).
*/
export function safeChmodSync(path: string, mode: number): void {
if (Deno.build.os !== "windows") {
try {
Deno.chmodSync(path, mode);
} catch (e) {
debug(`safeChmodSync: failed to chmod ${path}: ${e}`);
}
}
}
2 changes: 2 additions & 0 deletions src/deno_ral/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const isMac = Deno.build.os === "darwin";
export const isLinux = Deno.build.os === "linux";

export const os = Deno.build.os;

export const arch = Deno.build.arch;
Loading
Loading