From 96ea031efb6530d1678768f9b0516390ce64a158 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sun, 27 Sep 2026 12:01:17 -0700 Subject: [PATCH 1/2] ci: require native Dylint on ordinary PRs Refs #274 --- .github/workflows/linux-arm-lint.yml | 1 - .github/workflows/macos-arm-lint.yml | 1 - .github/workflows/macos-x86-lint.yml | 1 - .github/workflows/windows-x86-lint.yml | 1 - lint | 17 ++--------------- tests/unit/test_lint_script.py | 26 +++++++++++++++++++++----- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/.github/workflows/linux-arm-lint.yml b/.github/workflows/linux-arm-lint.yml index fbe29118..17bd8a19 100644 --- a/.github/workflows/linux-arm-lint.yml +++ b/.github/workflows/linux-arm-lint.yml @@ -20,7 +20,6 @@ concurrency: jobs: call: - if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci-full')) uses: ./.github/workflows/_lint.yml with: source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/macos-arm-lint.yml b/.github/workflows/macos-arm-lint.yml index 25277a79..71b0b302 100644 --- a/.github/workflows/macos-arm-lint.yml +++ b/.github/workflows/macos-arm-lint.yml @@ -20,7 +20,6 @@ concurrency: jobs: call: - if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci-full')) uses: ./.github/workflows/_lint.yml with: source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/macos-x86-lint.yml b/.github/workflows/macos-x86-lint.yml index 5b8b19bb..14b6c9a5 100644 --- a/.github/workflows/macos-x86-lint.yml +++ b/.github/workflows/macos-x86-lint.yml @@ -20,7 +20,6 @@ concurrency: jobs: call: - if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci-full')) uses: ./.github/workflows/_lint.yml with: source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/windows-x86-lint.yml b/.github/workflows/windows-x86-lint.yml index 4d5f5e4c..f8deac1b 100644 --- a/.github/workflows/windows-x86-lint.yml +++ b/.github/workflows/windows-x86-lint.yml @@ -20,7 +20,6 @@ concurrency: jobs: call: - if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci-full')) uses: ./.github/workflows/_lint.yml with: source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} diff --git a/lint b/lint index e1dd481c..b62b1cad 100755 --- a/lint +++ b/lint @@ -44,8 +44,8 @@ elif ! command -v rustup &> /dev/null; then DYLINT_SKIP_REASON="rustup is required for cargo-dylint's internal driver build" fi -if [ -n "$DYLINT_SKIP_REASON" ] && [ -n "${CI:-}" ]; then - echo "Error: dylint prerequisites are missing in CI: $DYLINT_SKIP_REASON" >&2 +if [ -n "$DYLINT_SKIP_REASON" ]; then + echo "Error: dylint prerequisites are missing: $DYLINT_SKIP_REASON" >&2 exit 1 fi @@ -112,18 +112,5 @@ EOF trap - EXIT fi -if [ -n "$DYLINT_SKIP_REASON" ]; then - # Local hosts without the dylint prerequisites still get a green lint: every - # other stage ran. CI never reaches here -- the guard above fails hard when - # $CI is set, so the dylint stage cannot be silently dropped in CI (#262). - echo "" >&2 - echo "==============================================================" >&2 - echo "dylint not run: $DYLINT_SKIP_REASON" >&2 - echo "Every other lint stage ran and passed." >&2 - echo "Install the prerequisite and re-run ./lint for full coverage." >&2 - echo "==============================================================" >&2 - echo "Linting complete (dylint skipped)!" - exit 0 -fi echo Linting complete! exit 0 diff --git a/tests/unit/test_lint_script.py b/tests/unit/test_lint_script.py index 8da140eb..377b429b 100644 --- a/tests/unit/test_lint_script.py +++ b/tests/unit/test_lint_script.py @@ -79,10 +79,8 @@ def _log_has_match(log_lines: list[str], pattern: str) -> bool: def test_python_stages_run_and_cover_ci_without_dylint(tmp_path: Path) -> None: result, log_lines = _run_lint(tmp_path, ci=False) - # Outside CI a missing dylint prerequisite is a loud skip, not a failure: - # the other stages all ran, so the script still succeeds. - assert result.returncode == 0, (result.stdout, result.stderr) - assert "dylint not run" in result.stderr + assert result.returncode != 0 + assert "dylint prerequisites are missing" in result.stderr # Every Python lint stage must cover src, tests, and ci -- the stub uv # collapses "uv run ..." into a single logged argv line, so match @@ -112,7 +110,7 @@ def test_missing_dylint_is_a_hard_failure_in_ci(tmp_path: Path) -> None: result, log_lines = _run_lint(tmp_path, ci=True) assert result.returncode != 0 - assert "Error: dylint prerequisites are missing in CI:" in result.stderr + assert "Error: dylint prerequisites are missing:" in result.stderr # The failure is reported only after the Python stages have run, so a CI # host missing cargo-dylint still reports every Python finding. assert _log_has_match(log_lines, r"^uv (run )?pyright src tests ci$") @@ -131,3 +129,21 @@ def test_python_stages_precede_the_rust_stages(tmp_path: Path) -> None: ) assert pyright_index < rust_index + + +@pytest.mark.parametrize( + "workflow", + [ + "linux-arm-lint.yml", + "macos-arm-lint.yml", + "macos-x86-lint.yml", + "windows-x86-lint.yml", + ], +) +def test_native_dylint_runs_on_unlabelled_pr(workflow: str) -> None: + source = (REPO_ROOT / ".github" / "workflows" / workflow).read_text( + encoding="utf-8" + ) + assert "pull_request:" in source + assert "uses: ./.github/workflows/_lint.yml" in source + assert "contains(github.event.pull_request.labels.*.name, 'ci-full')" not in source From caaca5e980e825dbb7c8978c7155a60652093d4b Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sun, 27 Sep 2026 12:24:52 -0700 Subject: [PATCH 2/2] Use published Dylint tools in native lint jobs --- .github/workflows/_lint.yml | 32 ++------ Cargo.toml | 4 +- dylints/ban_std_pathbuf/.cargo/config.toml | 4 + dylints/ban_std_pathbuf/README.md | 17 ++--- dylints/ban_std_pathbuf/rust-toolchain.toml | 2 +- lint | 85 ++------------------- tests/unit/test_dylint_version_check.py | 12 +-- tests/unit/test_lint_script.py | 8 +- 8 files changed, 39 insertions(+), 125 deletions(-) create mode 100644 dylints/ban_std_pathbuf/.cargo/config.toml diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml index 2e2ad035..2fa8093e 100644 --- a/.github/workflows/_lint.yml +++ b/.github/workflows/_lint.yml @@ -15,6 +15,8 @@ jobs: lint: runs-on: ${{ inputs.runs-on }} timeout-minutes: 30 + env: + SOLDR_FORCE_MANAGED_CARGO_SUBCOMMANDS: "1" defaults: run: shell: bash @@ -49,7 +51,7 @@ jobs: sudo apt-get update -qq sudo apt-get install -y -qq libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev - - uses: zackees/setup-soldr@v0.9.62 + - uses: zackees/setup-soldr@dfbe9627f6cb0226716b61625b99a58949162720 # v0.9.80 id: soldr env: GITHUB_TOKEN: ${{ github.token }} @@ -76,37 +78,15 @@ jobs: # the dylint driver dir, exposing dylint-cache-hit so we can skip the # expensive cargo installs below (#170). dylint-cache: true - dylint-toolchain: nightly-2026-03-26 + dylint-toolchain: nightly-2026-05-28 cargo-dylint-version: "6.0.3" dylint-link-version: "6.0.3" - name: Sync Python deps run: uv sync --group dev - # The dylint nightly toolchain must land in the job's RUSTUP_HOME - # (setup-soldr exports a managed rustup home to all later steps), so use - # bare rustup here. Routing this through `soldr rustup` installs into - # soldr's private toolchain home instead, and the dylint driver build - # then fails with E0463 (can't find crate rustc_driver) because the - # rustc-dev components are invisible (#170 regression from #172). - # Unconditional: the nightly is not part of the dylint cache paths. - - name: Install Dylint nightly toolchain - run: rustup toolchain install nightly-2026-03-26 --profile minimal --component llvm-tools-preview,rust-src,rustc-dev - - # Building cargo-dylint / dylint-link from source is the dominant fixed - # cost of every lint job (10+ min per platform). A restored cache should - # expose both binaries, but verify the commands themselves: a cache hit - # without a PATH-visible binary must not make the lint job fail. - - name: Install Dylint tools - run: | - DYLINT_VERSION="6.0.3" - if ! uv run python ci/lint_python/dylint_version_checker.py cargo-dylint "$DYLINT_VERSION"; then - soldr cargo install cargo-dylint --version "$DYLINT_VERSION" --locked --force - fi - if ! command -v dylint-link >/dev/null 2>&1; then - soldr cargo install dylint-link --version "$DYLINT_VERSION" --locked --force - fi - uv run python ci/lint_python/dylint_version_checker.py cargo-dylint "$DYLINT_VERSION" + - name: Prepare published Dylint tools + run: env -u RUSTUP_TOOLCHAIN soldr dylint prepare - name: Lint run: ./lint diff --git a/Cargo.toml b/Cargo.toml index cfd8126a..fe28f37c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,9 @@ members = [ "crates/fastled-cli", ] +[workspace.metadata.dylint] +libraries = [{ path = "dylints/*" }] + [workspace.package] version = "2.0.22" edition = "2021" @@ -25,4 +28,3 @@ codegen-units = 1 [profile.test.package.kernal-api] codegen-units = 1 - diff --git a/dylints/ban_std_pathbuf/.cargo/config.toml b/dylints/ban_std_pathbuf/.cargo/config.toml new file mode 100644 index 00000000..9d481d1b --- /dev/null +++ b/dylints/ban_std_pathbuf/.cargo/config.toml @@ -0,0 +1,4 @@ +# Dylint requires the toolchain-suffixed cdylib name. Its linker wrapper +# produces that name on every host, without a post-build copy or retry. +[target.'cfg(all())'] +rustflags = ["-C", "linker=dylint-link"] diff --git a/dylints/ban_std_pathbuf/README.md b/dylints/ban_std_pathbuf/README.md index 3ef68589..27ea9daf 100644 --- a/dylints/ban_std_pathbuf/README.md +++ b/dylints/ban_std_pathbuf/README.md @@ -20,27 +20,24 @@ regression from coming back into a different module. ## Toolchain -Builds against `nightly-2026-03-26` (see `rust-toolchain.toml`). The main +Builds against `nightly-2026-05-28` (see `rust-toolchain.toml`). The main workspace stays on stable; this sub-crate is intentionally not in the workspace. The local `[workspace]` table in `Cargo.toml` keeps Cargo from implicitly attaching this crate to the parent stable workspace. ## Running -The repository `./lint` script runs this lint after `cargo clippy`. It builds -the lint with the pinned nightly in a short target directory, copies the shared -library to Dylint's required `@toolchain` filename, then passes that file to -Dylint with `--lib-path`. The Dylint invocation keeps rustup shims ahead of any -direct toolchain binaries so Dylint's internal driver build can resolve the -nightly toolchain: +The repository `./lint` script runs this lint after `cargo clippy`. Soldr +prepares the published Dylint tools and driver for the pinned nightly; +`.cargo/config.toml` directs the lint crate through `dylint-link` to emit +the toolchain-suffixed library Dylint expects: ```bash ./lint ``` -The root `rust-toolchain.toml` remains stable. Only the Dylint build/check -invocation sets `RUSTUP_TOOLCHAIN=nightly-2026-03-26`, which is required by -`rustc_private`. +The root `rust-toolchain.toml` remains stable. The Dylint build/check clears +the ambient stable `RUSTUP_TOOLCHAIN` so Soldr selects the pinned nightly. ## Reference diff --git a/dylints/ban_std_pathbuf/rust-toolchain.toml b/dylints/ban_std_pathbuf/rust-toolchain.toml index 3b6ccbe8..b6bcaf65 100644 --- a/dylints/ban_std_pathbuf/rust-toolchain.toml +++ b/dylints/ban_std_pathbuf/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "nightly-2026-03-26" +channel = "nightly-2026-05-28" components = ["llvm-tools-preview", "rust-src", "rustc-dev"] diff --git a/lint b/lint index b62b1cad..0531d58c 100755 --- a/lint +++ b/lint @@ -29,88 +29,13 @@ soldr cargo fmt --all --check echo Running cargo clippy soldr cargo clippy --workspace --all-targets -- -D warnings -DYLINT_VERSION="6.0.3" -DYLINT_TOOLCHAIN="nightly-2026-03-26" -DYLINT_DIR="$SCRIPT_DIR/dylints/ban_std_pathbuf" -DYLINT_TARGET_DIR="${FASTLED_DYLINT_TARGET_DIR:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}/fastled-dylint-target}" -DYLINT_LIBRARY_DIR="$DYLINT_TARGET_DIR/dylint/libraries/$DYLINT_TOOLCHAIN" - -DYLINT_SKIP_REASON="" -if ! command -v cargo-dylint &> /dev/null; then - DYLINT_SKIP_REASON="cargo-dylint $DYLINT_VERSION is not installed (soldr cargo install cargo-dylint --version $DYLINT_VERSION --locked)" -elif ! uv run python "$SCRIPT_DIR/ci/lint_python/dylint_version_checker.py" cargo-dylint "$DYLINT_VERSION"; then - DYLINT_SKIP_REASON="cargo-dylint $DYLINT_VERSION is required (soldr cargo install cargo-dylint --version $DYLINT_VERSION --locked --force)" -elif ! command -v rustup &> /dev/null; then - DYLINT_SKIP_REASON="rustup is required for cargo-dylint's internal driver build" -fi - -if [ -n "$DYLINT_SKIP_REASON" ]; then - echo "Error: dylint prerequisites are missing: $DYLINT_SKIP_REASON" >&2 +echo Running ban_std_pathbuf dylint +export SOLDR_FORCE_MANAGED_CARGO_SUBCOMMANDS=1 +if ! env -u RUSTUP_TOOLCHAIN soldr dylint prepare; then + echo "Error: dylint prerequisites are missing: soldr dylint prepare failed" >&2 exit 1 fi - -if [ -z "$DYLINT_SKIP_REASON" ]; then - echo Running ban_std_pathbuf dylint - RUSTUP_BIN="$(command -v rustup || true)" - # cargo-dylint 6.x's driver toolchain check requires RUSTUP_HOME to be set - # explicitly; CI exports it but local shells typically rely on rustup's - # implicit default and fail with "environment variable not found: RUSTUP_HOME". - export RUSTUP_HOME="${RUSTUP_HOME:-$("$RUSTUP_BIN" show home)}" - RUSTUP_BIN_DIR="$(dirname "$RUSTUP_BIN")" - DYLINT_CARGO_SHIM_DIR="$(mktemp -d)" - DYLINT_PREVIOUS_DEFAULT="$(rustup default 2>/dev/null | sed 's/ (default)$//')" - - cleanup_dylint_toolchain() { - rm -rf "$DYLINT_CARGO_SHIM_DIR" - if [ -n "$DYLINT_PREVIOUS_DEFAULT" ]; then - rustup default "$DYLINT_PREVIOUS_DEFAULT" >/dev/null || true - fi - } - trap cleanup_dylint_toolchain EXIT - - cat > "$DYLINT_CARGO_SHIM_DIR/cargo" </dev/null - - ( - cd "$DYLINT_DIR" - RUSTUP_TOOLCHAIN="$DYLINT_TOOLCHAIN" CARGO_TARGET_DIR="$DYLINT_LIBRARY_DIR" soldr cargo build --release - ) - - case "$(uname -s)" in - MINGW*|MSYS*|CYGWIN*) - DYLINT_BUILT_LIBRARY="$(find "$DYLINT_LIBRARY_DIR" -path "*/release/ban_std_pathbuf.dll" ! -path "*/deps/*" -print -quit)" - DYLINT_LIBRARY_NAME="ban_std_pathbuf@$DYLINT_TOOLCHAIN.dll" - ;; - Darwin*) - DYLINT_BUILT_LIBRARY="$(find "$DYLINT_LIBRARY_DIR" -path "*/release/libban_std_pathbuf.dylib" ! -path "*/deps/*" -print -quit)" - DYLINT_LIBRARY_NAME="libban_std_pathbuf@$DYLINT_TOOLCHAIN.dylib" - ;; - *) - DYLINT_BUILT_LIBRARY="$(find "$DYLINT_LIBRARY_DIR" -path "*/release/libban_std_pathbuf.so" ! -path "*/deps/*" -print -quit)" - DYLINT_LIBRARY_NAME="libban_std_pathbuf@$DYLINT_TOOLCHAIN.so" - ;; - esac - - if [ -z "$DYLINT_BUILT_LIBRARY" ]; then - echo "Error: built ban_std_pathbuf dylint library was not found under $DYLINT_LIBRARY_DIR" >&2 - exit 1 - fi - - DYLINT_NAMED_LIBRARY="$DYLINT_LIBRARY_DIR/release/$DYLINT_LIBRARY_NAME" - mkdir -p "$DYLINT_LIBRARY_DIR/release" - cp "$DYLINT_BUILT_LIBRARY" "$DYLINT_NAMED_LIBRARY" - # cargo-dylint builds an internal driver and intentionally unsets RUSTUP_TOOLCHAIN - # for that nested build. Use a short-lived cargo shim plus a temporary rustup - # default so the nested driver build uses the pinned nightly in its temp dir. - PATH="$DYLINT_CARGO_SHIM_DIR:$RUSTUP_BIN_DIR:$PATH" RUSTUP_TOOLCHAIN="$DYLINT_TOOLCHAIN" CARGO_TARGET_DIR="$DYLINT_TARGET_DIR" \ - cargo-dylint dylint --workspace --lib-path "$DYLINT_NAMED_LIBRARY" -- --all-targets - cleanup_dylint_toolchain - trap - EXIT -fi +env -u RUSTUP_TOOLCHAIN soldr dylint --all -- --workspace --all-targets echo Linting complete! exit 0 diff --git a/tests/unit/test_dylint_version_check.py b/tests/unit/test_dylint_version_check.py index e3583a64..a6b7f0d4 100644 --- a/tests/unit/test_dylint_version_check.py +++ b/tests/unit/test_dylint_version_check.py @@ -73,9 +73,11 @@ def test_lint_entrypoints_enforce_the_exact_pin() -> None: encoding="utf-8" ) - assert 'DYLINT_VERSION="6.0.3"' in lint_script - invocation = r'dylint_version_checker\.py"? cargo-dylint "\$DYLINT_VERSION"' - assert re.search(invocation, lint_script) + assert "soldr dylint prepare" in lint_script + assert "soldr dylint --all -- --workspace --all-targets" in lint_script assert 'cargo-dylint-version: "6.0.3"' in lint_workflow - assert 'DYLINT_VERSION="6.0.3"' in lint_workflow - assert len(re.findall(invocation, lint_workflow)) >= 2 + assert "dylint-toolchain: nightly-2026-05-28" in lint_workflow + assert "soldr dylint prepare" in lint_workflow + assert "soldr cargo install cargo-dylint" not in lint_workflow + linker_config = (REPO_ROOT / "dylints/ban_std_pathbuf/.cargo/config.toml").read_text(encoding="utf-8") + assert 'rustflags = ["-C", "linker=dylint-link"]' in linker_config diff --git a/tests/unit/test_lint_script.py b/tests/unit/test_lint_script.py index 377b429b..86f782e2 100644 --- a/tests/unit/test_lint_script.py +++ b/tests/unit/test_lint_script.py @@ -14,6 +14,9 @@ _STUB_SCRIPT = """#!/bin/sh printf '%s\\n' "$(basename "$0") $*" >> "$LINT_STUB_LOG" +if [ "$(basename "$0") $1 $2" = "soldr dylint prepare" ]; then + exit 1 +fi exit 0 """ @@ -102,8 +105,9 @@ def test_python_stages_run_and_cover_ci_without_dylint(tmp_path: Path) -> None: for line in log_lines ) - # No cargo-dylint invocation should have been attempted at all. - assert not any("cargo-dylint" in line for line in log_lines) + # Preparation fails closed, so the Dylint pass cannot run. + assert "soldr dylint prepare" in log_lines + assert "soldr dylint --all -- --workspace --all-targets" not in log_lines def test_missing_dylint_is_a_hard_failure_in_ci(tmp_path: Path) -> None: