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
115 changes: 100 additions & 15 deletions .github/workflows/dylint.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Dylint

# One Linux Dylint pass and the policy checks feed the required Dylint status.
# Full CI calls the same workflow.
# The required PR Dylint status covers Linux host code quickly. ci-full and
# manual dispatch check Linux, Windows, and macOS targets on one Linux builder.

on:
workflow_dispatch: {}
Expand Down Expand Up @@ -48,7 +48,7 @@ jobs:
uv run --no-project python ci/enforce_platform_boundary.py --print-totals

dylint:
name: Dylint workspace
name: Dylint Full (Linux builder, all OS targets)
runs-on: ubuntu-latest
timeout-minutes: 90
defaults:
Expand Down Expand Up @@ -76,6 +76,14 @@ jobs:
# separate pinned nightly/driver lifecycle in the explicit steps
# below, so it cannot invalidate the stable target tree.
cache: true
save-cache: true
cargo-registry-cache: true
dylint: true
dylint-cache: true
dylint-foundation-cache: true
# This layer keys by the whole source SHA, so every PR commit misses.
# The lint-library cache below keys only the lint source tree.
dylint-output-cache: false
toolchain: 1.95.0
cache-key-suffix: dylint
prebuild-deps: none
Expand All @@ -100,24 +108,44 @@ jobs:
# Library UI tests invoke soldr cargo test separately, so preserve
# 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: Install cross-target nightly standard libraries
if: ${{ inputs.ref != '' || github.event_name == 'workflow_dispatch' }}
run: |
soldr rustup target add --toolchain nightly-2026-05-28 \
x86_64-pc-windows-msvc x86_64-apple-darwin
- name: Check Dylint library formatting
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: Restore compiled Dylint libraries
id: dylint_libraries_cache
uses: actions/cache/restore@v5
with:
path: target/dylint/libraries/nightly-2026-05-28/release
key: fbuild-dylint-libraries-v1-linux-nightly-2026-05-28-${{ hashFiles('dylints/**') }}
- name: Test Dylint libraries
# UI fixtures assert that each custom lint fires on its fixture.
# UI fixtures execute on the Linux host; workspace checks below use
# the same host lint libraries against each platform target.
run: |
# UI fixtures test the lint implementations themselves. Repeating
# all 27 suites on a workflow-only or application-only PR spends
# minutes compiling their separate test-profile trees.
if git diff --quiet FETCH_HEAD HEAD -- dylints; then
echo "Dylint sources unchanged; skipping library UI fixtures"
exit 0
fi
# UI fixtures launch nested Cargo. Keep the rustup proxy visible so
# it can select each lint's pinned nightly.
CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}"
CARGO_BIN="${CARGO_HOME}/bin"
export PATH="${CARGO_BIN}:${PATH}"
export CARGO_TARGET_DIR="$PWD/target/dylint-tests"
# UI fixtures invoke Cargo outside soldr's Dylint front door.
# Reuse the verified driver fetched by `soldr dylint prepare`.
export DYLINT_DRIVER_PATH="${SOLDR_CACHE_DIR:?}/dylint/drivers"
# Reuse the verified driver path exported by setup-soldr's Dylint
# mode; it owns the driver cache outside SOLDR_CACHE_DIR.
: "${DYLINT_DRIVER_PATH:?}"
# Dylint's UI runner asks for the short nightly directory; Soldr's
# verified asset lives under the host-qualified directory.
host="$(RUSTUP_TOOLCHAIN=nightly-2026-05-28 soldr rustc -vV | sed -n 's/^host: //p')"
Expand All @@ -134,18 +162,75 @@ jobs:
RUSTUP_TOOLCHAIN=nightly-2026-05-28 \
soldr cargo test --manifest-path "$manifest"
done < <(find dylints -mindepth 2 -maxdepth 2 -name Cargo.toml | sort)
- name: Reuse cached Dylint library tree
run: |
# setup-soldr saves the short nightly path; Soldr writes a
# host-qualified path. Point both names at one tree so warm runs
# reuse the 27 compiled lint libraries without a copy over
# read-only restored cache files. Do not cache the much larger
# three-platform workspace target tree here.
short="${SOLDR_DYLINT_CONFIGURED_TOOLCHAIN}"
qualified="${short}-x86_64-unknown-linux-gnu"
root="$PWD/target/dylint/libraries"
mkdir -p "$root/$short"
if test -L "$root/$qualified"; then
test "$(readlink "$root/$qualified")" = "$short"
else
test ! -e "$root/$qualified"
ln -s "$short" "$root/$qualified"
fi
- name: Run dylint over workspace
env:
FBUILD_PLATFORM_BOUNDARY_OBSERVED: ${{ github.workspace }}/target/platform-boundary-dylint-observed.tsv
FULL_TARGETS: ${{ inputs.ref != '' || github.event_name == 'workflow_dispatch' }}
run: |
# 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.
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
run: uv run --no-project python ci/enforce_platform_boundary.py --dylint-observed target/platform-boundary-dylint-observed.tsv --print-totals
status=0
# The Linux pass covers the whole workspace. Cross-target passes
# need only crates with target-specific source in the checked-in
# boundary baseline; deriving this list makes new platform code
# enter the cross-target check when its ledger is updated.
mapfile -t platform_packages < <(
awk -F '\t' '!/^#/ && $1 ~ /^crates\// { split($1, parts, "/"); print parts[2] }' \
dylints/enforce_platform_boundary/src/baseline.txt | sort -u
)
test "${#platform_packages[@]}" -gt 0
targets=(x86_64-unknown-linux-gnu)
if test "$FULL_TARGETS" = true; then
targets+=(x86_64-pc-windows-msvc x86_64-apple-darwin)
fi
for triple in "${targets[@]}"; do
if test "$triple" = x86_64-unknown-linux-gnu; then
package_args=(--workspace)
else
package_args=()
for package in "${platform_packages[@]}"; do
package_args+=(--package "$package")
done
fi
observed="$PWD/target/platform-boundary-dylint-observed-$triple.tsv"
export FBUILD_PLATFORM_BOUNDARY_OBSERVED="$observed"
rm -f "$observed"
if ! env -u RUSTUP_TOOLCHAIN soldr dylint --all -- "${package_args[@]}" --all-targets --target "$triple"; then
status=1
continue
fi
if ! uv run --no-project python ci/enforce_platform_boundary.py --dylint-observed "$observed" --print-totals; then
status=1
fi
done
test "$status" -eq 0
# Soldr's successful marker uses a host-qualified nightly, while
# setup-soldr expects the same compiler identity with a short name.
# Check all fields before letting the action cache these outputs.
identity="${SOLDR_DYLINT_CONFIGURED_TOOLCHAIN}|${SOLDR_DYLINT_CONFIGURED_RUSTC_RELEASE}|${SOLDR_DYLINT_CONFIGURED_RUSTC_COMMIT_HASH}"
qualified="${SOLDR_DYLINT_CONFIGURED_TOOLCHAIN}-x86_64-unknown-linux-gnu|${SOLDR_DYLINT_CONFIGURED_RUSTC_RELEASE}|${SOLDR_DYLINT_CONFIGURED_RUSTC_COMMIT_HASH}"
test "$(cat "$SOLDR_DYLINT_SUCCESS_MARKER")" = "$qualified"
printf '%s\n' "$identity" > "$SOLDR_DYLINT_SUCCESS_MARKER"
- name: Save compiled Dylint libraries
if: steps.dylint_libraries_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: target/dylint/libraries/nightly-2026-05-28/release
key: ${{ steps.dylint_libraries_cache.outputs.cache-primary-key }}

