ci(host): build web image from op-host-web-server + headless-boundary guard (Phase 6, Tasks 6.2-6.4)

Dockerfile.web-rust builds -p op-host-web-server (was op-host-desktop) and drops ALL GL/X11
build + runtime apt deps (libegl/libgles/libgbm/libxkbcommon/libwayland/libxcb) — the raster
server links none; only freetype/fontconfig + CJK fonts remain for skia text. COPY + CMD repointed
to /app/op-host-web-server. New tools/check-web-server-headless.sh fails CI if op-host-web-server's
isolated dep graph pulls winit/glutin/casement/muda/accesskit-adapters or skia-safe with gl (bare
accesskit core allowed per Codex Issue 1); wired into rust-check.yml + its paths filter. Stale
web_static.rs path comment fixed (op-host-desktop -> op-web-daemon). Desktop-app build job untouched.
This commit is contained in:
Kayshen-X 2026-06-19 22:14:56 +08:00
parent f198cdf76a
commit b7951fc598
4 changed files with 60 additions and 16 deletions

View file

@ -10,6 +10,7 @@ on:
- 'rustfmt.toml'
- 'deny.toml'
- 'tools/check-jian-boundaries.sh'
- 'tools/check-web-server-headless.sh'
- 'tools/check-widget-boundary.sh'
- 'tools/dump-diagnostics-golden.ts'
- 'packages/pen-ai-skills/**'
@ -27,6 +28,7 @@ on:
- 'rust-toolchain.toml'
- 'rustfmt.toml'
- 'tools/check-jian-boundaries.sh'
- 'tools/check-web-server-headless.sh'
- 'tools/check-widget-boundary.sh'
- 'tools/dump-diagnostics-golden.ts'
- 'packages/pen-ai-skills/**'
@ -92,6 +94,10 @@ jobs:
if: runner.os == 'Linux'
run: bash tools/check-widget-boundary.sh
- name: Verify op-host-web-server headless boundary (no winit/glutin/GL)
if: runner.os == 'Linux'
run: bash tools/check-web-server-headless.sh
- name: Install Bun (retry on transient infra flake)
if: runner.os == 'Linux'
run: |

View file

@ -113,7 +113,7 @@ jobs:
# wasm-bindgen `pkg/` output PLUS the vendored CanvasKit artifact under a
# `canvaskit/` subdir, all inside a single `web-bundle/` directory. This
# mirrors the `<exe_dir>/web-bundle` + `web-bundle/canvaskit` resolution
# order in `crates/op-host-desktop/src/web_static.rs`.
# order in `crates/op-web-daemon/src/web_static.rs`.
- name: Assemble deployable web-bundle/
run: |
set -euo pipefail

View file

