Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 6 additions & 51 deletions .github/workflows/dylint.yml
Original file line number Diff line number Diff line change
@@ -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: {}
Expand Down Expand Up @@ -48,55 +48,26 @@ 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
with:
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:
Expand Down Expand Up @@ -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 `<target>/<triple>/debug`, while each lint's `fn ui`
# clears `CARGO_BUILD_TARGET` so the library lands in
# `<target>/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.
Expand Down Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions ci/test_fractional_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]))
Expand Down
9 changes: 3 additions & 6 deletions crates/fbuild-cli/tests/ci_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down Expand Up @@ -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"]);
Expand All @@ -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"]);
Expand Down
8 changes: 3 additions & 5 deletions crates/fbuild-cli/tests/daemon_crash_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 3 additions & 6 deletions crates/fbuild-cli/tests/lib_select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down Expand Up @@ -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([
Expand All @@ -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"]);
Expand Down
6 changes: 2 additions & 4 deletions crates/fbuild-cli/tests/test_emu_exit_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions crates/fbuild-daemon/tests/legacy_daemon_transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 3 additions & 4 deletions crates/fbuild-daemon/tests/port_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down
4 changes: 2 additions & 2 deletions crates/fbuild-daemon/tests/process_containment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading