From 28bbaa52506d92448cdd06822ef537e4848d0f18 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 26 Jul 2026 12:07:22 +0200 Subject: [PATCH] tools: check and lint Windows-only Rust from a Linux dev box (scripts/wincheck.sh) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux CI never compiles `#[cfg(target_os = "windows")]` code, so a Windows-side edit was unverifiable until the Windows CI job ran or someone tested on a real box. The pf-capture sweep's Phases 3–6 used an ad-hoc version of this harness in a scratch directory; committing it means the next Windows change does not have to rediscover the recipe. The obvious in-tree command fails, and not because of the Windows code: cargo check --target x86_64-pc-windows-msvc -p pf-capture dies in build scripts that compile C for the target — audiopus_sys (cmake wants a VS generator), then ring (needs an MSVC C compiler plus the Windows SDK headers). Both enter through punktfunk-core's `quic` feature. So the script generates a workspace whose members are Cargo.toml copies with `src` SYMLINKED at the real crate directories — nothing to keep in sync, no copies to go stale — plus a ~30-line stub punktfunk-core carrying only `Mode` and `quic::HdrMeta`, which is provably all the Windows capture stack uses. rustls, ring and opus never enter the graph. Every path dep that is not a generated member points at the real crate. Covers pf-frame, pf-win-display and pf-capture with --all-targets, so the Windows `#[cfg(test)]` modules are type-checked too. ~10 s cold, ~1 s warm, into target/wincheck (gitignored). Verified non-vacuous: a deliberate type error planted in a windows-only file (idd_push/stall.rs) is caught and fails the run. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/wincheck.sh | 146 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100755 scripts/wincheck.sh diff --git a/scripts/wincheck.sh b/scripts/wincheck.sh new file mode 100755 index 00000000..d2fc5f85 --- /dev/null +++ b/scripts/wincheck.sh @@ -0,0 +1,146 @@ +#!/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 TWO items from punktfunk-core — `quic::HdrMeta` and +# `Mode`, both plain data (re-verify with: +# `grep -rho 'punktfunk_core::[A-Za-z_:]*' crates/pf-capture/src crates/pf-frame/src crates/pf-win-display/src | sort -u`). +# So this script generates a ~30-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. +# +# 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-frame|pf-win-display|pf-capture' +REAL_MEMBERS=(pf-frame pf-win-display pf-capture) + +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-frame", "pf-win-display", "pf-capture"]' + 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, +} + +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 + +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