Skip to content
Merged
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
6 changes: 5 additions & 1 deletion checksums.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
}
},
"quarto": {
"pinned_version": "1.8.24",
"pinned_version": "1.10.18",
"archive_format": "tar.gz / deb / pkg / msi",
"archive_url_template": "https://github.com/quarto-dev/quarto-cli/releases/download/v{version}/quarto-{version}-linux-{arch}.tar.gz",
"checksum_asset": "quarto-{version}-checksums.txt",
Expand All @@ -131,6 +131,10 @@
"1.8.24": {
"amd64": "6b83c1c9b6f2ce6454798b42260bd2ee184551d74debe817b8aaf28b09ac22d0",
"arm64": "89a97a65a242a5b9b010a9f9978928c1d8e4ac02a558c9cd91a110c3f2611fdd"
},
"1.10.18": {
"amd64": "afad071b5bd22c02f2d300695743189d3650e0537a53073e654b630cff2b0c73",
"arm64": "f6a07df68e25330b5df34f65d3df66bca605acce3b830c593a58e91884d4cf6c"
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions features/quarto-chromium/feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"email": "emmanuel.bruno@univ-tln.fr"
},
"postInstallCheck": {
"command": "sh -c 'command -v chromium >/dev/null 2>&1 || command -v chromium-browser >/dev/null 2>&1 || find ${HOME}/.local/share/quarto/chromium ${HOME}/.quarto ${HOME}/.local/share/quarto-chromium \\( -type f -o -type l \\) \\( -name chrome -o -name chromium -o -name chromium-browser -o -name headless_shell \\) -print -quit 2>/dev/null | grep -q .'",
"description": "Verify Chromium is available for Quarto rendering"
"command": "chromium_bin=\"$(command -v chromium || command -v chromium-browser || command -v chrome-headless-shell || command -v headless_shell || true)\"\nif [ -z \"${chromium_bin}\" ]; then\n echo \"quarto-chromium: CHECK FAIL - no chromium/chromium-browser/chrome-headless-shell on PATH\" >&2\n exit 1\nfi\nerr_file=\"$(mktemp)\"\nbin_base=\"$(basename \"${chromium_bin}\")\"\ncase \"${bin_base}\" in\n chrome-headless-shell|headless_shell)\n out=\"$(timeout 60 \"${chromium_bin}\" --disable-gpu --dump-dom about:blank 2>\"${err_file}\")\" && rc=0 || rc=$?\n ;;\n *)\n out=\"$(timeout 60 \"${chromium_bin}\" --headless --disable-gpu --dump-dom about:blank 2>\"${err_file}\")\" && rc=0 || rc=$?\n ;;\nesac\nif [ \"${rc}\" -ne 0 ]; then\n echo \"quarto-chromium: CHECK FAIL - headless chromium exited ${rc}\" >&2\n cat \"${err_file}\" >&2\n rm -f \"${err_file}\"\n exit 1\nfi\nrm -f \"${err_file}\"\nif [ -z \"${out}\" ]; then\n echo \"quarto-chromium: CHECK FAIL - headless chromium produced no output\" >&2\n exit 1\nfi\necho \"quarto-chromium: check OK (headless chromium rendered via ${chromium_bin})\"",
"description": "Functionally verify headless Chromium starts and renders (CI-safe shim + shared libraries), not just that the binary exists"
},
"documentationURL": "./features/quarto-chromium/README.md"
}
112 changes: 107 additions & 5 deletions features/quarto-chromium/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,38 @@ HOME_DIR="/home/${NB_USER}"

echo "quarto-chromium: installing Chromium runtime"

