feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate
Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0:
- crates/openpencil-shell-native/examples/basic_window.rs:
winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp)
+ JianPointerMapper integration. Paints chrome rect + "Hello 你好"
+ box outline; close → idempotent teardown. Demonstrates Phase B
Task 3 winit → Jian PointerTranslator → JianPointerMapper →
ShellEvent pipeline end-to-end.
- crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md:
manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS
recorded; Linux/Windows pending real-hardware run, deferred per
CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER).
- tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants.
Verifies that openpencil-app has no direct jian-* dep, mobile
(aarch64-linux-android, aarch64-apple-ios) and wasm32 closures
exclude jian-host-desktop / jian-skia, and openpencil-shell-web
declares no jian-host-desktop dep at the manifest level.
- .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh
on Linux runner with mobile + wasm32 targets installed.
- README.md: roadmap entry for the Step 1a milestone.
Verified locally on macOS aarch64:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --examples --workspace
- cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED)
- cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios}
- bash tools/check-jian-boundaries.sh (4 invariants PASS)
- spec §11 invariants 1–4 grep checks PASS
Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN.
vendor/jian pinned at c4a794dc.
This commit is contained in:
parent
786d10a1c6
commit
e66e8aefcc
10
.github/workflows/rust-check.yml
vendored
10
.github/workflows/rust-check.yml
vendored
|
|
@ -61,6 +61,16 @@ jobs:
|
|||
if: runner.os != 'Linux'
|
||||
run: cargo test --workspace
|
||||
- run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
# Step 1a Phase C Task 4: spec v19 §11 + §12.3 boundary invariants.
|
||||
# Linux runner has the full mobile target stdlib (`rustup target add`
|
||||
# in subsequent steps would handle ios/android cargo metadata too,
|
||||
# but the script uses `cargo tree --target` which only needs the
|
||||
# cfg-gate evaluation, not the actual target sysroot).
|
||||
- name: Verify Jian boundary invariants
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
rustup target add aarch64-linux-android aarch64-apple-ios wasm32-unknown-unknown
|
||||
bash tools/check-jian-boundaries.sh
|
||||
|
||||
deny:
|
||||
name: cargo-deny (native)
|
||||
|
|
|
|||
|
|
@ -502,6 +502,7 @@ Contributions are welcome! See [CLAUDE.md](./CLAUDE.md) for architecture details
|
|||
- [x] Native agent runtime (`agent-native` — Zig NAPI)
|
||||
- [x] Git integration — clone, branch, push/pull, folder-mode three-way merge
|
||||
- [x] Canvas raster export (PNG / JPEG / WEBP / PDF)
|
||||
- [x] Rust shell — Step 1a (G1 shared Skia context) on `v0.8.0`: `SharedSkiaContext` + `NativeBackend` (Jian-`DrawOp`-backed) + `JianPointerMapper` (Jian `PointerEvent` → `ShellEvent`) + `basic_window` demo, with the multi-platform CI matrix green (macOS / Linux / Windows desktop, iOS / Android cargo check, wasm32). Spec `v19.3` FROZEN; `vendor/jian` pinned at `c4a794dc`.
|
||||
- [ ] Collaborative editing
|
||||
- [ ] Plugin system
|
||||
|
||||
|
|
|
|||
240
crates/openpencil-shell-native/examples/basic_window.rs
Normal file
240
crates/openpencil-shell-native/examples/basic_window.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
//! Spec v19 §1.2 acceptance #1 — basic_window demo.
|
||||
//!
|
||||
//! Phase C Task 4 deliverable: a minimal winit + `SharedSkiaContext` +
|
||||
//! `NativeBackend` + `JianPointerMapper` integration that paints chrome
|
||||
//! (rect / text / box outline) on every frame and translates pointer
|
||||
//! events through the Phase B Task 3 mapper. The macOS / Linux / Windows
|
||||
//! runtime is exercised by maintainers manually (`notes/step-1a-{macos,
|
||||
//! linux, windows}-manual-smoke.md`); CI only verifies that
|
||||
//! `cargo build --examples --workspace` compiles on every desktop OS.
|
||||
//!
|
||||
//! Run with:
|
||||
//! ```text
|
||||
//! cargo run -p openpencil-shell-native --example basic_window
|
||||
//! ```
|
||||
//!
|
||||
//! The window should display:
|
||||
//! - White background (chrome canvas clear).
|
||||
//! - Red filled rect at (50, 50) - 100x100.
|
||||
//! - "Hello 你好" black text at (50, 200).
|
||||
//! - Blue stroked rect outline at (200, 50) - 200x150.
|
||||
//!
|
||||
//! Closing the window must run `SharedSkiaContext::teardown` exactly
|
||||
//! once (the `Drop` impl is the safety net) and exit cleanly.
|
||||
|
||||
#![cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
||||
|
||||
use jian_core::scene::Color as JianColor;
|
||||
use jian_host_desktop::pointer::PointerTranslator;
|
||||
use openpencil_shell_core::{Color, Point2D, Rect, TextLayout};
|
||||
use openpencil_shell_native::{
|
||||
JianPointerMapper, NativeBackend, SharedSkiaContext, SharedSkiaError,
|
||||
};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::{ActiveEventLoop, EventLoop};
|
||||
use winit::window::{Window, WindowId};
|
||||
|
||||
/// Per-frame chrome paint. Pulled into a free function so both the
|
||||
/// initial `Resumed` paint and `RedrawRequested` redraws share the
|
||||
/// exact same draw list (spec §5.2.1 frame-scoped backend pattern).
|
||||
fn paint_chrome(ctx: &mut SharedSkiaContext, backend: &mut NativeBackend) {
|
||||
let rect_fill = Rect {
|
||||
origin: Point2D::new(50.0, 50.0),
|
||||
size: Point2D::new(100.0, 100.0),
|
||||
};
|
||||
let rect_outline = Rect {
|
||||
origin: Point2D::new(200.0, 50.0),
|
||||
size: Point2D::new(200.0, 150.0),
|
||||
};
|
||||
let text = TextLayout::single_run(
|
||||
"Hello 你好",
|
||||
"",
|
||||
24.0,
|
||||
JianColor::rgb(0, 0, 0),
|
||||
Point2D::new(50.0, 200.0),
|
||||
);
|
||||
|
||||
ctx.begin_frame();
|
||||
ctx.with_frame(|canvas, _glow| {
|
||||
// White background — clear the framebuffer through Skia.
|
||||
canvas.clear(skia_safe::Color::WHITE);
|
||||
|
||||
// 1. Filled red rectangle (acceptance #1 chrome rect).
|
||||
backend.fill_rect(canvas, rect_fill, Color::RED);
|
||||
|
||||
// 2. Black "Hello 你好" — exercises CJK path through jian-skia
|
||||
// `textlayout` ParagraphBuilder.
|
||||
backend.draw_text(canvas, &text, Point2D::ZERO);
|
||||
|
||||
// 3. Blue stroked box (acceptance #1 chrome outline).
|
||||
backend.stroke_rect(canvas, rect_outline, Color::BLUE, 2.0);
|
||||
});
|
||||
ctx.present();
|
||||
}
|
||||
|
||||
struct BasicWindowApp {
|
||||
window: Option<Window>,
|
||||
ctx: Option<SharedSkiaContext>,
|
||||
backend: Option<NativeBackend>,
|
||||
/// Phase B Task 3 wiring: winit `WindowEvent` → Jian `PointerEvent`
|
||||
/// → `JianPointerMapper` → `ShellEvent`. We only translate pointer
|
||||
/// events here; window/resize/close events go straight to the
|
||||
/// match arms below.
|
||||
pointer_translator: PointerTranslator,
|
||||
pointer_mapper: JianPointerMapper,
|
||||
/// Fatal teardown / surface error captured for post-loop diagnosis.
|
||||
error: Option<SharedSkiaError>,
|
||||
}
|
||||
|
||||
impl BasicWindowApp {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
ctx: None,
|
||||
backend: None,
|
||||
pointer_translator: PointerTranslator::new(),
|
||||
pointer_mapper: JianPointerMapper::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for BasicWindowApp {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.window.is_some() {
|
||||
return;
|
||||
}
|
||||
let attrs = Window::default_attributes()
|
||||
.with_title("OpenPencil — basic_window (Step 1a §1.2 acceptance #1)")
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(800u32, 600u32));
|
||||
let window = match event_loop.create_window(attrs) {
|
||||
Ok(w) => w,
|
||||
Err(err) => {
|
||||
eprintln!("basic_window: create_window failed: {err}");
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Build the GL stack + Skia context bound to the new window.
|
||||
let dpi = window.scale_factor() as f32;
|
||||
match SharedSkiaContext::new_desktop(&window) {
|
||||
Ok(ctx) => {
|
||||
self.ctx = Some(ctx);
|
||||
self.backend = Some(NativeBackend::with_dpi(dpi));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("basic_window: SharedSkiaContext::new_desktop failed: {err}");
|
||||
self.error = Some(err);
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.window = Some(window);
|
||||
|
||||
// First paint so the window has visible content even before
|
||||
// the OS schedules a `RedrawRequested`. Subsequent redraws come
|
||||
// through the event match below.
|
||||
if let (Some(ctx), Some(backend)) = (self.ctx.as_mut(), self.backend.as_mut()) {
|
||||
paint_chrome(ctx, backend);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
// Phase B Task 3 wiring: route pointer-flavoured winit events
|
||||
// through Jian's PointerTranslator + our JianPointerMapper.
|
||||
// Non-pointer events (Resized / RedrawRequested / CloseRequested)
|
||||
// bypass the Jian path per spec §5.1.1.
|
||||
match &event {
|
||||
WindowEvent::ModifiersChanged(m) => {
|
||||
self.pointer_translator.update_modifiers(m.state());
|
||||
}
|
||||
WindowEvent::CursorMoved { .. }
|
||||
| WindowEvent::CursorLeft { .. }
|
||||
| WindowEvent::MouseInput { .. }
|
||||
| WindowEvent::Touch(_) => {
|
||||
if let Some(jian_event) = self.pointer_translator.translate(&event) {
|
||||
let shell_events = self.pointer_mapper.from_jian_pointer(&jian_event);
|
||||
// The demo doesn't act on pointer events — it just
|
||||
// proves the pipeline compiles + runs without
|
||||
// panicking. Real widget dispatch lands in Step 1c+.
|
||||
let _ = shell_events;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
event_loop.exit();
|
||||
}
|
||||
WindowEvent::Resized(size) => {
|
||||
if let Some(ctx) = self.ctx.as_mut() {
|
||||
if let Err(err) = ctx.resize(size.width, size.height) {
|
||||
eprintln!("basic_window: resize failed: {err}");
|
||||
self.error = Some(err);
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
if let Some(backend) = self.backend.as_mut() {
|
||||
backend.set_dpi(scale_factor as f32);
|
||||
}
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
if let (Some(ctx), Some(backend)) = (self.ctx.as_mut(), self.backend.as_mut()) {
|
||||
paint_chrome(ctx, backend);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
// Idempotent teardown — the `Drop` impl on `SharedSkiaContext`
|
||||
// is the safety net, but explicit teardown gives the demo a
|
||||
// visible "no leaks" signal at exit (acceptance #6).
|
||||
if let Some(mut ctx) = self.ctx.take() {
|
||||
if let Err(err) = ctx.teardown() {
|
||||
eprintln!("basic_window: teardown failed: {err}");
|
||||
}
|
||||
}
|
||||
self.backend.take();
|
||||
self.window.take();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Per-frame `#[instrument]` spans are emitted but no subscriber is
|
||||
// initialised by default — the demo deliberately stays free of
|
||||
// dev-only crates. Add `tracing_subscriber::fmt::try_init()` here
|
||||
// (and drop it as a dev-dep) if you want to inspect spans.
|
||||
|
||||
let event_loop = match EventLoop::new() {
|
||||
Ok(el) => el,
|
||||
Err(err) => {
|
||||
eprintln!("basic_window: EventLoop::new failed: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
|
||||
let mut app = BasicWindowApp::new();
|
||||
if let Err(err) = event_loop.run_app(&mut app) {
|
||||
eprintln!("basic_window: run_app exited with error: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
if let Some(err) = app.error {
|
||||
eprintln!("basic_window: fatal error during run: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Step 1a Linux manual GPU smoke (per spec v19 §1.2 acceptance #1)
|
||||
|
||||
**Status**: PENDING — to be filled by a maintainer running on a Linux
|
||||
desktop with a real or Mesa software-rendered GL driver.
|
||||
|
||||
The CI Linux runner ships Mesa llvmpipe via Xvfb but `gpu_smoke` /
|
||||
`gpu_chrome_stub_composition` are currently `#[ignore]`'d under
|
||||
`LINUX_GPU_SKIA_LOADER_TBD` (spec §3.1 mini-patch — `skia-safe`'s
|
||||
`Interface::new_native` cannot resolve GL syms from EGL pbuffer +
|
||||
llvmpipe; the proper fix is `new_load_with(eglGetProcAddress)` and is
|
||||
deferred to Step 1f). The spec acceptance #1 on Linux therefore needs
|
||||
a separate human runtime check.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Ubuntu 22.04+ / Fedora 40+ / Arch with `mesa` / `libglvnd` /
|
||||
`xkbcommon` / `wayland-client` / `freetype` / `fontconfig` installed
|
||||
(the same set the CI `Install Linux GL prereqs` step provisions).
|
||||
- X11 or Wayland session.
|
||||
- Rust toolchain 1.85.
|
||||
- Submodules at `vendor/jian@c4a794dc`.
|
||||
|
||||
## Required commands
|
||||
|
||||
```bash
|
||||
cargo run -p openpencil-shell-native --example basic_window
|
||||
# After Step 1f spec §3.1 mini-patch lands, also:
|
||||
# cargo test -p openpencil-shell-native --test gpu_smoke -- --include-ignored gpu_smoke
|
||||
# cargo test -p openpencil-shell-native --test gpu_chrome_stub_composition -- --include-ignored gpu_chrome_stub_composition
|
||||
```
|
||||
|
||||
## Expected outcomes
|
||||
|
||||
- `basic_window` opens an 800x600 window showing:
|
||||
- White background.
|
||||
- Red filled rect at `(50, 50) — 100x100`.
|
||||
- Black `Hello 你好` text at `(50, 200)`.
|
||||
- Blue stroked rect outline at `(200, 50) — 200x150`.
|
||||
- Closing the window exits cleanly.
|
||||
- Running under Xvfb + Mesa llvmpipe is acceptable for "no real GPU"
|
||||
hosts; the chrome must still render correctly.
|
||||
|
||||
## Where to record results
|
||||
|
||||
Append the run date, distro/version, GL renderer (`glxinfo | grep
|
||||
OpenGL.renderer`), and output excerpt to this file (replacing the
|
||||
`Status: PENDING` line) and commit on `v0.8.0` with
|
||||
`docs(shell-native): record Linux manual GPU smoke`.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Step 1a macOS manual GPU smoke (per spec v19 §1.2 acceptance #1)
|
||||
|
||||
**Status**: PASS — exercised on Apple Silicon during Phase C Task 4
|
||||
implementation (2026-05-05).
|
||||
|
||||
The macOS path through `gpu_smoke` / `gpu_chrome_stub_composition`
|
||||
already runs in CI on `macos-latest` (`SharedSkiaContext::new_desktop`
|
||||
|
||||
- raster + chrome+stub composition). This note captures the
|
||||
maintainer-run `cargo run --example basic_window` smoke in addition
|
||||
to the automated checks (spec v19 §1.2 acceptance #1).
|
||||
|
||||
## Run on Apple Silicon (macos-latest, M-series)
|
||||
|
||||
```bash
|
||||
cargo run -p openpencil-shell-native --example basic_window
|
||||
```
|
||||
|
||||
### Expected window contents
|
||||
|
||||
- 800x600 window titled `OpenPencil — basic_window (Step 1a §1.2 acceptance #1)`.
|
||||
- White background.
|
||||
- Red filled rect at `(50, 50) — 100x100` (chrome).
|
||||
- Black `Hello 你好` text at `(50, 200)` (chrome via Jian skia
|
||||
textlayout).
|
||||
- Blue stroked rect outline at `(200, 50) — 200x150`.
|
||||
- Closing the window via Cmd-W / red button → process exits with
|
||||
status 0 (no panic, no driver complaint).
|
||||
|
||||
### Verified
|
||||
|
||||
- Build: `cargo build --examples --workspace` succeeds clean.
|
||||
- Process: launches without stderr output, holds the window until
|
||||
closed, no hangs on teardown.
|
||||
- `SharedSkiaContext::teardown` runs once via `exiting()` and is a
|
||||
no-op on `Drop`.
|
||||
|
||||
## When to update this file
|
||||
|
||||
- After upgrading `vendor/jian` / `glutin` / `winit` / `skia-safe`.
|
||||
- After reworking `paint_chrome` to call new `NativeBackend` methods.
|
||||
- After macOS releases that change EAGL / Metal-translated bridging.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Step 1a Windows manual GPU smoke (per spec v19 §8.1)
|
||||
|
||||
**Status**: PENDING — to be filled by a maintainer running on a Windows
|
||||
desktop with a real GL driver.
|
||||
|
||||
The standard GitHub Actions `windows-latest` runner has no GPU driver
|
||||
(`WINDOWS_GPU_DEFERRED_NO_RUNNER`), so spec v19 §8.1 requires the
|
||||
following sequence to be exercised by a human on real hardware before
|
||||
Step 1a can be declared "live on Windows".
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Windows 10/11 (x86_64 or aarch64) with a working OpenGL 3.3+ driver
|
||||
(default factory drivers on most modern GPUs satisfy this).
|
||||
- Rust toolchain 1.85 (via `rustup toolchain install 1.85`).
|
||||
- Submodules checked out (`git submodule update --init --recursive`) so
|
||||
`vendor/jian` is at `c4a794dc` (Step 1a freeze).
|
||||
|
||||
## Required commands
|
||||
|
||||
```pwsh
|
||||
cargo run -p openpencil-shell-native --example basic_window
|
||||
cargo test -p openpencil-shell-native --test gpu_smoke -- --include-ignored gpu_smoke
|
||||
cargo test -p openpencil-shell-native --test gpu_chrome_stub_composition -- --include-ignored gpu_chrome_stub_composition
|
||||
```
|
||||
|
||||
## Expected outcomes
|
||||
|
||||
- `basic_window` opens an 800x600 window showing:
|
||||
- White background.
|
||||
- Red filled rect at `(50, 50) — 100x100` (chrome).
|
||||
- Black `Hello 你好` text at `(50, 200)` (chrome via Jian skia
|
||||
textlayout).
|
||||
- Blue stroked rect outline at `(200, 50) — 200x150`.
|
||||
- Closing the window exits cleanly (no panic, no driver complaint
|
||||
in the console).
|
||||
- `gpu_smoke` (`#[ignore]`'d on Windows by `WINDOWS_GPU_DEFERRED_NO_RUNNER`)
|
||||
passes when run with `--include-ignored` on a real-GPU host.
|
||||
- `gpu_chrome_stub_composition` likewise passes — chrome pixel reads
|
||||
back red even after `CanvasViewportStub::render_into` pollutes
|
||||
`STENCIL_TEST` and `BlendFunc(ONE, ZERO)`.
|
||||
|
||||
## Where to record results
|
||||
|
||||
Append the run date, Windows build, GPU/driver, and output excerpt to
|
||||
this file (replacing the `Status: PENDING` line) and commit on
|
||||
`v0.8.0` with `docs(shell-native): record Windows manual GPU smoke`.
|
||||
106
tools/check-jian-boundaries.sh
Executable file
106
tools/check-jian-boundaries.sh
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env bash
|
||||
# Step 1a Phase C Task 4 / spec v19 §11 + §12.3 boundary invariants.
|
||||
#
|
||||
# Verifies the following Jian crate boundary invariants from outside the
|
||||
# Rust build system. Run from the repo root.
|
||||
#
|
||||
# Invariant 1 (§12.3): openpencil-app must NOT depend directly on any
|
||||
# `jian-*` crate — Jian is a shell-native implementation detail; the
|
||||
# app only sees OP's `RenderBackend` / `ShellEvent` facade.
|
||||
#
|
||||
# Invariant 2 (§11.1, §12.3): mobile targets (`aarch64-linux-android`,
|
||||
# `aarch64-apple-ios`) must NOT pull `jian-host-desktop` or `jian-skia`
|
||||
# into the dependency closure — those carry the desktop GL stack +
|
||||
# `skia-safe` build.rs that fails on cross-compile.
|
||||
#
|
||||
# Invariant 3 (§11.1, §1.2): wasm32 builds of `openpencil-shell-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
|
||||
# `jian-host-desktop` at all — even as a non-default optional dep.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — all invariants pass.
|
||||
# 1+ — one or more invariants fail; the failing crate names are
|
||||
# echoed before the script exits.
|
||||
#
|
||||
# Dependencies: `cargo`, `jq` (for cargo metadata JSON parsing).
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "check-jian-boundaries.sh: \`jq\` is required but not installed." >&2
|
||||
echo " apt: sudo apt-get install -y jq" >&2
|
||||
echo " brew: brew install jq" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ── Invariant 1: openpencil-app has no direct jian-* dependency. ──────
|
||||
# `cargo metadata` returns a workspace-wide resolve graph; we filter
|
||||
# the `resolve.nodes[]` entry whose name matches `openpencil-app` and
|
||||
# inspect its direct `deps[]`. A direct dep on any `jian-*` crate
|
||||
# fails the invariant.
|
||||
metadata_full="$(cargo metadata --format-version 1)"
|
||||
forbidden_app="$(echo "$metadata_full" | jq -r '
|
||||
[.packages[] | select(.name == "openpencil-app") | .id] as $app_ids
|
||||
| .resolve.nodes[]
|
||||
| select(.id as $id | $app_ids | index($id))
|
||||
| .deps[].name
|
||||
' | grep -E '^jian-' || true)"
|
||||
if [ -n "$forbidden_app" ]; then
|
||||
echo "INVARIANT 1 FAILED: openpencil-app directly depends on jian-* crate(s):" >&2
|
||||
echo "$forbidden_app" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Invariant 2: mobile targets don't pull jian-host-desktop / jian-skia. ──
|
||||
# We use `cargo tree` (which honours `--target` cfg-gates) and inspect
|
||||
# the dependency closure of `openpencil-shell-native` — only the deps
|
||||
# that actually compile under the mobile target are listed.
|
||||
for target in aarch64-linux-android aarch64-apple-ios; do
|
||||
tree_mobile="$(cargo tree -p openpencil-shell-native \
|
||||
--target "$target" \
|
||||
--prefix none \
|
||||
--edges normal,build 2>/dev/null || true)"
|
||||
forbidden_mobile="$(echo "$tree_mobile" \
|
||||
| grep -oE '\bjian-(host-desktop|skia)\b' \
|
||||
| sort -u || true)"
|
||||
if [ -n "$forbidden_mobile" ]; then
|
||||
echo "INVARIANT 2 FAILED ($target): forbidden Jian crates in closure:" >&2
|
||||
echo "$forbidden_mobile" >&2
|
||||
exit 1
|
||||
fi
|
||||
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 \
|
||||
--target wasm32-unknown-unknown \
|
||||
--prefix none \
|
||||
--edges normal,build 2>/dev/null || true)"
|
||||
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 "$forbidden_wasm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Invariant 4: openpencil-shell-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 \
|
||||
--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 "$shell_web_deps" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "check-jian-boundaries.sh: all 4 Jian boundary invariants pass."
|
||||
Loading…
Reference in a new issue