diff --git a/.gitea/workflows/windows-host.yml b/.gitea/workflows/windows-host.yml index 12eedf58..4fe912c1 100644 --- a/.gitea/workflows/windows-host.yml +++ b/.gitea/workflows/windows-host.yml @@ -172,8 +172,8 @@ jobs: # openh264 rebuild), and keeps everything in one C:\t\release tree. Same reason # pf-vkhdr-layer's clippy below runs --release. # - # pf-encode and pf-capture are linted SEPARATELY with --all-targets so their Windows - # `#[cfg(test)]` modules are type-checked — pf-encode's AMF C-ABI layout assertions + # pf-encode, pf-capture and pf-vdisplay are linted SEPARATELY with --all-targets so their + # Windows `#[cfg(test)]` modules are type-checked — pf-encode's AMF C-ABI layout assertions # (`variant_layout_matches_c` and friends, which are the only guard on a hand-mirrored # vtable ABI), the QSV tests, the PyroWave-Windows smoke test; pf-capture's `StallWatch` # tests, the DXGI HDR self-tests and the cursor-conversion tables. The host lint above @@ -199,6 +199,7 @@ jobs: cargo clippy --release -p punktfunk-host --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "host clippy" } cargo clippy --release -p pf-encode --all-targets --features nvenc,amf-qsv,qsv -- -D warnings; if ($LASTEXITCODE) { throw "pf-encode clippy" } cargo clippy --release -p pf-capture --all-targets -- -D warnings; if ($LASTEXITCODE) { throw "pf-capture clippy" } + cargo clippy --release -p pf-vdisplay --all-targets -- -D warnings; if ($LASTEXITCODE) { throw "pf-vdisplay clippy" } cargo clippy --release -p punktfunk-tray -- -D warnings; if ($LASTEXITCODE) { throw "tray clippy" } - name: Test (pf-capture, Windows) @@ -223,6 +224,28 @@ jobs: run: | cargo test --release -p pf-capture; if ($LASTEXITCODE) { throw "pf-capture tests" } + - name: Test (pf-vdisplay, Windows) + shell: pwsh + # pf-vdisplay's Windows half is ~3,400 lines (manager.rs, pf_vdisplay.rs, ddc.rs, the three + # manager/ submodules) that NO other job compiles — the host lint above builds the crate as + # a dependency, so its test targets were reaching no compiler anywhere. Worse, the only two + # Windows `#[test]`s were `if env::var("PUNKTFUNK_PF_VDISPLAY_LIVE").is_err() { return; }` + # early-returns, so an unrun hardware test reported `ok`. They are `#[ignore]`d now and this + # step reports them as `ignored`, which is the truth. + # + # The link objection recorded for the host and pf-encode above does NOT apply here, for the + # same reason it does not apply to pf-capture: `pf-encode` is `default = []`, and nothing in + # `-p pf-vdisplay`'s graph turns on `nvenc`/`amf-qsv`/`qsv`, so no nvidia/ffmpeg/libvpl + # import libs are ever asked for. Settled empirically before this step was added, to the + # same standard as pf-capture's: the exact two commands were run on this runner against a + # checkout at C:\temp\pf-vd-check — clippy clean, then 46 passed / 2 ignored in 0.19 s. + # + # --release to reuse C:\t\release rather than spawning a debug tree (the C1069 trigger). + # Note this DOES build a second, featureless pf-encode; it is small precisely because none + # of the encoder features are on. + run: | + cargo test --release -p pf-vdisplay; if ($LASTEXITCODE) { throw "pf-vdisplay tests" } + - name: Build + lint the HDR Vulkan layer (pf-vkhdr-layer) shell: pwsh # Standalone cdylib (own [workspace]) the installer bundles + registers (it lets Vulkan games diff --git a/crates/pf-vdisplay/src/lib.rs b/crates/pf-vdisplay/src/lib.rs index ed5abf5c..6e94f66a 100644 --- a/crates/pf-vdisplay/src/lib.rs +++ b/crates/pf-vdisplay/src/lib.rs @@ -13,8 +13,13 @@ //! owned keepalive whose `Drop` releases the output (RAII — no explicit `destroy`). Capture //! consumes the node via the host `capture::capture_virtual_output`. -// Scaffold: some backend paths + Stage-3 identity are defined ahead of the target that uses them. -#![allow(dead_code)] +// `dead_code` is ENFORCED on Linux, where ~10k of this crate's ~17k lines live. Off elsewhere for +// one structural reason: `proc`, `session`, `routing`, `monitors` and `lifecycle` are declared +// unconditionally but exist to serve the Linux backends, so on Windows/macOS most of their surface +// is legitimately unreferenced. Scoping it this way rather than crate-wide keeps the platform that +// owns the code honest. (Was a bare crate-wide allow whose "scaffold, defined ahead of the target +// that uses them" rationale had stopped being true.) +#![cfg_attr(not(target_os = "linux"), allow(dead_code))] // Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). #![deny(clippy::undocumented_unsafe_blocks)] // …and that program only covers a whole `unsafe fn` body once the body needs its own block: in diff --git a/crates/pf-vdisplay/src/vdisplay/lifecycle.rs b/crates/pf-vdisplay/src/vdisplay/lifecycle.rs index d0d2f060..f5912069 100644 --- a/crates/pf-vdisplay/src/vdisplay/lifecycle.rs +++ b/crates/pf-vdisplay/src/vdisplay/lifecycle.rs @@ -59,11 +59,16 @@ pub enum Release { impl State { /// True while a backend display resource exists (Active/Lingering/Pinned) — the registry holds /// the keepalive in exactly these states, and `Idle` means it has been dropped. + // Read only by this module's tests, including the property test that pins `has_display()` to a + // shadow "resource alive" flag. That IS its job: it is the machine's observable invariant, not + // production API, so it is deliberately kept rather than inlined into the assertions. + #[allow(dead_code)] pub fn has_display(self) -> bool { !matches!(self, State::Idle) } /// Number of live sessions holding the display (0 unless Active). + #[allow(dead_code)] // as `has_display` above — the tests are the caller. pub fn refs(self) -> u32 { match self { State::Active { refs } => refs, diff --git a/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs b/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs index f71e4df5..023a584e 100644 --- a/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs +++ b/crates/pf-vdisplay/src/vdisplay/linux/kwin.rs @@ -20,7 +20,6 @@ //! "Could not find output". We talk raw Wayland on `$WAYLAND_DISPLAY`, so the host must run inside //! the KWin session's environment. -#![allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)] // Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program). #![deny(clippy::undocumented_unsafe_blocks)] @@ -600,12 +599,7 @@ fn output_modes(output: &str) -> Vec { /// of the request (which excludes the output's native 60 Hz entry for every rate we install a custom /// mode for). Widest wins, then fastest — so an exact-width mode always beats an aligned one, and a /// list carrying duplicate custom modes from earlier sessions still resolves. -fn pick_custom_mode<'a>( - modes: &'a [KModeRow], - width: u32, - height: u32, - hz: u32, -) -> Option<&'a KModeRow> { +fn pick_custom_mode(modes: &[KModeRow], width: u32, height: u32, hz: u32) -> Option<&KModeRow> { modes .iter() .filter(|m| { diff --git a/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs b/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs index e0e91937..f7e1d1e6 100644 --- a/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs +++ b/crates/pf-vdisplay/src/vdisplay/linux/kwin_output_mgmt.rs @@ -20,7 +20,6 @@ //! each output's name / enabled / priority / current-mode size, then build a //! `kde_output_configuration_v2` and `apply()` it, waiting for `applied` / `failed`. -#![allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)] #![deny(clippy::undocumented_unsafe_blocks)] use std::collections::HashMap; @@ -91,6 +90,10 @@ const DEVICE_REGISTRY_MAX: u32 = 24; /// The opcode of `kde_output_device_v2.mode` (0-based event index) — the event that creates a child /// `kde_output_device_mode_v2`. Kept in sync with the vendored `kde-output-device-v2.xml`. +// The `event_created_child!` macro hard-codes the literal 2, so nothing reads this constant at +// run time; `mode_event_opcode_is_two` asserts the two agree. That guard test is the point — it is +// what catches a re-vendored XML that reorders the events. +#[allow(dead_code)] const DEVICE_MODE_EVENT_OPCODE: u16 = 2; /// The opcode of `kde_output_device_registry_v2.output` — the event that creates a child /// `kde_output_device_v2`. `finished` is event 0, `output` is event 1. @@ -349,10 +352,13 @@ impl Dispatch for State { _: &QueueHandle, ) { match event { + // No catch-all: `kde_output_configuration_v2` has exactly these three events, so a + // future re-vendor of the XML that adds one should fail the build here rather than + // silently drop it — which is what the old `_ => {}` did (the compiler calls it + // unreachable today). ConfigEvent::Applied => state.applied = Some(true), ConfigEvent::Failed => state.applied = Some(false), ConfigEvent::FailureReason { reason } => state.failure_reason = Some(reason), - _ => {} } } } @@ -417,10 +423,11 @@ impl Session { // registry's `output` events during phase 2, so their property bursts are one round further // out than they are for per-output globals. One more barrier — skipped when no device is // still waiting for its `done`, so the classic path costs nothing. - if s.state.device_registry.is_some() && s.state.devices.values().any(|d| !d.seen_done) { - if !s.sync_barrier(deadline) { - return None; - } + if s.state.device_registry.is_some() + && s.state.devices.values().any(|d| !d.seen_done) + && !s.sync_barrier(deadline) + { + return None; } Some(s) } diff --git a/crates/pf-vdisplay/src/vdisplay/proc.rs b/crates/pf-vdisplay/src/vdisplay/proc.rs index 7a8fdf3e..6c8377ed 100644 --- a/crates/pf-vdisplay/src/vdisplay/proc.rs +++ b/crates/pf-vdisplay/src/vdisplay/proc.rs @@ -78,7 +78,11 @@ fn timed_out(cmd: &Command, budget: Duration) -> Error { ) } -#[cfg(test)] +// `unix` gate, not `test` alone: this module is compiled on every platform (lib.rs declares it +// unconditionally), but the cases below spawn `sleep`/`true`/`echo` as EXECUTABLES. On Windows +// `echo` is a shell builtin and there is no `sleep.exe`, so an ungated module turns a green suite +// red the first time anyone runs it there. The Windows twin is below. +#[cfg(all(test, unix))] mod tests { use super::*; @@ -112,3 +116,45 @@ mod tests { assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "punktfunk"); } } + +/// The same two cases through `cmd /c`, so the budget logic is covered on the platform whose +/// process model differs most (job objects, no `SIGKILL`). `ping -n` is the standard Windows +/// no-extra-tooling sleep. +#[cfg(all(test, windows))] +mod tests_windows { + use super::*; + + #[test] + fn a_hung_child_is_killed_at_the_budget() { + let started = Instant::now(); + let err = status_within( + Command::new("cmd").args(["/c", "ping -n 60 127.0.0.1 >NUL"]), + Duration::from_millis(150), + ) + .expect_err("must time out"); + assert_eq!(err.kind(), ErrorKind::TimedOut); + assert!( + started.elapsed() < Duration::from_secs(5), + "must return at its budget, not the child's lifetime (took {:?})", + started.elapsed() + ); + } + + #[test] + fn a_quick_child_returns_normally() { + let st = status_within( + Command::new("cmd").args(["/c", "exit 0"]), + Duration::from_secs(5), + ) + .expect("ran"); + assert!(st.success()); + + let out = output_within( + Command::new("cmd").args(["/c", "echo punktfunk"]), + Duration::from_secs(5), + ) + .expect("ran"); + assert!(out.status.success()); + assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "punktfunk"); + } +} diff --git a/crates/pf-vdisplay/src/vdisplay/registry.rs b/crates/pf-vdisplay/src/vdisplay/registry.rs index 79daebdb..604ac623 100644 --- a/crates/pf-vdisplay/src/vdisplay/registry.rs +++ b/crates/pf-vdisplay/src/vdisplay/registry.rs @@ -244,6 +244,11 @@ mod linux { life: lifecycle::State, /// The backend's keepalive (KWin Wayland conn / Mutter D-Bus session / gamescope child). Its /// `Drop` releases the compositor output — so it is dropped only on teardown/expiry. + // NEVER READ, ON PURPOSE — `dead_code` is right that nothing loads this field and wrong + // about what that means. It is an RAII guard: holding it is the whole behaviour, and its + // `Drop` is what releases the compositor output. Deleting it as "unused" would release + // every pooled display the instant it was created. + #[allow(dead_code)] keepalive: Box, node_id: u32, preferred_mode: Option<(u32, u32, u32)>, diff --git a/crates/pf-vdisplay/src/vdisplay/session.rs b/crates/pf-vdisplay/src/vdisplay/session.rs index 1f3d4991..6d61b2dd 100644 --- a/crates/pf-vdisplay/src/vdisplay/session.rs +++ b/crates/pf-vdisplay/src/vdisplay/session.rs @@ -186,6 +186,9 @@ pub struct ActiveSession { impl ActiveSession { /// A "nothing live" result carrying just the runtime-dir anchor. + // Only the non-Linux `detect_active_session` calls this (below); Linux always has a real + // session to describe. + #[cfg_attr(target_os = "linux", allow(dead_code))] fn none() -> ActiveSession { ActiveSession { kind: ActiveKind::None, @@ -712,13 +715,49 @@ mod tests { assert_eq!(got, None); } - /// Someone else's socket in a shared runtime dir is not ours to talk to. + /// A socket NAMED for another uid is not ours to talk to. This covers the filename filter + /// (`sway-ipc..` prefix) and nothing more — it was previously called + /// `another_uids_socket_is_ignored`, which claimed the ownership guard below it. It never + /// reached that guard: the prefix test `continue`s first, so the assertion held even with + /// `md.uid() != uid` deleted. See `another_uids_owned_socket_is_ignored` for the real leg. #[test] - fn another_uids_socket_is_ignored() { + fn another_uids_socket_name_is_ignored() { let rt = FakeRuntime::new("otheruid", &[]); let other = rt.uid.wrapping_add(1); std::fs::write(rt.dir.join(format!("sway-ipc.{other}.500.sock")), b"").unwrap(); let got = without_inherited_swaysock(|| find_sway_socket(rt.path(), rt.uid, Some(500))); assert_eq!(got, None); } + + /// The ownership guard proper: a socket named as ours but OWNED by someone else must be + /// rejected on its metadata. That is the case that matters — a hostile local user can pick the + /// filename, so the name is not evidence. + /// + /// Ignored by default because it needs to `chown` a file to another uid, i.e. root. Run it + /// where that is true (a container, or CI as root): + /// cargo test -p pf-vdisplay -- --ignored another_uids_owned + #[test] + #[ignore = "needs root to chown the socket to another uid"] + fn another_uids_owned_socket_is_ignored() { + use std::os::unix::fs::MetadataExt; + let rt = FakeRuntime::new("ownedbyother", &[]); + // Named exactly as one of OURS, so the prefix filter admits it and the metadata check is + // the only thing that can reject it. + let path = rt.dir.join(format!("sway-ipc.{}.500.sock", rt.uid)); + std::fs::write(&path, b"").unwrap(); + let target_uid = rt.uid.wrapping_add(1); + let c_path = std::ffi::CString::new(path.to_string_lossy().as_bytes()).unwrap(); + // SAFETY: `c_path` is a live NUL-terminated CString borrowed for the duration of the call, + // and `chown` reads it without retaining it. `u32::MAX` is the unsigned spelling of the + // `-1` gid that means "leave the group unchanged". + let rc = unsafe { libc::chown(c_path.as_ptr(), target_uid, u32::MAX) }; + assert_eq!(rc, 0, "chown failed — this test needs root"); + assert_eq!(std::fs::metadata(&path).unwrap().uid(), target_uid); + + // Query with a pid that does NOT match the filename: the exact-path shortcut earlier in + // `find_sway_socket` returns before the ownership guard, so hitting it would make this + // test vacuous in the same way the one above was. + let got = without_inherited_swaysock(|| find_sway_socket(rt.path(), rt.uid, Some(999))); + assert_eq!(got, None, "a socket owned by another uid must be rejected"); + } } diff --git a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs index 36ae1e04..c4884d57 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/pf_vdisplay.rs @@ -874,13 +874,11 @@ mod tests { use std::thread; use std::time::Duration; - /// Live hardware round trip — skipped unless `PUNKTFUNK_PF_VDISPLAY_LIVE=1` (needs the pf-vdisplay - /// driver installed). Exercises the real trait path: open -> create -> hold -> drop (REMOVE). + /// Live hardware round trip — `#[ignore]`d (needs the pf-vdisplay driver installed); run with + /// `cargo test -p pf-vdisplay -- --ignored live_create_drop`. Exercises the real trait path: open -> create -> hold -> drop (REMOVE). #[test] + #[ignore = "needs the pf-vdisplay driver on real hardware; run with --ignored"] fn live_create_drop() { - if std::env::var("PUNKTFUNK_PF_VDISPLAY_LIVE").is_err() { - return; - } let mut vd = PfVdisplayDisplay::new().expect("open pf-vdisplay"); let vout = vd .create(Mode { @@ -894,18 +892,16 @@ mod tests { drop(vout); // triggers REMOVE + stops the pinger } - /// Live in-place resize spike — skipped unless `PUNKTFUNK_PF_VDISPLAY_LIVE=1` (needs a v4 - /// pf-vdisplay driver installed + the host service STOPPED, single-instance guard). Answers the + /// Live in-place resize spike — `#[ignore]`d (needs a v4 pf-vdisplay driver installed + the host + /// service STOPPED, single-instance guard); run with `-- --ignored live_inplace_resize`. Answers the /// P2 open questions on real glass with no streaming client: create at one mode, then acquire /// the SAME session's slot at a DIFFERENT mode — the manager's resize branch runs UPDATE_MODES /// → mode-advertised wait → set_active_mode → verified settle. In-place success is visible as /// the SAME OS target id on the second output (a re-arrival fallback mints a new one) plus the /// committed active resolution; the test reports which path ran and asserts the mode landed. #[test] + #[ignore = "needs the pf-vdisplay driver on real hardware; run with --ignored"] fn live_inplace_resize() { - if std::env::var("PUNKTFUNK_PF_VDISPLAY_LIVE").is_err() { - return; - } // Live-run diagnostics: surface the manager/backend tracing (activation ladder, settle // waits, UPDATE_MODES) on stdout — a bare test harness has no subscriber, which made the // first on-glass run blind. diff --git a/scripts/wincheck.sh b/scripts/wincheck.sh index cc7b6e5b..f90918ae 100755 --- a/scripts/wincheck.sh +++ b/scripts/wincheck.sh @@ -1,198 +1,5 @@ #!/usr/bin/env bash -# -# Type-check and lint punktfunk's WINDOWS-only Rust from a Linux dev box. -# -# scripts/wincheck.sh # clippy -D warnings, the default -# scripts/wincheck.sh check # cargo check instead -# scripts/wincheck.sh clippy --fix # extra args are passed through to cargo -# -# Linux CI never compiles `#[cfg(target_os = "windows")]` code, so a Windows-side edit is otherwise -# unverifiable until the Windows CI job runs (minutes) or someone tests on a real box. This gets the -# same answer in ~1 s warm, against the WORKING TREE — the generated workspace symlinks each `src` -# at the real crate directory, so there is nothing to keep in sync and no copies to go stale. -# -# WHY A SEPARATE WORKSPACE — the obvious in-tree command -# -# cargo check --target x86_64-pc-windows-msvc -p pf-capture -# -# fails, and NOT because of the Windows code: it dies in build scripts that compile C for the -# target. `audiopus_sys` first (cmake wants a Visual Studio generator), then `ring` (needs an MSVC C -# compiler plus the Windows SDK headers; clang-cl and lld-link exist locally but there is no SDK). -# Both enter the graph through `punktfunk-core`'s `quic` feature. Stubbing opus with a fake -# `opus.pc` gets past audiopus but not ring. -# -# The whole Windows capture stack uses exactly FOUR items from punktfunk-core — `quic::HdrMeta`, -# `Mode`, `CompositorPref` and its `as_str`, all plain data (re-verify with: -# `grep -rho 'punktfunk_core::[A-Za-z_:]*' crates/pf-{capture,frame,win-display,vdisplay}/src | sort -u`). -# So this script generates a ~50-line stub core carrying just those, and rustls/ring/opus never -# enter the graph at all. Every path dep that is NOT a generated member points at the REAL crate by -# absolute path, so their own relative deps and `version.workspace` inheritance still resolve. -# -# pf-encode is stubbed for the same reason and needs the same care: pf-vdisplay's admission gate -# calls exactly ONE item from it (`can_open_another_session`, admission.rs), but the real crate -# drags ffmpeg-sys — whose build script wants a Windows FFmpeg tree this box does not have. The -# stub is a `cfg(windows) -> bool`; if admission ever consults a second encoder fact, mirror it -# here or the cross-check stops covering that call. -# -# Note: `cargo fmt` needs none of this — rustfmt follows `mod`/`#[path]` without evaluating cfg, so -# it already reaches Windows-only files. Mechanical edits can be normalised with plain `cargo fmt`. -set -euo pipefail - -REPO="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)" -OUT="${WINCHECK_DIR:-$REPO/target/wincheck}" -TARGET=x86_64-pc-windows-msvc - -# The generated members: crates whose Windows code we lint, plus the stub core they share. A path -# dep naming one of these must stay RELATIVE (resolving to the generated member); anything else is -# rewritten to the real crate. Getting that backwards silently drags the real punktfunk-core — and -# with it ring and opus — back in through pf-frame, which is the entire thing this avoids. -MEMBERS_RE='punktfunk-core|pf-encode|pf-frame|pf-win-display|pf-capture|pf-vdisplay' -REAL_MEMBERS=(pf-frame pf-win-display pf-capture pf-vdisplay) - -cmd="${1:-clippy}" -[ $# -gt 0 ] && shift -case "$cmd" in - check | clippy) ;; - *) - echo "usage: $(basename "$0") [check|clippy] [extra cargo args...]" >&2 - exit 2 - ;; -esac - -if ! rustc --print target-list | grep -qx "$TARGET"; then - echo "wincheck: rustc does not know $TARGET" >&2 - exit 1 -fi -if ! rustup target list --installed 2>/dev/null | grep -qx "$TARGET"; then - echo "wincheck: target $TARGET not installed for the active toolchain — run:" >&2 - echo " rustup target add $TARGET" >&2 - exit 1 -fi - -rm -rf "$OUT" -mkdir -p "$OUT" - -# Mirror the real [workspace.package] so the members' `version.workspace = true` resolves. -{ - echo '# GENERATED by scripts/wincheck.sh — do not edit; re-run the script.' - echo '[workspace]' - echo 'resolver = "2"' - echo 'members = ["punktfunk-core", "pf-encode", "pf-frame", "pf-win-display", "pf-capture", "pf-vdisplay"]' - echo - echo '[workspace.package]' - sed -n '/^\[workspace\.package\]/,/^$/p' "$REPO/Cargo.toml" | sed '1d;/^$/d' -} > "$OUT/Cargo.toml" - -mkdir -p "$OUT/punktfunk-core/src" -cat > "$OUT/punktfunk-core/Cargo.toml" <<'EOF' -# GENERATED by scripts/wincheck.sh — a STUB, not the real crate. -[package] -name = "punktfunk-core" -version.workspace = true -edition = "2021" -rust-version.workspace = true - -[features] -default = [] -# Present so dependents' `features = ["quic"]` resolves; carries no quinn/rustls/opus. -quic = [] -EOF - -# Field layout and derives must match the real definitions, or the cross-check goes stale exactly -# where it matters (the capture code builds HdrMeta literally and compares Modes). -cat > "$OUT/punktfunk-core/src/lib.rs" <<'EOF' -//! GENERATED by scripts/wincheck.sh — a STUB of punktfunk-core carrying only the two items the -//! Windows capture stack uses. Mirrors crates/punktfunk-core: `Mode` from config.rs, `HdrMeta` -//! from quic/datagram.rs. If either grows a field, mirror it here. - -/// Mirrors `punktfunk_core::Mode`. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Mode { - pub width: u32, - pub height: u32, - pub refresh_hz: u32, -} - -/// Mirrors `punktfunk_core::CompositorPref` (config.rs). pf-vdisplay matches on every variant and -/// calls `as_str`, so both the variant set and the strings must stay in step with the real enum. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -pub enum CompositorPref { - #[default] - Auto, - Kwin, - Wlroots, - Mutter, - Gamescope, -} - -impl CompositorPref { - pub fn as_str(self) -> &'static str { - match self { - CompositorPref::Auto => "auto", - CompositorPref::Kwin => "kwin", - CompositorPref::Wlroots => "wlroots", - CompositorPref::Mutter => "mutter", - CompositorPref::Gamescope => "gamescope", - } - } -} - -pub mod quic { - /// Mirrors `punktfunk_core::quic::HdrMeta`. - #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] - pub struct HdrMeta { - pub display_primaries: [[u16; 2]; 3], - pub white_point: [u16; 2], - pub max_display_mastering_luminance: u32, - pub min_display_mastering_luminance: u32, - pub max_cll: u16, - pub max_fall: u16, - } -} -EOF - -mkdir -p "$OUT/pf-encode/src" -cat > "$OUT/pf-encode/Cargo.toml" <<'EOF' -# GENERATED by scripts/wincheck.sh — a STUB, not the real crate. -[package] -name = "pf-encode" -version.workspace = true -edition = "2021" -rust-version.workspace = true -EOF - -cat > "$OUT/pf-encode/src/lib.rs" <<'EOF' -//! GENERATED by scripts/wincheck.sh — a STUB of pf-encode carrying only the one item pf-vdisplay's -//! admission gate uses. The real crate pulls ffmpeg-sys, whose build script needs a Windows FFmpeg -//! tree; nothing in the display path needs the encoder itself, only this predicate. - -/// Mirrors `pf_encode::can_open_another_session` (lib.rs, `#[cfg(target_os = "windows")]`). -#[cfg(target_os = "windows")] -pub fn can_open_another_session() -> bool { - true -} -EOF - -for name in "${REAL_MEMBERS[@]}"; do - mkdir -p "$OUT/$name" - # Park the member paths behind a sentinel first: a plain self-substitution would be a no-op and - # the next rule would rewrite them to absolute along with everything else. - { - echo "# GENERATED by scripts/wincheck.sh from crates/$name/Cargo.toml — src/ is a symlink." - sed -E "s#path = \"\.\./($MEMBERS_RE)\"#path = \"@@M@@\1\"#g; \ - s#path = \"\.\./([A-Za-z0-9_-]+)\"#path = \"$REPO/crates/\1\"#g; \ - s#path = \"@@M@@#path = \"../#g" "$REPO/crates/$name/Cargo.toml" - } > "$OUT/$name/Cargo.toml" - ln -sfn "$REPO/crates/$name/src" "$OUT/$name/src" -done - -cd "$OUT" -for name in "${REAL_MEMBERS[@]}"; do - echo "=== $cmd $name ($TARGET) ===" - if [ "$cmd" = clippy ]; then - cargo clippy --target "$TARGET" -p "$name" --all-targets "$@" -- -D warnings - else - cargo check --target "$TARGET" -p "$name" --all-targets "$@" - fi -done +# Compat shim: this script was renamed to xcheck.sh when it learned to cross-check the LINUX +# backends too (pf-vdisplay's five compositor backends are as invisible to a Mac as the Windows +# half is). Forwards to the windows half, which is all this name ever meant. +exec "$(dirname "$(readlink -f "$0")")/xcheck.sh" windows "$@" diff --git a/scripts/xcheck.sh b/scripts/xcheck.sh new file mode 100755 index 00000000..ee3b2a67 --- /dev/null +++ b/scripts/xcheck.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +# +# Type-check and lint punktfunk's PLATFORM-GATED Rust from a dev box that is neither platform. +# +# scripts/xcheck.sh # windows, clippy -D warnings — the default +# scripts/xcheck.sh linux # the Linux backends instead +# scripts/xcheck.sh windows check # cargo check instead of clippy +# scripts/xcheck.sh linux clippy --fix # extra args are passed through to cargo +# +# (`scripts/wincheck.sh` is a compat shim for the windows half — this script's former name.) +# +# CI compiles each platform's `#[cfg(target_os = ...)]` code only on that platform, so a Windows- or +# Linux-side edit made on a Mac is otherwise unverifiable until that CI job runs (minutes) or someone +# tests on a real box. This gets the same answer in ~1 s warm, against the WORKING TREE — the +# generated workspace symlinks each crate's real directory contents, so there is nothing to keep in +# sync and no copies to go stale. +# +# Coverage is deliberately asymmetric. Windows lints pf-frame, pf-win-display, pf-capture and +# pf-vdisplay; Linux lints pf-vdisplay only, because pf-capture's Linux half needs `pipewire`, whose +# build script wants libpipewire via pkg-config. pf-frame and pf-win-display stay generated MEMBERS +# for both targets even when nothing lints them: demote either to a real path dep and its +# punktfunk-core dependency resolves to the REAL crate, dragging ring and opus back into the graph — +# the exact thing the stub exists to prevent. +# +# WHY A SEPARATE WORKSPACE — the obvious in-tree command +# +# cargo check --target x86_64-pc-windows-msvc -p pf-capture +# +# fails, and NOT because of the Windows code: it dies in build scripts that compile C for the +# target. `audiopus_sys` first (cmake wants a Visual Studio generator), then `ring` (needs an MSVC C +# compiler plus the Windows SDK headers; clang-cl and lld-link exist locally but there is no SDK). +# Both enter the graph through `punktfunk-core`'s `quic` feature. Stubbing opus with a fake +# `opus.pc` gets past audiopus but not ring. +# +# The whole Windows capture stack uses exactly FOUR items from punktfunk-core — `quic::HdrMeta`, +# `Mode`, `CompositorPref` and its `as_str`, all plain data (re-verify with: +# `grep -rho 'punktfunk_core::[A-Za-z_:]*' crates/pf-{capture,frame,win-display,vdisplay}/src | sort -u`). +# So this script generates a ~50-line stub core carrying just those, and rustls/ring/opus never +# enter the graph at all. Every path dep that is NOT a generated member points at the REAL crate by +# absolute path, so their own relative deps and `version.workspace` inheritance still resolve. +# +# pf-encode is stubbed for the same reason and needs the same care: pf-vdisplay's admission gate +# calls exactly ONE item from it (`can_open_another_session`, admission.rs), but the real crate +# drags ffmpeg-sys — whose build script wants a Windows FFmpeg tree this box does not have. The +# stub is a `cfg(windows) -> bool`; if admission ever consults a second encoder fact, mirror it +# here or the cross-check stops covering that call. +# +# Note: `cargo fmt` needs none of this — rustfmt follows `mod`/`#[path]` without evaluating cfg, so +# it already reaches Windows-only files. Mechanical edits can be normalised with plain `cargo fmt`. +set -euo pipefail + +REPO="$(cd "$(dirname "$(readlink -f "$0")")/.." && pwd)" + +plat="${1:-windows}" +case "$plat" in + windows | linux) shift || true ;; + # No platform given: the first arg (if any) is the cargo subcommand. Default to windows, which is + # what this script did under its former name. + *) plat=windows ;; +esac + +case "$plat" in + windows) + TARGET=x86_64-pc-windows-msvc + LINT=(pf-frame pf-win-display pf-capture pf-vdisplay) + ;; + linux) + TARGET=x86_64-unknown-linux-gnu + # pf-capture is absent on purpose — see the header (libpipewire). + LINT=(pf-vdisplay) + ;; +esac +OUT="${XCHECK_DIR:-${WINCHECK_DIR:-$REPO/target/xcheck-$plat}}" + +# The generated members: every crate we lint on EITHER target, plus the two stubs they share. A path +# dep naming one of these must stay RELATIVE (resolving to the generated member); anything else is +# rewritten to the real crate. Getting that backwards silently drags the real punktfunk-core — and +# with it ring and opus — back in through pf-frame, which is the entire thing this avoids. +MEMBERS_RE='punktfunk-core|pf-encode|pf-frame|pf-win-display|pf-capture|pf-vdisplay' +REAL_MEMBERS=(pf-frame pf-win-display pf-capture pf-vdisplay) + +cmd="${1:-clippy}" +[ $# -gt 0 ] && shift +case "$cmd" in + check | clippy) ;; + *) + echo "usage: $(basename "$0") [windows|linux] [check|clippy] [extra cargo args...]" >&2 + exit 2 + ;; +esac + +if ! rustc --print target-list | grep -qx "$TARGET"; then + echo "xcheck: rustc does not know $TARGET" >&2 + exit 1 +fi +if ! rustup target list --installed 2>/dev/null | grep -qx "$TARGET"; then + echo "xcheck: target $TARGET not installed for the active toolchain — run:" >&2 + echo " rustup target add $TARGET" >&2 + exit 1 +fi + +rm -rf "$OUT" +mkdir -p "$OUT" + +# Mirror the real [workspace.package] so the members' `version.workspace = true` resolves. +{ + echo '# GENERATED by scripts/xcheck.sh — do not edit; re-run the script.' + echo '[workspace]' + echo 'resolver = "2"' + echo 'members = ["punktfunk-core", "pf-encode", "pf-frame", "pf-win-display", "pf-capture", "pf-vdisplay"]' + echo + echo '[workspace.package]' + sed -n '/^\[workspace\.package\]/,/^$/p' "$REPO/Cargo.toml" | sed '1d;/^$/d' +} > "$OUT/Cargo.toml" + +mkdir -p "$OUT/punktfunk-core/src" +cat > "$OUT/punktfunk-core/Cargo.toml" <<'EOF' +# GENERATED by scripts/xcheck.sh — a STUB, not the real crate. +[package] +name = "punktfunk-core" +version.workspace = true +edition = "2021" +rust-version.workspace = true + +[features] +default = [] +# Present so dependents' `features = ["quic"]` resolves; carries no quinn/rustls/opus. +quic = [] +EOF + +# Field layout and derives must match the real definitions, or the cross-check goes stale exactly +# where it matters (the capture code builds HdrMeta literally and compares Modes). +cat > "$OUT/punktfunk-core/src/lib.rs" <<'EOF' +//! GENERATED by scripts/xcheck.sh — a STUB of punktfunk-core carrying only the two items the +//! Windows capture stack uses. Mirrors crates/punktfunk-core: `Mode` from config.rs, `HdrMeta` +//! from quic/datagram.rs. If either grows a field, mirror it here. + +/// Mirrors `punktfunk_core::Mode`. +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Mode { + pub width: u32, + pub height: u32, + pub refresh_hz: u32, +} + +/// Mirrors `punktfunk_core::CompositorPref` (config.rs). pf-vdisplay matches on every variant and +/// calls `as_str`, so both the variant set and the strings must stay in step with the real enum. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum CompositorPref { + #[default] + Auto, + Kwin, + Wlroots, + Mutter, + Gamescope, +} + +impl CompositorPref { + pub fn as_str(self) -> &'static str { + match self { + CompositorPref::Auto => "auto", + CompositorPref::Kwin => "kwin", + CompositorPref::Wlroots => "wlroots", + CompositorPref::Mutter => "mutter", + CompositorPref::Gamescope => "gamescope", + } + } +} + +pub mod quic { + /// Mirrors `punktfunk_core::quic::HdrMeta`. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] + pub struct HdrMeta { + pub display_primaries: [[u16; 2]; 3], + pub white_point: [u16; 2], + pub max_display_mastering_luminance: u32, + pub min_display_mastering_luminance: u32, + pub max_cll: u16, + pub max_fall: u16, + } +} +EOF + +mkdir -p "$OUT/pf-encode/src" +cat > "$OUT/pf-encode/Cargo.toml" <<'EOF' +# GENERATED by scripts/xcheck.sh — a STUB, not the real crate. +[package] +name = "pf-encode" +version.workspace = true +edition = "2021" +rust-version.workspace = true +EOF + +cat > "$OUT/pf-encode/src/lib.rs" <<'EOF' +//! GENERATED by scripts/xcheck.sh — a STUB of pf-encode carrying only the one item pf-vdisplay's +//! admission gate uses. The real crate pulls ffmpeg-sys, whose build script needs a Windows FFmpeg +//! tree; nothing in the display path needs the encoder itself, only this predicate. + +/// Mirrors `pf_encode::can_open_another_session` (lib.rs, `#[cfg(target_os = "windows")]`). +#[cfg(target_os = "windows")] +pub fn can_open_another_session() -> bool { + true +} +EOF + +for name in "${REAL_MEMBERS[@]}"; do + mkdir -p "$OUT/$name" + # Park the member paths behind a sentinel first: a plain self-substitution would be a no-op and + # the next rule would rewrite them to absolute along with everything else. + { + echo "# GENERATED by scripts/xcheck.sh from crates/$name/Cargo.toml — contents are symlinks." + sed -E "s#path = \"\.\./($MEMBERS_RE)\"#path = \"@@M@@\1\"#g; \ + s#path = \"\.\./([A-Za-z0-9_-]+)\"#path = \"$REPO/crates/\1\"#g; \ + s#path = \"@@M@@#path = \"../#g" "$REPO/crates/$name/Cargo.toml" + } > "$OUT/$name/Cargo.toml" + # Symlink EVERY entry, not just src/: pf-vdisplay's `wayland_scanner::generate_interfaces!` reads + # `protocols/*.xml` relative to CARGO_MANIFEST_DIR, which is this generated member. Without the + # protocols dir the proc macro panics and every generated Wayland type resolves to nothing — ~20 + # cascading errors that look like a code problem and are not. + for entry in "$REPO/crates/$name"/*; do + case "$(basename "$entry")" in + Cargo.toml | target) continue ;; + esac + ln -sfn "$entry" "$OUT/$name/$(basename "$entry")" + done +done + +cd "$OUT" +for name in "${LINT[@]}"; do + echo "=== $cmd $name ($TARGET) ===" + if [ "$cmd" = clippy ]; then + cargo clippy --target "$TARGET" -p "$name" --all-targets "$@" -- -D warnings + else + cargo check --target "$TARGET" -p "$name" --all-targets "$@" + fi +done