Tier A of #299. No macOS backend, no shipped macOS host — this is the seam only.
The measurement
cargo check -p punktfunk-host on macOS 26 / rustc 1.96.0:
Date
Errors
Files
2026-08-01
9
5
2026-08-18
10
6
2026-08-30 (this branch's base)
11
6
after this branch
0
—
Roughly one new unguarded call site a fortnight, with nobody working on macOS. That growth rate is the argument for the gate, not the fix.
What was actually wrong
Not one error was missing functionality. Every one was a call site or a dependency assuming Linux-or-Windows without saying so.
opus and libc were declared only for the platforms with a capture backend. The code reaching for them is portable: the mic pump decodes, and both hooks.rs helpers are already #[cfg(unix)]. opus now covers cfg(any(unix, windows)) — punktfunk-core already vendors it for the Apple client, so cargo unifies the build and it costs nothing. libc moves to cfg(unix), which is what the code claimed all along.
pf-vdisplay re-exported try_recover_session and cancel_pending_tv_restore behind a Linux gate — although both already have #[cfg(not(target_os = "linux"))] arms. The functions were portable; only the pub use was not. Moving them into the ungated block fixed three call sites at once (gamestream/stream.rs, native/compositor.rs ×2).
cursor_blend_for split on Windows rather than on Linux. So a third platform fell into the "not Windows" arm, which reads encode::linux_zero_copy_is_vaapi(), zerocopy::enabled() and encode::cursor_blend_capable() — all Linux-only. Flipped to linux / not(linux). Windows behaviour is bit-identical: it returned false before and returns false now. The sibling gamescope_cursor_for is left alone, and still keeps gamescope_needs_host_cursor live off Linux.
gamelease reached past procscan's own platform-neutral wrappers to crate::procscan::Scanner::system(), which that module's header explicitly forbids ("Everything above the scanner is platform-neutral"). Added procscan::alive, mirroring the names / resolve wrappers beside it.
The one behavioural note
end_previous_launch used to hoist one Scanner and reuse it across the TERM_GRACE poll loop; through the wrapper it builds one per call, as names and resolve already do. That is getuid + sysconf + a small PathBuf on Linux and a unit struct on Windows, a few times over a bounded loop. Measured cheap enough not to warrant an exception to the module's own boundary.
The gate
.gitea/workflows/macos-host.yml, a separate workflow. Two reasons it is not a job in apple.yml, which already owns this runner:
That file's paths: filter is deliberately narrow — punktfunk-core and clients/apple — because the mini is shared and an xcframework build is heavy. A host-crate edit would never have fired it, so the gate would have been decorative.
Its distribute job is needs: swift. Anything that later grows the same dependency can block a TestFlight upload over a host compile hole the client has no part in.
cargo check, not clippy, and no -D warnings: the graph carries code that is dead on macOS (pf-encode::vbv_frames_env, the hidout sink in native/input.rs). Failing the gate on that would only teach people to ignore it. It asks one question.
I did try silencing the hidout warning and reverted it — gating the closure just moves the warning onto the function parameter, which would have meant a per-platform signature. Both warnings pre-date this branch and are unchanged by it.
⚠ xcheck.sh covers pf-vdisplay, not punktfunk-host. The Linux and Windows compile of the host crate itself is only checked by this PR's CI run — it has never been compiled on either platform. Please read those two legs before merging.
Not in scope
Tier B (a real macOS host on ScreenCaptureKit + VideoToolbox) and Tier C. #299 stays open.
Tier A of #299. No macOS backend, no shipped macOS host — this is the seam only.
## The measurement
`cargo check -p punktfunk-host` on macOS 26 / rustc 1.96.0:
| Date | Errors | Files |
| --- | --- | --- |
| 2026-08-01 | 9 | 5 |
| 2026-08-18 | 10 | 6 |
| 2026-08-30 (this branch's base) | 11 | 6 |
| after this branch | **0** | — |
Roughly one new unguarded call site a fortnight, with nobody working on macOS. That growth rate is the argument for the gate, not the fix.
## What was actually wrong
Not one error was missing functionality. Every one was a call site or a dependency assuming Linux-or-Windows without saying so.
1. **`opus` and `libc` were declared only for the platforms with a capture backend.** The code reaching for them is portable: the mic pump decodes, and both `hooks.rs` helpers are already `#[cfg(unix)]`. `opus` now covers `cfg(any(unix, windows))` — `punktfunk-core` already vendors it for the Apple client, so cargo unifies the build and it costs nothing. `libc` moves to `cfg(unix)`, which is what the code claimed all along.
2. **`pf-vdisplay` re-exported `try_recover_session` and `cancel_pending_tv_restore` behind a Linux gate — although both already have `#[cfg(not(target_os = "linux"))]` arms.** The functions were portable; only the `pub use` was not. Moving them into the ungated block fixed three call sites at once (`gamestream/stream.rs`, `native/compositor.rs` ×2).
3. **`cursor_blend_for` split on Windows rather than on Linux.** So a third platform fell into the "not Windows" arm, which reads `encode::linux_zero_copy_is_vaapi()`, `zerocopy::enabled()` and `encode::cursor_blend_capable()` — all Linux-only. Flipped to `linux` / `not(linux)`. Windows behaviour is bit-identical: it returned `false` before and returns `false` now. The sibling `gamescope_cursor_for` is left alone, and still keeps `gamescope_needs_host_cursor` live off Linux.
4. **`gamelease` reached past `procscan`'s own platform-neutral wrappers** to `crate::procscan::Scanner::system()`, which that module's header explicitly forbids ("Everything above the scanner is platform-neutral"). Added `procscan::alive`, mirroring the `names` / `resolve` wrappers beside it.
### The one behavioural note
`end_previous_launch` used to hoist one `Scanner` and reuse it across the `TERM_GRACE` poll loop; through the wrapper it builds one per call, as `names` and `resolve` already do. That is `getuid` + `sysconf` + a small `PathBuf` on Linux and a unit struct on Windows, a few times over a bounded loop. Measured cheap enough not to warrant an exception to the module's own boundary.
## The gate
`.gitea/workflows/macos-host.yml`, a **separate workflow**. Two reasons it is not a job in `apple.yml`, which already owns this runner:
- That file's `paths:` filter is deliberately narrow — `punktfunk-core` and `clients/apple` — because the mini is shared and an xcframework build is heavy. A host-crate edit would never have fired it, so the gate would have been decorative.
- Its `distribute` job is `needs: swift`. Anything that later grows the same dependency can block a TestFlight upload over a host compile hole the client has no part in.
`cargo check`, not clippy, and no `-D warnings`: the graph carries code that is dead on macOS (`pf-encode::vbv_frames_env`, the `hidout` sink in `native/input.rs`). Failing the gate on that would only teach people to ignore it. It asks one question.
I did try silencing the `hidout` warning and reverted it — gating the closure just moves the warning onto the function parameter, which would have meant a per-platform signature. Both warnings pre-date this branch and are unchanged by it.
## Verification
| Check | Result |
| --- | --- |
| `cargo check -p punktfunk-host --locked` (macOS) | exit 0 |
| `cargo check -p punktfunk-host --all-targets --locked` (macOS) | exit 0 |
| `scripts/xcheck.sh windows clippy` | exit 0 |
| `scripts/xcheck.sh linux clippy` | exit 0 |
| `cargo fmt --all` | clean |
| `Cargo.lock` | unchanged (both crates were already in the graph) |
⚠ **`xcheck.sh` covers `pf-vdisplay`, not `punktfunk-host`.** The Linux and Windows compile of the host crate itself is only checked by this PR's CI run — it has never been compiled on either platform. Please read those two legs before merging.
## Not in scope
Tier B (a real macOS host on ScreenCaptureKit + VideoToolbox) and Tier C. #299 stays open.
Refs #299
`cargo check -p punktfunk-host` failed on macOS with 11 errors across
six files. Not one was missing functionality: every one was a call site
or a dependency that assumed Linux-or-Windows without saying so, and the
count had grown from 9 to 11 in four weeks with nobody working on macOS.
`opus` and `libc` were declared only for the platforms with a capture
backend, though the code reaching for them is portable — the mic pump
just decodes, and both `hooks.rs` helpers are already `#[cfg(unix)]`.
`pf-vdisplay` re-exported `try_recover_session` and
`cancel_pending_tv_restore` behind a Linux gate even though both already
carry off-Linux arms. `cursor_blend_for` split on Windows rather than on
Linux, so a third platform fell into the arm holding the VAAPI/CUDA
terms that exist only there. `gamelease` reached past `procscan`'s own
platform-neutral wrappers to `Scanner::system()`, which the module's
documented boundary says it must not.
That last one now goes through a new `procscan::alive`, which builds a
scanner per call exactly as the `names` and `resolve` wrappers beside it
do — two syscalls on Linux, a unit struct on Windows.
Linux and Windows behaviour is unchanged.
Refs #299
The third-platform seam has no gate, which is why it rotted: the
unguarded call sites went from 9 to 11 in the four weeks after they were
first counted, and each one was only found by someone running the check
by hand.
Its own workflow rather than a job in apple.yml, for two reasons. That
file's paths filter is deliberately narrow — punktfunk-core and the
Apple client — because the mac mini is shared and an xcframework build
is heavy, so a host-crate edit would never have fired it. And its
`distribute` job is `needs: swift`, where anything that later grows the
same dependency can block a TestFlight upload over a host compile hole
the client has no part in.
`cargo check`, not clippy, and no `-D warnings`: the graph carries code
that is dead on macOS, and failing on that would only teach people to
ignore the gate. It asks one question.
Refs #299
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.
Tier A of #299. No macOS backend, no shipped macOS host — this is the seam only.
The measurement
cargo check -p punktfunk-hoston macOS 26 / rustc 1.96.0:Roughly one new unguarded call site a fortnight, with nobody working on macOS. That growth rate is the argument for the gate, not the fix.
What was actually wrong
Not one error was missing functionality. Every one was a call site or a dependency assuming Linux-or-Windows without saying so.
opusandlibcwere declared only for the platforms with a capture backend. The code reaching for them is portable: the mic pump decodes, and bothhooks.rshelpers are already#[cfg(unix)].opusnow coverscfg(any(unix, windows))—punktfunk-corealready vendors it for the Apple client, so cargo unifies the build and it costs nothing.libcmoves tocfg(unix), which is what the code claimed all along.pf-vdisplayre-exportedtry_recover_sessionandcancel_pending_tv_restorebehind a Linux gate — although both already have#[cfg(not(target_os = "linux"))]arms. The functions were portable; only thepub usewas not. Moving them into the ungated block fixed three call sites at once (gamestream/stream.rs,native/compositor.rs×2).cursor_blend_forsplit on Windows rather than on Linux. So a third platform fell into the "not Windows" arm, which readsencode::linux_zero_copy_is_vaapi(),zerocopy::enabled()andencode::cursor_blend_capable()— all Linux-only. Flipped tolinux/not(linux). Windows behaviour is bit-identical: it returnedfalsebefore and returnsfalsenow. The siblinggamescope_cursor_foris left alone, and still keepsgamescope_needs_host_cursorlive off Linux.gameleasereached pastprocscan's own platform-neutral wrappers tocrate::procscan::Scanner::system(), which that module's header explicitly forbids ("Everything above the scanner is platform-neutral"). Addedprocscan::alive, mirroring thenames/resolvewrappers beside it.The one behavioural note
end_previous_launchused to hoist oneScannerand reuse it across theTERM_GRACEpoll loop; through the wrapper it builds one per call, asnamesandresolvealready do. That isgetuid+sysconf+ a smallPathBufon Linux and a unit struct on Windows, a few times over a bounded loop. Measured cheap enough not to warrant an exception to the module's own boundary.The gate
.gitea/workflows/macos-host.yml, a separate workflow. Two reasons it is not a job inapple.yml, which already owns this runner:paths:filter is deliberately narrow —punktfunk-coreandclients/apple— because the mini is shared and an xcframework build is heavy. A host-crate edit would never have fired it, so the gate would have been decorative.distributejob isneeds: swift. Anything that later grows the same dependency can block a TestFlight upload over a host compile hole the client has no part in.cargo check, not clippy, and no-D warnings: the graph carries code that is dead on macOS (pf-encode::vbv_frames_env, thehidoutsink innative/input.rs). Failing the gate on that would only teach people to ignore it. It asks one question.I did try silencing the
hidoutwarning and reverted it — gating the closure just moves the warning onto the function parameter, which would have meant a per-platform signature. Both warnings pre-date this branch and are unchanged by it.Verification
cargo check -p punktfunk-host --locked(macOS)cargo check -p punktfunk-host --all-targets --locked(macOS)scripts/xcheck.sh windows clippyscripts/xcheck.sh linux clippycargo fmt --allCargo.lock⚠
xcheck.shcoverspf-vdisplay, notpunktfunk-host. The Linux and Windows compile of the host crate itself is only checked by this PR's CI run — it has never been compiled on either platform. Please read those two legs before merging.Not in scope
Tier B (a real macOS host on ScreenCaptureKit + VideoToolbox) and Tier C. #299 stays open.
Refs #299