Skip to content

Commit

Permalink
Add validation rule to check for an spdx license
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Bottriell <ryan@bottriell.ca>
  • Loading branch information
rydrman committed Jul 13, 2024
1 parent bac13dc commit f5badbd
Show file tree
Hide file tree
Showing 14 changed files with 356 additions and 14 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ serde_json = "1.0"
serde_yaml = "0.9.25"
serial_test = "3.1"
shellexpand = "3.1.0"
spdx = "0.10"
spfs = { path = "crates/spfs" }
spfs-cli-common = { path = "crates/spfs-cli/common" }
spfs-encoding = { path = "crates/spfs-encoding" }
Expand Down
1 change: 1 addition & 0 deletions crates/spk-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ spk-exec = { workspace = true }
spk-solve = { workspace = true }
spk-schema = { workspace = true }
spk-storage = { workspace = true }
spdx = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
miette = { workspace = true }
Expand Down
8 changes: 8 additions & 0 deletions crates/spk-build/src/validation/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use thiserror::Error;

pub type Result<T> = std::result::Result<T, Error>;

/// Errors returned by failed package validation rules.
#[derive(Diagnostic, Debug, Error, Clone, PartialEq, Eq)]
#[diagnostic(
url(
Expand Down Expand Up @@ -162,4 +163,11 @@ pub enum Error {
code(spk::build::validation::strong_inheritance_var_description)
)]
StrongInheritanceVarDescriptionRequired,

#[error("A valid SPDX license required, got {given:?}")]
#[diagnostic(severity(warning), code(spk::build::validation::spdx_license))]
SpdxLicenseRequired { given: String },
#[error("Package should not have a license specified")]
#[diagnostic(severity(warning), code(spk::build::validation::spdx_license))]
SpdxLicenseDenied,
}
2 changes: 2 additions & 0 deletions crates/spk-build/src/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod error;
mod inherit_requirements;
mod long_var_description;
mod recursive_build;
mod spdx_license;
mod strong_inheritance_var_desc;
mod validator;

Expand All @@ -21,5 +22,6 @@ pub use error::{Error, Result};
pub use inherit_requirements::InheritRequirementsValidator;
pub use long_var_description::LongVarDescriptionValidator;
pub use recursive_build::RecursiveBuildValidator;
pub use spdx_license::SpdxLicenseValidator;
pub use strong_inheritance_var_desc::StrongInheritanceVarDescriptionValidator;
pub use validator::{Outcome, Report, Status, Subject, Validator};
77 changes: 77 additions & 0 deletions crates/spk-build/src/validation/spdx_license.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Sony Pictures Imageworks, et al.
// SPDX-License-Identifier: Apache-2.0
// https://github.com/imageworks/spk

use spk_schema::validation::{
ValidationMatcherDiscriminants,
ValidationRuleDiscriminants as RuleKind,
};
use spk_schema::{Package, Variant};

use super::{Error, Outcome, Report, Status, Subject};
use crate::report::{BuildReport, BuildSetupReport};

#[cfg(test)]
#[path = "./spdx_license_test.rs"]
mod spdx_license_test;

pub struct SpdxLicenseValidator {
pub kind: RuleKind,
}

impl super::validator::sealed::Sealed for SpdxLicenseValidator {}

#[async_trait::async_trait]
impl super::Validator for SpdxLicenseValidator {
async fn validate_setup<P, V>(&self, setup: &BuildSetupReport<P, V>) -> Report
where
P: Package,
V: Variant + Send + Sync,
{
let meta = setup.package.metadata();
let exists = meta.license.is_some();
let is_valid = match meta.license.as_ref() {
Some(value) => spdx::license_id(value).is_some(),
None => true,
};
let status = match self.kind {
RuleKind::Require if !exists => Status::Required(Error::SpdxLicenseRequired {
given: meta.license.clone().unwrap_or_default(),
}),
RuleKind::Allow | RuleKind::Require if !is_valid => {
Status::Required(Error::SpdxLicenseRequired {
given: meta.license.clone().unwrap_or_default(),
})
}
RuleKind::Deny if exists && is_valid => Status::Denied(Error::SpdxLicenseDenied),
_ => Status::Allowed,
};
Outcome {
locality: String::new(),
subject: Subject::Everything,
status,
condition: ValidationMatcherDiscriminants::SpdxLicense,
}
.into()
}

async fn validate_build<P, V>(&self, report: &BuildReport<P, V>) -> Report
where
P: Package,
V: Variant + Send + Sync,
{
let is_empty = report.output.collected_changes.is_empty();
let status = match self.kind {
RuleKind::Deny if is_empty => Status::Denied(Error::EmptyPackageDenied),
RuleKind::Require if !is_empty => Status::Required(Error::EmptyPackageRequired),
_ => Status::Allowed,
};
Outcome {
locality: String::new(),
subject: Subject::Everything,
status,
condition: ValidationMatcherDiscriminants::EmptyPackage,
}
.into()
}
}
192 changes: 192 additions & 0 deletions crates/spk-build/src/validation/spdx_license_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright (c) Sony Pictures Imageworks, et al.
// SPDX-License-Identifier: Apache-2.0
// https://github.com/imageworks/spk

use std::sync::Arc;

use spfs::tracking::Manifest;
use spk_schema::foundation::option_map;
use spk_schema::validation::ValidationMatcher;
use spk_schema::{spec, Package, ValidationRule};
use spk_solve::Solution;

use crate::report::BuildSetupReport;
use crate::validation::Validator;

