Skip to content

Commit

Permalink
Implement a solution for additional license exceptions. (#545)
Browse files Browse the repository at this point in the history
Implement a solution for additional license exceptions configuration as
described in #541

Happy to entertain other solutions or approaches.

Minor typo fixed as well.

---------

Co-authored-by: Jake Shadle <jake.shadle@embark-studios.com>
  • Loading branch information
dsully and Jake-Shadle committed Sep 3, 2023
1 parent ad72615 commit 30fb0d0
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 26 deletions.
15 changes: 15 additions & 0 deletions docs/src/checks/licenses/cfg.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ The name of the crate that you are adding an exception for

An optional version constraint specifying the range of crate versions you are excepting. Defaults to any version.

### Additional exceptions configuration file

In some cases it's useful to have global cargo-deny config and project-local exceptions. This can be accomplished with a project exceptions file in any of these locations relative to your top level `Cargo.toml` manifest file.

`cargo-deny` will look for the following files: `<cwd>/deny.exceptions.toml`, `<cwd>/.deny.exceptions.toml` and `<cwd>/.cargo/deny.exceptions.toml`

Only the exceptions field should be set:

```ini
exceptions = [
# Each entry is the crate and version constraint, and its specific allow list.
{ allow = ["CDDL-1.0"], name = "inferno", version = "*" },
]
```

#### The `allow` field

This is the exact same as the general `allow` field.
Expand Down
15 changes: 12 additions & 3 deletions src/cargo-deny/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ struct ValidConfig {
impl ValidConfig {
fn load(
cfg_path: Option<PathBuf>,
exceptions_cfg_path: Option<PathBuf>,
files: &mut Files,
log_ctx: crate::common::LogContext,
) -> Result<Self, Error> {
Expand Down Expand Up @@ -189,10 +190,17 @@ impl ValidConfig {
.validate(id, files, &mut diags);

let bans = cfg.bans.unwrap_or_default().validate(id, files, &mut diags);
let licenses = cfg
let mut licenses = cfg
.licenses
.unwrap_or_default()
.validate(id, files, &mut diags);

// Allow for project-local exceptions. Relevant in corporate environments.
// https://github.com/EmbarkStudios/cargo-deny/issues/541
if let Some(ecp) = exceptions_cfg_path {
licenses::cfg::load_exceptions(&mut licenses, ecp, files, &mut diags);
};

let sources = cfg
.sources
.unwrap_or_default()
Expand Down Expand Up @@ -270,6 +278,7 @@ pub(crate) fn cmd(
features,
} = ValidConfig::load(
krate_ctx.get_config_path(args.config.clone()),
krate_ctx.get_local_exceptions_path(),
&mut files,
log_ctx,
)?;
Expand Down Expand Up @@ -332,7 +341,7 @@ pub(crate) fn cmd(
match cl {
CodeOrLevel::Code(code) => {
if let Some(current) = code_overrides.get(code.as_str()) {
anyhow::bail!("unable to override code '{code}' to '{severity:?}', it has already been overriden to '{current:?}'");
anyhow::bail!("unable to override code '{code}' to '{severity:?}', it has already been overridden to '{current:?}'");
}

code_overrides.insert(code.as_str(), severity);
Expand All @@ -348,7 +357,7 @@ pub(crate) fn cmd(
}
})
{
anyhow::bail!("unable to override level '{level:?}' to '{severity:?}', it has already been overriden to '{current:?}'");
anyhow::bail!("unable to override level '{level:?}' to '{severity:?}', it has already been overridden to '{current:?}'");
}

level_overrides.push((ls, severity));
Expand Down
29 changes: 29 additions & 0 deletions src/cargo-deny/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,35 @@ impl KrateContext {
}
}

pub fn get_local_exceptions_path(&self) -> Option<PathBuf> {
let mut p = self.manifest_path.parent();

while let Some(parent) = p {
let mut config_path = parent.join("deny.exceptions.toml");

if config_path.exists() {
return Some(config_path);
}

config_path.pop();
config_path.push(".deny.exceptions.toml");

if config_path.exists() {
return Some(config_path);
}

config_path.pop();
config_path.push(".cargo/deny.exceptions.toml");
if config_path.exists() {
return Some(config_path);
}

p = parent.parent();
}

None
}

#[inline]
pub fn fetch_krates(&self) -> anyhow::Result<()> {
fetch(MetadataOptions {
Expand Down
7 changes: 6 additions & 1 deletion src/licenses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,14 @@ pub fn check(
.zip(ctx.cfg.exceptions.into_iter())
.filter_map(|(hit, exc)| if !hit { Some(exc) } else { None })
{
// Don't print warnings for exception overrides
if exc.file_id != ctx.cfg.file_id {
continue;
}

pack.push(diags::UnmatchedLicenseException {
license_exc_cfg: CfgCoord {
file: ctx.cfg.file_id,
file: exc.file_id,
span: exc.name.span,
},
});
Expand Down
108 changes: 86 additions & 22 deletions src/licenses/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,30 @@ impl Default for Config {
}
}

fn parse_license(
ls: &Spanned<String>,
v: &mut Vec<Licensee>,
diags: &mut Vec<Diagnostic>,
cfg_file: FileId,
) {
match spdx::Licensee::parse(ls.as_ref()) {
Ok(licensee) => {
v.push(Licensee::new(licensee, ls.span.clone()));
}
Err(pe) => {
let offset = ls.span.start + 1;
let span = pe.span.start + offset..pe.span.end + offset;
diags.push(
Diagnostic::error()
.with_message("invalid licensee")
.with_labels(vec![
Label::primary(cfg_file, span).with_message(format!("{}", pe.reason))
]),
);
}
}
}

impl crate::cfg::UnvalidatedConfig for Config {
type ValidCfg = ValidConfig;

Expand Down Expand Up @@ -223,32 +247,14 @@ impl crate::cfg::UnvalidatedConfig for Config {
}
}

let mut parse_license = |ls: &Spanned<String>, v: &mut Vec<Licensee>| {
match spdx::Licensee::parse(ls.as_ref()) {
Ok(licensee) => {
v.push(Licensee::new(licensee, ls.span.clone()));
}
Err(pe) => {
let offset = ls.span.start + 1;
let span = pe.span.start + offset..pe.span.end + offset;
diags.push(
Diagnostic::error()
.with_message("invalid licensee")
.with_labels(vec![Label::primary(cfg_file, span)
.with_message(format!("{}", pe.reason))]),
);
}
}
};

let mut denied = Vec::with_capacity(self.deny.len());
for d in &self.deny {
parse_license(d, &mut denied);
parse_license(d, &mut denied, diags, cfg_file);
}

let mut allowed: Vec<Licensee> = Vec::with_capacity(self.allow.len());
for a in &self.allow {
parse_license(a, &mut allowed);
parse_license(a, &mut allowed, diags, cfg_file);
}

denied.par_sort();
Expand All @@ -259,13 +265,14 @@ impl crate::cfg::UnvalidatedConfig for Config {
let mut allowed = Vec::with_capacity(exc.allow.len());

for allow in &exc.allow {
parse_license(allow, &mut allowed);
parse_license(allow, &mut allowed, diags, cfg_file);
}

exceptions.push(ValidException {
name: exc.name,
version: exc.version,
allowed,
file_id: cfg_file,
});
}

Expand Down Expand Up @@ -299,7 +306,7 @@ impl crate::cfg::UnvalidatedConfig for Config {
Diagnostic::error()
.with_message("unable to parse license expression")
.with_labels(vec![Label::primary(cfg_file, expr_span)
.with_message(format!("{}", err.reason))]),
.with_message(err.reason.to_string())]),
);

continue;
Expand Down Expand Up @@ -336,6 +343,61 @@ impl crate::cfg::UnvalidatedConfig for Config {
}
}

pub fn load_exceptions(
cfg: &mut ValidConfig,
path: crate::PathBuf,
files: &mut crate::diag::Files,
diags: &mut Vec<Diagnostic>,
) {
// TOML can't have unnamed arrays at the root.
#[derive(Deserialize)]
pub struct ExceptionsConfig {
pub exceptions: Vec<Exception>,
}

let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(err) => {
diags.push(
Diagnostic::error()
.with_message("failed to read exceptions override")
.with_notes(vec![format!("path = '{path}'"), format!("error = {err:#}")]),
);
return;
}
};

let exc_cfg: ExceptionsConfig = match toml::from_str(&content) {
Ok(ec) => ec,
Err(err) => {
diags.push(
Diagnostic::error()
.with_message("failed to deserialize exceptions override")
.with_notes(vec![format!("path = '{path}'"), format!("error = {err:#}")]),
);
return;
}
};

let file_id = files.add(path, content);

cfg.exceptions.reserve(exc_cfg.exceptions.len());
for exc in exc_cfg.exceptions {
let mut allowed = Vec::with_capacity(exc.allow.len());

for allow in &exc.allow {
parse_license(allow, &mut allowed, diags, file_id);
}

cfg.exceptions.push(ValidException {
name: exc.name,
version: exc.version,
allowed,
file_id,
});
}
}

#[doc(hidden)]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct ValidClarification {
Expand All @@ -352,6 +414,7 @@ pub struct ValidException {
pub name: crate::Spanned<String>,
pub version: Option<VersionReq>,
pub allowed: Vec<Licensee>,
pub file_id: FileId,
}

pub type Licensee = Spanned<spdx::Licensee>;
Expand Down Expand Up @@ -423,6 +486,7 @@ mod test {
name: "adler32".to_owned().fake(),
allowed: vec![spdx::Licensee::parse("Zlib").unwrap().fake()],
version: Some(semver::VersionReq::parse("0.1.1").unwrap()),
file_id: cd.id,
}]
);
let p: PathBuf = "LICENSE".into();
Expand Down

0 comments on commit 30fb0d0

Please sign in to comment.