From 735734474f74eb294aeb2e530ce36f83cec8b895 Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 19 May 2026 10:17:59 -0400 Subject: [PATCH] buildRustCrate: add useClippy parameter to lint with clippy-driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildRustCrate calls rustc directly, so there is no built-in equivalent of `cargo clippy`. The capLints doc already hints at the existing workaround — overriding the `rust` attribute with a wrapper script that sniffs argv for build_script_build and routes everything else to clippy-driver. That works because clippy-driver IS rustc internally (rustc_driver plus lint passes) and produces compatible rlibs and metadata, but the wrapper is fragile and every downstream reinvents it. Add a `useClippy` boolean (default false) that swaps the lib/bin/test compilation to clippy-driver while keeping plain rustc for build.rs — the same lib-vs-build-script split as extraRustcOptsForBuildRs, since build scripts are typically auto-generated and clippy noise there is not actionable. The clippy-driver binary comes from a new `clippy` parameter, auto-filled from pkgs.clippy by callPackage and overridable alongside `rust` for toolchain bundles that ship their own. Implemented by introducing a RUSTC_DRIVER shell variable in build-crate.nix that lib.sh's build_lib/build_bin consume; the build_script_build path in configure-crate.nix never sees it. Note that the default `capLints = "allow"` cap suppresses clippy lints along with everything else, so `useClippy` is usually paired with `capLints = "warn"` and `-W clippy::*`/`-D warnings` flags via extraRustcOpts. Documented in the manual. Test fixtures cover the three load-bearing properties: a denied clippy lint fails the build (proving clippy-driver ran), the default capLints silences clippy lints just like rustc lints, and an rlib emitted by clippy-driver is link-compatible with a plain-rustc dependent. --- doc/languages-frameworks/rust.section.md | 31 ++++++++- .../rust/build-rust-crate/build-crate.nix | 2 + .../rust/build-rust-crate/default.nix | 30 ++++++++ .../rust/build-rust-crate/lib.sh | 4 +- .../rust/build-rust-crate/test/default.nix | 69 +++++++++++++++++++ 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/doc/languages-frameworks/rust.section.md b/doc/languages-frameworks/rust.section.md index d7bb32d856af..87d916cb4a12 100644 --- a/doc/languages-frameworks/rust.section.md +++ b/doc/languages-frameworks/rust.section.md @@ -876,8 +876,7 @@ general. A number of other parameters can be overridden: empty, or `"forbid"` (no cap) when `lints` is set. Because `rustc` only honours the first `--cap-lints` it receives, this cannot be changed via `extraRustcOpts`; use this attribute instead. Useful - when overriding the `rust` attribute to point at `clippy-driver`, - since clippy lints are also capped by this flag: + with `useClippy`, since clippy lints are also capped by this flag: ```nix (hello { }).override { capLints = "warn"; } @@ -903,6 +902,34 @@ general. A number of other parameters can be overridden: } ``` +- Whether to compile the crate with `clippy-driver` instead of `rustc`. + Build scripts (`build.rs`) keep plain `rustc`. The default `capLints` + of `"allow"` suppresses all lints including clippy's, so this is + usually paired with `capLints` and lint flags via `extraRustcOpts`: + + ```nix + (hello { }).override { + useClippy = true; + capLints = "warn"; + extraRustcOpts = [ + "-Dwarnings" + "-Wclippy::all" + ]; + } + ``` + + When using a Rust toolchain that bundles its own `clippy-driver` + (rust-overlay, Fenix), pass it via `clippy` so the sysroot matches: + + ```nix + (hello { }).override { + rust = myToolchain; + clippy = myToolchain; + useClippy = true; + capLints = "warn"; + } + ``` + - Phases, just like in any other derivation, can be specified using the following attributes: `preUnpack`, `postUnpack`, `prePatch`, `patches`, `postPatch`, `preConfigure` (in the case of a Rust crate, diff --git a/pkgs/build-support/rust/build-rust-crate/build-crate.nix b/pkgs/build-support/rust/build-rust-crate/build-crate.nix index d5b7166207fa..9c2005907275 100644 --- a/pkgs/build-support/rust/build-rust-crate/build-crate.nix +++ b/pkgs/build-support/rust/build-rust-crate/build-crate.nix @@ -25,6 +25,7 @@ buildTests, codegenUnits, capLints, + useClippy, }: let @@ -94,6 +95,7 @@ in runHook preBuild # configure & source common build functions + RUSTC_DRIVER="${if useClippy then "clippy-driver" else "rustc"}" LIB_RUSTC_OPTS="${libRustcOpts}" BIN_RUSTC_OPTS="${binRustcOpts}" LIB_EXT="${stdenv.hostPlatform.extensions.library}" diff --git a/pkgs/build-support/rust/build-rust-crate/default.nix b/pkgs/build-support/rust/build-rust-crate/default.nix index af1ec421da9b..bf2a6af10fa9 100644 --- a/pkgs/build-support/rust/build-rust-crate/default.nix +++ b/pkgs/build-support/rust/build-rust-crate/default.nix @@ -12,6 +12,7 @@ pkgsBuildBuild, rustc, cargo, + clippy, jq, libiconv, # Controls codegen parallelization for all crates. @@ -163,6 +164,31 @@ lib.makeOverridable # # Default: pkgs.cargo cargo ? cargo, + # Whether to compile the crate's library, binary, and test targets with + # `clippy-driver` instead of `rustc`. Build scripts (`build.rs`) keep + # plain `rustc` — they are typically auto-generated and clippy findings + # there are not actionable. + # + # `clippy-driver` wraps `rustc_driver` with extra lint passes and emits + # link-compatible `.rlib`/`.rmeta`, so dependency crates built with plain + # `rustc` are still usable; only the crate being linted needs this flag. + # + # Note that the default `capLints` of `"allow"` suppresses ALL lints, + # including clippy's. Set `capLints = "warn"` (or `"forbid"`) or supply + # a `lints` table — otherwise `useClippy` is a silent no-op. Lint flags + # such as `-D warnings` or `-W clippy::pedantic` go through the regular + # `extraRustcOpts` (clippy-driver forwards rustc flags unchanged). + # + # Example: true + # Default: false + useClippy, + # The clippy package providing `clippy-driver`. Only consulted when + # `useClippy = true`. Override this together with `rust` when using a + # toolchain (rust-overlay, Fenix) that bundles its own `clippy-driver`, + # so the sysroot matches. + # + # Default: pkgs.clippy + clippy ? clippy, # Whether to build a release version (`true`) or a debug # version (`false`). Debug versions are faster to build # but might be much slower at runtime. @@ -412,6 +438,7 @@ lib.makeOverridable cargo jq ] + ++ lib.optional useClippy clippy ++ lib.optionals stdenv.hasCC [ stdenv.cc ] ++ lib.optionals stdenv.buildPlatform.isDarwin [ libiconv ] ++ (crate.nativeBuildInputs or [ ]) @@ -559,6 +586,7 @@ lib.makeOverridable buildTests codegenUnits capLints + useClippy ; }; dontStrip = !release; @@ -594,6 +622,8 @@ lib.makeOverridable { rust = crate_.rust or rustc; cargo = crate_.cargo or cargo; + useClippy = crate_.useClippy or false; + clippy = crate_.clippy or clippy; release = crate_.release or true; verbose = crate_.verbose or true; extraRustcOpts = [ ]; diff --git a/pkgs/build-support/rust/build-rust-crate/lib.sh b/pkgs/build-support/rust/build-rust-crate/lib.sh index 4d736302a0c9..28a705c7ce6a 100644 --- a/pkgs/build-support/rust/build-rust-crate/lib.sh +++ b/pkgs/build-support/rust/build-rust-crate/lib.sh @@ -10,7 +10,7 @@ build_lib() { lib_src=$1 echo_build_heading $lib_src ${libName} - noisily env "${CARGO_BIN_EXE_ENV[@]}" rustc \ + noisily env "${CARGO_BIN_EXE_ENV[@]}" "${RUSTC_DRIVER:-rustc}" \ --crate-name $CRATE_NAME \ $lib_src \ --out-dir target/lib \ @@ -42,7 +42,7 @@ build_bin() { main_file=$2 fi echo_build_heading $crate_name $main_file - noisily env "${CARGO_BIN_EXE_ENV[@]}" rustc \ + noisily env "${CARGO_BIN_EXE_ENV[@]}" "${RUSTC_DRIVER:-rustc}" \ --crate-name $crate_name_ \ $main_file \ --crate-type bin \ diff --git a/pkgs/build-support/rust/build-rust-crate/test/default.nix b/pkgs/build-support/rust/build-rust-crate/test/default.nix index 75a483a0096f..003e7cafce7b 100644 --- a/pkgs/build-support/rust/build-rust-crate/test/default.nix +++ b/pkgs/build-support/rust/build-rust-crate/test/default.nix @@ -1042,6 +1042,75 @@ rec { touch $out ''; + # `useClippy = true` plus a denied clippy lint should fail the build, + # proving clippy-driver (not plain rustc) compiled the crate. The + # `clippy::` prefix in the diagnostic is the fingerprint: rustc has no + # such lint group. + useClippyDenyFails = + let + crate = mkHostCrate { + crateName = "useClippyDenyFails"; + useClippy = true; + lints.clippy.eq_op = "deny"; + src = mkFile "src/lib.rs" '' + pub fn check() -> bool { + 1 == 1 + } + ''; + }; + failed = testers.testBuildFailure crate; + in + runCommand "assert-useClippyDenyFails" { inherit failed; } '' + grep -q 'clippy::eq.op' "$failed/testBuildFailure.log" + grep -q 'equal expressions' "$failed/testBuildFailure.log" + touch $out + ''; + + # `useClippy = true` with the default `capLints` (which resolves to + # `"allow"` when `lints` is empty) must still build: the cap silences + # clippy lints just like rustc lints. Same source as the failing test + # above — only the `lints` table differs. + useClippyDefaultCapAllows = mkHostCrate { + crateName = "useClippyDefaultCapAllows"; + useClippy = true; + src = mkFile "src/lib.rs" '' + pub fn check() -> bool { + 1 == 1 + } + ''; + }; + + # A library compiled by clippy-driver must produce an `.rlib` that a + # plain-rustc dependent can link against and run. This is the property + # that makes `useClippy` safe to flip per-crate. + useClippyRlibLinkCompat = + let + libCrate = mkHostCrate { + crateName = "clippylib"; + useClippy = true; + src = mkFile "src/lib.rs" '' + pub fn test() -> i32 { + 23 + } + ''; + }; + binCrate = mkHostCrate { + crateName = "clippybin"; + dependencies = [ libCrate ]; + src = mkBinExtern "src/main.rs" "clippylib"; + }; + in + runCommand "run-useClippyRlibLinkCompat" { nativeBuildInputs = [ binCrate ]; } ( + if stdenv.hostPlatform == stdenv.buildPlatform then + '' + ${binCrate}/bin/clippybin && touch $out + '' + else + '' + test -x '${binCrate}/bin/clippybin' && touch $out + '' + ); + rcgenTest = let pkg = rcgenCrates.rootCrate.build;