fix(shell-native): cfg-gate desktop GL stack so iOS/Android cargo check passes

Spec v19 §11 invariant 1 requires shell-native to compile on iOS / Android
cargo check, with the `GlContextProvider` trait (invariant 2) importable on
every non-wasm target. Previously the desktop GL stack (glutin / winit /
skia-safe) was referenced unconditionally in src/, so mobile cargo check
broke the moment the Cargo.toml target-gated those deps to macOS / Linux /
Windows.

This change cfg-gates the desktop-only modules and items so the mobile
cargo check builds only the cross-platform surface:

- src/lib.rs: gate `backend` + `canvas_view_stub` modules and their
  re-exports to desktop OS targets; add `EaglProvider` / `AndroidEglProvider`
  re-exports under `target_os = "ios"` / `"android"`. `GlContextProvider`,
  `ProviderError`, `ProviderResult` stay always-on (per §11 invariant 2).
- src/context/mod.rs: split into a cross-platform trait surface +
  per-platform provider re-exports; gate `shared` (depends on `skia_safe` +
  `winit`) to desktop only.
- src/context/provider.rs: cfg-gate `GlutinProvider` struct + impls + the
  `pick_display_api` helper to desktop OS only; localize `CString` /
  `NonZeroU32` imports inside fn bodies; gate `from_error` to desktop to
  silence dead_code on mobile (the only caller is `GlutinProvider`).
- Cargo.toml: split deps into a cross-platform `cfg(not(wasm32))` block
  (jian-core + glow + raw-window-handle, all required by the trait
  signature on every non-wasm target) and a desktop-only block (skia-safe,
  glutin, glutin-winit, winit, scopeguard, jian-skia, jian-host-desktop).
  Merges the previously duplicate desktop `[target...]` table headers that
  cargo rejected.
- ci: rust-multiplatform.yml mobile-check job now runs cargo check on
  shell-native too (per the comment update there).

Verification:
- cargo check -p openpencil-shell-native --target aarch64-apple-darwin: PASS
- cargo check -p openpencil-shell-native --target aarch64-apple-ios: PASS
- cargo check -p openpencil-shell-native --target aarch64-linux-android: PASS
- cargo check -p openpencil-shell-native --target wasm32-unknown-unknown:
  FAILS with the spec §1.2 `compile_error!` (intended).
- cargo test -p openpencil-shell-native: 14/14 PASS.
- cargo clippy -p openpencil-shell-native --all-targets -- -D warnings: clean
  on macOS, iOS, Android targets.
- cargo fmt --check: clean.
This commit is contained in:
Kayshen-X 2026-05-05 12:23:22 +08:00
parent e35174ff00
commit 968f1fda88
5 changed files with 113 additions and 49 deletions

View file

@ -156,11 +156,12 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
key: mobile-${{ matrix.target }}
# Step 1a kill-spike scope:
# - shell-core MUST compile on iOS / Android / WASM (spec §11 mobile invariant 1).
# - shell-native is desktop-only until Step 1f wires real EaglProvider /
# AndroidEglProvider impls + sdk linking. Mobile cargo check on shell-native
# is deferred to Step 1f; the spec §11 contract is verified via shell-core
# wasm/ios/android compile + API surface (GlContextProvider trait public,
# on_pause cfg(android) surface.take(), TouchForce in ShellEvent — Phase B).
# 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 /
# 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 }}

View file

