From cb66d79cdda461e3e655376f2fd8f6aaddcd80d1 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 29 Jul 2026 20:31:08 +0200 Subject: [PATCH 1/2] fix(build): de-conflate std from bazel-bindings; select artifacts by name; register components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the core-module incident, addressing "why isn't this in the bazel rules" — the investigation found the script and bazel are not just duplicated, they DIVERGE. 1. FEATURE DE-CONFLATION (the real design flaw, mine). `bazel-bindings` means "where do the WIT bindings come from" in every stage crate. In rate I gave it a SECOND meaning — std vs no_std — so the two build systems produced materially different artifacts from one source: bazel (sets bazel-bindings) got std + WASI imports; cargo-component (--no-default-features) got the WASI-free one we publish. Split into an independent `std` feature. Because rules_rust does not apply Cargo's default-features, a bazel build enabling only `bazel-bindings` now gets no_std — i.e. bazel produces the WASI-free component too. Verified the cargo path is unchanged: component header, 4581 bytes, ZERO wasi imports. 2. ARTIFACT SELECTION BY NAME (second latent bug, found while verifying the above). `ls target/.../*.wasm | head -1` picks whatever sorts first — with a shared CARGO_TARGET_DIR (a common CI disk-saving setting) building `rate` selected falcon_flight_component.wasm because "flight" < "rate", and would have published it under the wrong name. Now derives the expected artifact from the crate name and honours CARGO_TARGET_DIR. 3. wasm.directory REGISTRATION. Filed the four verified components (iekf, position, attitude, mixer -> issues #474-#477, auto-PRs #478-#481; adding to an existing namespace auto-merges). falcon-rate is deliberately HELD: the published 1.129.0 artifact is still the core module, and registering a broken artifact in a public index is worse than a late listing. Documented, with the header-check recipe — config.mediaType claims "component" regardless of the bytes, so metadata cannot be trusted for this. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG --- docs/OCI-DISTRIBUTION.md | 18 ++++++++++++++++++ scripts/build-components.sh | 24 +++++++++++++++++++----- wasm/cm/rate/Cargo.toml | 9 ++++++++- wasm/cm/rate/src/lib.rs | 18 +++++++++++++----- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/docs/OCI-DISTRIBUTION.md b/docs/OCI-DISTRIBUTION.md index dcb34b1..ca75f3d 100644 --- a/docs/OCI-DISTRIBUTION.md +++ b/docs/OCI-DISTRIBUTION.md @@ -83,6 +83,24 @@ cosign verify ghcr.io/pulseengine/falcon-flight:1.127.0 \ already exist and be **public** (step 1) before submitting — an unpullable entry is rejected. Do this only after a real release has populated the package. + Adding a component to an **existing** namespace auto-merges (no maintainer + review) — only a brand-new namespace is held for review. Registered so far: + + | component | wasm.directory | note | + |---|---|---| + | `falcon-flight` | ✅ indexed (#466 → #467) | | + | `falcon-iekf` | filed #474 → PR #478 | | + | `falcon-position` | filed #475 → PR #479 | | + | `falcon-attitude` | filed #476 → PR #480 | | + | `falcon-mixer` | filed #477 → PR #481 | | + | `falcon-rate` | **HELD — do not register yet** | published 1.129.0 is a CORE MODULE, not a component; register after v1.130 republishes it | + + **Verify the payload before registering.** The OCI `config.mediaType` reads + `application/vnd.wasm.config.v0+json` whether or not the bytes are actually a + component, so metadata cannot be trusted here — pull the blob and check the + 8-byte header (`0061736d0d000100` = component, `0061736d01000000` = core + module). This is how `falcon-rate:1.129.0` was caught. + > Status: **LIVE** — the `pulseengine` namespace was accepted > ([wasm.directory#466](https://github.com/yoshuawuyts/wasm.directory/issues/466) > → [PR #467](https://github.com/yoshuawuyts/wasm.directory/pull/467) merged); diff --git a/scripts/build-components.sh b/scripts/build-components.sh index 440fa8b..a251d5b 100755 --- a/scripts/build-components.sh +++ b/scripts/build-components.sh @@ -65,11 +65,25 @@ build_component() { exit 1 fi # Prefer the wasip2 path; cargo-component <0.20 emits wasip1. - local src - src=$(ls "$wasm_dir"/target/wasm32-wasip2/release/*.wasm 2>/dev/null | head -1 || true) - if [ -z "$src" ]; then - src=$(ls "$wasm_dir"/target/wasm32-wasip1/release/*.wasm 2>/dev/null | head -1 || true) - fi + # Select the artifact BY NAME, never `ls *.wasm | head -1` (v1.130). The old + # glob picked whatever sorted first in the target dir — with a shared + # CARGO_TARGET_DIR (a common CI disk-saving setting) that is a DIFFERENT + # crate's component: building `rate` picked up falcon_flight_component.wasm + # because "flight" < "rate". It would then be published under the wrong name. + # cargo turns - into _ for the artifact filename. + local crate_name artifact src + crate_name=$(grep -m1 '^name *= *"' "$wasm_dir/Cargo.toml" | sed -E 's/.*"(.*)".*/\1/') + artifact="${crate_name//-/_}.wasm" + src="" + for tgt in wasm32-wasip2 wasm32-wasip1; do + if [ -f "$wasm_dir/target/$tgt/release/$artifact" ]; then + src="$wasm_dir/target/$tgt/release/$artifact"; break + fi + # honour a shared CARGO_TARGET_DIR if one is set + if [ -n "${CARGO_TARGET_DIR:-}" ] && [ -f "$CARGO_TARGET_DIR/$tgt/release/$artifact" ]; then + src="$CARGO_TARGET_DIR/$tgt/release/$artifact"; break + fi + done if [ -z "$src" ]; then echo " ERROR: no .wasm found" >&2 exit 1 diff --git a/wasm/cm/rate/Cargo.toml b/wasm/cm/rate/Cargo.toml index 1d67dc0..9f6ae22 100644 --- a/wasm/cm/rate/Cargo.toml +++ b/wasm/cm/rate/Cargo.toml @@ -39,5 +39,12 @@ codegen-units = 1 [workspace] [features] -default = ["bazel-bindings"] +# Two INDEPENDENT concerns — do not conflate them again (see lib.rs): +# bazel-bindings : where the WIT bindings come from (bazel-generated crate +# vs cargo-component's local `mod bindings`) +# std : link std, or build no_std (WASI-free, lowerable) +# Bazel sets crate_features explicitly and rules_rust does NOT apply Cargo's +# default-features, so a Bazel build gets bazel-bindings WITHOUT std -> no_std. +default = ["bazel-bindings", "std"] bazel-bindings = [] +std = [] diff --git a/wasm/cm/rate/src/lib.rs b/wasm/cm/rate/src/lib.rs index 9126144..0817dda 100644 --- a/wasm/cm/rate/src/lib.rs +++ b/wasm/cm/rate/src/lib.rs @@ -6,19 +6,27 @@ //! comes from `vehicle-state.{wx,wy,wz}` (the gyro reading the EKF component //! passes through). //! -//! `no_std` on the cargo-component path so the component imports NO WASI — a +//! `no_std` unless the `std` feature is on, so the component imports NO WASI — a //! pure `falcon:cascade/types` -> `rate` transformer, which is what lowers to //! bare metal (synth -> gale on M4/M7/F100) and what a WASI-less wasmtime host //! can drive directly. (The single-threaded statics replace `thread_local!`, //! whose `std` dependency was the sole reason WASI was linked.) The Bazel //! bindings path keeps its existing environment. +//! +//! FEATURE SPLIT (v1.130): `bazel-bindings` selects the BINDINGS SOURCE only; +//! `std` selects std-vs-no_std. These were briefly conflated on one flag, which +//! made the Bazel build (which sets bazel-bindings) silently produce a +//! std/WASI-carrying artifact while the cargo-component build produced the +//! WASI-free one — two build systems, same source, materially different output. +//! rules_rust does not apply Cargo's default-features, so a Bazel build enables +//! ONLY the features it lists: `bazel-bindings` without `std` => no_std. -#![cfg_attr(not(feature = "bazel-bindings"), no_std)] +#![cfg_attr(not(feature = "std"), no_std)] // no_std (cargo-component path) global allocator — the component ABI's // cabi_realloc needs one. Single-threaded is sound: a component instance is // never re-entered concurrently by the runtime. -#[cfg(not(feature = "bazel-bindings"))] +#[cfg(not(feature = "std"))] #[global_allocator] static ALLOC: lol_alloc::AssumeSingleThreaded = unsafe { lol_alloc::AssumeSingleThreaded::new(lol_alloc::FreeListAllocator::new()) }; @@ -31,7 +39,7 @@ static ALLOC: lol_alloc::AssumeSingleThreaded = // `wasm-component-ld` failed with "module does not export a function named // `cabi_realloc`" — leaving a raw CORE MODULE where a component was expected. // Export it here, backed by the same single-threaded allocator. -#[cfg(not(feature = "bazel-bindings"))] +#[cfg(not(feature = "std"))] #[unsafe(no_mangle)] pub unsafe extern "C" fn cabi_realloc( old_ptr: *mut u8, @@ -114,7 +122,7 @@ bindings::export!(Component with_types_in bindings); // no_std (cargo-component path) needs a panic handler; the Bazel path (std) // provides its own. -#[cfg(not(feature = "bazel-bindings"))] +#[cfg(not(feature = "std"))] #[panic_handler] fn panic(_: &core::panic::PanicInfo) -> ! { loop {} From 2a374d34887af2d9439681b33e168cd0ce9b7dc4 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 29 Jul 2026 21:28:05 +0200 Subject: [PATCH 2/2] fix(bazel): opt the falcon-rate target into `std` explicitly (de-conflation, honestly scoped) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught that my feature de-conflation was correct in principle but incomplete. Flipping the semantics DID move the bazel build onto the no_std path — and that immediately exposed that the bazel target cannot support no_std yet: error[E0433]: cannot find module or crate `lol_alloc` error[E0152]: found duplicate lang item `panic_impl` Bazel does not read Cargo.toml deps (lol_alloc is absent from the target's deps), and rules_rust still links std, so the crate's own #[panic_handler] collides. Making bazel emit the WASI-free variant is therefore real work — a crate_universe entry plus a genuine no_std config — not a feature flip. So: keep the de-conflation (it is a genuine improvement and the prerequisite for convergence) and have the bazel target opt into `std` EXPLICITLY. Behaviour is unchanged from before this PR — bazel builds the std variant, cargo-component builds the WASI-free one we publish — but the two concerns are no longer riding one flag, and the remaining gap is named rather than hidden. Documented the divergence and the follow-up in docs/OCI-DISTRIBUTION.md: once bazel can build no_std, the release should consume bazel outputs and the script becomes a thin bundler — which structurally removes the bug class that shipped falcon-rate:1.129.0 as a core module. Corrects my own PR claim: I said this would make bazel produce the WASI-free component. It does not, and I am not asserting it until measured. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG --- BUILD.bazel | 13 ++++++++++++- docs/OCI-DISTRIBUTION.md | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/BUILD.bazel b/BUILD.bazel index a518d72..42cd2fd 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -754,7 +754,18 @@ rust_wasm_component_bindgen( rust_wasm_component_bindgen( name = "falcon-rate", - crate_features = ["bazel-bindings"], + # `std` is EXPLICIT here (v1.130). The features are now independent: + # bazel-bindings = bindings source, std = std-vs-no_std. Bazel opts into std + # because this target cannot build no_std yet — it would need `lol_alloc` in + # the crate_universe deps and a genuine no_std rules_rust config (without + # them: "cannot find crate `lol_alloc`" + "duplicate lang item `panic_impl`", + # since rules_rust still links std). Until that lands, the WASI-free + # component comes from the cargo-component path and Bazel builds the std + # variant. Making these converge is the follow-up (see docs/OCI-DISTRIBUTION). + crate_features = [ + "bazel-bindings", + "std", + ], srcs = ["wasm/cm/rate/src/lib.rs"], wit = ":falcon_rate_wit", deps = [":relay-rate"], diff --git a/docs/OCI-DISTRIBUTION.md b/docs/OCI-DISTRIBUTION.md index ca75f3d..ea7dd13 100644 --- a/docs/OCI-DISTRIBUTION.md +++ b/docs/OCI-DISTRIBUTION.md @@ -111,6 +111,31 @@ cosign verify ghcr.io/pulseengine/falcon-flight:1.127.0 \ channel, not a dependency. The durable, signed artifacts remain the GitHub Release and the ghcr OCI ref. +## Two build paths — and why they still differ + +The components are built **twice**, and the paths do not yet agree: + +| | Bazel (`rust_wasm_component_bindgen`) | `scripts/build-components.sh` | +|---|---|---| +| used by | the cascade / `wac_plug` / `meld_fuse` / `synth_compile` graph | the **release** (what we publish) | +| variant | **std** (carries WASI imports) | **no_std, WASI-free** | +| on build error | fails the build | fails the build (since v1.130; it used to `|| true`) | + +The published artifact is the WASI-free one, and only the script produces it today. +The feature flags are now properly independent — `bazel-bindings` selects the +bindings source, `std` selects std-vs-no_std — but the Bazel target must still +opt into `std`, because building it `no_std` needs two things it does not have: + +1. `lol_alloc` in the Bazel `deps` (Bazel does not read `Cargo.toml` deps), and +2. a genuine no_std rules_rust configuration — otherwise std is still linked and + the crate's own `#[panic_handler]` collides ("duplicate lang item `panic_impl`"). + +**Converging them is the open follow-up**: once Bazel can build the no_std +variant, the release should consume Bazel outputs and this script becomes a thin +bundler. That removes the whole class of bug that shipped `falcon-rate:1.129.0` +as a core module (silent build failure + untyped artifact selection), because a +Bazel action failure cannot be swallowed and its outputs are declared. + ## Component metadata contract (what wasm.directory renders) wasm.directory shows a component's **WIT interface** (with its `///` doc comments)