From 7a399bb7465111bf63f70c522463fe9c46a9ca6d Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Costa Date: Mon, 23 Mar 2026 18:06:06 +0000 Subject: [PATCH] buildRustCrate: support Cargo.toml [lints] table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cargo's `[lints]` table is a cargo-only feature: cargo translates the entries into `-A`/`-W`/`-D`/`-F` rustc flags before invoking the compiler. Since buildRustCrate calls rustc directly, these lints were silently ignored, forcing users to duplicate them as raw flags in `extraRustcOpts`. Accept a `lints` attr with the same shape as the Cargo.toml section (tool → lint name → level string or `{ level, priority }` attrset), translate it to rustc flags in Nix, and append to the rustc command line. Entries are sorted by ascending priority so lower-priority lint groups are emitted first and can be overridden by more specific lints, matching cargo's behaviour. The `capLints` default changes from `"allow"` to `null`, resolved to `"allow"` when `lints` is empty (the usual case for third-party dependencies) and `"forbid"` when `lints` is set — otherwise a `deny`/`forbid` lint would be silently capped and the table would be a no-op. Explicit `capLints` still overrides. This is the same model cargo uses: your own crate's `[lints]` always apply; only dependencies get `--cap-lints allow`. Generators like crate2nix can populate `lints` directly from the manifest at generation time. --- doc/languages-frameworks/rust.section.md | 33 +++++- .../rust/build-rust-crate/default.nix | 108 +++++++++++++++++- .../rust/build-rust-crate/test/default.nix | 40 +++++++ 3 files changed, 171 insertions(+), 10 deletions(-) diff --git a/doc/languages-frameworks/rust.section.md b/doc/languages-frameworks/rust.section.md index f78ff222ba0d..1c199ac3f26e 100644 --- a/doc/languages-frameworks/rust.section.md +++ b/doc/languages-frameworks/rust.section.md @@ -842,17 +842,38 @@ general. A number of other parameters can be overridden: (hello { }).override { extraRustcOpts = "-Z debuginfo=2"; } ``` -- The lint level cap passed to `rustc` (`allow` by default, which - silences all lints). 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: +- The lint level cap passed to `rustc`. Defaults to `null`, which + auto-resolves to `"allow"` (silences all lints) when `lints` is + 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: ```nix (hello { }).override { capLints = "warn"; } ``` +- Lint configuration mirroring Cargo.toml's `[lints]` table. Keys are + tool names (`rust`, `clippy`, `rustdoc`); values map lint names to + either a level string (`"allow"`, `"warn"`, `"deny"`, `"forbid"`) or + `{ level = "..."; priority = ; }`. Lower priorities are emitted + first so that more specific lints can override them. Setting a + non-empty `lints` raises the default `capLints` to `"forbid"` so the + lints actually apply: + + ```nix + (hello { }).override { + lints.rust = { + unsafe_code = "forbid"; + unused = { + level = "deny"; + priority = -1; + }; + }; + } + ``` + - 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/default.nix b/pkgs/build-support/rust/build-rust-crate/default.nix index 276e51d01b48..ca0d7331daaf 100644 --- a/pkgs/build-support/rust/build-rust-crate/default.nix +++ b/pkgs/build-support/rust/build-rust-crate/default.nix @@ -62,6 +62,65 @@ let # Create feature arguments for rustc. mkRustcFeatureArgs = lib.concatMapStringsSep " " (f: ''--cfg feature=\"${f}\"''); + # Translate a Cargo.toml `[lints]` table into rustc flags. + # + # See . + # + # Cargo normally translates `[lints.]` entries into `-A`/`-W`/`-D`/`-F` + # flags when invoking rustc. Since buildRustCrate calls rustc directly we + # must perform that translation ourselves. + # + # Example: + # + # lintsToRustcFlags { + # rust = { + # unsafe_code = "forbid"; + # unused = { level = "deny"; priority = -1; }; + # }; + # clippy.all = "warn"; + # } + # => [ "-D unused" "-W clippy::all" "-F unsafe_code" ] + # + # Entries are sorted by ascending priority (default 0) so that lower-priority + # groups are emitted first and can be overridden by higher-priority specific + # lints — matching cargo's behaviour where later rustc flags win. + lintsToRustcFlags = + lints: + let + levelFlag = { + allow = "-A"; + warn = "-W"; + force-warn = "--force-warn"; + deny = "-D"; + forbid = "-F"; + }; + toolPrefix = tool: if tool == "rust" then "" else "${tool}::"; + normalize = + val: + if builtins.isString val then + { + level = val; + priority = 0; + } + else + { priority = 0; } // val; + entries = lib.concatMap ( + tool: + lib.mapAttrsToList ( + name: val: + let + e = normalize val; + in + { + inherit (e) priority; + flag = "${levelFlag.${e.level}} ${toolPrefix tool}${name}"; + } + ) lints.${tool} + ) (builtins.attrNames lints); + sorted = lib.sort (a: b: a.priority < b.priority) entries; + in + map (e: e.flag) sorted; + # Whether we need to use unstable command line flags # # Currently just needed for standard library dependencies, which have a @@ -203,9 +262,35 @@ lib.makeOverridable # second one via `extraRustcOpts` has no effect. Use this parameter # instead if you need lints to fire (e.g. when running clippy). # + # When left at `null`, resolves to `"allow"` if `lints` is empty (the + # usual case for third-party dependencies), or `"forbid"` if `lints` + # is set (so your own crate's lint policy actually applies). + # # Example: "warn" - # Default: "allow" + # Default: null (auto: "allow" or "forbid" depending on `lints`) capLints, + # Lint configuration mirroring Cargo.toml's `[lints]` table. + # See . + # + # Keys are tool names (`rust`, `clippy`, `rustdoc`); values are attrsets + # mapping lint names to either a level string (`"allow"`, `"warn"`, + # `"force-warn"`, `"deny"`, `"forbid"`) or an attrset + # `{ level = "..."; priority = ; }`. Lower priorities are emitted + # first so that higher-priority (more specific) lints can override them. + # + # Setting a non-empty `lints` raises the default `capLints` from + # `"allow"` to `"forbid"` so the lints actually fire. + # + # Example: + # { + # rust = { + # unsafe_code = "forbid"; + # unused = { level = "deny"; priority = -1; }; + # }; + # clippy.all = "warn"; + # } + # Default: {} + lints, # Whether to enable building tests. # Use true to enable. # Default: false @@ -260,14 +345,26 @@ lib.makeOverridable "codegenUnits" "links" "capLints" + "lints" ]; extraDerivationAttrs = removeAttrs crate processedAttrs; nativeBuildInputs_ = nativeBuildInputs; buildInputs_ = buildInputs; extraRustcOpts_ = extraRustcOpts; extraRustcOptsForBuildRs_ = extraRustcOptsForBuildRs; - capLints_ = capLints; buildTests_ = buildTests; + resolvedLints = crate.lints or lints; + lintFlags = lintsToRustcFlags resolvedLints; + resolvedCapLints = + let + requested = crate.capLints or capLints; + in + if requested != null then + requested + else if resolvedLints != { } then + "forbid" + else + "allow"; # crate2nix has a hack for the old bash based build script that did split # entries at `,`. No we have to work around that hack. @@ -390,12 +487,14 @@ lib.makeOverridable extraRustcOpts = lib.optionals (crate ? extraRustcOpts) crate.extraRustcOpts ++ extraRustcOpts_ + ++ lintFlags ++ (lib.optional (edition != null) "--edition ${edition}"); extraRustcOptsForBuildRs = lib.optionals (crate ? extraRustcOptsForBuildRs) crate.extraRustcOptsForBuildRs ++ extraRustcOptsForBuildRs_ + ++ lintFlags ++ (lib.optional (edition != null) "--edition ${edition}"); - capLints = crate.capLints or capLints_; + capLints = resolvedCapLints; configurePhase = configureCrate { inherit @@ -487,7 +586,8 @@ lib.makeOverridable verbose = crate_.verbose or true; extraRustcOpts = [ ]; extraRustcOptsForBuildRs = [ ]; - capLints = "allow"; + capLints = null; + lints = { }; features = [ ]; nativeBuildInputs = [ ]; buildInputs = [ ]; 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 d5c56858c6a4..ee51b9173df0 100644 --- a/pkgs/build-support/rust/build-rust-crate/test/default.nix +++ b/pkgs/build-support/rust/build-rust-crate/test/default.nix @@ -8,6 +8,7 @@ runCommandCC, stdenv, symlinkJoin, + testers, writeTextFile, pkgsCross, }: @@ -726,6 +727,26 @@ rec { ]; }; }; + # The `lints` attr mirrors Cargo.toml's `[lints]` table and is + # translated to rustc `-A`/`-W`/`-D`/`-F` flags. Lower-priority + # entries are emitted first so that higher-priority specific lints + # can override them. Here `-D unused` (priority -1) is followed by + # `-A dead_code` (default priority 0); the build only succeeds if + # both flags reach rustc in that order. + lintsPriority = { + lints.rust = { + unused = { + level = "deny"; + priority = -1; + }; + dead_code = "allow"; + }; + src = mkFile "src/lib.rs" '' + #![allow(nonstandard_style)] + fn dead() {} + pub fn alive() {} + ''; + }; }; brotliCrates = (callPackage ./brotli-crates.nix { }); rcgenCrates = callPackage ./rcgen-crates.nix { @@ -893,6 +914,25 @@ rec { test -e ${pkg}/bin/brotli-decompressor && touch $out ''; + # A `deny` lint from the lints table should actually fail the build. + lintsDenyFails = + let + crate = mkHostCrate { + crateName = "lintsDenyFails"; + lints.rust.dead_code = "deny"; + src = mkFile "src/lib.rs" '' + fn dead() {} + pub fn alive() {} + ''; + }; + failed = testers.testBuildFailure crate; + in + runCommand "assert-lintsDenyFails" { inherit failed; } '' + grep -q 'function .dead. is never used' "$failed/testBuildFailure.log" + grep -q '\-D dead.code' "$failed/testBuildFailure.log" + touch $out + ''; + rcgenTest = let pkg = rcgenCrates.rootCrate.build;