@ -28,47 +28,60 @@ openpencil-shell-core = { path = "../openpencil-shell-core", version = "0.1.0" }
# OP runs its own GPU event loop and does not call jian_host_desktop::run (softbuffer
# raster present is not needed).
#
# winit features note: on Linux you MUST explicitly enable `x11` and/or `wayland`,
# otherwise `platform_impl/mod.rs` triggers `compile_error!`. macOS / Windows backends
# are auto-enabled via cfg(target_os) and need no feature flag. Step 1a runs CI on all
# three desktop OSes, so enabling both x11 + wayland Linux backends is sufficient.
# Desktop GL stack — target-gated to macOS / Linux / Windows. iOS / Android
# pull EaglProvider / AndroidEglProvider stubs (Step 1f) which don't need
# glutin / winit / desktop skia-safe gl bindings; spec §11 invariant 1 says
# shell-native must compile on mobile cargo check (verified by CI mobile-check
# job in rust-multiplatform.yml). winit features note: on Linux you MUST
# explicitly enable `x11` and/or `wayland`, otherwise `platform_impl/mod.rs`
# triggers `compile_error!`. macOS / Windows backends are auto-enabled via
# cfg(target_os) and need no feature flag.
# Cross-platform abstraction deps — pulled for ALL non-wasm targets including
# iOS / Android. The `GlContextProvider` trait (spec §3.1) references
# `glow::Context` in its method signatures and Step 1f Eagl / AndroidEgl
# stubs reference `raw_window_handle` for `on_resume`; both must be importable
# on mobile per spec §11 invariant 2. `glow` and `raw-window-handle` are
# pure-Rust thin bindings — no native build steps on iOS / Android.
# `jian-core` is wasm32-clean per P0.5 and platform-neutral on mobile.
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
jian-core = { path = "../../vendor/jian/crates/jian-core", version = "0.0.1" }
glow = "0.17.0"
raw-window-handle = "0.6.2"
# Desktop GL stack — target-gated to macOS / Linux / Windows. iOS / Android
# pull only the cross-platform `glow` + `raw-window-handle` above for the
# `GlContextProvider` trait surface; the actual `GlutinProvider` desktop
# implementation, `SharedSkiaContext`, `NativeBackend`, and
# `CanvasViewportStub` are cfg-gated out of the mobile build (see
# src/lib.rs module-level `#[cfg(...)]`). spec §11 invariant 1 says
# shell-native must compile on mobile cargo check (verified by CI
# mobile-check job in rust-multiplatform.yml). winit features note: on
# Linux you MUST explicitly enable `x11` and/or `wayland`, otherwise
# `platform_impl/mod.rs` triggers `compile_error!`. macOS / Windows
# backends are auto-enabled via cfg(target_os) and need no feature flag.
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
skia-safe = { version = "0.97.0", features = ["gl"] }
glutin = "0.32.3"
glutin-winit = "0.5.0"
glow = "0.17.0"
winit = { version = "0.30.13", default-features = false, features = [
"x11",
"wayland",
"wayland-csd-adwaita",
"rwh_06",
] }
raw-window-handle = "0.6.2"
scopeguard = "1.2"
# Jian path deps — both path + version per spec §12.2.
# - jian-core: exposes DrawOp / Paint / TextRun / geometry / scene::Color. `shell-core`
# also pulls jian-core; shell-native uses it directly so the NativeBackend translation
# path can construct `jian_core::render::DrawOp::*` without bouncing through the
# shell-core re-export each frame.
# - jian-skia: provides SkiaBackend (RenderBackend impl) + skia textlayout (the textlayout
# feature pulls ICU + harfbuzz, ~15MB; P0.5 already bumped skia-safe 0.78→0.97 and
# added a public draw_on_canvas).
jian-core = { path = "../../vendor/jian/crates/jian-core", version = "0.0.1" }
jian-skia = { path = "../../vendor/jian/crates/jian-skia", version = "0.0.1", features = [
"textlayout",
] }
# jian-host-desktop: target-gated desktop only (Linux/macOS/Windows); not pulled into
# android/ios metadata (verified by Task 1 Step 26 boundary check).
# - default-features = false: Jian's default features include `run = ["dep:softbuffer"]`
# for raster present; OP runs its own GPU event loop and doesn't need softbuffer.
# - features = ["textlayout"]: aligns the text path with jian-skia.
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
jian-host-desktop = { path = "../../vendor/jian/crates/jian-host-desktop", version = "0.0.1", default-features = false, features = [
"textlayout",
] }
# jian-skia + jian-host-desktop are now in the desktop-only `[target...]`
# block above (merged to avoid duplicate table headers). Per spec §11 +
# §12.3 boundary invariants 2 & 3 they're not pulled into iOS / Android
# cargo check (verified by check-jian-boundaries.sh).
# tracing for span instrumentation across SharedSkiaContext / NativeBackend
# (spec §3.3 / §5.2.1; Task 2 Step 15 dictates per-method spans).
[dependencies.tracing]

View file

@ -1,13 +1,32 @@
//! Shared GL + Skia context module (spec v19 §3).
//!
//! - [`provider`] — `GlContextProvider` trait + `GlutinProvider` (desktop)
//! + iOS / Android stubs (Step 1f).
//! - [`provider`] — `GlContextProvider` trait (cross-platform) +
//! `GlutinProvider` (desktop) + iOS / Android stubs (Step 1f). The
//! trait is exposed on every non-wasm target per spec §11 invariant 2.
//! - [`shared`] — `SharedSkiaContext` owning the GL stack +
//! `skia_safe::DirectContext` + `skia_safe::Surface`. Frame-scoped
//! `with_frame` callback + idempotent teardown.
//! `with_frame` callback + idempotent teardown. Desktop-only — the
//! skia-safe / jian-skia deps that back it aren't fetched on
//! iOS / Android (Cargo.toml target-gates them).
pub mod provider;
pub mod shared;
pub use provider::{GlContextProvider, GlutinProvider, ProviderError, ProviderResult};
// Cross-platform: trait + error types, importable on every non-wasm target.
pub use provider::{GlContextProvider, ProviderError, ProviderResult};
// Per-platform provider implementations.
#[cfg(target_os = "android")]
pub use provider::AndroidEglProvider;
#[cfg(target_os = "ios")]
pub use provider::EaglProvider;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use provider::GlutinProvider;
// `SharedSkiaContext` is desktop-only — depends on `skia_safe` + `winit`
// (cfg-gated dependencies). Step 1f mobile wiring will introduce a mobile
// twin (or generalize this one) once iOS / Android providers go from
// `unimplemented!()` placeholders to real GL contexts.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
mod shared;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use shared::{SharedSkiaContext, SharedSkiaError, SharedSkiaResult, SurfaceConfig};

View file