@ -47,18 +47,16 @@ ARG WEB_BUNDLE_SOURCE=build
ARG SERVE_PORT=3100
# binaryen -> wasm-opt -Oz ; nodejs -> the 0-env-import assert in the gate
# script ; the rest are op-host-desktop's Linux link-time deps (winit/skia GL
# stack) so the daemon binary links. fonts-noto-cjk gives the daemon real CJK
# glyphs at runtime (matches the rust-check.yml Linux prereqs).
# script. op-host-web-server is the headless raster daemon (links op-web-daemon
# only — no winit/glutin/skia-GL under Approach Y), so NONE of the GL/X11
# link-time libs the desktop binary needed are required here; only
# freetype/fontconfig (+ CJK fonts) for skia's raster text shaping at export.
RUN apt-get update && apt-get install -y --no-install-recommends \
binaryen \
nodejs \
gzip \
ca-certificates \
pkg-config \
libxkbcommon-dev libxkbcommon-x11-dev \
libwayland-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
libegl1-mesa-dev libgles2-mesa-dev libgbm-dev \
libfreetype-dev libfontconfig1-dev fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
@ -69,9 +67,10 @@ WORKDIR /src
# the CI checkout uses `submodules: recursive`.
COPY . .
# Build the desktop daemon binary (the `--serve-web` host). Always built; this
# is the runtime binary regardless of how the web bundle is produced.
RUN cargo build -p op-host-desktop --release
# Build the headless web/MCP server binary (the `--serve-web` host) — links
# op-web-daemon only, no winit/skia-GL. Always built; this is the runtime
# binary regardless of how the web bundle is produced.
RUN cargo build -p op-host-web-server --release
# Build the canvaskit wasm bundle in-image, UNLESS WEB_BUNDLE_SOURCE=copy.
# Mirrors `tools/check-wasm-bundle.sh` exactly (cargo build --features canvaskit
@ -111,10 +110,6 @@ ENV OPENPENCIL_SERVE_PORT=${SERVE_PORT}
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libegl1 libgles2 libgbm1 \
libxkbcommon0 libxkbcommon-x11-0 \
libwayland-client0 libwayland-egl1 \
libxcb-render0 libxcb-shape0 libxcb-xfixes0 \
libfreetype6 libfontconfig1 fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
@ -124,7 +119,7 @@ WORKDIR /app
# bundle as `<exe_dir>/web-bundle` (and CanvasKit as `<exe_dir>/web-bundle/
# canvaskit`). Keep the binary + the bundle dir as siblings under /app so that
# `<exe_dir> == /app`.
COPY --from=builder /src/target/release/openpencil-desktop /app/openpencil-desktop
COPY --from=builder /src/target/release/op-host-web-server /app/op-host-web-server
COPY --from=builder /out/web-bundle /app/web-bundle
EXPOSE ${SERVE_PORT}
@ -134,4 +129,4 @@ EXPOSE ${SERVE_PORT}
# reverse proxy for anything beyond a trusted network. The port is taken from
# the build-time SERVE_PORT (baked into OPENPENCIL_SERVE_PORT); `sh -c` lets the
# env var expand at container start.
CMD ["sh", "-c", "exec /app/openpencil-desktop --serve-web \"${OPENPENCIL_SERVE_PORT}\" --host 0.0.0.0"]
CMD ["sh", "-c", "exec /app/op-host-web-server --serve-web \"${OPENPENCIL_SERVE_PORT}\" --host 0.0.0.0"]

View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Headless-boundary guard for the web / MCP server binary.
#
# The whole point of `op-host-web-server` (Approach Y) is that it links the
# extracted `op-web-daemon` and NOTHING from the desktop GUI stack: no winit /
# glutin / casement (windowing), no muda (native menus), no accesskit platform
# ADAPTERS (a11y bridges), and skia-safe WITHOUT the `gl` feature (raster only).
# This guard fails the build if any of those leak into the isolated dep graph,
# so the web image stays GUI-free.
#
# Codex Issue 1: the bare `accesskit` CORE crate is allowed — `op-editor-ui`
# pulls it unconditionally for the platform-free `Node` / `TreeUpdate` types
# (no GUI runtime). We grep the platform ADAPTER crates only.
set -euo pipefail
cd "$(dirname "$0")/.."
fail=0
# 1. No windowing / menu / a11y-adapter crates in the feature-resolved tree.
gui=$(cargo tree -p op-host-web-server -e features 2>/dev/null \
| grep -E 'winit|glutin|casement|muda|accesskit_(macos|unix|windows|winit)' || true)
if [ -n "${gui}" ]; then
printf 'FAIL: op-host-web-server links a desktop GUI crate:\n%s\n\n' "${gui}" >&2
fail=1
fi
# 2. skia-safe must be raster — NO `gl` feature. Under Approach Y, gl arrives
# only via the desktop-only `gl-host` edge; its presence in this isolated
# graph means a consumer leaked gl-host into the headless server.
skia_feats=$(cargo tree -p op-host-web-server -f '{p} {f}' 2>/dev/null \
| grep 'skia-safe v' | sed 's/.*skia-safe v[^ ]*//' | head -1)
if printf '%s' "${skia_feats}" | grep -qE '(^| |,)gl(,| |$)'; then
printf 'FAIL: op-host-web-server skia-safe carries the `gl` feature (must be raster):\n features:%s\n\n' "${skia_feats}" >&2
fail=1
fi
if [ "${fail}" -ne 0 ]; then
printf 'FAIL: op-host-web-server headless boundary check\n' >&2
exit 1
fi
echo "PASS: op-host-web-server headless boundary check (no winit/glutin/casement/muda/accesskit-adapter; skia raster, no gl)"