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
54 changes: 34 additions & 20 deletions .github/workflows/dylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,25 +122,21 @@ jobs:
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, and the reason is a scoping one rather than a workaround.
# These are the lint crates' OWN unit/ui tests — they assert that each
# lint fires on its fixture, which is platform-independent behavior
# already covered once. What the Windows leg uniquely provides is
# compiling Windows-gated *workspace* source so the lints can see it;
# re-running the lint crates' self-tests on a second OS adds ~10 min per
# PR and proves nothing new.
# 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 does not currently work there: Dylint 6.0.1 looks for its test
# library under `<target>/debug`, while soldr sets `CARGO_BUILD_TARGET`
# so cargo writes to `<target>/<host>/debug`. Each lint's `fn ui` clears
# that variable, but on the runner the .dll still fails to load:
#
# error: could not load library
# `.../target/dylint-tests/debug/ban_manual_slash_normalize@nightly-2026-04-16.dll`:
# LoadLibraryExW failed
#
# That is a dylint/soldr/compiletest interaction, not an fbuild defect,
# and is tracked separately rather than blocking this gate.
# 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'
run: |
# Dylint's driver builder intentionally clears RUSTUP_TOOLCHAIN and
Expand All @@ -151,7 +147,23 @@ jobs:
# below would silently prepend a bare `/bin` and the proxy would
# never be found.
CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
export PATH="${CARGO_HOME}/bin:${PATH}"
# FastLED/fbuild#1373. On Windows `CARGO_HOME` is a native path
# (`C:\Users\runneradmin\.cargo`), and `$PATH` inside Git Bash is
# POSIX and `:`-separated. Prepending one to the other produced
# `C:\Users\runneradmin\.cargo/bin:/usr/bin...`, whose drive-letter
# colon reads as a separator — the runner saw a `C` entry followed by
# a bogus `D:\Users\runneradmin\.cargo\bin`, the compiletest driver
# could not resolve the lint library's dependencies, and every ui
# fixture failed with `LoadLibraryExW failed`.
#
# `cygpath -u` converts to `/c/Users/.../.cargo/bin`, which is safe to
# join with `:`. Absent on Linux/macOS, where the path is already
# POSIX.
CARGO_BIN="${CARGO_HOME}/bin"
if command -v cygpath >/dev/null 2>&1; then
CARGO_BIN="$(cygpath -u "${CARGO_BIN}")"
fi
export PATH="${CARGO_BIN}:${PATH}"
export CARGO_TARGET_DIR="$PWD/target/dylint-tests"
while IFS= read -r manifest; do
RUSTUP_TOOLCHAIN=nightly-2026-04-16 \
Expand All @@ -170,7 +182,9 @@ jobs:
export RUSTFLAGS="${RUSTFLAGS:+${RUSTFLAGS} }--cfg fbuild_platform_boundary_observation_run_${GITHUB_RUN_ID}"
rm -f "$FBUILD_PLATFORM_BOUNDARY_OBSERVED"
CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
# No `.exe` suffix needed: Git Bash resolves it on Windows.
# Invoked as a single argument rather than through `PATH`, so the
# separator hazard in #1373 does not apply here. No `.exe` suffix
# needed: Git Bash resolves it on Windows.
"${CARGO_HOME}/bin/cargo-dylint" dylint --all -- --workspace --all-targets
- name: Compare scanner with actual Dylint observations
run: uv run --no-project python ci/enforce_platform_boundary.py --dylint-observed target/platform-boundary-dylint-observed.tsv --print-totals
31 changes: 29 additions & 2 deletions crates/fbuild-core/src/subprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,22 @@ pub async fn run_command_retrying_exec_busy(
cwd: Option<&Path>,
env: Option<&[(&str, &str)]>,
timeout: Option<Duration>,
) -> Result<ToolOutput> {
run_command_retrying_exec_busy_with_backoff(args, cwd, env, timeout, EXEC_BUSY_BACKOFF).await
}

/// [`run_command_retrying_exec_busy`] with the backoff injected.
///
/// Exists so the retry test can give itself a margin that no runner load can
/// close. With the production 25 ms backoff the test was a race between a
/// releasing task and the second spawn attempt, and it lost one on a busy CI
/// runner (FastLED/fbuild#1373).
async fn run_command_retrying_exec_busy_with_backoff(
args: &[&str],
cwd: Option<&Path>,
env: Option<&[(&str, &str)]>,
timeout: Option<Duration>,
backoff: Duration,
) -> Result<ToolOutput> {
if args.is_empty() {
return Err(FbuildError::Other("empty command".to_string()));
Expand All @@ -475,7 +491,7 @@ pub async fn run_command_retrying_exec_busy(
// `tokio::time::sleep`, not `std::thread::sleep`: this runs on
// a tokio worker and blocking it would stall every other task
// on that thread (FastLED/fbuild#844).
tokio::time::sleep(EXEC_BUSY_BACKOFF * attempt).await;
tokio::time::sleep(backoff * attempt).await;
attempt += 1;
}
Err(error) => return Err(spawn_err(args, error)),
Expand Down Expand Up @@ -831,7 +847,18 @@ exit 0
});
armed_rx.await.expect("releaser task must start");

let result = run_command_retrying_exec_busy(&[path.as_str()], None, None, None).await;
// A 500 ms backoff against a 10 ms hold: the second attempt fires
// fifty times later than the release. The production 25 ms backoff
// made this a race, and a loaded CI runner won it (#1373). Injecting
// the backoff costs nothing and removes the only timing dependence.
let result = run_command_retrying_exec_busy_with_backoff(
&[path.as_str()],
None,
None,
None,
Duration::from_millis(500),
)
.await;
releaser.await.expect("releaser task must finish");
assert!(
result.is_ok(),
Expand Down
Loading
Loading