ci(release): build both musl legs with cargo-zigbuild via napi -x; retire the hand-written zig cc wrappers (#339) - #373
Merged
Merged
Conversation
Appends describe('B3b: musl legs build with cargo-zigbuild (#339)') with
spec S22 and three module-level helpers (jobSteps, stepMatching,
matrixEntries). The spec asserts the required shape of build-napi AFTER
Phase B2 replaces the hand-written zig-cc wrappers with napi build -x
(cargo-zigbuild 0.23.0).
RED on main (211 pass / 1 fail): first real-file assertion fires because
the current musl build: lines lack -x. All 211 existing tests pass.
node scripts/verify-no-control-bytes.mjs passes.
…0; delete the zig cc wrappers (#339) - Both musl matrix entries: append -x to the napi build command; delete the setup: block (zig cc heredocs, CARGO_TARGET_*_MUSL_LINKER exports, aarch64 fake-zig self-check). cargo-zigbuild owns the linker-arg filter internally (src/zig/linker_args.rs); napi still injects -C target-feature=-crt-static. - Insert "Install cargo-zigbuild (pinned)" (taiki-e/install-action SHA, cargo-zigbuild@0.23.0, fallback: none) BEFORE Swatinem/rust-cache so the pin is installed before rust-cache can delete ~/.cargo/bin (PF-040, PF-038). - Upgrade mlugg/setup-zig to SHA d1434d08867e3ee9daa34448df10607b98908d29 (= v2.2.1) per PF-040 (composite action; SHA pin is correct here). - Add "Assert cargo-zigbuild is the pinned version" step (after Install zig): positive control, cargo zigbuild --version match, cargo help zigbuild check. - Add "Verify no lingering musl linker export or wrapper" (no-op detector, before Build addon): asserts both MUSL_LINKER vars are unset, no /tmp/zig-cc-* exists, and ~/.cache/cargo-zigbuild/0.23.0 does not yet exist. - Add "Assert cargo-zigbuild 0.23.0 wrappers were generated" (after Build addon): proves cargo zigbuild ran by finding zigcc-* under ~/.cache/cargo-zigbuild/0.23.0; re-asserts version to catch a mid-build replacement by napi. - Extend the readelf gate with ALLOWED_NEEDED='libc\.so|libgcc_s\.so\.1' plus a libunwind.so.1 planted positive control (PF-013, PF-038). Turns spec S22 (RED since eff86f4) GREEN: 212 pass / 0 fail.
…ls (#339) B3a: tighten S22 — replace readelfStep.body.includes('libgcc_s.so.1') with assert.match against /ALLOWED_NEEDED='[^']*libgcc_s\\\.so\\\.1[^']*'/ and add a libc\.so regex assertion; add PC(B3a) planted control (ALLOWED_NEEDED with only libc.so must not pass the libgcc_s assertion); remove the now-redundant inline comment from the ALLOWED_NEEDED line in release.yml (the spec now pins to the variable assignment literal, not a comment); suite stays 212/0. B3b-e: document the cargo-zigbuild migration across RELEASING.md (Notes bullet — musl build mechanism, pre-install-before-rust-cache, add_env_if_missing, version-keyed wrapper cache, ALLOWED_NEEDED, Alpine load tests as acceptance instrument), CLAUDE.md (Linux cross-builds gotcha), CHANGELOG.md ([Unreleased] Internal bullet), and .devflow/features/release-pipeline/KNOWLEDGE.md (new build-napi musl subsection, three new anti-patterns, four new gotchas, updated S22 spec description in Key Files). B4a: all verification commands pass (212/0, musl matrix -x present, no MUSL_LINKER SET forms, no zig cc residue, no PF-045 violations, versions OK). B4b: six PF-013 real-file controls each turn S22 RED (and mutation 5 also turns S20 RED); suite returns to 212/0 after each revert.
…ersion` (#339) `cargo zigbuild --version` is not a version probe: cargo-zigbuild's clap enum (src/bin/cargo-zigbuild.rs) sets `version` on the top-level command only and never sets `propagate_version`, and neither `cargo_options::Build` nor `CommonOptions` defines a `--version` arg. The subcommand form therefore exits 2 with `error: unexpected argument '--version' found`. Under `set -euo pipefail` that aborted both `Assert cargo-zigbuild is the pinned version` and `Assert cargo-zigbuild 0.23.0 wrappers were generated`, failing BOTH musl legs of `build-napi` on every tag push, dispatch and release-surface PR. Verified by building cargo-zigbuild 0.23.0 locally: the subcommand form exits 2, and `cargo-zigbuild --version` prints exactly `cargo-zigbuild 0.23.0`. Both steps now probe the binary. `grep -qx` becomes `grep -Fqx` so the dots in the pinned version are literal. `cargo help zigbuild` (napi's own predicate) is unchanged and confirmed non-vacuous: exit 101 absent, 0 present. Also hardened, each verified against cargo-zigbuild 0.23.0's own source: - Wrapper-cache path now resolves as `${XDG_CACHE_HOME:-$HOME/.cache}` in both the no-op detector and the post-build assert, mirroring cargo-zigbuild's `cache_dir()` (src/zig.rs, `dirs::cache_dir()`). A hard-coded `$HOME/.cache` makes the pre-build absence check vacuous wherever XDG_CACHE_HOME is set (PF-013). - The post-build wrapper search captures `find` output instead of piping into `grep -q`, removing the pipefail/SIGPIPE interaction with a short-circuiting reader and printing the matched wrappers. cargo-zigbuild writes `zigcc-<target>-<hash>.sh` under `<cache>/wrappers/<exe-hash>/`, so the search stays recursive. - Corrected `src/zig/linker_args.rs` to `src/zig.rs` (0.23.0 has no `src/zig/` directory) and rewrote the ALLOWED_NEEDED rationale: cargo-zigbuild rewrites `-lgcc_s` to `-lunwind` and zig links its own libunwind in, so a `libunwind.so.1` DT_NEEDED is exactly the thing the control must reject. Spec S22 gains Trap 3 (the broken probe must never appear in `build-napi`), a `${XDG_CACHE_HOME:-$HOME/.cache}` assertion on both cache-path steps, and three new planted controls: PC9 (broken vs. binary probe form), PC10 (bare assignment, `$GITHUB_ENV` append and YAML `env:` mapping SET forms all fail the READ regex, extending PC7's `export` case), PC11 (a hard-coded `$HOME/.cache` path is rejected). Stale "Phase B2 / RED until" wording replaced with the end state. Verification: `npm run test:gates` 212/212; control-bytes gate clean; release.yml parses. Five mutation controls each turn S22 red and restore green — drop ` -x` from a musl build line, move install-action below rust-cache, add a `CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER` export to `Setup cross-compilation tools`, revert to the subcommand probe, hard-code `$HOME/.cache`. All three new run blocks were extracted and executed against a real cargo-zigbuild 0.23.0 (9 happy/negative scenarios) and the readelf gate against a stubbed `readelf` (6 fixtures incl. libunwind, glibc, empty).
…ds to the detector; drop the stale RELEASE-FLOW pointer (#339)
93 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Both musl napi legs (
x86_64-unknown-linux-musl,aarch64-unknown-linux-musl) ofbuild-napinow cross-compile withnapi build --platform --release --target <t> --no-js -x—@napi-rs/cli3.8.6's--cross-compile, which runscargo zigbuild— instead of the hand-written/tmp/zig-cc-*wrapper scripts. Deleted: both wrapper heredocs, the aarch64 fake-zig self-check, and BOTHCARGO_TARGET_*_MUSL_LINKERexports (the muslsetup:blocks are gone; the aarch64-gnusetup:is untouched).taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad(v2.85.10, 2026-08-07; a composite action, so a SHA pin is the right shape) withtool: cargo-zigbuild@0.23.0,fallback: none,env: GITHUB_TOKEN, placed BEFORESwatinem/rust-cache.Install zigis SHA-pinned tomlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29(v2.2.1), zig stays 0.16.0.Assert cargo-zigbuild is the pinned version(afterInstall zig: a positive control that rejects 9.9.9,cargo-zigbuild --versionmust be exactlycargo-zigbuild 0.23.0, andcargo help zigbuild— napi's own presence predicate — must succeed); a no-op detector beforeBuild addon(no musl linker env set, no/tmp/zig-cc-*, no pre-existing${XDG_CACHE_HOME:-$HOME/.cache}/cargo-zigbuild/0.23.0); and a post-build step (that cache directory now holdszigcc-*wrappers — proof that cargo-zigbuild 0.23.0 ran in THIS job — and the version is re-asserted, because napi could have replaced the binary mid-build).libc.soassertion and adds a NEEDED allowlistlibc\.so|libgcc_s\.so\.1with a plantedlibunwind.so.1control that must be rejected; the full NEEDED list is printed.scripts/__test__/release-auth-probe.spec.mjs, one test, planted controls PC1–PC12) pins the-xflag on both musl build lines (word-boundary matcher;-xyzdoes not count), the absence ofsetup:on musl entries, zero wrapper residue outside comments, zero musl-linker SETs anywhere (only[ -zREADs, and only inside the detector step), the install-step pin/tool/fallback: none/GITHUB_TOKEN, the ordering install-action < rust-cache <Install zig< version assert < detector <Build addon< post-build < readelf, the binary-form version probe, the XDG-aware cache path, the allowlist literal, the setup-zig SHA,use-zig: trueon both musl entries, and (with S20) the rust-cachekey:. The rust-cache step and its per-leg-cache comment are byte-identical to main..release/RELEASE-FLOW.md(its "see ci(release): move both musl zig wrappers to cargo-zigbuild's maintained linker-arg filter #339" pointer).Options considered
napi build … -x(chosen). Deletes both wrappers, removes the after-rust-cache export ordering entirely, and matches napi-rs ecosystem practice; cargo-zigbuild owns the linker-arg filter (--fix-cortex-a53-843419,--no-undefined-version,-znostart-stop-gc,-plugin-opt*,-lgcc_sto-lunwind, self-contained musl CRT skip, response files) with unit tests, instead of the subset the wrappers carried. napi still injects-C target-feature=-crt-static, so dynamic musl stays and the readelflibc.soassertion is unchanged.exec cargo-zigbuild zig cc -target <triple> "$@"— keeps the export ordering and the wrapper files; rejected.--use-napi-crosshas no musl toolchain (and cannot be combined with-x).Why 0.23.0
Released 2026-06-18 (82 days soaked at decision time); its
src/zig.rscarries the filter with unit tests; its own CI tests zig 0.16.0; the 32 commits up to 0.23.4 touch nothing on x86_64/aarch64 musl.The two traps this PR is built around
cargo help zigbuild; on failure it runs an UNPINNEDcargo install cargo-zigbuildDURING the build. rust-cache deletes every~/.cargo/binbinary that pre-dated it before saving, so a tool installed BEFORE rust-cache never enters the cache and cannot be shadowed by a restore — hence install first, assert after (fallback: noneforbids a silent cargo-binstall/source-build substitution).add_env_if_missing. cargo-zigbuild yields to a pre-setCARGO_TARGET_*_LINKER, so one leftover export would silently revert the migration with every gate green. Hence zero exports, the no-op detector, and S22's SET-anywhere rule.Corrections to the issue text
~/.cache/cargo-zigbuild/<version>/…), so an upgrade always gets a fresh directory. It is deliberately NOT cached: its presence after the build is the run-proof.cargo zigbuild --version; that is rejected by cargo-zigbuild's clap setup (error: unexpected argument '--version', verified against a built 0.23.0). The gate probes thecargo-zigbuildbinary directly, and S22 rejects the subcommand form outside comments.XDG_CACHE_HOME(cargo-zigbuild usesdirs::cache_dir()), so the pre-build absence assertion cannot go vacuous on a runner that sets it.readelf NEEDED before/after
Before: the gate rejected glibc sonames only. After: every NEEDED entry must match
libc\.so|libgcc_s\.so\.1; an unexpected soname (e.g.libunwind.so.1) fails the leg with a message pointing at the Alpine load tests — which are the acceptance instrument for any linkage delta: #340 (PR #370) loads both musl addons onnode:22-alpineand callscompile()before anything publishes. Observed NEEDED list on this PR:0x0000000000000001 (NEEDED) Shared library: [libc.so];libgcc_s.so.1is allowed but did not appear; the planted control printedpositive control OK: the NEEDED allowlist rejects libunwind.so.1.Verification
Local:
npm run test:gates211 → 212 pass / 0 fail; control-bytes and version gates pass; js-yaml: 7 matrix entries, both musl builds end in-xwith nosetup:anduse-zig: true, step order as pinned, 11 jobs, both Alpine run blocks still byte-equal. PF-013 real-file/scratch mutation controls, each red: remove-x; install step below rust-cache;fallback: cargo-binstall; a musl linker export re-added (bash export,>> "$GITHUB_ENV", and YAMLenv:forms); a READ-form line outside the detector; asetup:key with a wrapper heredoc on a musl entry; rust-cachekey:dropped (S20 AND S22 red); setup-zig@v2; the subcommand version probe; a hard-coded$HOME/.cache;libgcc_s\.so\.1removed from the allowlist;cargo-zigbuild@0.23.4. Bash dry runs of the three new step bodies with shims: correct/wrong version, failingcargo help zigbuild, linker env set, pre-existing cache, missing wrappers, wrong post-build version, and readelf fixtures (libc.so;+libgcc_s.so.1;+libunwind.so.1rejected) all behaved as designed.CI (filled in before merge):
info: installing cargo-zigbuild@0.23.0/downloading …/v0.23.0/cargo-zigbuild-x86_64-unknown-linux-musl.tar.xz/verifying sha256 checksum;positive control OK: the matcher rejects a version that is not the pin;cargo-zigbuild --version -> cargo-zigbuild 0.23.0;cargo help zigbuild OK - napi will use the pinned binary;no-op detector OK: no musl linker export, no wrapper, no pre-existing cargo-zigbuild cache at /home/runner/.cache/cargo-zigbuild/0.23.0; wrappers…/0.23.0/wrappers/d7b6/zigcc-x86_64-unknown-linux-musl-e859.shand…/zigcc-aarch64-unknown-linux-musl-5d3b.sh;cargo-zigbuild 0.23.0 wrappers generated by this build; zero hits forInstalling cargo binary/cargo install cargo-zigbuildin the build step. Both Alpine load tests:positive control OK: load failed without the musl package, on the musl key,musl detected via /usr/bin/ldd (52 bytes) … arch=x64/(53 bytes) … arch=arm64,resolved @mdscript/mds-napi-linux-<arch>-musl -> /w/node_modules/…,load ok: compile(...) -> "Hello alpine!\n"; arm64 runner queue 5 s.CI history OK for 4657a20433a725bea2ca3f0febd55b1045994d80; same musl/Alpine evidence. Note: rust-cache was already a full-match restore for all 7 legs in the PR run and dispatch refactor: tech debt — EvalContext, Lexer struct, Arc sharing, IndexSet #1 with keys of the formv0-rust-<target>-build-napi-<os>-<arch>-<envhash>-4c33221b(e.g.…-Linux-x64-6ff13d87-4c33221b), consistent with the TestPyPI dispatch on main (run 34261427934) having populated the main scope with the identical key: the deleted linker exports were written toGITHUB_ENVafter rust-cache and never entered the env hash, so this change does not move the key.Restored from cache key "<own key>" full match: true.+Cache up-to-date.with keys identical to dispatch refactor: tech debt — EvalContext, Lexer struct, Arc sharing, IndexSet #1 (v0-rust-aarch64-unknown-linux-musl-build-napi-Linux-x64-6ff13d87-4c33221b,v0-rust-x86_64-unknown-linux-musl-build-napi-Linux-x64-6ff13d87-4c33221b,v0-rust-aarch64-unknown-linux-gnu-build-napi-Linux-x64-6ff13d87-4c33221b,v0-rust-x86_64-unknown-linux-gnu-build-napi-Linux-x64-6ff13d87-4c33221b,v0-rust-aarch64-apple-darwin-build-napi-Darwin-arm64-2eab217e-4c33221b,v0-rust-x86_64-apple-darwin-build-napi-Darwin-arm64-2eab217e-4c33221b,v0-rust-x86_64-pc-windows-msvc-build-napi-Windows_NT-x64-2113753f-4c33221b); the no-op detector still found no pre-existing wrapper cache (it is not in the cache), wrappers generated fresh, both Alpine load tests green.node scripts/verify-pr-checks.mjs 373PASS: three release.yml suites (92890731994 dispatch, 92889763796 dispatch, 92887984866 PR), five allowed-skipped lines each,release surface touched (1 file(s): .github/workflows/release.yml), all 15 required contexts completed+success, verified SHA 4657a20.Snyk: the local Snyk MCP server failed to connect; the
security/snyk (dean0x)PR check is the scan of record (YAML/shell/JS spec only).Related Issues
Closes #339
Refs #340 (PR #370 — the Alpine load tests that accept this change)
Refs #371 (product bug surfaced by #370's first run; unchanged here)
#351 unchanged (upstream maturin-action defect)