diff --git a/.github/workflows/dylint.yml b/.github/workflows/dylint.yml index 43ed82a5..1086b55f 100644 --- a/.github/workflows/dylint.yml +++ b/.github/workflows/dylint.yml @@ -1,7 +1,7 @@ name: Dylint -# The required Dylint status covers both policy and real workspace lints on -# Linux, Windows, and macOS for every PR. Full CI calls the same workflow. +# One Linux Dylint pass and the policy checks feed the required Dylint status. +# Full CI calls the same workflow. on: workflow_dispatch: {} @@ -48,37 +48,11 @@ jobs: uv run --no-project python ci/enforce_platform_boundary.py --print-totals dylint: - name: ${{ matrix.name }} - runs-on: ${{ matrix.os }} - strategy: - # Independent native legs expose each OS's cfg-gated workspace code. - # One platform's failure must not hide another's findings. - fail-fast: false - matrix: - include: - - os: ubuntu-latest - name: Dylint Full (linux) - # FastLED/fbuild#1359 — `cargo check` only compiles the modules the - # target platform selects, so every `#[cfg(windows)]` / - # `platform/windows/**` module was invisible to a ubuntu-only gate. - # That is not hypothetical: the platform-facade migration (#1306) - # moved `display_slash` into `platform/windows/fs.rs` and its - # `ban_manual_slash_normalize` violation sat on main unnoticed, - # because CI had never compiled the file. - # - # Windows is also where the bugs these lints exist to prevent - # actually bite — #875, #885, #890 and #912 were all Windows path - # handling. - - os: windows-latest - name: Dylint Full (windows) - - os: macos-15 - name: Dylint Full (macos) + name: Dylint workspace + runs-on: ubuntu-latest timeout-minutes: 90 defaults: run: - # Git Bash on the Windows runner. The steps below use process - # substitution and POSIX `find`, so PowerShell (the Windows default) - # would fail to parse them. shell: bash steps: - uses: actions/checkout@v6 @@ -86,17 +60,14 @@ jobs: ref: ${{ inputs.ref }} - uses: astral-sh/setup-uv@v3 - name: Validate Dylint allowlist paths - if: matrix.os == 'ubuntu-latest' run: uv run --no-project python ci/check_dylint_allowlists.py - name: Enforce shrink-only .fbuild allowlist - if: matrix.os == 'ubuntu-latest' # The baseline in dylints/ban_raw_fbuild_path may only shrink # (FastLED/fbuild#1349). Needs main fetched to diff against. run: | git fetch --no-tags --depth=1 origin main uv run --no-project python ci/check_fbuild_path_baseline.py --base FETCH_HEAD - name: Validate platform-boundary ledgers - if: matrix.os == 'ubuntu-latest' run: uv run --no-project python ci/enforce_platform_boundary.py --print-totals - uses: zackees/setup-soldr@v0 with: @@ -130,29 +101,13 @@ jobs: # the prebuilt dylint-link directory for their linker config. sed -n 's/^soldr: dylint prepare: tool bin dir //p' "$RUNNER_TEMP/dylint-prepare.log" >> "$GITHUB_PATH" - name: Check Dylint library formatting - if: matrix.os == 'ubuntu-latest' run: | while IFS= read -r manifest; do RUSTUP_TOOLCHAIN=nightly-2026-05-28 \ soldr cargo fmt --manifest-path "$manifest" --all -- --check done < <(find dylints -mindepth 2 -maxdepth 2 -name Cargo.toml | sort) - name: Test Dylint libraries - # ubuntu only. These are the lint crates' OWN ui fixtures, asserting - # each lint fires on its own fixture — platform-independent behavior - # that the ubuntu leg already covers. What the Windows leg uniquely - # provides is compiling Windows-gated *workspace* source so the lints - # can see it, and that runs below. - # - # It also still does not work there. FastLED/fbuild#1373 has the - # evidence: two target-dir layouts coexist (soldr builds the test - # binary under `//debug`, while each lint's `fn ui` - # clears `CARGO_BUILD_TARGET` so the library lands in - # `/debug`), and the driver fails with `LoadLibraryExW failed` - # on the library itself. Neither normalizing the PATH separator nor - # adding the untargeted `debug`/`debug/deps` to PATH changed it, so - # the missing dependency is somewhere else again. Tracked rather than - # guessed at further. - if: matrix.os == 'ubuntu-latest' + # UI fixtures assert that each custom lint fires on its fixture. run: | # UI fixtures launch nested Cargo. Keep the rustup proxy visible so # it can select each lint's pinned nightly. @@ -186,7 +141,7 @@ jobs: # Force fresh workspace traversal for the observation ledger while # preserving Cargo's cached dependencies. A per-run RUSTFLAGS value # invalidated every dependency, causing a cold rebuild on each run. - rg --files crates -g '*.rs' -0 | xargs -0 -r touch + find crates -type f -name '*.rs' -exec touch {} + rm -f "$FBUILD_PLATFORM_BOUNDARY_OBSERVED" env -u RUSTUP_TOOLCHAIN soldr dylint --all -- --workspace --all-targets - name: Compare scanner with actual Dylint observations diff --git a/ci/test_fractional_workflows.py b/ci/test_fractional_workflows.py index 071e4d2c..44323632 100644 --- a/ci/test_fractional_workflows.py +++ b/ci/test_fractional_workflows.py @@ -47,10 +47,8 @@ def test_full_matrix_matches_every_supported_board(self): dylint = self.load("dylint.yml") self.assertEqual("Dylint policy", dylint["jobs"]["policy"]["name"]) self.assertNotIn("if", dylint["jobs"]["dylint"]) - self.assertEqual( - {"ubuntu-latest", "windows-latest", "macos-15"}, - {cell["os"] for cell in dylint["jobs"]["dylint"]["strategy"]["matrix"]["include"]}, - ) + self.assertEqual("ubuntu-latest", dylint["jobs"]["dylint"]["runs-on"]) + self.assertNotIn("strategy", dylint["jobs"]["dylint"]) gate = dylint["jobs"]["gate"] self.assertEqual("Dylint", gate["name"]) self.assertEqual({"policy", "dylint"}, set(gate["needs"])) diff --git a/crates/fbuild-cli/tests/ci_command.rs b/crates/fbuild-cli/tests/ci_command.rs index 6f83e423..5ecd180e 100644 --- a/crates/fbuild-cli/tests/ci_command.rs +++ b/crates/fbuild-cli/tests/ci_command.rs @@ -44,8 +44,7 @@ fn run_cli_or_timeout(mut cmd: Command) -> Output { #[test] fn ci_help_lists_pio_compat_flags() { - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: integration test driver invoking the compiled fbuild binary. let mut cmd = Command::new(bin); cmd.args(["ci", "--help"]); @@ -77,8 +76,7 @@ fn ci_help_lists_pio_compat_flags() { #[test] fn ci_without_board_is_a_usage_error() { - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: integration test driver. let mut cmd = Command::new(bin); cmd.args(["ci", "examples/Blink/Blink.ino"]); @@ -94,8 +92,7 @@ fn ci_without_board_is_a_usage_error() { #[test] fn ci_without_sketches_is_a_usage_error() { - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: integration test driver. let mut cmd = Command::new(bin); cmd.args(["ci", "--board", "uno"]); diff --git a/crates/fbuild-cli/tests/daemon_crash_recovery.rs b/crates/fbuild-cli/tests/daemon_crash_recovery.rs index 24b18387..c5d23830 100644 --- a/crates/fbuild-cli/tests/daemon_crash_recovery.rs +++ b/crates/fbuild-cli/tests/daemon_crash_recovery.rs @@ -57,8 +57,7 @@ fn free_port() -> u16 { } fn cli_command(args: &[&str], port: u16, cache_dir: &std::path::Path) -> Command { - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: test driver invoking the fbuild CLI binary under test. let mut command = Command::new(bin); command @@ -174,9 +173,8 @@ fn client_recovers_after_daemon_is_killed_uncleanly() { // The production spawn path resolves the daemon binary as a sibling of // the CLI. Under `cargo test --workspace` (and any full build) it exists; // under an isolated `-p fbuild-cli` test run it may not — skip then. - let cli = NormalizedPath::new( - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"), - ); + let cli = + NormalizedPath::new(std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path")); let daemon_name = fbuild_core::platform::executable::name("fbuild-daemon", "fbuild-daemon.exe"); let sibling = cli .parent() diff --git a/crates/fbuild-cli/tests/lib_select.rs b/crates/fbuild-cli/tests/lib_select.rs index d6c3de0a..8a391ca0 100644 --- a/crates/fbuild-cli/tests/lib_select.rs +++ b/crates/fbuild-cli/tests/lib_select.rs @@ -46,8 +46,7 @@ fn run_cli_or_timeout(mut cmd: Command) -> Output { /// `fbuild lib-select --help` must exit 0 and document both modes. #[test] fn lib_select_help_lists_command() { - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: integration test driver invoking the compiled fbuild binary. let mut cmd = Command::new(bin); cmd.args(["lib-select", "--help"]); @@ -77,8 +76,7 @@ fn lib_select_help_lists_command() { /// the precise code, only that callers (CI, scripts) can detect the error. #[test] fn lib_select_missing_project_exits_nonzero() { - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: integration test driver invoking the compiled fbuild binary. let mut cmd = Command::new(bin); cmd.args([ @@ -103,8 +101,7 @@ fn lib_select_missing_project_exits_nonzero() { /// Passing both must fail at argument-parse time, not silently pick one. #[test] fn lib_select_explain_and_json_conflict() { - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: integration test driver invoking the compiled fbuild binary. let mut cmd = Command::new(bin); cmd.args(["lib-select", ".", "--explain", "--json"]); diff --git a/crates/fbuild-cli/tests/test_emu_exit_code.rs b/crates/fbuild-cli/tests/test_emu_exit_code.rs index a6b24572..1e679353 100644 --- a/crates/fbuild-cli/tests/test_emu_exit_code.rs +++ b/crates/fbuild-cli/tests/test_emu_exit_code.rs @@ -173,8 +173,7 @@ fn deploy_output_survives_nonzero_exit() { let port = spawn_mock_daemon(Arc::clone(&stop)); let project = make_test_project(); - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // allow-direct-spawn: integration test driver that invokes the compiled fbuild binary. let mut cmd = Command::new(bin); @@ -216,8 +215,7 @@ fn test_emu_exits_non_zero_when_daemon_returns_failure() { let port = spawn_mock_daemon(Arc::clone(&stop)); let project = make_test_project(); - let bin = - option_env!("CARGO_BIN_EXE_fbuild").expect("cargo test must provide CARGO_BIN_EXE_fbuild"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild").expect("fbuild binary path"); // Drive the CLI at the mock daemon. We clear FBUILD_DEV_MODE so the // CLI sticks to prod-mode path assumptions, and pin diff --git a/crates/fbuild-daemon/tests/legacy_daemon_transition.rs b/crates/fbuild-daemon/tests/legacy_daemon_transition.rs index a65add77..ca00ea17 100644 --- a/crates/fbuild-daemon/tests/legacy_daemon_transition.rs +++ b/crates/fbuild-daemon/tests/legacy_daemon_transition.rs @@ -192,8 +192,7 @@ async fn wait_for_http_health(port: u16, timeout: Duration) -> bool { async fn real_daemon_root_ownership_released_on_kill() { let temp_home = tempfile::tempdir().expect("temp home"); let port = free_port(); - let bin = option_env!("CARGO_BIN_EXE_fbuild-daemon") - .expect("cargo test must provide CARGO_BIN_EXE_fbuild-daemon"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild-daemon").expect("fbuild-daemon binary path"); let home_key = if fbuild_core::platform::host::is_windows() { "USERPROFILE" } else { diff --git a/crates/fbuild-daemon/tests/port_recovery.rs b/crates/fbuild-daemon/tests/port_recovery.rs index fbe9a54c..90b893cb 100644 --- a/crates/fbuild-daemon/tests/port_recovery.rs +++ b/crates/fbuild-daemon/tests/port_recovery.rs @@ -58,12 +58,11 @@ fn wait_with_timeout(child: &mut Child, budget: Duration) -> bool { #[ignore = "expects a real fbuild-daemon binary; run with --ignored"] fn daemon_rebinds_cleanly_after_hard_kill_with_open_connection() { let port: u16 = 18900; // dedicated test port, avoids 8765 collisions - let bin = option_env!("CARGO_BIN_EXE_fbuild-daemon") - .expect("cargo test must provide CARGO_BIN_EXE_fbuild-daemon"); + let bin = std::env::var("CARGO_BIN_EXE_fbuild-daemon").expect("fbuild-daemon binary path"); // 1) Spawn the first daemon. // allow-direct-spawn: test driver spawns the real fbuild-daemon binary under test. - let mut d1 = Command::new(bin) + let mut d1 = Command::new(&bin) .env("FBUILD_DAEMON_PORT", port.to_string()) .stdout(Stdio::null()) .stderr(Stdio::null()) @@ -96,7 +95,7 @@ fn daemon_rebinds_cleanly_after_hard_kill_with_open_connection() { // With a *correct* fix (graceful shutdown + SO_EXCLUSIVEADDRUSE // on Windows) this should succeed without permissive REUSEADDR. // allow-direct-spawn: test driver spawns the real fbuild-daemon binary under test. - let mut d2 = Command::new(bin) + let mut d2 = Command::new(&bin) .env("FBUILD_DAEMON_PORT", port.to_string()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/crates/fbuild-daemon/tests/process_containment.rs b/crates/fbuild-daemon/tests/process_containment.rs index 3d383448..1dc6b5b1 100644 --- a/crates/fbuild-daemon/tests/process_containment.rs +++ b/crates/fbuild-daemon/tests/process_containment.rs @@ -60,8 +60,8 @@ fn wait_with_timeout(child: &mut Child, budget: Duration) -> bool { #[test] #[ignore = "spawns real subprocesses and issues hard-kills; run with --ignored"] fn daemon_children_die_when_daemon_dies() { - let harness = option_env!("CARGO_BIN_EXE_containment_harness") - .expect("cargo test must provide CARGO_BIN_EXE_containment_harness"); + let harness = std::env::var("CARGO_BIN_EXE_containment_harness") + .expect("containment harness binary path"); // Start the parent role. // allow-direct-spawn: integration-test driver invoking the containment harness binary.