Version bump, release notes, CHANGELOG section and Play "What's new" for v0.32.0, cut at db0f4f1e — plus one fix that had to ride along, because it blocks the Windows installer.
90 commits since v0.31.4 (63 non-merge).
🛑 A release blocker found while watching CI, fixed here (87cb4314)
package's "Clippy (host + tray, Windows)" step has been failing on every push since 11217341 landed (#413). The package job dies before "Pack + sign installer", so a tag cut over this main would publish a release with no Windows host attached.
error[E0433]: cannot find `stall` in `super`
--> crates\pf-capture\src\windows\idd_push.rs:2528:27
mod stall; is declared at idd_push.rs:332, so stall is a child of idd_push and the test module is its sibling. A nested fn does not add a module level for path resolution, which is what the extra super:: was reaching for — the use super::stall::{…} two lines above, in the same test fn, already had the depth right.
It is Windows-only and test-only, so it reached main behind a green Build: the error appears solely under --all-targets, when the lib test target is compiled. Same shape as #408 — a Windows break the Mac and Linux dev loops cannot see. The commit that introduced it verified the logic by extracting stall.rs into a native scratch crate, so this test was never compiled for Windows before merging.
Reproduced and verified from the Mac with scripts/xcheck.sh, in seconds:
Command
Before
After
scripts/xcheck.sh windows clippy
exit 101, error[E0433] at :2528
exit 0
scripts/xcheck.sh linux clippy
—
exit 0
One other red run on main is NOT a code defect
ci.yml run 20723 on db0f4f1e failed at Doc-tests pf_capture with can't find crate for pf_frame — while the same run's Build and Clippy steps compiled pf-capture fine, and the failing rustdoc command has --extern pf_frame=… right there in it. cargo test -p pf-capture --doc passes locally (the crate has no doctests). That is a stale artefact in the shared cache, not a defect — no fix needed, but worth knowing before someone chases it.
The C ABI steps 25 → 26
punktfunk_connect_opts closes the punktfunk_connect_ex* ladder. Eleven generations each added a field by minting a new exported symbol plus a 20-something-parameter forwarding shim; the replacement takes every option in one size-prefixed PunktfunkConnectOpts. Every ex keeps its symbol and its behaviour, so this is an added surface rather than a changed one.
⚠️This landed while the notes were being written. The first cut of this branch sat on d2286da9 and its version table said "C ABI 25, unchanged". PRs #415–#419 merged in between, and 9c164aa6 carried the step. The branch was rebased onto db0f4f1e and every table row re-derived — tagging the first cut would have shipped a version table that was false.
The minor bump was already right without the ABI step
ABR Phase 4 redefines every control-plane bitrate as the total wire budget rather than the encoder rate, deliberately without a capability handshake. include/punktfunk_core.h already documented the 10-bit SDR ask as "0.32" and said "every pre-0.32 client sets the two bits together", so the tree had pre-decided the number.
Version table
v0.31.4
v0.32.0
Wire protocol
2
2 — three additive, ignorable additions
C ABI
25
26 — punktfunk_connect_opts
Crate dirs / members
27 / 39
27 / 39
Driver protocol
6
6 — no diff at all
Gamepad channel
3
3
Plugin index schema
1
1
Host event schema
1
1 — no diff at all
api/openapi.json
0.31.4
0.32.0 — content-identical, re-stamped
gamescope patch level
pfhdr8
pfhdr8
@punktfunk/host
0.1.6
0.1.6
@punktfunk/plugin-kit
0.4.4
0.4.4 — deliberately
plugin-kit stays put on purpose: plugin-kit/src/wire.ts changes exactly one line, and it is a doc comment in the launch.kind table. launcher_ui is a bare string validated host-side, so nothing to republish.
Seven behavioural breaking changes, none of them a version step
The wire-budget redefinition · FLOOR_KBPS 5000 → 2000 · DSCP defaults to AUTO (local peers only) · GameStream video and control encryption default on — both graduated only after their own on-glass pass on .173, and in both the client opted in unprompted on a LAN · PyroWave forces Automatic bitrate · the console pairing routes move behind the console password · deep links auto-dial by stable record id only.
A CHANGELOG entry was filed under a frozen section
ae13b29a (launcher_ui gains heroic-console, #402) landed after the v0.31.4 tag, but its CHANGELOG block was appended to the v0.31.4 section — so a shipped release's notes described a change it does not contain. Moved into the v0.32.0 section verbatim.
Release notes are written to ISO 24495 plain language
Sentences under 30 words, paragraphs under six sentences, bold bullet lead-ins, an explicit overview, a table of contents worded identically to its headings, and acronyms defined on first use. The plugin's own auditor reports 0 findings on the Play notes and 2 on the release notes — the words NVIDIA and AMD, which are company names rather than acronyms, left unexpanded deliberately.
Play notes gate 412/500 chars, unique against 15 files
Both openapi.json copies cmp identical, stamped 0.32.0
cargo auditgreen — h2 0.4.18 → 0.4.19 closed RUSTSEC-2026-0258, left open deliberately at the v0.31.1 cut
⚠️api/openapi.json is re-stamped, not regenerated (punktfunk-host does not build on macOS). It is content-identical to v0.31.4, so there is nothing to regenerate.
After merging
Re-read main's tip before pushing the tag — it moved twice during this prep alone. Then tag, wait for CI green with no run in flight on the merge sha, and dispatch announce with the tag.
Version bump, release notes, CHANGELOG section and Play "What's new" for **v0.32.0**, cut at `db0f4f1e` — plus one fix that had to ride along, because it blocks the Windows installer.
90 commits since v0.31.4 (63 non-merge).
## 🛑 A release blocker found while watching CI, fixed here (`87cb4314`)
**`package`'s "Clippy (host + tray, Windows)" step has been failing on every push since `11217341` landed (#413).** The `package` job dies before "Pack + sign installer", so **a tag cut over this main would publish a release with no Windows host attached.**
```
error[E0433]: cannot find `stall` in `super`
--> crates\pf-capture\src\windows\idd_push.rs:2528:27
```
`mod stall;` is declared at `idd_push.rs:332`, so `stall` is a child of `idd_push` and the test module is its sibling. A nested `fn` does not add a module level for path resolution, which is what the extra `super::` was reaching for — the `use super::stall::{…}` two lines above, in the same test fn, already had the depth right.
It is **Windows-only and test-only**, so it reached main behind a green `Build`: the error appears solely under `--all-targets`, when the lib **test** target is compiled. Same shape as #408 — a Windows break the Mac and Linux dev loops cannot see. The commit that introduced it verified the logic by extracting `stall.rs` into a native scratch crate, so this test was never compiled for Windows before merging.
Reproduced and verified from the Mac with `scripts/xcheck.sh`, in seconds:
| Command | Before | After |
|---|---|---|
| `scripts/xcheck.sh windows clippy` | exit **101**, `error[E0433]` at `:2528` | exit **0** |
| `scripts/xcheck.sh linux clippy` | — | exit **0** |
### One other red run on main is NOT a code defect
`ci.yml` run 20723 on `db0f4f1e` failed at `Doc-tests pf_capture` with `can't find crate for pf_frame` — while the same run's `Build` and `Clippy` steps compiled `pf-capture` fine, and the failing rustdoc command has `--extern pf_frame=…` right there in it. `cargo test -p pf-capture --doc` passes locally (the crate has no doctests). That is a stale artefact in the shared cache, not a defect — no fix needed, but worth knowing before someone chases it.
## The C ABI steps 25 → 26
`punktfunk_connect_opts` closes the `punktfunk_connect_ex*` ladder. Eleven generations each added a field by minting a new exported symbol plus a 20-something-parameter forwarding shim; the replacement takes every option in one size-prefixed `PunktfunkConnectOpts`. **Every `ex` keeps its symbol and its behaviour**, so this is an added surface rather than a changed one.
⚠️ **This landed while the notes were being written.** The first cut of this branch sat on `d2286da9` and its version table said "C ABI 25, unchanged". PRs #415–#419 merged in between, and `9c164aa6` carried the step. The branch was rebased onto `db0f4f1e` and **every table row re-derived** — tagging the first cut would have shipped a version table that was false.
## The minor bump was already right without the ABI step
ABR Phase 4 redefines every control-plane bitrate as the **total wire budget** rather than the encoder rate, deliberately without a capability handshake. `include/punktfunk_core.h` already documented the 10-bit SDR ask as "0.32" and said "every pre-0.32 client sets the two bits together", so the tree had pre-decided the number.
## Version table
| | v0.31.4 | v0.32.0 |
|---|---|---|
| Wire protocol | 2 | **2** — three additive, ignorable additions |
| C ABI | 25 | **26** — `punktfunk_connect_opts` |
| Crate dirs / members | 27 / 39 | **27 / 39** |
| Driver protocol | 6 | **6** — no diff at all |
| Gamepad channel | 3 | **3** |
| Plugin index schema | 1 | **1** |
| Host event schema | 1 | **1** — no diff at all |
| `api/openapi.json` | 0.31.4 | **0.32.0** — content-identical, re-stamped |
| gamescope patch level | pfhdr8 | **pfhdr8** |
| `@punktfunk/host` | 0.1.6 | **0.1.6** |
| `@punktfunk/plugin-kit` | 0.4.4 | **0.4.4** — deliberately |
`plugin-kit` stays put on purpose: `plugin-kit/src/wire.ts` changes exactly one line, and it is a doc comment in the `launch.kind` table. `launcher_ui` is a bare string validated host-side, so nothing to republish.
## Seven behavioural breaking changes, none of them a version step
The wire-budget redefinition · `FLOOR_KBPS` 5000 → 2000 · DSCP defaults to AUTO (local peers only) · **GameStream video *and* control encryption default on** — both graduated only after their own on-glass pass on `.173`, and in both the client opted in unprompted on a LAN · PyroWave forces Automatic bitrate · the console pairing routes move behind the console password · deep links auto-dial by stable record id only.
## A CHANGELOG entry was filed under a frozen section
`ae13b29a` (`launcher_ui` gains `heroic-console`, #402) landed **after** the v0.31.4 tag, but its CHANGELOG block was appended to the **v0.31.4 section** — so a shipped release's notes described a change it does not contain. Moved into the v0.32.0 section verbatim.
## Release notes are written to ISO 24495 plain language
Sentences under 30 words, paragraphs under six sentences, bold bullet lead-ins, an explicit overview, a table of contents worded identically to its headings, and acronyms defined on first use. The plugin's own auditor reports **0 findings on the Play notes** and **2 on the release notes** — the words NVIDIA and AMD, which are company names rather than acronyms, left unexpanded deliberately.
## Gates on the cut (the Mac)
- `cargo fmt --all --check`
- `cargo metadata --offline`; lock diff **versions-only 36/36**
- `cargo test -p punktfunk-core --lib --features quic` — **503 passed / 0 failed**
- **C ABI harness PASSED (`abi_version=26`)**; the regenerated header is not stale
- `scripts/xcheck.sh windows clippy` and `scripts/xcheck.sh linux clippy` — both **exit 0**
- `scripts/ci/check-docs-drift.sh`, `scripts/ci/check-docs-links.sh`
- Play notes gate **412/500 chars**, unique against 15 files
- Both `openapi.json` copies `cmp` identical, stamped 0.32.0
- `cargo audit` **green** — h2 0.4.18 → 0.4.19 closed RUSTSEC-2026-0258, left open deliberately at the v0.31.1 cut
⚠️ `api/openapi.json` is **re-stamped, not regenerated** (`punktfunk-host` does not build on macOS). It is content-identical to v0.31.4, so there is nothing to regenerate.
## After merging
Re-read main's tip **before** pushing the tag — it moved twice during this prep alone. Then tag, wait for CI green with no run in flight on the merge sha, and dispatch `announce` with the tag.
90 commits since v0.31.4 (63 non-merge), cut at db0f4f1e.
**The C ABI steps 25 -> 26.** `punktfunk_connect_opts` closes the
`punktfunk_connect_ex*` ladder: eleven generations each added a field by minting
a new exported symbol plus a 20-something-parameter forwarding shim, and the
replacement takes every option in ONE size-prefixed `PunktfunkConnectOpts`.
Every `ex` keeps its symbol and behaviour, so this is an added surface, not a
changed one. Layout is locked padding-free on both pointer widths (96/68 B) by
const asserts AND by the C harness compiling the same sizes.
The minor bump was already right before the ABI moved: ABR Phase 4 redefines
every control-plane bitrate as the TOTAL WIRE BUDGET rather than the encoder
rate, deliberately without a capability handshake. `include/punktfunk_core.h`
documents the 10-bit SDR ask as "0.32" and says "every pre-0.32 client sets the
two bits together", so the tree had pre-decided the number.
Version table, re-derived at HEAD rather than read off the diff:
* wire 2 -- unchanged. Three additive, ignorable additions: USER_FLAG_REPEAT,
CLIENT_CAP_KEEP_HOST_AUDIO, and Welcome::host_caps2 (trailing, absent -> 0)
carrying HOST_CAP2_REPEAT_MARK
* C ABI 25 -> 26 (above)
* crate dirs 27, 39 members, edition 2024, MSRV 1.85 -- unchanged
* driver proto 6, gamepad channel 3, plugin index 1, host event schema 1,
gamescope +pfhdr8, SDK 0.1.6 -- all unchanged; pf-driver-proto and
punktfunk-host/src/events.rs show no diff at all
* api/openapi.json 0.31.4 -> 0.32.0, content-identical (info.version is the
whole diff); both copies re-stamped and byte-identical to each other
* @punktfunk/plugin-kit stays 0.4.4 ON PURPOSE: plugin-kit/src/wire.ts changes
exactly one line and it is a doc comment in the launch.kind table.
`launcher_ui` is a bare string validated host-side, so nothing to republish.
The CHANGELOG's `heroic-console` entry was filed under v0.31.4, but ae13b29a
landed AFTER that tag -- so that frozen section described a change v0.31.4 does
not contain. Moved into the v0.32.0 section verbatim.
Seven behavioural breaking changes, none of them a version step, all listed in
CHANGELOG.md: the wire-budget redefinition; FLOOR_KBPS 5000 -> 2000; DSCP
defaults to AUTO (local peers only); GameStream video AND control encryption
default on (both graduated after their own on-glass pass on .173, and
`PUNKTFUNK_GS_ENCRYPT=video` is the new middle rung); PyroWave forces Automatic
bitrate; the console pairing routes move behind the console password (the
security review's critical finding -- a console session cookie alone reached
code execution); deep links auto-dial by stable record id only.
Release notes rewritten to the ISO 24495-1/-5 plain-language rules: sentences
under 30 words, paragraphs under 6 sentences, bold bullet lead-ins, an explicit
overview, a table of contents, and acronyms defined on first use. The plugin's
own text auditor reports 0 findings on the Play notes and 2 on the release
notes, both of which are the words NVIDIA and AMD -- company names, not
acronyms, deliberately left unexpanded.
`cargo audit` stays GREEN: h2 0.4.18 -> 0.4.19 closed RUSTSEC-2026-0258, left
open deliberately at the v0.31.1 cut. Only two allowed `unmaintained` warnings
remain (audiopus_sys, paste).
The Play whatsnew leads with the Android black screen, which is the one fault a
phone or TV user in this cycle could not work around.
Gates on the cut (the Mac): cargo fmt --all --check; cargo metadata --offline;
lock diff versions-only 36/36; cargo test -p punktfunk-core --lib --features
quic 503 passed / 0 failed; the C ABI harness PASSED (abi_version=26); the
regenerated header is not stale; check-docs-drift.sh; check-docs-links.sh; the
Play notes gate 412/500 chars and unique against 15 files; both openapi copies
cmp identical; repo voice scan clean; cargo audit green.
`super::super::stall::classify` in `idd_push.rs`'s test module resolves to
`windows::stall`, which does not exist — `mod stall;` is declared at
`idd_push.rs:332`, so `stall` is a child of `idd_push`, and the test module is
its sibling. A nested `fn` does not add a module level for path resolution,
which is what the extra `super::` was reaching for. The `use
super::stall::{ProbeWindow, StallClass, StallVerdict}` two lines above, in the
same test fn, already had the depth right.
Windows-only and test-only, so it reached main behind a green Build: the error
appears solely under `--all-targets`, when the lib TEST target is compiled.
`package`'s "Clippy (host + tray, Windows)" step has failed on every push since
`11217341` landed it (#413), which is the same shape as #408 — a Windows break
the Mac and Linux dev loops cannot see. The commit that introduced it verified
the logic by extracting `stall.rs` into a native scratch crate, so this test was
never compiled for Windows before it merged.
Reproduced and fixed with `scripts/xcheck.sh`, which covers exactly this and
takes seconds on the Mac:
scripts/xcheck.sh windows clippy # broken: exit 101, error[E0433] at :2528
scripts/xcheck.sh windows clippy # fixed: exit 0
scripts/xcheck.sh linux clippy # exit 0
Rides in the 0.32.0 release branch because it blocks the Windows installer: the
`package` job fails before "Pack + sign installer", so a tag cut over this main
would publish a release with no Windows host attached.
`ci.yml`'s `cargo test --workspace --locked` has been failing at the very last
step for weeks, on a crate the diff never touched:
Doc-tests pf_capture
error[E0463]: can't find crate for `pf_frame`
--> crates/pf-capture/src/lib.rs:11:5
`pf-frame` is an unconditional dependency of `pf-capture`, the rustdoc command
line carries `--extern pf_frame=<path>`, and Format, both Clippy legs and Build
had all compiled `pf-capture` seconds earlier. The path simply no longer existed
by the time rustdoc opened it.
`tests/c_abi.rs::ensure_staticlib` is what removed it. It shells out to a NESTED
`cargo build -p punktfunk-core --features quic` while the OUTER `cargo test` is
mid-run. That resolves features for one package instead of the workspace union
the outer run resolved, so cargo rebuilds punktfunk-core's subgraph under
different metadata into the SHARED target directory — and the outer run's
pending units, which name `target/<profile>/deps/*.rlib` by explicit `--extern`
path, are left pointing at artifacts that have been replaced. Doctests run last,
so they are what falls over.
The old comment asserted the opposite, and that is the bug in one line:
// The outer cargo's build lock is released during test execution, so this is safe.
The released lock is why the nested build RUNS. It was never why it is safe.
Fix: build into `target/c-abi-harness/` and read the staticlib from there. The
nested build can no longer perturb the outer one, and the harness keeps doing
exactly what it did. Unconditional `--features quic` stays — that is `9c164aa6`'s
fix for a featureless `.a` being silently reused, and it is orthogonal.
Reproduced and verified on Ubuntu 26.04 (the CI base — 24.04's PipeWire is too
old to compile pf-capture's lib test), one variable at a time:
cargo test -p punktfunk-core -p pf-capture --locked --no-fail-fast
before -> exit 101, E0463 on pf_frame
before, --skip c_abi_harness_round_trips -> exit 0 (isolates the cause)
after -> exit 0, harness still PASSes
(abi_version=26, 4 frames round-tripped)
Each from a wiped target dir. `cargo test -p punktfunk-core --test c_abi` also
passes on macOS, where the staticlib path moved.
Why it looked intermittent: whether the doctest step is reached at all depends on
what else in the workspace fails first, and `9c164aa6` made the nested build
unconditional — before that it ran only when the `.a` was missing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Version bump, release notes, CHANGELOG section and Play "What's new" for v0.32.0, cut at
db0f4f1e— plus one fix that had to ride along, because it blocks the Windows installer.90 commits since v0.31.4 (63 non-merge).
🛑 A release blocker found while watching CI, fixed here (
87cb4314)package's "Clippy (host + tray, Windows)" step has been failing on every push since11217341landed (#413). Thepackagejob dies before "Pack + sign installer", so a tag cut over this main would publish a release with no Windows host attached.mod stall;is declared atidd_push.rs:332, sostallis a child ofidd_pushand the test module is its sibling. A nestedfndoes not add a module level for path resolution, which is what the extrasuper::was reaching for — theuse super::stall::{…}two lines above, in the same test fn, already had the depth right.It is Windows-only and test-only, so it reached main behind a green
Build: the error appears solely under--all-targets, when the lib test target is compiled. Same shape as #408 — a Windows break the Mac and Linux dev loops cannot see. The commit that introduced it verified the logic by extractingstall.rsinto a native scratch crate, so this test was never compiled for Windows before merging.Reproduced and verified from the Mac with
scripts/xcheck.sh, in seconds:scripts/xcheck.sh windows clippyerror[E0433]at:2528scripts/xcheck.sh linux clippyOne other red run on main is NOT a code defect
ci.ymlrun 20723 ondb0f4f1efailed atDoc-tests pf_capturewithcan't find crate for pf_frame— while the same run'sBuildandClippysteps compiledpf-capturefine, and the failing rustdoc command has--extern pf_frame=…right there in it.cargo test -p pf-capture --docpasses locally (the crate has no doctests). That is a stale artefact in the shared cache, not a defect — no fix needed, but worth knowing before someone chases it.The C ABI steps 25 → 26
punktfunk_connect_optscloses thepunktfunk_connect_ex*ladder. Eleven generations each added a field by minting a new exported symbol plus a 20-something-parameter forwarding shim; the replacement takes every option in one size-prefixedPunktfunkConnectOpts. Everyexkeeps its symbol and its behaviour, so this is an added surface rather than a changed one.⚠️ This landed while the notes were being written. The first cut of this branch sat on
d2286da9and its version table said "C ABI 25, unchanged". PRs #415–#419 merged in between, and9c164aa6carried the step. The branch was rebased ontodb0f4f1eand every table row re-derived — tagging the first cut would have shipped a version table that was false.The minor bump was already right without the ABI step
ABR Phase 4 redefines every control-plane bitrate as the total wire budget rather than the encoder rate, deliberately without a capability handshake.
include/punktfunk_core.halready documented the 10-bit SDR ask as "0.32" and said "every pre-0.32 client sets the two bits together", so the tree had pre-decided the number.Version table
punktfunk_connect_optsapi/openapi.json@punktfunk/host@punktfunk/plugin-kitplugin-kitstays put on purpose:plugin-kit/src/wire.tschanges exactly one line, and it is a doc comment in thelaunch.kindtable.launcher_uiis a bare string validated host-side, so nothing to republish.Seven behavioural breaking changes, none of them a version step
The wire-budget redefinition ·
FLOOR_KBPS5000 → 2000 · DSCP defaults to AUTO (local peers only) · GameStream video and control encryption default on — both graduated only after their own on-glass pass on.173, and in both the client opted in unprompted on a LAN · PyroWave forces Automatic bitrate · the console pairing routes move behind the console password · deep links auto-dial by stable record id only.A CHANGELOG entry was filed under a frozen section
ae13b29a(launcher_uigainsheroic-console, #402) landed after the v0.31.4 tag, but its CHANGELOG block was appended to the v0.31.4 section — so a shipped release's notes described a change it does not contain. Moved into the v0.32.0 section verbatim.Release notes are written to ISO 24495 plain language
Sentences under 30 words, paragraphs under six sentences, bold bullet lead-ins, an explicit overview, a table of contents worded identically to its headings, and acronyms defined on first use. The plugin's own auditor reports 0 findings on the Play notes and 2 on the release notes — the words NVIDIA and AMD, which are company names rather than acronyms, left unexpanded deliberately.
Gates on the cut (the Mac)
cargo fmt --all --checkcargo metadata --offline; lock diff versions-only 36/36cargo test -p punktfunk-core --lib --features quic— 503 passed / 0 failedabi_version=26); the regenerated header is not stalescripts/xcheck.sh windows clippyandscripts/xcheck.sh linux clippy— both exit 0scripts/ci/check-docs-drift.sh,scripts/ci/check-docs-links.shopenapi.jsoncopiescmpidentical, stamped 0.32.0cargo auditgreen — h2 0.4.18 → 0.4.19 closed RUSTSEC-2026-0258, left open deliberately at the v0.31.1 cut⚠️
api/openapi.jsonis re-stamped, not regenerated (punktfunk-hostdoes not build on macOS). It is content-identical to v0.31.4, so there is nothing to regenerate.After merging
Re-read main's tip before pushing the tag — it moved twice during this prep alone. Then tag, wait for CI green with no run in flight on the merge sha, and dispatch
announcewith the tag.`super::super::stall::classify` in `idd_push.rs`'s test module resolves to `windows::stall`, which does not exist — `mod stall;` is declared at `idd_push.rs:332`, so `stall` is a child of `idd_push`, and the test module is its sibling. A nested `fn` does not add a module level for path resolution, which is what the extra `super::` was reaching for. The `use super::stall::{ProbeWindow, StallClass, StallVerdict}` two lines above, in the same test fn, already had the depth right. Windows-only and test-only, so it reached main behind a green Build: the error appears solely under `--all-targets`, when the lib TEST target is compiled. `package`'s "Clippy (host + tray, Windows)" step has failed on every push since `11217341` landed it (#413), which is the same shape as #408 — a Windows break the Mac and Linux dev loops cannot see. The commit that introduced it verified the logic by extracting `stall.rs` into a native scratch crate, so this test was never compiled for Windows before it merged. Reproduced and fixed with `scripts/xcheck.sh`, which covers exactly this and takes seconds on the Mac: scripts/xcheck.sh windows clippy # broken: exit 101, error[E0433] at :2528 scripts/xcheck.sh windows clippy # fixed: exit 0 scripts/xcheck.sh linux clippy # exit 0 Rides in the 0.32.0 release branch because it blocks the Windows installer: the `package` job fails before "Pack + sign installer", so a tag cut over this main would publish a release with no Windows host attached.`ci.yml`'s `cargo test --workspace --locked` has been failing at the very last step for weeks, on a crate the diff never touched: Doc-tests pf_capture error[E0463]: can't find crate for `pf_frame` --> crates/pf-capture/src/lib.rs:11:5 `pf-frame` is an unconditional dependency of `pf-capture`, the rustdoc command line carries `--extern pf_frame=<path>`, and Format, both Clippy legs and Build had all compiled `pf-capture` seconds earlier. The path simply no longer existed by the time rustdoc opened it. `tests/c_abi.rs::ensure_staticlib` is what removed it. It shells out to a NESTED `cargo build -p punktfunk-core --features quic` while the OUTER `cargo test` is mid-run. That resolves features for one package instead of the workspace union the outer run resolved, so cargo rebuilds punktfunk-core's subgraph under different metadata into the SHARED target directory — and the outer run's pending units, which name `target/<profile>/deps/*.rlib` by explicit `--extern` path, are left pointing at artifacts that have been replaced. Doctests run last, so they are what falls over. The old comment asserted the opposite, and that is the bug in one line: // The outer cargo's build lock is released during test execution, so this is safe. The released lock is why the nested build RUNS. It was never why it is safe. Fix: build into `target/c-abi-harness/` and read the staticlib from there. The nested build can no longer perturb the outer one, and the harness keeps doing exactly what it did. Unconditional `--features quic` stays — that is `9c164aa6`'s fix for a featureless `.a` being silently reused, and it is orthogonal. Reproduced and verified on Ubuntu 26.04 (the CI base — 24.04's PipeWire is too old to compile pf-capture's lib test), one variable at a time: cargo test -p punktfunk-core -p pf-capture --locked --no-fail-fast before -> exit 101, E0463 on pf_frame before, --skip c_abi_harness_round_trips -> exit 0 (isolates the cause) after -> exit 0, harness still PASSes (abi_version=26, 4 frames round-tripped) Each from a wiped target dir. `cargo test -p punktfunk-core --test c_abi` also passes on macOS, where the staticlib path moved. Why it looked intermittent: whether the doctest step is reached at all depends on what else in the workspace fails first, and `9c164aa6` made the nested build unconditional — before that it ran only when the `.a` was missing.