Merge "Update the parser to parse validations" into main
diff --git a/tools/crate-updater/src/main.rs b/tools/crate-updater/src/main.rs
index 0b2afc1..79dcf47 100644
--- a/tools/crate-updater/src/main.rs
+++ b/tools/crate-updater/src/main.rs
@@ -28,7 +28,7 @@
#[derive(Parser)]
struct Cli {
- /// Absolute path to a repo checkout of aosp-main.
+ /// Absolute path to a repo checkout of main-without-vendor.
/// It is strongly recommended that you use a source tree dedicated to
/// running this updater.
android_root: PathBuf,
@@ -96,7 +96,7 @@
.run_and_stream_output()?;
Command::new("git")
- .args(["checkout", "aosp/main"])
+ .args(["checkout", "goog/main"])
.current_dir(monorepo_path)
.run_and_stream_output()?;
@@ -132,7 +132,7 @@
let output = Command::new("/google/data/ro/projects/android/ab")
.args([
"lkgb",
- "--branch=aosp-main",
+ "--branch=git_main-without-vendor",
"--target=aosp_arm64-trunk_staging-userdebug",
"--raw",
"--custom_raw_format={o[buildId]}",
diff --git a/tools/external_crates/crate_tool/Cargo.toml b/tools/external_crates/crate_tool/Cargo.toml
index ccb5102..f70ed5c 100644
--- a/tools/external_crates/crate_tool/Cargo.toml
+++ b/tools/external_crates/crate_tool/Cargo.toml
@@ -7,11 +7,11 @@
[dependencies]
anyhow = "1"
-cargo = "0.76"
+cargo_metadata = "0.19"
cfg-expr = "0.17"
chrono = "0.4"
clap = { version = "4.4.6", features = ["derive"] }
-crates-index = "3.2.0"
+crates-index = "3.8"
glob = "0.3"
itertools = "0.11"
protobuf = "3"
diff --git a/tools/external_crates/crate_tool/src/crate_type.rs b/tools/external_crates/crate_tool/src/crate_type.rs
index 51ba6ec..a1e97e3 100644
--- a/tools/external_crates/crate_tool/src/crate_type.rs
+++ b/tools/external_crates/crate_tool/src/crate_type.rs
@@ -12,12 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use anyhow::{anyhow, Result};
-use cargo::{
- core::{Manifest, SourceId},
- util::toml::read_manifest,
- Config,
-};
+use anyhow::{bail, Result};
+use cargo_metadata::Package;
use name_and_version::{NameAndVersionRef, NamedAndVersioned};
use rooted_path::RootedPath;
use semver::Version;
@@ -26,16 +22,16 @@
#[derive(Debug, Clone)]
pub struct Crate {
- manifest: Manifest,
+ metadata: Package,
path: RootedPath,
}
impl NamedAndVersioned for Crate {
fn name(&self) -> &str {
- self.manifest.name().as_str()
+ self.metadata.name.as_str()
}
fn version(&self) -> &Version {
- self.manifest.version()
+ &self.metadata.version
}
fn key(&self) -> NameAndVersionRef {
NameAndVersionRef::new(self.name(), self.version())
@@ -43,32 +39,30 @@
}
impl Crate {
- pub fn new(manifest: Manifest, path: RootedPath) -> Crate {
- Crate { manifest, path }
+ pub fn new(metadata: Package, path: RootedPath) -> Crate {
+ Crate { metadata, path }
}
pub fn from(manifest_dir: RootedPath) -> Result<Crate> {
- let source_id = SourceId::for_path(manifest_dir.abs())?;
- let (manifest, _nested) = read_manifest(
- manifest_dir.join("Cargo.toml").unwrap().as_ref(),
- source_id,
- &Config::default()?,
- )?;
- match manifest {
- cargo::core::EitherManifest::Real(r) => Ok(Crate::new(r, manifest_dir)),
- cargo::core::EitherManifest::Virtual(_) => {
- Err(anyhow!(CrateError::VirtualCrate(manifest_dir.as_ref().to_path_buf())))
- }
+ let manifest_path = manifest_dir.abs().join("Cargo.toml");
+ let metadata = cargo_metadata::MetadataCommand::new()
+ .manifest_path(manifest_path)
+ .no_deps()
+ .other_options(["--frozen".to_string()])
+ .exec()?;
+ if metadata.packages.len() != 1 {
+ bail!(CrateError::VirtualCrate(manifest_dir.as_ref().to_path_buf()));
}
+ Ok(Crate::new(metadata.packages[0].clone(), manifest_dir))
}
pub fn description(&self) -> &str {
- self.manifest.metadata().description.as_deref().unwrap_or("")
+ self.metadata.description.as_deref().unwrap_or("")
}
pub fn license(&self) -> Option<&str> {
- self.manifest.metadata().license.as_deref()
+ self.metadata.license.as_deref()
}
pub fn repository(&self) -> Option<&str> {
- self.manifest.metadata().repository.as_deref()
+ self.metadata.repository.as_deref()
}
pub fn path(&self) -> &RootedPath {
&self.path
diff --git a/tools/external_crates/crate_tool/src/lib.rs b/tools/external_crates/crate_tool/src/lib.rs
index ef4537d..30a0593 100644
--- a/tools/external_crates/crate_tool/src/lib.rs
+++ b/tools/external_crates/crate_tool/src/lib.rs
@@ -25,7 +25,6 @@
mod crate_collection;
mod crate_type;
mod crates_io;
-mod license;
mod managed_crate;
mod managed_repo;
mod patch;
diff --git a/tools/external_crates/crate_tool/src/license.rs b/tools/external_crates/crate_tool/src/license.rs
deleted file mode 100644
index ba18d17..0000000
--- a/tools/external_crates/crate_tool/src/license.rs
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use std::{
- collections::BTreeMap,
- fs::{remove_file, write},
- path::Path,
- sync::LazyLock,
-};
-
-use anyhow::{anyhow, Result};
-use glob::glob;
-use google_metadata::metadata::LicenseType;
-use license_checker::LicenseState;
-use spdx::{LicenseReq, Licensee};
-
-/// Update MODULE_LICENSE_* files in a directory based on the applicable licenses.
-/// These files are typically empty, and their name indicates the type of license that
-/// applies to the code, for example MODULE_LICENSE_APACHE2.
-pub fn update_module_license_files(path: &impl AsRef<Path>, licenses: &LicenseState) -> Result<()> {
- let path = path.as_ref();
- for old_module_license_file in glob(
- path.join("MODULE_LICENSE*").to_str().ok_or(anyhow!("Failed to convert path to string"))?,
- )? {
- remove_file(old_module_license_file?)?;
- }
- for license in licenses.satisfied.keys().chain(&licenses.unsatisfied) {
- if let Some(mod_lic) = MODULE_LICENSE_FILES.get(license) {
- write(path.join(mod_lic), "")?; // Write an empty file. Essentially "touch".
- }
- }
- Ok(())
-}
-
-fn discriminant(lt: LicenseType) -> u8 {
- // Smaller --> more restricted
- // Larger --> less restricted
- match lt {
- LicenseType::UNKNOWN => 0,
- LicenseType::BY_EXCEPTION_ONLY => 1,
- LicenseType::RESTRICTED => 2,
- LicenseType::RESTRICTED_IF_STATICALLY_LINKED => 3,
- LicenseType::RECIPROCAL => 4,
- LicenseType::NOTICE => 5,
- LicenseType::PERMISSIVE => 6,
- LicenseType::UNENCUMBERED => 7,
- }
-}
-
-pub fn most_restrictive_type(licenses: &LicenseState) -> LicenseType {
- licenses
- .satisfied
- .keys()
- .chain(&licenses.unsatisfied)
- .map(|req| LICENSE_TYPES.get(req).cloned().unwrap_or(LicenseType::UNKNOWN))
- .min_by(|a, b| discriminant(*a).cmp(&discriminant(*b)))
- .unwrap_or(LicenseType::UNKNOWN)
-}
-
-static MODULE_LICENSE_FILES: LazyLock<BTreeMap<LicenseReq, &'static str>> = LazyLock::new(|| {
- vec![
- ("Apache-2.0", "MODULE_LICENSE_APACHE2"),
- ("MIT", "MODULE_LICENSE_MIT"),
- ("MIT-0", "MODULE_LICENSE_MIT_0"),
- ("BSD-3-Clause", "MODULE_LICENSE_BSD"),
- ("BSD-2-Clause", "MODULE_LICENSE_BSD"),
- ("ISC", "MODULE_LICENSE_ISC"),
- ("MPL-2.0", "MODULE_LICENSE_MPL"),
- ("0BSD", "MODULE_LICENSE_PERMISSIVE"),
- ("Unlicense", "MODULE_LICENSE_PERMISSIVE"),
- ("Zlib", "MODULE_LICENSE_ZLIB"),
- ("Unicode-3.0", "MODULE_LICENSE_UNICODE_3"),
- ("Unicode-DFS-2016", "MODULE_LICENSE_UNICODE"),
- ("NCSA", "MODULE_LICENSE_NCSA"),
- ("OpenSSL", "MODULE_LICENSE_OPENSSL"),
- ]
- .into_iter()
- .map(|l| (Licensee::parse(l.0).unwrap().into_req(), l.1))
- .collect()
-});
-static LICENSE_TYPES: LazyLock<BTreeMap<LicenseReq, LicenseType>> = LazyLock::new(|| {
- vec![
- ("Apache-2.0", LicenseType::NOTICE),
- ("MIT", LicenseType::NOTICE),
- ("MIT-0", LicenseType::PERMISSIVE),
- ("BSD-3-Clause", LicenseType::NOTICE),
- ("BSD-2-Clause", LicenseType::NOTICE),
- ("ISC", LicenseType::NOTICE),
- ("MPL-2.0", LicenseType::RECIPROCAL),
- ("0BSD", LicenseType::PERMISSIVE),
- ("Unlicense", LicenseType::PERMISSIVE),
- ("Zlib", LicenseType::NOTICE),
- ("Unicode-3.0", LicenseType::NOTICE),
- ("Unicode-DFS-2016", LicenseType::NOTICE),
- ("NCSA", LicenseType::NOTICE),
- ("OpenSSL", LicenseType::NOTICE),
- ("CC0-1.0", LicenseType::UNENCUMBERED),
- ]
- .into_iter()
- .map(|l| (Licensee::parse(l.0).unwrap().into_req(), l.1))
- .collect()
-});
diff --git a/tools/external_crates/crate_tool/src/managed_crate.rs b/tools/external_crates/crate_tool/src/managed_crate.rs
index adf3fae..181a36a 100644
--- a/tools/external_crates/crate_tool/src/managed_crate.rs
+++ b/tools/external_crates/crate_tool/src/managed_crate.rs
@@ -37,7 +37,6 @@
copy_dir,
crate_type::Crate,
ensure_exists_and_empty,
- license::{most_restrictive_type, update_module_license_files},
patch::Patch,
pseudo_crate::{CargoVendorClean, PseudoCrate},
SuccessOrError,
@@ -450,7 +449,7 @@
)?;
}
- update_module_license_files(&self.temporary_build_directory(), &self.extra.licenses)?;
+ self.extra.licenses.update_module_license_files(&self.temporary_build_directory())?;
Ok(())
}
/// Runs cargo_embargo on the crate in the temporary build directory.
@@ -486,7 +485,7 @@
self.name(),
self.extra.vendored_crate.version().to_string(),
self.extra.vendored_crate.description(),
- most_restrictive_type(&self.extra.licenses),
+ self.extra.licenses.most_restrictive_type(),
);
metadata.write()?;
diff --git a/tools/external_crates/crate_tool/src/managed_repo.rs b/tools/external_crates/crate_tool/src/managed_repo.rs
index 3e6b53e..399ecb4 100644
--- a/tools/external_crates/crate_tool/src/managed_repo.rs
+++ b/tools/external_crates/crate_tool/src/managed_repo.rs
@@ -38,7 +38,6 @@
crate_collection::CrateCollection,
crate_type::Crate,
crates_io::{AndroidDependencies, DependencyChanges, SafeVersions},
- license::{most_restrictive_type, update_module_license_files},
managed_crate::ManagedCrate,
pseudo_crate::{CargoVendorDirty, PseudoCrate},
upgradable::{IsUpgradableTo, MatchesWithCompatibilityRule, SemverCompatibilityRule},
@@ -265,7 +264,7 @@
println!(" Finding license files");
let licenses = find_licenses(krate.path().abs(), krate.name(), krate.license())?;
- update_module_license_files(&krate.path().abs(), &licenses)?;
+ licenses.update_module_license_files(&krate.path().abs())?;
println!(" Creating METADATA");
let metadata = GoogleMetadata::init(
@@ -273,7 +272,7 @@
krate.name(),
krate.version().to_string(),
krate.description(),
- most_restrictive_type(&licenses),
+ licenses.most_restrictive_type(),
)?;
metadata.write()?;
@@ -284,7 +283,13 @@
.success_or_error()
.context("Failed to generate cargo_embargo.json with 'cargo_embargo autoconfig'")?;
} else {
- write(krate.path().abs().join("cargo_embargo.json"), "{}")?;
+ write(
+ krate.path().abs().join("cargo_embargo.json"),
+ r#"{
+ "run_cargo": false,
+ "min_sdk_version": "29"
+}"#,
+ )?;
}
if !licenses.unsatisfied.is_empty() {
diff --git a/tools/external_crates/license_checker/Android.bp b/tools/external_crates/license_checker/Android.bp
index abe5df8..f7b10de 100644
--- a/tools/external_crates/license_checker/Android.bp
+++ b/tools/external_crates/license_checker/Android.bp
@@ -17,6 +17,7 @@
edition: "2021",
rustlibs: [
"libglob",
+ "libgoogle_metadata",
"libitertools",
"libspdx",
"libtextdistance",
@@ -38,6 +39,7 @@
edition: "2021",
rustlibs: [
"libglob",
+ "libgoogle_metadata",
"libitertools",
"libspdx",
"libtextdistance",
diff --git a/tools/external_crates/license_checker/Cargo.toml b/tools/external_crates/license_checker/Cargo.toml
index aa9b7fd..b951f2c 100644
--- a/tools/external_crates/license_checker/Cargo.toml
+++ b/tools/external_crates/license_checker/Cargo.toml
@@ -8,4 +8,5 @@
itertools = "0.14"
spdx = "0.10"
textdistance = "1.1.1"
-thiserror = "1.0"
\ No newline at end of file
+thiserror = "1.0"
+google_metadata = { path = "../google_metadata"}
\ No newline at end of file
diff --git a/tools/external_crates/license_checker/src/file_classifier.rs b/tools/external_crates/license_checker/src/file_classifier.rs
index 10e3be2..e4f5d7c 100644
--- a/tools/external_crates/license_checker/src/file_classifier.rs
+++ b/tools/external_crates/license_checker/src/file_classifier.rs
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use spdx::Licensee;
use std::{
collections::BTreeSet,
fs::read_to_string,
@@ -26,8 +25,8 @@
pub(crate) struct Classifier {
file_path: PathBuf,
contents: String,
- by_content: OnceLock<BTreeSet<Licensee>>,
- by_content_fuzzy: OnceLock<Option<Licensee>>,
+ by_content: OnceLock<BTreeSet<spdx::LicenseReq>>,
+ by_content_fuzzy: OnceLock<Option<spdx::LicenseReq>>,
}
impl Classifier {
@@ -56,13 +55,13 @@
pub fn file_path(&self) -> &Path {
self.file_path.as_path()
}
- pub fn by_name(&self) -> Option<&Licensee> {
+ pub fn by_name(&self) -> Option<&spdx::LicenseReq> {
LICENSE_DATA.classify_file_name(self.file_path())
}
- pub fn by_content(&self) -> &BTreeSet<Licensee> {
+ pub fn by_content(&self) -> &BTreeSet<spdx::LicenseReq> {
self.by_content.get_or_init(|| LICENSE_DATA.classify_file_contents(&self.contents))
}
- pub fn by_content_fuzzy(&self) -> Option<&Licensee> {
+ pub fn by_content_fuzzy(&self) -> Option<&spdx::LicenseReq> {
self.by_content_fuzzy
.get_or_init(|| LICENSE_DATA.classify_file_contents_fuzzy(&self.contents))
.as_ref()
diff --git a/tools/external_crates/license_checker/src/lib.rs b/tools/external_crates/license_checker/src/lib.rs
index dc4f9e4..6b7f3fe 100644
--- a/tools/external_crates/license_checker/src/lib.rs
+++ b/tools/external_crates/license_checker/src/lib.rs
@@ -16,10 +16,13 @@
use std::{
collections::{BTreeMap, BTreeSet},
+ fs::{remove_file, write},
path::{Path, PathBuf},
};
use file_classifier::Classifier;
+use glob::glob;
+use google_metadata::LicenseType;
use license_data::{CrateLicenseSpecialCase, License};
use license_terms::LicenseTerms;
use licenses::LICENSE_DATA;
@@ -113,14 +116,23 @@
/// The list of license preferences contains an unknown license
#[error("The license preference list contains unknown license {0}")]
LicensePreferenceForUnknownLicense(String),
+ /// Invalid MODULE_LICENSE_* file name.
+ #[error("The MODULE_LICENSE_* file name '{0}' does not begin with 'MODULE_LICENSE_'")]
+ InvalidModuleLicenseFileName(String),
+ /// License type is too restrictive.
+ #[error("The license type {0:?} is too restrictive")]
+ LicenseTypeTooRestrictive(LicenseType),
+ /// I/O error.
+ #[error(transparent)]
+ IoError(#[from] std::io::Error),
}
/// The result of license file verification, containing a set of acceptable licenses, and the
/// corresponding license files, if present.
#[derive(Debug)]
pub struct LicenseState {
- /// Unsatisfied licenses. These are licenses that are required by evaluation of SPDX license in
- /// Cargo.toml, but for which no matching license file was found.
+ /// Unsatisfied licenses. These are licenses that are required by evaluation of the SPDX
+ /// license in Cargo.toml, but for which no matching license file was found.
pub unsatisfied: BTreeSet<LicenseReq>,
/// Licenses for which a license file file was found, and the path to that file.
pub satisfied: BTreeMap<LicenseReq, PathBuf>,
@@ -143,12 +155,11 @@
let not_required = terms.not_required;
for classifier in file_classifiers {
- if let Some(licensee) = classifier.by_name() {
- let req = licensee.clone().into_req();
- if state.unsatisfied.remove(&req) {
+ if let Some(req) = classifier.by_name() {
+ if state.unsatisfied.remove(req) {
state.satisfied.insert(req.clone(), classifier.file_path().to_owned());
- } else if !state.satisfied.contains_key(&req) {
- if not_required.contains(&req) {
+ } else if !state.satisfied.contains_key(req) {
+ if not_required.contains(req) {
state.unneeded.insert(req.clone(), classifier.file_path().to_owned());
} else {
state.unexpected.insert(req.clone(), classifier.file_path().to_owned());
@@ -159,17 +170,16 @@
if !state.unsatisfied.is_empty() {
for classifier in file_classifiers {
- for licensee in classifier.by_content() {
- let req = licensee.clone().into_req();
- if state.unsatisfied.remove(&req) {
+ for req in classifier.by_content() {
+ if state.unsatisfied.remove(req) {
state.satisfied.insert(req.clone(), classifier.file_path().to_owned());
- } else if !state.satisfied.contains_key(&req) && !not_required.contains(&req) {
+ } else if !state.satisfied.contains_key(req) && !not_required.contains(req) {
state.unexpected.insert(req.clone(), classifier.file_path().to_owned());
}
}
if classifier.by_content().len() == 1 {
- let req = classifier.by_content().first().unwrap().clone().into_req();
- if !state.satisfied.contains_key(&req) && not_required.contains(&req) {
+ let req = classifier.by_content().first().unwrap();
+ if !state.satisfied.contains_key(req) && not_required.contains(req) {
state.unneeded.insert(req.clone(), classifier.file_path().to_owned());
}
}
@@ -181,9 +191,8 @@
if classifier.by_name().is_some() || !classifier.by_content().is_empty() {
continue;
}
- if let Some(licensee) = classifier.by_content_fuzzy() {
- let req = licensee.clone().into_req();
- if state.unsatisfied.remove(&req) {
+ if let Some(req) = classifier.by_content_fuzzy() {
+ if state.unsatisfied.remove(req) {
state.satisfied.insert(req.clone(), classifier.file_path().to_owned());
if state.unsatisfied.is_empty() {
break;
@@ -195,6 +204,30 @@
state
}
+
+ /// Update MODULE_LICENSE_* files in a directory based on the applicable licenses.
+ /// These files are typically empty, and their name indicates the type of license that
+ /// applies to the code, for example MODULE_LICENSE_APACHE2.
+ pub fn update_module_license_files(&self, path: &impl AsRef<Path>) -> Result<(), Error> {
+ let path = path.as_ref();
+ for old_module_license_file in glob(&path.join("MODULE_LICENSE*").to_string_lossy())? {
+ remove_file(old_module_license_file?)?;
+ }
+ for file in self.required_terms().map(|req| LICENSE_DATA.module_license_file(req).unwrap())
+ {
+ write(path.join(file), "")?; // Write an empty file. Essentially "touch".
+ }
+ Ok(())
+ }
+
+ /// Find the most restrictive type of license, for reporting in the METADATA file.
+ pub fn most_restrictive_type(&self) -> LicenseType {
+ LICENSE_DATA.most_restrictive_type(self.required_terms())
+ }
+
+ fn required_terms(&self) -> impl Iterator<Item = &LicenseReq> {
+ self.satisfied.keys().chain(&self.unsatisfied)
+ }
}
/// Evaluates the license expression for a crate at a given path and returns a minimal set of
diff --git a/tools/external_crates/license_checker/src/license_data.rs b/tools/external_crates/license_checker/src/license_data.rs
index 3330a8f..7399614 100644
--- a/tools/external_crates/license_checker/src/license_data.rs
+++ b/tools/external_crates/license_checker/src/license_data.rs
@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use google_metadata::LicenseType;
+
/// A license and the means for recognizing it.
/// We must be able to recognize it either by content, by the `text` field,
/// or by file name, by the `file_names` field, or both.
@@ -23,13 +25,23 @@
pub text: Option<&'static str>,
/// A set of file names, any of which unambiguously identify the license.
pub file_names: &'static [&'static str],
+ /// The name of the MODULE_LICENSE_* file for this license.
+ pub module_license_file_name: &'static str,
+ /// The license type, per http://go/thirdpartylicenses#types
+ pub license_type: LicenseType,
}
/// A list of all the licenses we recognize and accept.
/// Only acceptable licenses should go in this list. Please see go/thirdpartylicenses
/// for information on what this means.
pub(crate) static LICENSES: &[License] = &[
- License { name: "0BSD", text: None, file_names: &["LICENSE-0BSD"] },
+ License {
+ name: "0BSD",
+ text: None,
+ file_names: &["LICENSE-0BSD"],
+ module_license_file_name: "MODULE_LICENSE_PERMISSIVE",
+ license_type: LicenseType::PERMISSIVE,
+ },
License {
name: "Apache-2.0",
text: Some(include_str!("licenses/Apache-2.0.txt")),
@@ -39,58 +51,118 @@
"LICENSES/Apache-2.0",
"docs/LICENSE-APACHE",
],
+ module_license_file_name: "MODULE_LICENSE_APACHE2",
+ license_type: LicenseType::NOTICE,
},
License {
name: "Apache-2.0 WITH LLVM-exception",
text: None,
file_names: &["LICENSE-Apache-2.0_WITH_LLVM-exception"],
+ module_license_file_name: "MODULE_LICENSE_APACHE2",
+ license_type: LicenseType::NOTICE,
},
License {
name: "BSD-2-Clause",
text: Some(include_str!("licenses/BSD-2-Clause.txt")),
// Note: LICENSE-BSD is not unambiguous. Could be 2-clause or 3-clause
file_names: &["LICENSE-BSD-2-Clause", "LICENSE.BSD-2-Clause"],
+ module_license_file_name: "MODULE_LICENSE_BSD",
+ license_type: LicenseType::NOTICE,
},
License {
name: "BSD-3-Clause",
text: Some(include_str!("licenses/BSD-3-Clause.txt")),
// Note: LICENSE-BSD is not unambiguous. Could be 2-clause or 3-clause
file_names: &["LICENSE-BSD-3-Clause"],
+ module_license_file_name: "MODULE_LICENSE_BSD",
+ license_type: LicenseType::NOTICE,
},
- License { name: "ISC", text: Some(include_str!("licenses/ISC.txt")), file_names: &[] },
+ License {
+ name: "ISC",
+ text: Some(include_str!("licenses/ISC.txt")),
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_ISC",
+ license_type: LicenseType::NOTICE,
+ },
License {
name: "MIT",
text: Some(include_str!("licenses/MIT.txt")),
file_names: &["LICENSE-MIT", "LICENSES/MIT", "docs/LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: LicenseType::NOTICE,
},
- License { name: "MIT-0", text: Some(include_str!("licenses/MIT-0.txt")), file_names: &[] },
- License { name: "MPL-2.0", text: Some(include_str!("licenses/MPL-2.0.txt")), file_names: &[] },
- License { name: "NCSA", text: Some(include_str!("licenses/NCSA.txt")), file_names: &[] },
- License { name: "OpenSSL", text: Some(include_str!("licenses/OpenSSL.txt")), file_names: &[] },
+ License {
+ name: "MIT-0",
+ text: Some(include_str!("licenses/MIT-0.txt")),
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_MIT_0",
+ license_type: LicenseType::PERMISSIVE,
+ },
+ License {
+ name: "MPL-2.0",
+ text: Some(include_str!("licenses/MPL-2.0.txt")),
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_MPL",
+ license_type: LicenseType::RECIPROCAL,
+ },
+ License {
+ name: "NCSA",
+ text: Some(include_str!("licenses/NCSA.txt")),
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_NCSA",
+ license_type: LicenseType::NOTICE,
+ },
+ License {
+ name: "OpenSSL",
+ text: Some(include_str!("licenses/OpenSSL.txt")),
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_OPENSSL",
+ license_type: LicenseType::NOTICE,
+ },
License {
name: "Unicode-3.0",
text: Some(include_str!("licenses/Unicode-3.0.txt")),
// Note: LICENSE-UNICODE is not unambiguous
file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_UNICODE_3",
+ license_type: LicenseType::NOTICE,
},
License {
name: "Unicode-DFS-2016",
text: Some(include_str!("licenses/Unicode-DFS-2016.txt")),
// Note: LICENSE-UNICODE is not unambiguous
file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_UNICODE",
+ license_type: LicenseType::NOTICE,
},
License {
name: "Unlicense",
text: Some(include_str!("licenses/Unlicense.txt")),
file_names: &["UNLICENSE"],
+ module_license_file_name: "MODULE_LICENSE_PERMISSIVE",
+ license_type: LicenseType::PERMISSIVE,
},
License {
name: "Zlib",
text: Some(include_str!("licenses/Zlib.txt")),
file_names: &["LICENSE-ZLIB"],
+ module_license_file_name: "MODULE_LICENSE_ZLIB",
+ license_type: LicenseType::NOTICE,
},
- License { name: "BSL-1.0", text: None, file_names: &["LICENSE-BOOST"] },
- License { name: "CC0-1.0", text: None, file_names: &["LICENSES/CC0-1.0"] },
+ License {
+ name: "BSL-1.0",
+ text: None,
+ file_names: &["LICENSE-BOOST"],
+ module_license_file_name: "MODULE_LICENSE_BOOST",
+ license_type: LicenseType::NOTICE,
+ },
+ License {
+ name: "CC0-1.0",
+ text: None,
+ file_names: &["LICENSES/CC0-1.0"],
+ module_license_file_name: "MODULE_LICENSE_CC0",
+ license_type: LicenseType::UNENCUMBERED,
+ },
];
/// Specifies the order of preference for choosing a license.
diff --git a/tools/external_crates/license_checker/src/license_data_tests.rs b/tools/external_crates/license_checker/src/license_data_tests.rs
index 68a6312..5ab65d9 100644
--- a/tools/external_crates/license_checker/src/license_data_tests.rs
+++ b/tools/external_crates/license_checker/src/license_data_tests.rs
@@ -26,32 +26,32 @@
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("UNLICENSE")),
- Some(&Licensee::parse("Unlicense").unwrap()),
+ Some(&Licensee::parse("Unlicense").unwrap().into_req()),
"Unlicense"
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("LICENSE-MIT")),
- Some(&Licensee::parse("MIT").unwrap()),
+ Some(&Licensee::parse("MIT").unwrap().into_req()),
"Standard name"
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("LICENSE-APACHE")),
- Some(&Licensee::parse("Apache-2.0").unwrap()),
+ Some(&Licensee::parse("Apache-2.0").unwrap().into_req()),
"Standard name"
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("LICENSE-apache")),
- Some(&Licensee::parse("Apache-2.0").unwrap()),
+ Some(&Licensee::parse("Apache-2.0").unwrap().into_req()),
"Case-insensitive"
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("LICENSE-APACHE.md")),
- Some(&Licensee::parse("Apache-2.0").unwrap()),
+ Some(&Licensee::parse("Apache-2.0").unwrap().into_req()),
".md extension ignored"
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("LICENSE-0BSD.txt")),
- Some(&Licensee::parse("0BSD").unwrap()),
+ Some(&Licensee::parse("0BSD").unwrap().into_req()),
".txt extension ignored"
);
assert!(
@@ -60,12 +60,12 @@
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("LICENSES/MIT")),
- Some(&Licensee::parse("MIT").unwrap()),
+ Some(&Licensee::parse("MIT").unwrap().into_req()),
"Subdirectory"
);
assert_eq!(
LICENSE_DATA.classify_file_name(Path::new("LICENSES/Apache-2.0")),
- Some(&Licensee::parse("Apache-2.0").unwrap()),
+ Some(&Licensee::parse("Apache-2.0").unwrap().into_req()),
"Subdirectory"
);
}
@@ -85,7 +85,7 @@
assert_eq!(
LICENSE_DATA
.classify_file_contents(include_str!("testdata/LICENSE-MIT-aarch64-paging.txt")),
- BTreeSet::from([Licensee::parse("MIT").unwrap()])
+ BTreeSet::from([Licensee::parse("MIT").unwrap().into_req()])
);
}
@@ -97,7 +97,7 @@
assert_eq!(
LICENSE_DATA
.classify_file_contents_fuzzy(include_str!("testdata/BSD-3-Clause-bindgen.txt")),
- Some(Licensee::parse("BSD-3-Clause").unwrap())
+ Some(Licensee::parse("BSD-3-Clause").unwrap().into_req())
);
}
@@ -113,8 +113,8 @@
.as_str()
),
BTreeSet::from([
- Licensee::parse("MIT").unwrap(),
- Licensee::parse("Apache-2.0").unwrap()
+ Licensee::parse("MIT").unwrap().into_req(),
+ Licensee::parse("Apache-2.0").unwrap().into_req()
])
);
}
diff --git a/tools/external_crates/license_checker/src/licenses.rs b/tools/external_crates/license_checker/src/licenses.rs
index 8f88de6..1e1af73 100644
--- a/tools/external_crates/license_checker/src/licenses.rs
+++ b/tools/external_crates/license_checker/src/licenses.rs
@@ -19,8 +19,9 @@
sync::LazyLock,
};
+use google_metadata::LicenseType;
use itertools::Itertools;
-use spdx::Licensee;
+use spdx::{LicenseReq, Licensee};
use textdistance::str::ratcliff_obershelp;
use crate::{
@@ -31,10 +32,10 @@
#[derive(Debug)]
pub(crate) struct Licenses {
- licenses: BTreeMap<Licensee, ParsedLicense>,
+ licenses: BTreeMap<LicenseReq, ParsedLicense>,
license_preference: Vec<Licensee>,
crate_license_special_cases: CrateLicenseSpecialCases,
- license_file_names: BTreeMap<OsString, Licensee>,
+ license_file_names: BTreeMap<OsString, LicenseReq>,
}
impl Licenses {
@@ -51,33 +52,38 @@
let mut license_file_names = BTreeMap::new();
for license in raw_licenses {
let parsed = ParsedLicense::try_from(license)?;
- let licensee = parsed.licensee().clone();
+ let req = parsed.license_req();
for file_name in parsed.file_names() {
- if let Some(other) = license_file_names.insert(file_name.clone(), licensee.clone())
- {
+ if let Some(other) = license_file_names.insert(file_name.clone(), req.clone()) {
return Err(Error::DuplicateLicenseFileName {
file_name: file_name.to_string_lossy().into_owned(),
- license: parsed.licensee().to_string(),
+ license: parsed.license_req().to_string(),
other_license: other.to_string(),
});
}
}
- if licenses.insert(licensee.clone(), parsed).is_some() {
- return Err(Error::DuplicateLicense(licensee.to_string()));
+ if licenses.insert(req.clone(), parsed).is_some() {
+ return Err(Error::DuplicateLicense(req.to_string()));
}
}
let mut ranked_licenses = Vec::new();
for pref in license_preference {
let licensee = Licensee::parse(pref)?;
- if !licenses.contains_key(&licensee) {
+ if !licenses.contains_key(&licensee.clone().into_req()) {
return Err(Error::LicensePreferenceForUnknownLicense(pref.to_string()));
}
- ranked_licenses.push(licensee);
+ ranked_licenses.push(licensee.clone());
}
let unranked_licenses = licenses
- .keys()
- .filter_map(|l| if !ranked_licenses.contains(l) { Some(l.clone()) } else { None })
+ .values()
+ .filter_map(|l| {
+ if !ranked_licenses.contains(l.licensee()) {
+ Some(l.licensee().clone())
+ } else {
+ None
+ }
+ })
.collect::<Vec<_>>();
let license_preference = ranked_licenses.into_iter().chain(unranked_licenses).collect();
@@ -127,24 +133,24 @@
)
}
- pub fn classify_file_name(&self, file: impl AsRef<Path>) -> Option<&Licensee> {
+ pub fn classify_file_name(&self, file: impl AsRef<Path>) -> Option<&LicenseReq> {
self.license_file_names.get(&normalize_filename(file))
}
/// Classify file contents by exact substring match on the license text.
- pub fn classify_file_contents(&self, contents: &str) -> BTreeSet<Licensee> {
+ pub fn classify_file_contents(&self, contents: &str) -> BTreeSet<LicenseReq> {
let contents = strip_punctuation(contents);
let mut matches = BTreeSet::new();
for license in self.licenses.values() {
if license.is_substring_of(contents.as_str()) {
- matches.insert(license.licensee().clone());
+ matches.insert(license.license_req());
}
}
matches
}
- pub fn classify_file_contents_fuzzy(&self, contents: &str) -> Option<Licensee> {
+ pub fn classify_file_contents_fuzzy(&self, contents: &str) -> Option<LicenseReq> {
let contents = strip_punctuation(contents);
// Fuzzy match. This is expensive, so start with licenses that are closest in length to the file,
@@ -165,13 +171,43 @@
if let Some(processed_text) = license.processed_text() {
let similarity = ratcliff_obershelp(contents.as_str(), processed_text);
if similarity > 0.95 {
- return Some(license.licensee().clone());
+ return Some(license.license_req());
}
}
}
None
}
+
+ pub fn module_license_file(&self, req: &LicenseReq) -> Option<&'static str> {
+ self.licenses.get(req).map(|l| l.module_license_file_name())
+ }
+
+ pub fn most_restrictive_type<'a>(
+ &self,
+ terms: impl Iterator<Item = &'a LicenseReq>,
+ ) -> LicenseType {
+ let discriminant = |lt: LicenseType| -> u8 {
+ // Smaller --> more restricted
+ // Larger --> less restricted
+ match lt {
+ LicenseType::UNKNOWN => 0,
+ LicenseType::BY_EXCEPTION_ONLY => 1,
+ LicenseType::RESTRICTED => 2,
+ LicenseType::RESTRICTED_IF_STATICALLY_LINKED => 3,
+ LicenseType::RECIPROCAL => 4,
+ LicenseType::NOTICE => 5,
+ LicenseType::PERMISSIVE => 6,
+ LicenseType::UNENCUMBERED => 7,
+ }
+ };
+ terms
+ .map(|req| {
+ self.licenses.get(req).map(|l| l.license_type()).unwrap_or(LicenseType::UNKNOWN)
+ })
+ .min_by(|a, b| discriminant(*a).cmp(&discriminant(*b)))
+ .unwrap_or(LicenseType::UNKNOWN)
+ }
}
pub(crate) static LICENSE_DATA: LazyLock<Licenses> = LazyLock::new(|| {
@@ -194,9 +230,27 @@
fn basic() {
assert!(Licenses::new(
&[
- License { name: "Apache-2.0", text: None, file_names: &["LICENSE-APACHE"] },
- License { name: "MIT", text: None, file_names: &["LICENSE-MIT"] },
- License { name: "BSD-3-Clause", text: None, file_names: &["LICENSE-BSD-3-Clause"] },
+ License {
+ name: "Apache-2.0",
+ text: None,
+ file_names: &["LICENSE-APACHE"],
+ module_license_file_name: "MODULE_LICENSE_APACHE2",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
+ License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
+ License {
+ name: "BSD-3-Clause",
+ text: None,
+ file_names: &["LICENSE-BSD-3-Clause"],
+ module_license_file_name: "MODULE_LICENSE_BSD",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
],
&["Apache-2.0", "MIT"],
&[],
@@ -214,8 +268,20 @@
assert!(matches!(
Licenses::new(
&[
- License { name: "MIT", text: None, file_names: &["LICENSE-foo"] },
- License { name: "MIT", text: None, file_names: &["LICENSE-bar"] }
+ License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-foo"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
+ License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-bar"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ }
],
&[],
&[],
@@ -229,8 +295,20 @@
assert!(matches!(
Licenses::new(
&[
- License { name: "Apache-2.0", text: Some("foo"), file_names: &[] },
- License { name: "MIT", text: Some("foobar"), file_names: &[] }
+ License {
+ name: "Apache-2.0",
+ text: Some("foo"),
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_APACHE2",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
+ License {
+ name: "MIT",
+ text: Some("foobar"),
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ }
],
&[],
&[],
@@ -244,8 +322,20 @@
assert!(matches!(
Licenses::new(
&[
- License { name: "Apache-2.0", text: None, file_names: &["LICENSE"] },
- License { name: "MIT", text: None, file_names: &["LICENSE"] }
+ License {
+ name: "Apache-2.0",
+ text: None,
+ file_names: &["LICENSE"],
+ module_license_file_name: "MODULE_LICENSE_APACHE2",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
+ License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ }
],
&[],
&[],
@@ -257,7 +347,17 @@
#[test]
fn unfindable_license_file() {
assert!(matches!(
- Licenses::new(&[License { name: "MIT", text: None, file_names: &["foo"] },], &[], &[],),
+ Licenses::new(
+ &[License {
+ name: "MIT",
+ text: None,
+ file_names: &["foo"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },],
+ &[],
+ &[],
+ ),
Err(Error::LicenseFileNotFindable(_, _))
));
}
@@ -266,7 +366,13 @@
fn preference_for_unknown_license() {
assert!(matches!(
Licenses::new(
- &[License { name: "MIT", text: None, file_names: &["LICENSE-MIT"] }],
+ &[License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ }],
&["foo"],
&[],
),
@@ -274,7 +380,13 @@
));
assert!(matches!(
Licenses::new(
- &[License { name: "MIT", text: None, file_names: &["LICENSE-MIT"] }],
+ &[License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ }],
&["Apache-2.0"],
&[],
),
@@ -286,8 +398,20 @@
fn evaluate_crate_license() {
let licenses = Licenses::new(
&[
- License { name: "Apache-2.0", text: None, file_names: &["LICENSE-APACHE"] },
- License { name: "MIT", text: None, file_names: &["LICENSE-MIT"] },
+ License {
+ name: "Apache-2.0",
+ text: None,
+ file_names: &["LICENSE-APACHE"],
+ module_license_file_name: "MODULE_LICENSE_APACHE2",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
+ License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: google_metadata::LicenseType::NOTICE,
+ },
],
&["Apache-2.0", "MIT"],
&[],
diff --git a/tools/external_crates/license_checker/src/parsed_license_data.rs b/tools/external_crates/license_checker/src/parsed_license_data.rs
index f16c39b..9ebcea9 100644
--- a/tools/external_crates/license_checker/src/parsed_license_data.rs
+++ b/tools/external_crates/license_checker/src/parsed_license_data.rs
@@ -17,6 +17,8 @@
ffi::{OsStr, OsString},
};
+use google_metadata::LicenseType;
+
use crate::{
license_file_finder::is_findable, util::strip_punctuation, CrateLicenseSpecialCase, Error,
License,
@@ -30,18 +32,31 @@
processed_text: Option<String>,
// A set of file names, any of which unambiguously identify the license.
file_names: BTreeSet<OsString>,
+ /// The name of the MODULE_LICENSE_* file for this license.
+ module_license_file_name: &'static str,
+ /// The license type, per http://go/thirdpartylicenses#types
+ license_type: LicenseType,
}
impl ParsedLicense {
pub fn licensee(&self) -> &spdx::Licensee {
&self.licensee
}
+ pub fn license_req(&self) -> spdx::LicenseReq {
+ self.licensee.clone().into_req()
+ }
pub fn processed_text(&self) -> Option<&str> {
self.processed_text.as_deref()
}
pub fn file_names(&self) -> &BTreeSet<OsString> {
&self.file_names
}
+ pub fn module_license_file_name(&self) -> &'static str {
+ self.module_license_file_name
+ }
+ pub fn license_type(&self) -> LicenseType {
+ self.license_type
+ }
pub fn is_substring_of(&self, other: &str) -> bool {
self.processed_text.as_ref().is_some_and(|text| other.contains(text.as_str()))
}
@@ -71,10 +86,28 @@
file_names.insert(OsString::from(license_file.to_uppercase()));
}
+ if !value.module_license_file_name.starts_with("MODULE_LICENSE_") {
+ return Err(Error::InvalidModuleLicenseFileName(
+ value.module_license_file_name.to_string(),
+ ));
+ }
+
+ if matches!(
+ value.license_type,
+ LicenseType::UNKNOWN
+ | LicenseType::BY_EXCEPTION_ONLY
+ | LicenseType::RESTRICTED_IF_STATICALLY_LINKED
+ | LicenseType::RESTRICTED
+ ) {
+ return Err(Error::LicenseTypeTooRestrictive(value.license_type));
+ }
+
Ok(ParsedLicense {
licensee: spdx::Licensee::parse(value.name)?,
processed_text: value.text.map(strip_punctuation),
file_names,
+ module_license_file_name: value.module_license_file_name,
+ license_type: value.license_type,
})
}
}
@@ -162,7 +195,13 @@
#[test]
fn invalid_license_name() {
assert!(matches!(
- ParsedLicense::try_from(&License { name: "foo", text: None, file_names: &["LICENSE"] }),
+ ParsedLicense::try_from(&License {
+ name: "foo",
+ text: None,
+ file_names: &["LICENSE"],
+ module_license_file_name: "MODULE_LICENSE_FOO",
+ license_type: LicenseType::NOTICE,
+ }),
Err(Error::LicenseParseError(_))
));
}
@@ -170,7 +209,13 @@
#[test]
fn missing_both_text_and_file_name() {
assert!(matches!(
- ParsedLicense::try_from(&License { name: "MIT", text: None, file_names: &[] }),
+ ParsedLicense::try_from(&License {
+ name: "MIT",
+ text: None,
+ file_names: &[],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: LicenseType::NOTICE,
+ }),
Err(Error::LicenseWithoutTextOrFileNames(_))
));
}
@@ -181,18 +226,57 @@
ParsedLicense::try_from(&License {
name: "MIT",
text: Some(" "),
- file_names: &["LICENSE-MIT"]
+ file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: LicenseType::NOTICE,
}),
Err(Error::EmptyLicenseText(_))
));
}
#[test]
+ fn invalid_module_license_file() {
+ assert!(matches!(
+ ParsedLicense::try_from(&License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-MIT"],
+ module_license_file_name: "blah",
+ license_type: LicenseType::NOTICE,
+ }),
+ Err(Error::InvalidModuleLicenseFileName(_))
+ ));
+ }
+
+ #[test]
+ fn restrictive_licenses_rejected() {
+ for license_type in [
+ LicenseType::UNKNOWN,
+ LicenseType::BY_EXCEPTION_ONLY,
+ LicenseType::RESTRICTED,
+ LicenseType::RESTRICTED_IF_STATICALLY_LINKED,
+ ] {
+ assert!(matches!(
+ ParsedLicense::try_from(&License {
+ name: "MIT",
+ text: None,
+ file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type,
+ }),
+ Err(Error::LicenseTypeTooRestrictive(_))
+ ));
+ }
+ }
+
+ #[test]
fn is_substring_of() {
let license: ParsedLicense = ParsedLicense::try_from(&License {
name: "MIT",
text: Some("foo"),
file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: LicenseType::NOTICE,
})
.unwrap();
assert!(license.is_substring_of("foobar"));
@@ -201,6 +285,8 @@
name: "MIT",
text: None,
file_names: &["LICENSE-MIT"],
+ module_license_file_name: "MODULE_LICENSE_MIT",
+ license_type: LicenseType::NOTICE,
})
.unwrap();
assert!(