fix(quarto-chromium): CI-safe headless Chromium for typst/mermaid renders - #21
Conversation
… shim and functional post-install check
There was a problem hiding this comment.
🟡 Changes recommended
The Chromium shim currently derives the .real path from dirname "$0", which breaks when Chromium is invoked via the symlinks created by this feature (common PATH resolution path).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens the quarto-full image’s headless Chromium setup so Quarto typst/mermaid renders don’t crash in CI, by ensuring required shared libraries are present, adding a CI-safe Chromium invocation path, and making Quarto’s bundled tools discoverable on PATH for direct use.
Changes:
- Install the verified Ubuntu runtime libraries needed for Quarto’s downloaded Chromium to start successfully.
- Add an in-place Chromium shim (and a functional post-install smoke test) so broken headless Chromium fails the image build rather than failing downstream render CI.
- Extend Quarto CLI wrappers to add Quarto’s bundled tools directory (e.g.,
typst) toPATHfor direct user invocations.
File summaries
| File | Description |
|---|---|
| features/quarto-cli/install.sh | Adds Quarto bundled tools directory discovery and appends it to PATH in system and wrapper scripts. |
| features/quarto-chromium/install.sh | Installs Chromium runtime libs and introduces an in-place shim wrapper for CI-safe Chromium flags. |
| features/quarto-chromium/feature.json | Replaces existence checks with a functional headless Chromium smoke test. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| #!/bin/sh | ||
| # solen-chromium-shim: CI-safe flags for headless Chromium in restricted | ||
| # containers (baked by features/quarto-chromium; original: ${bin_name}.real) | ||
| exec "\$(dirname "\$0")/${bin_name}.real" --no-sandbox --disable-gpu --disable-dev-shm-usage "\$@" | ||
| SHIM |
There was a problem hiding this comment.
Confirmed — the readlink guard I added only ran at wrap time (resolving where the binary to wrap lives); the generated wrapper itself still used dirname "$0" at runtime, so invocation via the /usr/local/bin/chromium or ~/.local/bin/chromium symlinks looked for <bin>.real in the symlink directory and died with exit 127. Fixed in b0bb6f7: the wrapper now resolves its own canonical path first (self="$(readlink -f "$0" 2>/dev/null || echo "$0")") before deriving the .real target. Verified in-container with a fake binary reproducing the exact symlink layout: old logic failed via symlink (exec: /usr/local/bin/chrome.real: not found, rc=127); new logic passes via both symlinks and direct invocation, with re-wrap idempotency and original-restoration still passing.
The generated wrapper derived the .real target from dirname("$0"),
which breaks when the shim is invoked via the /usr/local/bin or
~/.local/bin symlinks (the common PATH resolution route): $0 points at
the symlink, so the wrapper looked for <bin>.real in the symlink
directory and failed with exit 127. The wrapper now resolves its own
canonical path via readlink -f first.
Verified in-container with a fake binary: old logic failed via symlink
(exec: /usr/local/bin/chrome.real: not found, rc=127); new logic passes
via both symlinks and direct invocation, and re-wrap idempotency /
original-restoration scenarios still pass.
Why
Render CI on
ghcr.io/ebpro/jupyter-base:quarto-full-feat-post-install-checkscrashes on the mermaid step of the notes profile (typst). Evidence:ebpro/notebook-cirun34048342510, job101527336817(step "Render with Quarto (Docker)", exit 1), and run33537153211(identical, on thequarto-full-develop-b511eaetag):ERROR: Chrome not found) — this one is silent.quarto-full-feat-post-install-checksis a GHCR-only image tag, built from branchfeat/post-install-checks(HEAD45e4ad6). The published image also carries a stale build-cache layer (quarto 1.8.24 vs the versions SoT 1.8.26) — a post-merge rebuild refreshes that too.Root cause — CONFIRMED (verified locally, emulated amd64, indicative)
21 shared libraries are missing from the image (verified via
lddon the bundled Chromium build): libnss3, libnssutil3, libsmime3, libnspr4, libatk-1.0, libatk-bridge-2.0, libcups, libdrm, libxkbcommon, libXcomposite, libXdamage, libXfixes, libXrandr, libgbm, libpango-1.0, libcairo, libgtk-3, libgdk-3, libasound, libatspi, libxshmfence.Chromium therefore dies at startup with exit 127 (
error while loading shared libraries: libnss3.so: cannot open shared object file) — before any flag matters. Quarto'scriClienterror path (anassert(readerResult.done)after reading a single stderr chunk) then masks the real failure as the bareERROR: AssertionError:, and the cleanup handler reportsChild process has already terminated.Refinements from the code + local runs:
--no-sandbox --disable-gputo the browser spawn, so the sandbox was not the gap./dev/shmis 64 MB in CI (faithful locally); A/B runs with/without--disable-dev-shm-usagefailed identically, so the shim's--disable-dev-shm-usageis defense-in-depth, not the primary fix.quarto renderof a minimal typst+mermaid doc on the unmodified image;lddis clean after installing the 18 verified noble/t64 packages (package set installed live in-container, not guessed). QEMU user-mode emulation cannot execute this Chrome build (zygote fatal / SEGV), so a full-render PASS could not be demonstrated locally — compensated by ldd-clean + exact repro + in-container logic tests of the new check.Changes
features/quarto-chromium/install.sh— apt block installing the 18 verified runtime libraries (with the ldd evidence as a comment), plus an idempotent in-place shim of the discovered Chromium binary: original moved to<bin>.real, POSIX sh wrapper at the same path adding--no-sandbox --disable-gpu --disable-dev-shm-usage(marker-guarded,chmod 755, chowned to the notebook user). In-place wrapping is layout-agnostic and covers both of Quarto's discovery routes (the/usr/local/bin+~/.local/binsymlinks and the~/.local/share/quarto/chromiuminstall dir all resolve to this file).features/quarto-chromium/feature.json— the existence-grep post-install check is replaced with a functional headless smoke:timeout 60 chromium --headless --disable-gpu --dump-dom about:blank, asserting exit 0 and non-empty output (real chrome stderr is surfaced on failure). A broken Chromium now fails the build, not courseware CI.features/quarto-cli/install.sh— build-time discovery of the bundled tools dir ($(dirname "$QUARTO_EXE")/tools/*/typst, no hardcoded arch) appended to PATH in both the/usr/local/bin/quartowrapper and/etc/profile.d/quarto.sh. Quarto itself resolves bundled typst viaQUARTO_BIN_PATH; this makes directtypstinvocations from user notebook cells resolve.Validation
ci-validateonly (static: feature validation, profile/devcontainer generation, versions sync, pytest, ruff) —ci-buildis gated todevelop/workflow_dispatch and does not run for this PR.RUN_POSTINSTALL_CHECKS=true) and post-build via therun-postinstall-checksworkflow input (defaultfalse).bash,set -uo pipefail,eval) for success / non-zero-exit / empty-output paths.After merge
ci-publish) — also refreshes the stale 1.8.24 → 1.8.26 layer.ebpro/notebook-cicanary (itsci-render.ymlalready points atquarto-full-feat-post-install-checks).If still failing
--shm-sizeon thedocker runin the render workflow as a workflow-side fallback for the 64 MB/dev/shm.Addition (2026-09-09): Quarto 1.8.24 → 1.10.18. The canary on the PR image got past the Chromium boot failure but exposed a second, internal Quarto SVG→PNG bug (
Couldn't find an svg element in svg string); the upgrade addresses it. New image build to follow.