Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a solution for additional license exceptions. #545

Merged
merged 2 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading