feat: fbuild install, cache observability and split CI caches (#1433) #1795
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |