Skip to content

feat: fbuild install, cache observability and split CI caches (#1433) #1795

feat: fbuild install, cache observability and split CI caches (#1433)

feat: fbuild install, cache observability and split CI caches (#1433) #1795

Workflow file for this run

name: Dylint
# Runs the custom dylints over the workspace. This is enforced as its own
# required GitHub status check rather than being folded into `./lint`, because
# Dylint builds the lint crates and rustc-dev driver and would force a heavy
# recompile on every local lint call. See #264 and #994.
on:
workflow_dispatch: {}
push:
branches: [main]
pull_request:
branches: [main]
# Auto-cancel superseded PR runs: pushing again to a feature branch
# supersedes the in-flight run instead of queueing behind it. Non-PR
# events key on run_id so each gets its own group -- a shared group keeps
# only ONE pending run, which would silently drop queued main SHAs.
concurrency:
group: dylint.yml-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Read-only token. Every step here reads: checkout, a shallow `git fetch
# origin main` for the shrink-only allowlist diff, and lint runs. Nothing
# writes, so nothing should be able to. Without this the job inherits the
# repository/organization default, which can include write access.
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# Dylint owns a separate nightly/driver compile domain. It explicitly opts
# into two Cargo scheduling slots so Windows can install cargo-dylint within
# the ten-minute step budget. Compiler admission remains serialized: two
# concurrent Soldr admissions can race Dylint build-script executables on
# Linux (`Text file busy`). Normal ci-test consumers retain the action's
# safe one-job default for the shared stable test domain.
CARGO_BUILD_JOBS: "2"
SOLDR_JOBS: "1"
jobs:
dylint:
# The matrix `name` is pinned per-leg rather than derived, so the ubuntu
# leg keeps reporting as exactly `Dylint` — that string is a required
# status check, and letting the matrix rename it to `Dylint
# (ubuntu-latest)` would leave the required check permanently pending on
# every PR. The Windows leg is additive and reports under its own name.
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
# Independent legs: a Windows-only violation should not hide a Linux
# one, or the reverse. Finding both in one run is the whole point.
fail-fast: false
matrix:
include:
- os: ubuntu-latest
name: Dylint
# 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 (windows)
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
- 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:
# ci-tests applies the prescribed bounded resource contract for the
# workspace validation domain. Dylint deliberately remains on its
# separate pinned nightly/driver lifecycle in the explicit steps
# below, so it cannot invalidate the stable target tree.
cache: true
toolchain: 1.95.0
cache-key-suffix: dylint
prebuild-deps: none
ci-tests: true
dylint-toolchain: nightly-2026-04-16
cargo-dylint-version: 6.0.1
dylint-link-version: 6.0.1
# Opt out of the implicit `SOLDR_LINKER=fast` injection so cargo /
# rust-toolchain.toml stays in charge. Silences the routine
# "defaulting SOLDR_LINKER=fast" warning (issue #400 /
# setup-soldr#377).
linker: platform-default
# Dylint's build pulls in a full rustc-dev tree on first run
# which can push the build-cache past the 512 MiB action default
# (issue #400). Raise the soft warn threshold; hard cap stays
# at the action default (6 GiB).
cache-payload-warn-bytes: 2GiB
- name: Install Dylint nightly toolchain
timeout-minutes: 10
run: soldr rustup toolchain install nightly-2026-04-16 --component llvm-tools-preview --component rust-src --component rustc-dev --component rustfmt --profile minimal
- name: Install Dylint
timeout-minutes: 10
run: soldr cargo install cargo-dylint dylint-link --version 6.0.1 --locked
- name: Build published Dylint driver
# Dylint is explicitly permitted to build under its separate nightly
# until its matching Windows driver becomes a published prebuilt.
timeout-minutes: 15
run: uv run --no-project python ci/build_dylint_driver.py
- name: Check Dylint library formatting
if: matrix.os == 'ubuntu-latest'
run: |
while IFS= read -r manifest; do
RUSTUP_TOOLCHAIN=nightly-2026-04-16 \
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'
run: |
# Dylint's driver builder intentionally clears RUSTUP_TOOLCHAIN and
# relies on the rustup cargo proxy to re-select each lint's pinned
# toolchain. Keep that proxy ahead of Soldr's compiler shim.
#
# `CARGO_HOME` is defaulted rather than assumed: unset, the export
# below would silently prepend a bare `/bin` and the proxy would
# never be found.
CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
# 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 \
soldr cargo test --manifest-path "$manifest"
done < <(find dylints -mindepth 2 -maxdepth 2 -name Cargo.toml | sort)
- name: Run dylint over workspace
env:
FBUILD_PLATFORM_BOUNDARY_OBSERVED: ${{ github.workspace }}/target/platform-boundary-dylint-observed.tsv
run: |
# Invoke the exact binary installed above. The Soldr cargo front
# door otherwise injects its managed cargo-dylint toolchain, which
# overrides the shared nightly pinned by the library manifests.
# Give every job a distinct, inert cfg so Cargo cannot reuse check
# artifacts without running rustc/Dylint. Actual traversal is what
# makes the observation comparison below meaningful.
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}"
# 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