fix(vdisplay): stop the suite reporting ok for tests that never ran, and let clippy see the KWin backends

Three coverage holes, all of the same shape — a gate that looks green because
nothing is behind it.

The Windows half (~3,400 lines) had no executed tests. Its only two `#[test]`s
opened with `if env::var("PUNKTFUNK_PF_VDISPLAY_LIVE").is_err() { return; }`, so
without the hardware they passed without running, and no CI job compiled the
crate's test targets at all — `-p punktfunk-host` builds pf-vdisplay as a
dependency, which never reaches a `#[cfg(test)]` module. Both live tests are
`#[ignore]`d now (an unrun hardware test should say `ignored`), and windows-host.yml
gains the `--all-targets` lint plus a real test step. The link question that keeps
the host and pf-encode on clippy-only does not apply: pf-encode is `default = []`
and nothing here turns on nvenc/amf-qsv/qsv. Settled on the runner to the same
standard as pf-capture's step — 46 passed, 2 ignored.

`#![allow(clippy::all, ...)]` at the top of kwin.rs and kwin_output_mgmt.rs was
meant for wayland-scanner output, but the generated modules already carry their own
copy of that attribute — so the file-level one was only ever silencing 2,366 lines
of hand-written backend, with `clippy::correctness` (deny-by-default) among the
casualties. Dropping it found a dead match arm in the `kde_output_configuration_v2`
dispatch; that one is now exhaustive on purpose, so a re-vendored XML that adds an
event fails the build instead of dropping it silently. (The sibling `_ => {}` in the
DeviceMode dispatch is NOT dead — `ModeEvent::Removed` really does reach it and get
swallowed, which is a separate defect.)

`dead_code` is now enforced on Linux, where ~10k of the ~17k lines live, instead of
allowed crate-wide behind a rationale that had stopped being true. It turned up no
genuinely dead code: four items, every one live from tests or another platform, now
annotated with which. One deserved the comment most — `Entry::keepalive` is "never
read" because it is an RAII guard whose `Drop` releases the compositor output, so
deleting it as unused would release every pooled display the moment it was created.

Two tests were not testing what they claimed. `another_uids_socket_is_ignored`
asserted the sway-socket ownership guard but never reached it — `find_sway_socket`
filters on the `sway-ipc.<uid>.` filename prefix first, so the foreign-uid file was
rejected by name and the assertion held even with `md.uid() != uid` deleted. It is
renamed to what it does cover, and the real leg is a new `#[ignore]`d root test that
chowns a correctly-named socket (querying with a non-matching pid, or the exact-path
shortcut would return before the guard and make it vacuous all over again). And
proc.rs's tests were ungated while the module is compiled everywhere, so they would
have gone red on Windows the first time anyone ran them: `#[cfg(all(test, unix))]`
now, with a `cmd /c` twin that does run there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 19:45:11 +02:00
co-authored by Claude Opus 5
parent 995cac5d03
commit 6782aa0054
11 changed files with 391 additions and 227 deletions
+25 -2
View File
@@ -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
+7 -2
View File
@@ -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
@@ -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,
@@ -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<KModeRow> {
/// 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| {
@@ -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<OutputConfig, ()> for State {
_: &QueueHandle<Self>,
) {
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)
}
+47 -1
View File
@@ -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");
}
}
@@ -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<dyn Send>,
node_id: u32,
preferred_mode: Option<(u32, u32, u32)>,
+41 -2
View File
@@ -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.<uid>.` 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");
}
}
@@ -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.
+4 -197
View File
@@ -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 "$@"
+237
View File
@@ -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