diff --git a/docs/src/checks/licenses/cfg.md b/docs/src/checks/licenses/cfg.md index 308366ba..a6bd24ed 100644 --- a/docs/src/checks/licenses/cfg.md +++ b/docs/src/checks/licenses/cfg.md @@ -45,6 +45,10 @@ allow = [ If `true`, licenses are checked even for `dev-dependencies`. By default this is false as `dev-dependencies` are not used by downstream crates, nor part of binary artifacts. +### The `include-build` field (optional) + +If `true`, licenses are checked for `build-dependencies`. By default this is true because build-dependencies can influence build artifacts and are often relevant to licensing. Set this to `false` if you wish to exclude `build-dependencies` from license checks. + ### The `version` field (optional) ```ini diff --git a/src/licenses/cfg.rs b/src/licenses/cfg.rs index 5c7810d2..e6e321f8 100644 --- a/src/licenses/cfg.rs +++ b/src/licenses/cfg.rs @@ -224,6 +224,9 @@ pub struct Config { /// If true, performs license checks for dev-dependencies for workspace /// crates as well pub include_dev: bool, + /// If true, performs license checks for build-dependencies for workspace + /// crates as well + pub include_build: bool, deprecated_spans: Vec, } @@ -237,6 +240,7 @@ impl Default for Config { clarify: Vec::new(), exceptions: Vec::new(), include_dev: false, + include_build: true, deprecated_spans: Vec::new(), } } @@ -266,7 +270,8 @@ impl<'de> Deserialize<'de> for Config { .unwrap_or(LintLevel::Warn); let clarify = th.optional("clarify").unwrap_or_default(); let exceptions = th.optional("exceptions").unwrap_or_default(); - let include_dev = th.optional("include-dev").unwrap_or_default(); + let include_dev = th.optional("include-dev").unwrap_or_default(); + let include_build = th.optional("include-build").unwrap_or(true); th.finalize(None)?; @@ -278,6 +283,7 @@ impl<'de> Deserialize<'de> for Config { clarify, exceptions, include_dev, + include_build, deprecated_spans: fdeps, }) } @@ -380,6 +386,7 @@ impl crate::cfg::UnvalidatedConfig for Config { allowed, ignore_sources, include_dev: self.include_dev, + include_build: self.include_build, } } } @@ -474,6 +481,7 @@ pub struct ValidConfig { pub exceptions: Vec, pub ignore_sources: Vec, pub include_dev: bool, + pub include_build: bool, } #[cfg(test)] @@ -509,4 +517,23 @@ mod test { insta::assert_json_snapshot!(validated); } + + #[test] + fn include_build_field_deserializes() { + let cd = ConfigData::::load_str( + "tests/cfg/include-build.toml", + "[licenses]\ninclude-build = false\n", + ); + + let validated = cd.validate_with_diags( + |l| l.licenses, + |files, diags| { + let diags = write_diagnostics(files, diags.into_iter()); + // There should be no diagnostics for a simple config + assert!(diags.is_empty()); + }, + ); + + assert!(!validated.include_build, "include-build should be false"); + } } diff --git a/src/licenses/gather.rs b/src/licenses/gather.rs index be3eaf33..2195cacc 100644 --- a/src/licenses/gather.rs +++ b/src/licenses/gather.rs @@ -492,11 +492,36 @@ impl Gatherer { let files_lock = std::sync::Arc::new(parking_lot::RwLock::new(files)); - // Most users will not care about licenses for dev dependencies - let krates = if cfg.is_some_and(|cfg| cfg.include_dev) { - krates.krates().collect() - } else { - krates.krates_filtered(krates::DepKind::Dev) + // Determine which dependency kinds to include in license checking. + // By default dev-dependencies are excluded (include_dev = false), + // while build-dependencies are included (include_build = true). + let (include_dev, include_build) = match cfg { + Some(cfg) => (cfg.include_dev, cfg.include_build), + None => (false, true), + }; + + let krates = match (include_dev, include_build) { + (true, true) => krates.krates().collect(), + (true, false) => krates.krates_filtered(krates::DepKind::Build), + (false, true) => krates.krates_filtered(krates::DepKind::Dev), + (false, false) => { + // Exclude crates that are only reachable via dev _or_ build + // dependency edges. Compute the intersection of the sets that + // remain when dev-only and build-only crates are removed. + let filtered_dev = krates.krates_filtered(krates::DepKind::Dev); + let filtered_build = krates.krates_filtered(krates::DepKind::Build); + + // Both filtered lists are sorted by id; compute intersection by + // comparing ids. We'll build a vector of crates present in both. + let mut build_ids: Vec<_> = filtered_build.iter().map(|k| k.id.clone()).collect(); + // Ensure sorted for binary_search (should already be sorted, but be safe) + build_ids.sort(); + + filtered_dev + .into_iter() + .filter(|k| build_ids.binary_search(&k.id).is_ok()) + .collect() + } }; let lic_rx = regex::Regex::new(LICENSE_RX).expect("failed to compile regex"); diff --git a/src/licenses/snapshots/cargo_deny__licenses__cfg__test__deserializes_licenses_cfg-2.snap b/src/licenses/snapshots/cargo_deny__licenses__cfg__test__deserializes_licenses_cfg-2.snap index cb0d44e8..342d79e9 100644 --- a/src/licenses/snapshots/cargo_deny__licenses__cfg__test__deserializes_licenses_cfg-2.snap +++ b/src/licenses/snapshots/cargo_deny__licenses__cfg__test__deserializes_licenses_cfg-2.snap @@ -45,5 +45,6 @@ expression: validated } ], "ignore_sources": [], - "include_dev": false + "include_dev": false, + "include_build": true }