# Shared libraries required by the Chromium binary that 'quarto install
# chromium' downloads (verified via ldd against the bundled build:
# libnss3, libnspr4, libatk*, libatk-bridge*, libcups, libdrm,
# libxkbcommon, libXcomposite, libXdamage, libXfixes, libXrandr, libgbm,
# libpango, libcairo, libgtk-3, libasound, libatspi, libxshmfence).
# Without these the binary dies at startup with
# "error while loading shared libraries: libnss3.so", which Quarto's
# server-side render reports only as a bare "ERROR: AssertionError:".
# (noble 24.04 t64 package names; verified with apt-get install --dry-run.)
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y --no-install-recommends \
libnss3 \
libnspr4 \
libatk1.0-0t64 \
libatk-bridge2.0-0t64 \
libcups2t64 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libpango-1.0-0 \
libcairo2 \
libasound2t64 \
libatspi2.0-0t64 \
libgtk-3-0t64 \
libxshmfence1
rm -rf /var/lib/apt/lists/*

# Use the available Quarto binary (search PATH or common /opt/quarto/* location)
TMP_SCRIPT="/tmp/quarto-chromium-install-${NB_USER}.sh"
cat > "${TMP_SCRIPT}" <<'BASH'
Expand All @@ -33,7 +65,9 @@ if [ -z "${QUARTO_BIN}" ]; then
done
fi
if [ -n "${QUARTO_BIN}" ] && [ -x "${QUARTO_BIN}" ]; then
CI=true "${QUARTO_BIN}" install --no-prompt --log "${HOME}/.quarto-chromium-install.log" --log-level debug chromium \
CI=true "${QUARTO_BIN}" install --no-prompt --log "${HOME}/.quarto-chromium-install.log" --log-level debug chrome-headless-shell \
|| CI=true "${QUARTO_BIN}" install chrome-headless-shell --no-prompt \
|| CI=true "${QUARTO_BIN}" install --no-prompt --log "${HOME}/.quarto-chromium-install.log" --log-level debug chromium \
|| CI=true "${QUARTO_BIN}" install chromium --no-prompt \
|| true
else
Expand All @@ -51,7 +85,10 @@ find_chromium_bin() {
"${HOME_DIR}/.quarto/bin/chromium" \
"${HOME_DIR}/.quarto/bin/chromium-browser" \
"${HOME_DIR}/.quarto/bin/chrome" \
"${HOME_DIR}/.quarto/bin/chrome-headless-shell" \
"${HOME_DIR}/.quarto/bin/headless_shell" \
"${HOME_DIR}/.local/share/quarto/chromium/linux-"*/chrome-linux/chrome \
"${HOME_DIR}/.local/share/quarto/chrome-headless-shell/linux-"*/chrome-headless-shell \
"${HOME_DIR}/.local/share/quarto-chromium/chromium" \
"${HOME_DIR}/.local/share/quarto-chromium/chromium-browser" \
"${HOME_DIR}/.local/share/quarto-chromium/chrome" \
Expand Down Expand Up @@ -84,7 +121,7 @@ find_chromium_bin() {
do
[ -d "${search_dir}" ] || continue
found=$(
find "${search_dir}" \( -type f -o -type l \) \( -name chrome -o -name chromium -o -name chromium-browser -o -name headless_shell \) 2>/dev/null |
find "${search_dir}" \( -type f -o -type l \) \( -name chrome -o -name chrome-headless-shell -o -name chromium -o -name chromium-browser -o -name headless_shell \) 2>/dev/null |
while IFS= read -r candidate; do
if [ -x "${candidate}" ]; then
printf '%s\n' "${candidate}"
Expand All @@ -101,6 +138,63 @@ find_chromium_bin() {
return 1
}

# Wrap the real Chromium binary IN PLACE with a CI-safe shim.
#
# Why: courseware render CI runs the container as root in a restricted
# docker context. Quarto's own browser spawn (criClient in quarto.js)
# passes --no-sandbox --disable-gpu but NOT --disable-dev-shm-usage, and
# the default 64 MB /dev/shm is too small for headless Chrome, so the
# Chromium child dies at startup and Quarto's error path reports a bare
# "ERROR: AssertionError:" instead of the real cause. The shim bakes in
# the CI-safe flags for every invocation.
#
# In-place wrapping keeps all of Quarto's discovery routes on the shim:
# the /usr/local/bin + ~/.local/bin symlinks created below, and Quarto's
# own install dir (~/.local/share/quarto/chromium/...), all resolve to
# this same file.
wrap_chromium_shim() {
local bin="$1"
local real bin_name resolved
resolved="$(readlink -f "${bin}" 2>/dev/null || true)"
if [ -n "${resolved}" ]; then
bin="${resolved}"
fi
real="${bin}.real"
bin_name="$(basename "${bin}")"

# Idempotent: a shim is already in place.
if [ -e "${bin}" ] && head -n 5 "${bin}" 2>/dev/null | grep -q 'solen-chromium-shim'; then
echo "quarto-chromium: shim already in place at ${bin}"
return 0
fi

if [ -e "${bin}" ]; then
# Fresh binary at the public path (first run, or 'quarto install
# chromium' replaced the shim on a re-run): move it aside, overwriting
# any stale .real.
mv -f "${bin}" "${real}"
elif [ ! -e "${real}" ]; then
echo "quarto-chromium: ERROR: no Chromium binary at ${bin} to wrap" >&2
return 1
fi
# else: public path gone but .real remains — fall through and re-write
# the shim pointing at it.

cat > "${bin}" <<SHIM
#!/bin/sh
# solen-chromium-shim: CI-safe flags for headless Chromium in restricted
# containers (baked by features/quarto-chromium; original: ${bin_name}.real)
# Resolve our canonical path first: this shim is commonly invoked through
# the /usr/local/bin + ~/.local/bin symlinks, where \$0 would point at the
# symlink and dirname(\$0) would miss ${bin_name}.real next to the original.
self="\$(readlink -f "\$0" 2>/dev/null || echo "\$0")"
exec "\$(dirname "\$self")/${bin_name}.real" --no-sandbox --disable-gpu --disable-dev-shm-usage "\$@"
SHIM
chmod 0755 "${bin}"
chown "${NB_UID}":"${NB_GID}" "${bin}" "${real}" || true
echo "quarto-chromium: installed CI-safe shim at ${bin} (original: ${real})"
}

CHROMIUM_BIN="$(find_chromium_bin || true)"
if [ -n "${CHROMIUM_BIN}" ]; then
echo "quarto-chromium: found Chromium binary at ${CHROMIUM_BIN}"
Expand Down Expand Up @@ -146,11 +240,18 @@ if [ -n "${CHROMIUM_BIN}" ]; then
mkdir -p "${HOME_DIR}/.local/bin" || true
ln -sf "${CHROMIUM_BIN}" /usr/local/bin/chromium || true
ln -sf "${CHROMIUM_BIN}" /usr/local/bin/chromium-browser || true
ln -sf "${CHROMIUM_BIN}" /usr/local/bin/chrome-headless-shell || true
ln -sf "${CHROMIUM_BIN}" /usr/local/bin/headless_shell || true
ln -sf "${CHROMIUM_BIN}" "${HOME_DIR}/.local/bin/chromium" || true
ln -sf "${CHROMIUM_BIN}" "${HOME_DIR}/.local/bin/chromium-browser" || true
chown "${NB_UID}":"${NB_GID}" "${HOME_DIR}/.local/bin/chromium" "${HOME_DIR}/.local/bin/chromium-browser" || true
ln -sf "${CHROMIUM_BIN}" "${HOME_DIR}/.local/bin/chrome-headless-shell" || true
ln -sf "${CHROMIUM_BIN}" "${HOME_DIR}/.local/bin/headless_shell" || true
chown "${NB_UID}":"${NB_GID}" "${HOME_DIR}/.local/bin/chromium" "${HOME_DIR}/.local/bin/chromium-browser" "${HOME_DIR}/.local/bin/chrome-headless-shell" "${HOME_DIR}/.local/bin/headless_shell" || true

# Bake the CI-safe flags into the binary itself (idempotent).
wrap_chromium_shim "${CHROMIUM_BIN}"
else
echo "quarto-chromium: WARNING: Chromium binary not found after installation" >&2
echo "quarto-chromium: ERROR: Chromium/Chrome Headless Shell binary not found after installation" >&2
for search_dir in \
"${HOME_DIR}/.local/share/quarto/chromium" \
"${HOME_DIR}/.local/share/quarto" \
Expand All @@ -162,12 +263,13 @@ else
/var/cache
do
[ -d "${search_dir}" ] || continue
find "${search_dir}" -maxdepth 6 \( -type f -o -type l \) \( -name chrome -o -name chromium -o -name chromium-browser -o -name headless_shell \) 2>/dev/null | head -n 20 || true
find "${search_dir}" -maxdepth 6 \( -type f -o -type l \) \( -name chrome -o -name chrome-headless-shell -o -name chromium -o -name chromium-browser -o -name headless_shell \) 2>/dev/null | head -n 20 || true
done
if [ -f "${HOME_DIR}/.quarto-chromium-install.log" ]; then
echo "quarto-chromium: Quarto install log tail:" >&2
tail -n 40 "${HOME_DIR}/.quarto-chromium-install.log" >&2 || true
fi
exit 1
fi

echo "quarto-chromium: installation complete"
Expand Down
2 changes: 1 addition & 1 deletion features/quarto-cli/feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"options": {
"QUARTO_VERSION": {
"type": "string",
"default": "1.8.24",
"default": "1.10.18",
"description": "Specific Quarto version to install"
}
},
Expand Down
25 changes: 23 additions & 2 deletions features/quarto-cli/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,32 @@ fi

echo "quarto-cli: found runtime at ${INSTDIR} -> ${QUARTO_EXE}"

# The tarball ships bundled tools (typst, pandoc, deno, ...) under
# <install-dir>/bin/tools/<arch>/ (arch = x86_64 | aarch64). Quarto
# itself locates them relative to its own binary (QUARTO_BIN_PATH), so
# renders work without PATH; expose the dir on PATH so that direct
# `typst` invocations (user notebook cells, scripts) resolve too.
QUARTO_TOOLS_DIR=""
for cand in "$(dirname "$QUARTO_EXE")"/tools/*/typst; do
if [ -x "$cand" ]; then
QUARTO_TOOLS_DIR="$(dirname "$cand")"
break
fi
done
if [ -n "${QUARTO_TOOLS_DIR}" ]; then
echo "quarto-cli: bundled tools dir at ${QUARTO_TOOLS_DIR}"
fi

QUARTO_PATH_ADD="${CONDA_DIR}/bin:$(dirname "$QUARTO_EXE")"
if [ -n "${QUARTO_TOOLS_DIR}" ]; then
QUARTO_PATH_ADD="${QUARTO_PATH_ADD}:${QUARTO_TOOLS_DIR}"
fi

# Create wrapper in /usr/local/bin
mkdir -p /usr/local/bin
cat > /usr/local/bin/quarto <<WRAPPER
#!/bin/sh
export PATH="${CONDA_DIR}/bin:$(dirname "$QUARTO_EXE"):\$PATH"
export PATH="${QUARTO_PATH_ADD}:\$PATH"
export QUARTO_PYTHON="${CONDA_DIR}/bin/python3"
exec "$QUARTO_EXE" "\$@"
WRAPPER
Expand All @@ -163,7 +184,7 @@ chown root:root /usr/local/bin/quarto
# Add to system PATH via profile.d
mkdir -p /etc/profile.d
cat > /etc/profile.d/quarto.sh <<PROFILE
export PATH="${CONDA_DIR}/bin:$(dirname "$QUARTO_EXE"):\$PATH"
export PATH="${QUARTO_PATH_ADD}:\$PATH"
export QUARTO_PYTHON="${CONDA_DIR}/bin/python3"
PROFILE
chmod 644 /etc/profile.d/quarto.sh
Expand Down
2 changes: 1 addition & 1 deletion scripts/utils/update-checksums.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail

# Script to download binaries for pinned versions and compute SHA256 checksums
# It updates `checksums.json` (repo root) with entries like:
# { "quarto": { "1.8.24": { "amd64": "<sha256>" } } }
# { "quarto": { "1.10.18": { "amd64": "<sha256>" } } }

REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
# Single source of truth: repo-root checksums.json and versions.json
Expand Down
2 changes: 1 addition & 1 deletion versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"buildx": "v0.30.1",
"compose": "v5.0.1",
"docker": "27-cli",
"quarto": "v1.8.26",
"quarto": "v1.10.18",
"node": "22.12.0",
"docker-ce": "27.5.1",
"docker-compose": "v5.0.1",
Expand Down
2 changes: 1 addition & 1 deletion versions/versions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ tools:
- type: github
repo: "docker/cli"
quarto:
version: "v1.8.26"
version: "v1.10.18"
sources:
- type: github
repo: "quarto-dev/quarto-cli"
Expand Down
Loading