@ -10,9 +10,8 @@
//! exist as compile-time placeholders so the public API surface is frozen
//! before Step 1f real implementations land.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
use std::error::Error;
use std::ffi::CString;
use std::num::NonZeroU32;
use std::sync::Arc;
/// Errors raised by GL context providers.
@ -25,6 +24,11 @@ pub enum ProviderError {
}
impl ProviderError {
/// Wrap any `Error` impl into a `ProviderError::Failure`. Used by the
/// desktop `GlutinProvider` to convert glutin / glutin-winit errors;
/// `cfg(any(...))`-gated to silence `dead_code` on iOS / Android where
/// no in-tree caller exists yet (Step 1f mobile providers will use it).
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub(crate) fn from_error<E: Error>(err: E) -> Self {
Self::Failure(err.to_string())
}
@ -98,7 +102,9 @@ pub trait GlContextProvider {
}
// ────────────────────────────────────────────────────────────────────────────
// Desktop: GlutinProvider
// Desktop: GlutinProvider (cfg-gated to macOS / Linux / Windows; the
// glutin / winit / skia-safe dep stack is desktop-only per spec §11
// invariant 1).
// ────────────────────────────────────────────────────────────────────────────
/// Desktop GL provider built on top of `glutin 0.32` + `glutin-winit 0.5`.
@ -106,6 +112,7 @@ pub trait GlContextProvider {
/// All non-`Send` glutin handles live in [`Option`]s so `release` can
/// drop them in a defined order (surface → context → display) without
/// requiring `&mut self` to consume `self`.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub struct GlutinProvider {
/// Currently-current context. `None` after `release`.
context: Option<glutin::context::PossiblyCurrentContext>,
@ -117,6 +124,7 @@ pub struct GlutinProvider {
glow: Arc<glow::Context>,
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
impl GlutinProvider {
/// Construct from an existing winit window. Builds a glutin display
/// directly from the window's display handle (bypassing the sealed
@ -134,6 +142,7 @@ impl GlutinProvider {
use glutin::prelude::*;
use glutin_winit::GlWindow;
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use std::ffi::CString;
let raw_window_handle = window
.window_handle()
@ -235,13 +244,7 @@ fn pick_display_api(
glutin::display::DisplayApiPreference::Egl
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn pick_display_api(
_raw: raw_window_handle::RawWindowHandle,
) -> glutin::display::DisplayApiPreference {
glutin::display::DisplayApiPreference::Egl
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
impl GlContextProvider for GlutinProvider {
#[tracing::instrument(skip(self))]
fn make_current(&mut self) -> ProviderResult<()> {
@ -285,6 +288,7 @@ impl GlContextProvider for GlutinProvider {
#[tracing::instrument(skip(self))]
fn resize(&mut self, width: u32, height: u32) -> ProviderResult<()> {
use glutin::prelude::*;
use std::num::NonZeroU32;
let ctx = self
.context
.as_ref()

View file

@ -26,17 +26,44 @@ compile_error!(
Use openpencil-shell-web for browser builds (spec v19 §1.2)."
);
pub mod backend;
pub mod canvas_view_stub;
// Cross-platform context module: re-exports the `GlContextProvider` trait
// + `ProviderError` / `ProviderResult` on every (non-wasm) target so spec
// §11 invariant 2 holds — mobile callers can name the trait. Internal
// cfg-gates select between `GlutinProvider` (desktop), `EaglProvider` (iOS)
// and `AndroidEglProvider` (Android), and `SharedSkiaContext` is only
// compiled in on desktop where the GL + Skia stack is available.
pub mod context;
// Desktop-only modules — pull `skia_safe` / `jian_skia` / `glutin` types
// that aren't fetched on iOS / Android (see Cargo.toml target-gated deps).
// Spec §11 invariants 1 & 3: mobile builds compile shell-native without
// these modules at all; mobile widget rendering lands in Step 1f.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub mod backend;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub mod canvas_view_stub;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use backend::{to_jian_color, to_jian_rect, NativeBackend};
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use canvas_view_stub::CanvasViewportStub;
// Cross-platform re-exports — visible on every (non-wasm) target.
pub use context::{GlContextProvider, ProviderError, ProviderResult};
// Desktop-only re-exports.
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use context::{
GlContextProvider, GlutinProvider, ProviderError, ProviderResult, SharedSkiaContext,
SharedSkiaError, SharedSkiaResult, SurfaceConfig,
GlutinProvider, SharedSkiaContext, SharedSkiaError, SharedSkiaResult, SurfaceConfig,
};
// Mobile stub re-exports — Step 1f real impls; today they're zero-sized
// placeholder structs whose `GlContextProvider` impls `unimplemented!()`.
#[cfg(target_os = "android")]
pub use context::AndroidEglProvider;
#[cfg(target_os = "ios")]
pub use context::EaglProvider;
// `placeholder()` from Task 1 was removed by Codex Phase A Gate round 1
// NIT 1 — Task 2's full re-export chain (`SharedSkiaContext`,
// `NativeBackend`, etc.) already proves the shell-core ↔ shell-native