macro_rules! basic_setup {
($pkg:tt) => {{
let package = Arc::new(spec!($pkg));

let environment = Solution::default();
BuildSetupReport {
environment,
variant: option_map! {},
environment_filesystem: Manifest::new(
spfs::tracking::Entry::empty_dir_with_open_perms_with_data(package.ident().clone()),
),
package,
}
}};
}

#[tokio::test]
async fn test_license_allowed_empty() {
let setup = basic_setup!(
{
"pkg": "base/1.0.0/3TCOOP2W",
"meta": {},
"sources": [],
"build": {
"script": "echo building...",
},
}
);

ValidationRule::Allow {
condition: ValidationMatcher::SpdxLicense,
}
.validate_setup(&setup)
.await
.into_result()
.expect("Should allow no license with default allow rule");
}

#[tokio::test]
async fn test_license_allowed_valid() {
let setup = basic_setup!(
{
"pkg": "base/1.0.0/3TCOOP2W",
"meta": {
"license": "Apache-2.0" // from spdx license list
},
"sources": [],
"build": {
"script": "echo building...",
},
}
);

ValidationRule::Allow {
condition: ValidationMatcher::SpdxLicense,
}
.validate_setup(&setup)
.await
.into_result()
.expect("Should allow a known license with default allow rule");
}

#[tokio::test]
async fn test_license_allowed_invalid() {
let setup = basic_setup!(
{
"pkg": "base/1.0.0/3TCOOP2W",
"meta": {
"license": "unknown" // NOT from spdx license list
},
"sources": [],
"build": {
"script": "echo building...",
},
}
);

ValidationRule::Allow {
condition: ValidationMatcher::SpdxLicense,
}
.validate_setup(&setup)
.await
.into_result()
.expect_err("Should fail with default allow rule and invalid license");
}

#[tokio::test]
async fn test_license_require_empty() {
let setup = basic_setup!(
{
"pkg": "base/1.0.0/3TCOOP2W",
"meta": {},
"sources": [],
"build": {
"script": "echo building...",
},
}
);

ValidationRule::Require {
condition: ValidationMatcher::SpdxLicense,
}
.validate_setup(&setup)
.await
.into_result()
.expect_err("Should fail when no license and require rule");
}

#[tokio::test]
async fn test_license_deny_empty() {
let setup = basic_setup!(
{
"pkg": "base/1.0.0/3TCOOP2W",
"meta": {},
"sources": [],
"build": {
"script": "echo building...",
},
}
);

ValidationRule::Deny {
condition: ValidationMatcher::SpdxLicense,
}
.validate_setup(&setup)
.await
.into_result()
.expect("Should allow empty license with deny rule");
}

#[tokio::test]
async fn test_license_deny_invalid() {
let setup = basic_setup!(
{
"pkg": "base/1.0.0/3TCOOP2W",
"meta": {
"license": "unknown" // NOT from license list
},
"sources": [],
"build": {
"script": "echo building...",
},
}
);

ValidationRule::Deny {
condition: ValidationMatcher::SpdxLicense,
}
.validate_setup(&setup)
.await
.into_result()
.expect("Should allow invalid license with deny rule");
}

#[tokio::test]
async fn test_license_deny_valid() {
let setup = basic_setup!(
{
"pkg": "base/1.0.0/3TCOOP2W",
"meta": {
"license": "Apache-2.0"
},
"sources": [],
"build": {
"script": "echo building...",
},
}
);

ValidationRule::Deny {
condition: ValidationMatcher::SpdxLicense,
}
.validate_setup(&setup)
.await
.into_result()
.expect_err("Should fail with valid license and deny rule");
}
4 changes: 4 additions & 0 deletions crates/spk-build/src/validation/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ macro_rules! rule_to_validator {
let kind = spk_schema::validation::ValidationRuleDiscriminants::from($rule);
#[allow(unused_braces)]
match $rule.condition() {
ValidationMatcher::SpdxLicense => {
let $bind = super::SpdxLicenseValidator { kind };
$op
}
ValidationMatcher::EmptyPackage => {
let $bind = super::EmptyPackageValidator { kind };
$op
Expand Down
15 changes: 15 additions & 0 deletions crates/spk-schema/src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub trait Package:
/// This includes the version and optional build
fn ident(&self) -> &BuildIdent;

/// The additional metadata attached to this package
fn metadata(&self) -> &crate::metadata::Meta;

/// The values for this packages options used for this build.
fn option_values(&self) -> OptionMap;

Expand Down Expand Up @@ -142,6 +145,10 @@ impl<T: Package + Send + Sync> Package for std::sync::Arc<T> {
(**self).ident()
}

fn metadata(&self) -> &crate::metadata::Meta {
(**self).metadata()
}

fn option_values(&self) -> OptionMap {
(**self).option_values()
}
Expand Down Expand Up @@ -214,6 +221,10 @@ impl<T: Package + Send + Sync> Package for Box<T> {
(**self).ident()
}

fn metadata(&self) -> &crate::metadata::Meta {
(**self).metadata()
}

fn option_values(&self) -> OptionMap {
(**self).option_values()
}
Expand Down Expand Up @@ -286,6 +297,10 @@ impl<T: Package + Send + Sync> Package for &T {
(**self).ident()
}

fn metadata(&self) -> &crate::metadata::Meta {
(**self).metadata()
}

fn option_values(&self) -> OptionMap {
(**self).option_values()
}
Expand Down
Loading

0 comments on commit f5badbd

Please sign in to comment.