chore(ci): re-point tooling + CI workflows at the op-host-* crates

Phase 7.3 strangler reorg — update every reference to the renamed /
dissolved crates across the boundary scripts and CI workflows.

tools/:
- check-wasm-bundle.sh: openpencil-shell-web -> op-host-web; the
  wasm-bindgen output filenames follow the op_host_web lib name
- check-jian-boundaries.sh: shell-native -> op-host-native,
  shell-web -> op-host-web
- check-widget-boundary.sh: WEB_SRC + path-exclusion regexes ->
  crates/op-host-web; openpencil_shell_core::widgets ->
  op_editor_ui::widgets (the dissolved shim's real source crate)

.github/workflows/:
- rust-multiplatform.yml: wasm + mobile-check jobs -> op-host-web /
  op-host-native; the mobile shell-core clean check -> op-editor-ui
- rust-release.yml: cargo build -p openpencil-desktop ->
  -p op-host-desktop (the shipped executable name is unchanged)
- wasm-bundle-check.yml: -p openpencil-shell-web -> -p op-host-web

Cargo.toml skia-patch comment updated. Boundary checks pass; the
wasm-bundle gate cleanly skips while EMSDK is unset.
This commit is contained in:
Kayshen-X 2026-05-17 00:01:35 +08:00
parent 4b8e0ce956
commit 08c07604d0
7 changed files with 66 additions and 64 deletions

View file

@ -115,11 +115,11 @@ jobs:
if: matrix.cross == false && matrix.check_only != true && runner.os != 'Linux'
run: cargo test --workspace --target ${{ matrix.target }}
# Desktop binary artifact upload is intentionally not wired here yet.
# The real desktop binary lives in the `openpencil-desktop` crate; a
# The real desktop binary lives in the `op-host-desktop` crate; a
# build + upload step can be added once release packaging is finalized.
wasm-web:
name: wasm32-unknown-unknown / openpencil-shell-web (compile guard)
name: wasm32-unknown-unknown / op-host-web (compile guard)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -140,7 +140,7 @@ jobs:
# (brew emscripten install + EMSDK + .wasm.a symlink hack +
# wasm-bindgen + wasm-opt) lands. Until then this job only proves
# the public surface compiles wasm32-clean.
- run: cargo build -p openpencil-shell-web --target wasm32-unknown-unknown --release --no-default-features --features web
- run: cargo build -p op-host-web --target wasm32-unknown-unknown --release --no-default-features --features web
# Artifact upload deliberately removed: the stub `.wasm` from this
# path would mislead downstream consumers into thinking the shell can
# render. Restore the upload step once the skia-enabled build runs on
@ -179,11 +179,12 @@ jobs:
with:
key: mobile-${{ matrix.target }}
# Step 1a spec §11 mobile invariants verify on iOS / Android cargo check:
# - shell-core wasm32/ios/android-clean (no platform deps).
# - shell-native compiles on mobile targets with EaglProvider /
# - op-editor-ui wasm32/ios/android-clean (no platform deps) — the
# widget facade / editor-UI composition crate.
# - op-host-native compiles on mobile targets with EaglProvider /
# AndroidEglProvider stubs (`unimplemented!("Step 1f")`); desktop GL
# stack (glutin / winit) is target-gated to desktop in Cargo.toml +
# GlutinProvider source is cfg-gated to desktop OS only. Real SDK
# linking and iOS/Android runtime is Step 1f.
- run: cargo check -p openpencil-shell-core --target ${{ matrix.target }}
- run: cargo check -p openpencil-shell-native --target ${{ matrix.target }}
- run: cargo check -p op-editor-ui --target ${{ matrix.target }}
- run: cargo check -p op-host-native --target ${{ matrix.target }}

View file

@ -2,9 +2,9 @@ name: Rust release artifacts
# Triggered on tag push (v*) — builds release binaries across desktop targets
# and assembles a GitHub Release draft. The `build` job builds the real
# `openpencil-desktop` crate (winit + skia-safe runner that drives the Rust
# shell); the deleted `openpencil-app` placeholder crate is gone (Phase 1
# Task 1.2). Full DMG / AppImage / EXE installer packaging lands in Step 1f.
# `op-host-desktop` crate (winit + skia-safe runner that drives the Rust
# editor); its shipped executable keeps the stable `openpencil-desktop`
# name. Full DMG / AppImage / EXE installer packaging lands in Step 1f.
# The wasm web bundle is currently DEFERRED (see comment block below); when
# the CI-side C-hard pipeline (emscripten install + EMSDK + symlink hack +
# wasm-bindgen + wasm-opt) lands it will re-enter the matrix.
@ -76,10 +76,10 @@ jobs:
run: cargo install cross --locked --version 0.2.5
- name: Build (host)
if: matrix.cross != true
run: cargo build -p openpencil-desktop --target ${{ matrix.target }} --release
run: cargo build -p op-host-desktop --target ${{ matrix.target }} --release
- name: Build (cross)
if: matrix.cross == true
run: cross build -p openpencil-desktop --target ${{ matrix.target }} --release
run: cross build -p op-host-desktop --target ${{ matrix.target }} --release
- name: Package archive (unix)
if: matrix.archive == 'tar.gz'
shell: bash

View file

@ -29,7 +29,7 @@ jobs:
- name: Forward — bucket A crates compile on wasm32
run: |
cargo check --target wasm32-unknown-unknown \
-p openpencil-shell-web --no-default-features --features web
-p op-host-web --no-default-features --features web
wasm32-deny:
name: cargo-deny --target wasm32-unknown-unknown check bans

View file

@ -38,7 +38,7 @@ tracing = "0.1"
#
# IMPORTANT: `[patch.crates-io]` is workspace-global, NOT target-scoped.
# Every `skia-safe` / `skia-bindings` consumer in this workspace —
# including `openpencil-shell-native` (macOS / Linux / Windows desktop,
# including `op-host-native` (macOS / Linux / Windows desktop,
# `gl` feature) — now resolves through this fork on every target.
# Cargo does not natively support per-target patches, so this is the
# accepted blast radius.

View file

@ -4,10 +4,11 @@
# Verifies the following Jian crate boundary invariants from outside the
# Rust build system. Run from the repo root.
#
# (The former Invariant 1 — "openpencil-app must not depend directly on
# (The former Invariant 1 — "the app crate must not depend directly on
# any jian-* crate" — was dropped in Phase 1 Task 1.2 along with the
# `openpencil-app` placeholder crate. Step 1f reintroduces a real app
# crate; reinstate an equivalent facade check against it then.)
# old placeholder crate. The Phase 7.3 reorg reintroduced a real
# composition-root crate, `op-app`; reinstate an equivalent facade
# check against it if/when op-app grows beyond a thin re-export.)
#
# Invariant 2 (§11.1, §12.3 — REVISED 2026-05-10): mobile targets
# (`aarch64-linux-android`, `aarch64-apple-ios`) must NOT pull
@ -21,12 +22,12 @@
# Step 1f via the existing `EaglProvider` / `AndroidEglProvider`
# plugin point.
#
# Invariant 3 (§11.1, §1.2): wasm32 builds of `openpencil-shell-web`
# Invariant 3 (§11.1, §1.2): wasm32 builds of `op-host-web`
# must NOT pull `jian-host-desktop` or `jian-skia` (skia-safe build.rs
# fails on wasm32; Jian-core is wasm32-clean per P0.5 and is the only
# Jian crate allowed in the bundle).
#
# Invariant 4 (§1.2): `openpencil-shell-web` must NOT depend on
# Invariant 4 (§1.2): `op-host-web` must NOT depend on
# `jian-host-desktop` at all — even as a non-default optional dep.
#
# Exit codes:
@ -52,7 +53,7 @@ fi
# because it pulls winit / glutin / desktop GLContextProvider impls
# that have no mobile equivalent.
for target in aarch64-linux-android aarch64-apple-ios; do
tree_mobile="$(cargo tree -p openpencil-shell-native \
tree_mobile="$(cargo tree -p op-host-native \
--target "$target" \
--prefix none \
--edges normal,build 2>/dev/null || true)"
@ -68,7 +69,7 @@ done
# ── Invariant 3: wasm32 has no jian-host-desktop / jian-skia. ──────────
# `jian-core` IS allowed (P0.5 wasm32-clean).
tree_wasm="$(cargo tree -p openpencil-shell-web \
tree_wasm="$(cargo tree -p op-host-web \
--target wasm32-unknown-unknown \
--prefix none \
--edges normal,build 2>/dev/null || true)"
@ -76,22 +77,22 @@ forbidden_wasm="$(echo "$tree_wasm" \
| grep -oE '\bjian-(host-desktop|skia)\b' \
| sort -u || true)"
if [ -n "$forbidden_wasm" ]; then
echo "INVARIANT 3 FAILED: wasm32 openpencil-shell-web pulls forbidden Jian crates:" >&2
echo "INVARIANT 3 FAILED: wasm32 op-host-web pulls forbidden Jian crates:" >&2
echo "$forbidden_wasm" >&2
exit 1
fi
# ── Invariant 4: openpencil-shell-web has no jian-host-desktop dep. ───
# ── Invariant 4: op-host-web has no jian-host-desktop dep. ───
# Distinct from invariant 3 (which checks the resolved closure on the
# wasm32 target): this checks the manifest itself across all targets.
# `cargo tree --all-targets` would include dev-deps; we explicitly
# filter `--edges normal,build` for the manifest-level invariant.
shell_web_deps="$(cargo tree -p openpencil-shell-web \
shell_web_deps="$(cargo tree -p op-host-web \
--prefix none \
--edges normal,build 2>/dev/null \
| grep -E '\bjian-host-desktop\b' || true)"
if [ -n "$shell_web_deps" ]; then
echo "INVARIANT 4 FAILED: openpencil-shell-web depends on jian-host-desktop:" >&2
echo "INVARIANT 4 FAILED: op-host-web depends on jian-host-desktop:" >&2
echo "$shell_web_deps" >&2
exit 1
fi

View file

@ -7,9 +7,9 @@
# + wasm-aware clang (build-time only; emscripten runtime is NOT
# linked into the bundle — see spec §2.2).
# 2. cargo build → wasm-bindgen → wasm-opt -Oz pipeline produces
# crates/openpencil-shell-web/pkg/openpencil_shell_web_bg.wasm.
# crates/op-host-web/pkg/op_host_web_bg.wasm.
# 3. Post-bindgen bundle has 0 env.* imports
# (i.e. all imports come from `./openpencil_shell_web_bg.js`,
# (i.e. all imports come from `./op_host_web_bg.js`,
# the wasm-bindgen JS shim). Any env.* import = LinkError at
# load time → regression → fail.
# 4. Post wasm-opt -Oz gzip size ≤ STEP1B_SHELL_WASM_GZIP_LIMIT_BYTES
@ -31,11 +31,11 @@
set -euo pipefail
CRATE_DIR="crates/openpencil-shell-web"
CRATE_DIR="crates/op-host-web"
PKG_DIR="${CRATE_DIR}/pkg"
WASM_RAW="${PKG_DIR}/openpencil_shell_web_bg.wasm"
WASM_OPT="${PKG_DIR}/openpencil_shell_web_bg.opt.wasm"
TARGET_WASM="target/wasm32-unknown-unknown/release/openpencil_shell_web.wasm"
WASM_RAW="${PKG_DIR}/op_host_web_bg.wasm"
WASM_OPT="${PKG_DIR}/op_host_web_bg.opt.wasm"
TARGET_WASM="target/wasm32-unknown-unknown/release/op_host_web.wasm"
# Spec §6 row "Per-component ceiling — shell-web wasm (cdylib) gzip"
# = 1 MiB. Override via env for experiments only.
@ -54,7 +54,7 @@ need gzip
[ -n "${EMSDK:-}" ] || { printf 'EMSDK env var unset (needed for emsdk libcxx headers + wasm-aware clang)\n' >&2; exit 2; }
step 2 5 "Build shell-web wasm32-unknown-unknown with --features skia"
cargo build -p openpencil-shell-web \
cargo build -p op-host-web \
--target wasm32-unknown-unknown --features skia --release >/dev/null
step 3 5 "wasm-bindgen --target web → ${PKG_DIR}/"

View file

@ -3,7 +3,7 @@
#
# Per spec §1.4: widget logic (Widget impls, layout/paint/access_node)
# lives in `crates/op-editor-ui/src/widgets/` (Phase 7 reorg moved it
# out of openpencil-shell-core). shell-web's
# out of the old openpencil-shell-core). The op-host-web crate's
# widget glue is scoped to a single module — `widget_host.rs` plus
# its sibling submodules under `widget_host/` (spec amendment
# 2026-05-11: the original "one file" constraint conflicted with
@ -12,35 +12,35 @@
# span multiple sibling files inside the `widget_host/` directory).
# The `// glue:` marker on the paint signature is still the only
# `fn paint(` allowed to drive widgets from this crate.
# Any function or file in shell-web that pulls
# `openpencil_shell_core::widgets::*` must live in the widget_host
# module (the spec's "any function pulling shell-core widgets" clause).
# Any function or file in op-host-web that pulls
# `op_editor_ui::widgets::*` must live in the widget_host
# module (the spec's "any function pulling the widget facade" clause).
#
# Reverse direction: op-editor-ui/src/widgets/ MUST contain four impl
# files (tree, prop_row, dropdown, text_input) AND each file must
# carry a real `impl Widget for X` — a stale file with the impl
# removed must not silently pass. (Phase 7 reorg: the widget facade
# moved out of openpencil-shell-core into the op-editor-ui crate;
# shell-web still pulls `openpencil_shell_core::widgets` via the
# shell-core re-export shim, so the forward F4 path is unchanged.)
# removed must not silently pass. (Phase 7.3 reorg: the
# openpencil-shell-core re-export shim was dissolved; op-host-web
# now pulls `op_editor_ui::widgets` directly from the real source
# crate, so the forward F4 path is unchanged.)
#
# Exit semantics:
# 0 PASS — both invariants hold.
# 1 FAIL — widget logic leaked into shell-web OR shell-core
# widget impl files are missing / empty.
# 1 FAIL — widget logic leaked into op-host-web OR the
# op-editor-ui widget impl files are missing / empty.
set -euo pipefail
WEB_SRC="crates/openpencil-shell-web/src"
WEB_SRC="crates/op-host-web/src"
CORE_WIDGETS="crates/op-editor-ui/src/widgets"
fail_lines=()
# ---------------------------------------------------------------------
# Forward F1: no `impl Widget for X` (with optional generic params and
# arbitrary namespace segments) anywhere under shell-web/src/.
# arbitrary namespace segments) anywhere under op-host-web/src/.
# Even widget_host.rs is not allowed to host a Widget impl — all real
# impls live in shell-core. The `// glue:` exemption is for the paint
# impls live in op-editor-ui. The `// glue:` exemption is for the paint
# signature only, not for Widget trait impls (codex B4 R2 CONCERN-2).
# ---------------------------------------------------------------------
# `impl(<...>)?[[:space:]]+(ns::)*Widget[[:space:]]+for[[:space:]]`:
@ -54,12 +54,12 @@ impl_hits="$(grep -RInE \
'impl(<[^>]+>)?[[:space:]]+([[:alnum:]_]+::)*Widget[[:space:]]+for[[:space:]]' \
"${WEB_SRC}" 2>/dev/null || true)"
if [ -n "${impl_hits}" ]; then
fail_lines+=("Forward F1: Widget trait impl in shell-web (must live in shell-core):" "${impl_hits}")
fail_lines+=("Forward F1: Widget trait impl in op-host-web (must live in op-editor-ui):" "${impl_hits}")
fi
# ---------------------------------------------------------------------
# Forward F2: no `fn layout(` / `fn access_node(` anywhere under
# shell-web/src/. These are widget-trait method signatures and have
# op-host-web/src/. These are widget-trait method signatures and have
# no business in the platform crate. (Note: `fn paint(` is allowed
# only on the `// glue:` marked line in widget_host.rs — handled by
# F3 below to keep the exemption tight.)
@ -73,11 +73,11 @@ trait_method_hits="$(grep -RInE \
'fn[[:space:]]+(layout|access_node)\(' \
"${WEB_SRC}" 2>/dev/null || true)"
if [ -n "${trait_method_hits}" ]; then
fail_lines+=("Forward F2: Widget trait method (layout / access_node) in shell-web:" "${trait_method_hits}")
fail_lines+=("Forward F2: Widget trait method (layout / access_node) in op-host-web:" "${trait_method_hits}")
fi
# ---------------------------------------------------------------------
# Forward F3: `fn paint(` under shell-web is allowed ONLY when the
# Forward F3: `fn paint(` under op-host-web is allowed ONLY when the
# line ALSO carries the `// glue:` marker AND the file is
# widget_host.rs. Tight exemption (codex B4 R2 CONCERN-2 — broad
# `// glue:` exemption could hide leaked impl/method lines if anyone
@ -118,13 +118,13 @@ if [ -n "${paint_hits}" ]; then
}
{ prev_was_glue = 0 }
' "${WEB_SRC}/widget_host.rs" "${WEB_SRC}/widget_host"/*.rs 2>/dev/null || true)"
# `outside_module_files` — files in shell-web/src/ that
# `outside_module_files` — files in op-host-web/src/ that
# define `fn paint(` but are NOT in the widget_host module
# at all. These violate F3 unconditionally.
outside_module_files="$(grep -RIlE \
'fn[[:space:]]+paint[[:space:]]*[<(]' \
"${WEB_SRC}" 2>/dev/null \
| grep -vE '^crates/openpencil-shell-web/src/widget_host(\.rs|/[^/]+\.rs)$' \
| grep -vE '^crates/op-host-web/src/widget_host(\.rs|/[^/]+\.rs)$' \
| LC_ALL=C sort -u || true)"
outside_module_hits=""
if [ -n "${outside_module_files}" ]; then
@ -142,29 +142,29 @@ if [ -n "${paint_hits}" ]; then
fi
fi
if [ -n "${combined_paint}" ]; then
fail_lines+=("Forward F3: fn paint( in shell-web without a '// glue:' marker on the immediately preceding line, OR outside the widget_host module (allowed locations: widget_host.rs and widget_host/* sibling files):" "${combined_paint}")
fail_lines+=("Forward F3: fn paint( in op-host-web without a '// glue:' marker on the immediately preceding line, OR outside the widget_host module (allowed locations: widget_host.rs and widget_host/* sibling files):" "${combined_paint}")
fi
fi
# ---------------------------------------------------------------------
# Forward F4: only widget_host.rs may import `openpencil_shell_core::
# Forward F4: only widget_host.rs may import `op_editor_ui::
# widgets` in any form. Catches:
# - `use openpencil_shell_core::widgets;` (direct)
# - `use openpencil_shell_core::widgets::Foo;` (direct)
# - `use openpencil_shell_core::{widgets};` (grouped)
# - `use openpencil_shell_core::{widgets::Foo};` (grouped)
# - `openpencil_shell_core::widgets::call()` (path expr)
# - `use op_editor_ui::widgets;` (direct)
# - `use op_editor_ui::widgets::Foo;` (direct)
# - `use op_editor_ui::{widgets};` (grouped)
# - `use op_editor_ui::{widgets::Foo};` (grouped)
# - `op_editor_ui::widgets::call()` (path expr)
# - Multi-line grouped imports are NOT caught (single-line
# scope only — code review enforces single-line use stmts in this
# crate; the cost-benefit doesn't justify a multi-line parser).
#
# Codex B4 R2 BLOCK-fix introduced F4; R3 BLOCK refined for grouped
# import forms — the previous regex required a literal `::widgets`
# right after `openpencil_shell_core`, missing the brace form.
# Solution: any line mentioning `openpencil_shell_core` AND `widgets`
# right after `op_editor_ui`, missing the brace form.
# Solution: any line mentioning `op_editor_ui` AND `widgets`
# is a hit (single grep pass picks both patterns up).
# ---------------------------------------------------------------------
core_ref_hits="$(grep -RIn 'openpencil_shell_core' "${WEB_SRC}" 2>/dev/null || true)"
core_ref_hits="$(grep -RIn 'op_editor_ui' "${WEB_SRC}" 2>/dev/null || true)"
if [ -n "${core_ref_hits}" ]; then
# Filter to lines that ALSO mention `widgets`, then drop the
# widget_host.rs allowance.
@ -172,10 +172,10 @@ if [ -n "${core_ref_hits}" ]; then
# `widget_host/` (spec amendment 2026-05-11).
illegal_imports="$(printf '%s\n' "${core_ref_hits}" \
| grep 'widgets' \
| grep -vE '^crates/openpencil-shell-web/src/widget_host(\.rs|/[^/]+\.rs):' \
| grep -vE '^crates/op-host-web/src/widget_host(\.rs|/[^/]+\.rs):' \
|| true)"
if [ -n "${illegal_imports}" ]; then
fail_lines+=("Forward F4: openpencil_shell_core::widgets reference outside widget_host.rs (covers direct + grouped use forms):" "${illegal_imports}")
fail_lines+=("Forward F4: op_editor_ui::widgets reference outside widget_host.rs (covers direct + grouped use forms):" "${illegal_imports}")
fi
fi
@ -189,7 +189,7 @@ fi
# not silently satisfy the check. The stripping uses sed to drop
# `//`-prefixed content (after optional leading whitespace) before
# grepping; block comments (`/* … */`) are not handled because the
# Rust style in shell-core/src/widgets/ uses line comments only.
# Rust style in op-editor-ui/src/widgets/ uses line comments only.
# ---------------------------------------------------------------------
required_widgets=("tree" "prop_row" "dropdown" "text_input")
for w in "${required_widgets[@]}"; do