tools: check and lint Windows-only Rust from a Linux dev box (scripts/wincheck.sh)
ci / web (push) Successful in 1m30s
ci / docs-site (push) Successful in 1m31s
ci / rust-arm64 (push) Successful in 9m50s
ci / bench (push) Successful in 7m15s
apple / swift (push) Successful in 5m41s
android / android (push) Successful in 12m23s
arch / build-publish (push) Successful in 17m26s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
windows-host / package (push) Successful in 18m55s
deb / build-publish (push) Successful in 9m9s
deb / build-publish-client-arm64 (push) Successful in 7m29s
ci / rust (push) Successful in 22m14s
deb / build-publish-host (push) Successful in 11m27s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 22m15s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 23s
apple / screenshots (push) Successful in 26m13s

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 12:07:22 +02:00
co-authored by Claude Opus 5
parent 0d69ebf60b
commit 28bbaa5250
+146
View File
@@ -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