blob: 3682d15c4e9e02a61b320678a7fe54136dfde88d [file] [log] [blame]
//! ## `cargo deny check licenses`
//!
//! One important aspect that one must always keep in mind when using code from
//! other people is what the licensing of that code is and whether it fits the
//! requirements of your project. Luckily, most of the crates in the Rust
//! ecosystem tend to follow the example set forth by Rust itself, namely
//! dual-license `MIT OR Apache-2.0`, but of course, that is not always the case.
//!
//! `cargo-deny` allows you to ensure that all of your dependencies have license
//! requirements that are satisfied by the licenses you choose to use for your
//! project, and notifies you via warnings or errors if the license requirements
//! for any crate aren't compatible with your configuration.
/// Configuration for license checking
pub mod cfg;
mod diags;
mod gather;
use crate::{
diag::{CfgCoord, Check, Diagnostic, Label, Pack, Severity},
LintLevel,
};
use cfg::BlanketAgreement;
pub use gather::{Gatherer, LicenseInfo, LicenseStore};
use gather::{KrateLicense, LicenseExprInfo, LicenseExprSource, Summary};
pub use diags::Code;
use bitvec::prelude::*;
struct Hits {
allowed: BitVec<usize, LocalBits>,
exceptions: BitVec<usize, LocalBits>,
}
fn evaluate_expression(
ctx: &crate::CheckCtx<'_, cfg::ValidConfig>,
krate_lic_nfo: &KrateLicense<'_>,
expr: &spdx::Expression,
nfo: &LicenseExprInfo,
hits: &mut Hits,
) -> Diagnostic {
// TODO: If an expression with the same hash is encountered
// just use the same result as a memoized one
#[derive(Debug)]
enum Reason {
Denied,
IsFsfFree,
IsOsiApproved,
IsBothFreeAndOsi,
ExplicitAllowance,
ExplicitException,
IsCopyleft,
Default,
NotExplicitlyAllowed,
}
let mut reasons = smallvec::SmallVec::<[(Reason, bool); 8]>::new();
macro_rules! deny {
($reason:ident) => {
reasons.push((Reason::$reason, false));
return false;
};
}
macro_rules! allow {
($reason:ident) => {
reasons.push((Reason::$reason, true));
return true;
};
}
let mut warnings = 0;
let cfg = &ctx.cfg;
// Check to see if the crate matches an exception, which is additional to
// the general allow list
let exception_ind = cfg
.exceptions
.iter()
.position(|exc| crate::match_krate(krate_lic_nfo.krate, &exc.spec));
let eval_res = expr.evaluate_with_failures(|req| {
// 1. Exceptions are additional per-crate licenses that aren't blanket
// allowed by all crates, note that we check these before denials so you
// can allow an exception
if let Some(ind) = exception_ind {
let exception = &cfg.exceptions[ind];
for allow in &exception.allowed {
if allow.0.value.satisfies(req) {
// Note that hit the exception
hits.exceptions.as_mut_bitslice().set(ind, true);
allow!(ExplicitException);
}
}
}
// 2. Licenses explicitly denied are of course hard failures,
// but failing one license in an expression is not necessarily
// going to actually ban the crate, for example, the canonical
// "Apache-2.0 OR MIT" used in by a lot crates means that
// banning Apache-2.0, but allowing MIT, will allow the crate
// to be used as you are upholding at least one license requirement
for deny in &cfg.denied {
if deny.0.value.satisfies(req) {
deny!(Denied);
}
}
// 3. A license that is specifically allowed will of course mean
// that the requirement is met.
for (i, allow) in cfg.allowed.iter().enumerate() {
if allow.0.value.satisfies(req) {
hits.allowed.as_mut_bitslice().set(i, true);
allow!(ExplicitAllowance);
}
}
if let Some(dep_cfg) = &cfg.deprecated {
// 4. If the license isn't explicitly allowed, it still may
// be allowed by the blanket "OSI Approved" or "FSF Free/Libre"
// allowances
if let spdx::LicenseItem::Spdx { id, .. } = req.license {
if id.is_copyleft() {
match dep_cfg.copyleft {
LintLevel::Allow => {
allow!(IsCopyleft);
}
LintLevel::Warn => {
warnings += 1;
allow!(IsCopyleft);
}
LintLevel::Deny => {
deny!(IsCopyleft);
}
}
}
match dep_cfg.allow_osi_fsf_free {
BlanketAgreement::Neither => {}
BlanketAgreement::Either => {
if id.is_osi_approved() {
allow!(IsOsiApproved);
} else if id.is_fsf_free_libre() {
allow!(IsFsfFree);
}
}
BlanketAgreement::Both => {
if id.is_fsf_free_libre() && id.is_osi_approved() {
allow!(IsBothFreeAndOsi);
}
}
BlanketAgreement::Osi => {
if id.is_osi_approved() {
allow!(IsOsiApproved);
}
}
BlanketAgreement::Fsf => {
if id.is_fsf_free_libre() {
allow!(IsFsfFree);
}
}
BlanketAgreement::OsiOnly => {
if id.is_osi_approved() {
if id.is_fsf_free_libre() {
deny!(IsFsfFree);
} else {
allow!(IsOsiApproved);
}
}
}
BlanketAgreement::FsfOnly => {
if id.is_fsf_free_libre() {
if id.is_osi_approved() {
deny!(IsOsiApproved);
} else {
allow!(IsFsfFree);
}
}
}
}
}
// 5. Whelp, this license just won't do!
match dep_cfg.default {
LintLevel::Deny => {
deny!(Default);
}
LintLevel::Warn => {
warnings += 1;
allow!(Default);
}
LintLevel::Allow => {
allow!(Default);
}
}
} else {
deny!(NotExplicitlyAllowed);
}
});
let (message, severity) = match eval_res {
Err(_) => ("failed to satisfy license requirements", Severity::Error),
Ok(_) => (
"license requirements satisfied",
if warnings > 0 {
Severity::Warning
} else {
Severity::Help
},
),
};
let mut labels = Vec::with_capacity(reasons.len() + 1);
labels.extend(krate_lic_nfo.labels.clone());
labels.push(
Label::secondary(nfo.file_id, nfo.offset..nfo.offset + expr.as_ref().len()).with_message(
format!(
"license expression retrieved via {}",
match &nfo.source {
LicenseExprSource::Metadata => "Cargo.toml `license`".to_owned(),
LicenseExprSource::UserOverride => "user override".to_owned(),
LicenseExprSource::LicenseFiles(lfs) => lfs.join(", "),
LicenseExprSource::OverlayOverride => unreachable!(),
}
),
),
);
let mut notes = krate_lic_nfo.notes.clone();
for ((reason, accepted), failed_req) in reasons.into_iter().zip(expr.requirements()) {
if accepted && ctx.log_level < log::LevelFilter::Info {
continue;
}
if !accepted && severity == Severity::Error {
if let Some(id) = failed_req.req.license.id() {
notes.push(format!("{} - {}:", id.name, id.full_name));
let len = notes.len();
if id.is_deprecated() {
notes.push(" - **DEPRECATED**".into());
}
if id.is_osi_approved() {
notes.push(" - OSI approved".into());
}
if id.is_fsf_free_libre() {
notes.push(" - FSF Free/Libre".into());
}
if id.is_copyleft() {
notes.push(" - Copyleft".into());
}
if len == notes.len() {
notes.push(" - No additional metadata available for license".into());
}
} else {
// This would only happen if askalono used a newer license list than spdx, but we update
// both simultaneously
notes.push(format!("{} is not an SPDX license", failed_req.req));
}
}
labels.push(
Label::primary(
nfo.file_id,
nfo.offset + failed_req.span.start as usize
..nfo.offset + failed_req.span.end as usize,
)
.with_message(format!(
"{}: {}",
if accepted { "accepted" } else { "rejected" },
match reason {
Reason::Denied => "explicitly denied",
Reason::IsFsfFree =>
"license is FSF approved https://www.gnu.org/licenses/license-list.en.html",
Reason::IsOsiApproved =>
"license is OSI approved https://opensource.org/licenses",
Reason::ExplicitAllowance => "license is explicitly allowed",
Reason::ExplicitException => "license is explicitly allowed via an exception",
Reason::NotExplicitlyAllowed => "license was not explicitly allowed",
Reason::IsBothFreeAndOsi => "license is FSF AND OSI approved",
Reason::IsCopyleft => "license is considered copyleft",
Reason::Default => {
match cfg
.deprecated
.as_ref()
.map_or(LintLevel::Deny, |d| d.default)
{
LintLevel::Deny => "not explicitly allowed",
LintLevel::Warn => "warned by default",
LintLevel::Allow => "allowed by default",
}
}
}
)),
);
}
Diagnostic::new(severity)
.with_message(message)
.with_code(if severity != Severity::Error {
diags::Code::Accepted
} else {
diags::Code::Rejected
})
.with_labels(labels)
.with_notes(notes)
}
pub fn check(
ctx: crate::CheckCtx<'_, cfg::ValidConfig>,
summary: Summary<'_>,
mut sink: crate::diag::ErrorSink,
) {
let mut hits = Hits {
allowed: BitVec::repeat(false, ctx.cfg.allowed.len()),
exceptions: BitVec::repeat(false, ctx.cfg.exceptions.len()),
};
let private_registries: Vec<_> = ctx
.cfg
.private
.registries
.iter()
.map(|s| s.as_str())
.collect();
for krate_lic_nfo in summary.nfos {
let mut pack = Pack::with_kid(Check::Licenses, krate_lic_nfo.krate.id.clone());
// If the user has set this, check if it's a private workspace
// crate or a crate from a private registry and just print out
// a help message that we skipped it
if ctx.cfg.private.ignore
&& (krate_lic_nfo.krate.is_private(&private_registries)
|| ctx
.cfg
.ignore_sources
.iter()
.any(|url| krate_lic_nfo.krate.matches_url(url, true)))
{
pack.push(diags::SkippedPrivateWorkspaceCrate {
krate: krate_lic_nfo.krate,
});
sink.push(pack);
continue;
}
match &krate_lic_nfo.lic_info {
LicenseInfo::SpdxExpression { expr, nfo } => {
pack.push(evaluate_expression(
&ctx,
&krate_lic_nfo,
expr,
nfo,
&mut hits,
));
}
LicenseInfo::Unlicensed => {
let severity = match ctx
.cfg
.deprecated
.as_ref()
.map_or(LintLevel::Deny, |d| d.unlicensed)
{
LintLevel::Allow => Severity::Note,
LintLevel::Warn => Severity::Warning,
LintLevel::Deny => Severity::Error,
};
pack.push(diags::Unlicensed {
krate: krate_lic_nfo.krate,
severity,
breadcrumbs: krate_lic_nfo.labels.into_iter().collect(),
});
}
}
if !pack.is_empty() {
sink.push(pack);
}
}
{
let mut pack = Pack::new(Check::Licenses);
// Print out warnings for exceptions that pertain to crates that
// weren't actually encountered
for exc in hits
.exceptions
.into_iter()
.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: exc.file_id,
span: exc.spec.name.span,
},
});
}
if !pack.is_empty() {
sink.push(pack);
}
}
{
let mut pack = Pack::new(Check::Licenses);
// Print diagnostics for allowed licenses that weren't encountered.
// Note that we don't do the same for denied licenses
for allowed in hits
.allowed
.into_iter()
.zip(ctx.cfg.allowed.into_iter())
.filter_map(|(hit, allowed)| if !hit { Some(allowed) } else { None })
{
pack.push(diags::UnmatchedLicenseAllowance {
severity: ctx.cfg.unused_allowed_license.into(),
allowed_license_cfg: CfgCoord {
file: ctx.cfg.file_id,
span: allowed.0.span,
},
});
}
if !pack.is_empty() {
sink.push(pack);
}
}
}