gate:
name: Dylint
Expand Down
26 changes: 24 additions & 2 deletions ci/test_fractional_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,30 @@ 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", dylint["jobs"]["dylint"]["runs-on"])
self.assertNotIn("strategy", dylint["jobs"]["dylint"])
lint_job = dylint["jobs"]["dylint"]
self.assertEqual("ubuntu-latest", lint_job["runs-on"])
self.assertNotIn("strategy", lint_job)
lint_run = next(
step["run"]
for step in lint_job["steps"]
if step.get("name") == "Run dylint over workspace"
)
for triple in (
"x86_64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
"x86_64-apple-darwin",
):
self.assertIn(triple, lint_run)
self.assertIn("inputs.ref != ''", next(step for step in lint_job["steps"] if step.get("name") == "Run dylint over workspace")["env"]["FULL_TARGETS"])
target_install = next(step for step in lint_job["steps"] if step.get("name") == "Install cross-target nightly standard libraries")
self.assertIn("inputs.ref != ''", target_install["if"])
setup = next(step for step in lint_job["steps"] if step.get("uses", "").startswith("zackees/setup-soldr@"))
self.assertIs(setup["with"]["dylint-output-cache"], False)
restore = next(step for step in lint_job["steps"] if step.get("name") == "Restore compiled Dylint libraries")
self.assertIn("hashFiles('dylints/**')", restore["with"]["key"])
self.assertTrue(any(step.get("name") == "Save compiled Dylint libraries" for step in lint_job["steps"]))
ui_tests = next(step for step in lint_job["steps"] if step.get("name") == "Test Dylint libraries")
self.assertIn("git diff --quiet FETCH_HEAD HEAD -- dylints", ui_tests["run"])
gate = dylint["jobs"]["gate"]
self.assertEqual("Dylint", gate["name"])
self.assertEqual({"policy", "dylint"}, set(gate["needs"]))
Expand Down
Loading