blob: 6bb911cd449ae1dd1e0a7a67b715842e07410705 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::config::BuildConfig;
use crate::group::Group;
use crate::paths::{self, get_build_dir_for_package, get_vendor_dir_for_package};
use anyhow::{bail, ensure, format_err, Context, Result};
use guppy::graph::PackageMetadata;
use guppy::PackageId;
use itertools::Itertools;
use semver::Version;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[derive(Clone, Debug, Serialize)]
pub enum UpdateMechanism {
Autoroll,
Manual,
}
#[derive(Clone, Debug, Serialize)]
pub struct ReadmeFile {
name: String,
url: String,
description: String,
version: Version,
security_critical: &'static str,
shipped: &'static str,
license: String,
license_files: Vec<String>,
revision: Option<String>,
update_mechanism: UpdateMechanism,
}
/// Returns a map keyed by the directory where the README file should be
/// written. The value is the contents of the README file, which can be
/// consumed by a handlebars template.
pub fn readme_files_from_packages<'a>(
deps: impl IntoIterator<Item = PackageMetadata<'a>>,
paths: &paths::ChromiumPaths,
extra_config: &BuildConfig,
mut find_group: impl FnMut(&'a PackageId) -> Group,
mut find_security_critical: impl FnMut(&'a PackageId) -> Option<bool>,
mut find_shipped: impl FnMut(&'a PackageId) -> Option<bool>,
) -> Result<HashMap<PathBuf, ReadmeFile>> {
let mut map = HashMap::new();
for package in deps {
let (dir, readme) = readme_file_from_package(
package,
paths,
extra_config,
&mut find_group,
&mut find_security_critical,
&mut find_shipped,
)
.with_context(|| {
format!(
"Can't generate a `README.chromium` file for `{name}-{version}`.",
name = package.name(),
version = package.version(),
)
})?;
map.insert(dir, readme);
}
Ok(map)
}
fn readme_file_from_package<'a>(
package: PackageMetadata<'a>,
paths: &paths::ChromiumPaths,
extra_config: &BuildConfig,
find_group: &mut dyn FnMut(&'a PackageId) -> Group,
find_security_critical: &mut dyn FnMut(&'a PackageId) -> Option<bool>,
find_shipped: &mut dyn FnMut(&'a PackageId) -> Option<bool>,
) -> Result<(PathBuf, ReadmeFile)> {
let crate_build_dir = get_build_dir_for_package(paths, package.name(), package.version());
let crate_vendor_dir = get_vendor_dir_for_package(paths, package.name(), package.version());
let crate_config = extra_config.per_crate_config.get(package.name());
let group = find_group(package.id());
let security_critical = find_security_critical(package.id()).unwrap_or(match group {
Group::Safe | Group::Sandbox => true,
Group::Test => false,
});
let shipped = find_shipped(package.id()).unwrap_or(match group {
Group::Safe | Group::Sandbox => true,
Group::Test => false,
});
let license = {
if let Some(config_license) = crate_config.and_then(|config| config.license.clone()) {
config_license
} else if let Some(pkg_license) = package.license() {
let license_kinds = parse_license_string(pkg_license)?;
license_kinds_to_string(&license_kinds)
} else {
return Err(format_err!(
"No license field found in Cargo.toml for {} crate",
package.name()
));
}
};
let config_license_files = crate_config.and_then(|config| {
let config_license_files = config
.license_files
.iter()
.map(Path::new)
.map(|p| crate_vendor_dir.join(p))
.collect::<Vec<_>>();
if config_license_files.is_empty() {
None
} else {
Some(config_license_files)
}
});
let license_files = if let Some(config_license_files) = config_license_files {
for path in config_license_files.iter() {
ensure!(
does_license_file_exist(path)?,
"`gnrt_config.toml` for `{crate_name}` crate listed \
a license file that doesn't actually exist: {path}",
crate_name = package.name(),
path = path.display(),
);
}
config_license_files
.into_iter()
.map(|p| format!("//{}", paths::normalize_unix_path_separator(&p)))
.collect()
} else if let Some(pkg_license) = package.license() {
let license_kinds = parse_license_string(pkg_license)?;
find_license_files_for_kinds(&license_kinds, &crate_vendor_dir)?
} else {
Vec::new()
};
if license_files.is_empty() {
bail!(
r#"License file not found for crate `{name}-{version}`.
* If a license file exists but `gnrt` can't find it under
`{crate_vendor_dir}` then,
* Specify `license_files` in `[crate.{name}]`
section of `chromium_crates_io/gnrt_config.toml`
* Or tweak the `LICENSE_KIND_TO_LICENSE_FILES` map in
`tools/crates/gnrt/lib/readme.rs` to teach `gnrt`
about alternative filenames
* If the crate didn't publish the license, then this may need
to be fixed upstream before the crate can be imported.
See also:
- https://crbug.com/369075726
- https://github.com/brendanzab/codespan/pull/355
- https://github.com/rust-lang/rustc-demangle/issues/72
- https://github.com/udoprog/relative-path/pull/60"#,
name = package.name(),
version = package.version(),
crate_vendor_dir = crate_vendor_dir.display(),
);
}
let revision = {
if let Ok(file) = std::fs::File::open(
get_vendor_dir_for_package(paths, package.name(), package.version())
.join(".cargo_vcs_info.json"),
) {
#[derive(Deserialize)]
struct VcsInfo {
git: GitInfo,
}
#[derive(Deserialize)]
struct GitInfo {
sha1: String,
}
let json: VcsInfo = serde_json::from_reader(file)?;
Some(json.git.sha1)
} else {
None
}
};
let readme = ReadmeFile {
name: package.name().to_string(),
url: format!("https://crates.io/crates/{}", package.name()),
description: package.description().unwrap_or_default().to_string(),
version: package.version().clone(),
security_critical: if security_critical { "yes" } else { "no" },
shipped: if shipped { "yes" } else { "no" },
license,
license_files,
revision,
// TODO(crbug.com/427084604): Currently all packages are rolled manually.
// Set to `UpdateMechanism::Autoroll` when automatic rolls are supported.
update_mechanism: UpdateMechanism::Manual,
};
Ok((crate_build_dir, readme))
}
/// REVIEW REQUIREMENT: When adding a new `LicenseKind`, please consult
/// `readme.rs-third-party-license-review.md`.
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Eq, Hash, PartialEq, Clone, Copy, EnumIter)]
enum LicenseKind {
/// https://spdx.org/licenses/Apache-2.0.html
Apache2,
/// https://spdx.org/licenses/BSD-3-Clause.html
BSD3,
/// https://spdx.org/licenses/MIT.html
MIT,
/// https://spdx.org/licenses/MPL-2.0.html
MPL2,
/// https://spdx.org/licenses/ISC.html
ISC,
/// https://spdx.org/licenses/Zlib.html
Zlib,
/// https://spdx.org/licenses/Unicode-3.0.html
Unicode3,
/// https://spdx.org/licenses/NCSA.html
NCSA,
/// https://spdx.org/licenses/BSL-1.0.html
BSL,
}
impl Display for LicenseKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// NOTE: The strings used below should match the SPDX "Short identifier"
// which can be found on the https://spdx.org website. (e.g. see
// https://spdx.org/licenses/Apache-2.0.html).
match self {
LicenseKind::Apache2 => write!(f, "Apache-2.0"),
LicenseKind::BSD3 => write!(f, "BSD-3-Clause"),
LicenseKind::MIT => write!(f, "MIT"),
LicenseKind::MPL2 => write!(f, "MPL-2.0"),
LicenseKind::ISC => write!(f, "ISC"),
LicenseKind::NCSA => write!(f, "NCSA"),
LicenseKind::Zlib => write!(f, "Zlib"),
LicenseKind::Unicode3 => write!(f, "Unicode-3.0"),
LicenseKind::BSL => write!(f, "BSL-1.0"),
}
}
}
/// LICENSE_STRING_TO_LICENSE_KIND, converts licenses from the format they are
/// specified in Cargo.toml files from crates.io, to the LicenseKind that will
/// be written to README.chromium.
/// Each entry looks like the following:
/// h.insert(
/// "Cargo.toml string",
/// vec![LicenseKind::<License for README.chromium>]
/// );
static LICENSE_STRING_TO_LICENSE_KIND: LazyLock<HashMap<&'static str, Vec<LicenseKind>>> =
LazyLock::new(|| {
HashMap::from([
("Apache-2.0", vec![LicenseKind::Apache2]),
("MIT OR Apache-2.0", vec![LicenseKind::Apache2]),
("MIT/Apache-2.0", vec![LicenseKind::Apache2]),
("MIT / Apache-2.0", vec![LicenseKind::Apache2]),
("Apache-2.0 / MIT", vec![LicenseKind::Apache2]),
("Apache-2.0 OR MIT", vec![LicenseKind::Apache2]),
("Apache-2.0/MIT", vec![LicenseKind::Apache2]),
("(Apache-2.0 OR MIT) AND BSD-3-Clause", vec![LicenseKind::Apache2, LicenseKind::BSD3]),
("MIT OR Apache-2.0 OR Zlib", vec![LicenseKind::Apache2]),
("(MIT OR Apache-2.0) AND NCSA", vec![LicenseKind::Apache2, LicenseKind::NCSA]),
("MIT", vec![LicenseKind::MIT]),
("MPL-2.0", vec![LicenseKind::MPL2]),
("Unlicense OR MIT", vec![LicenseKind::MIT]),
("Unlicense/MIT", vec![LicenseKind::MIT]),
("Apache-2.0 OR BSL-1.0", vec![LicenseKind::Apache2]),
("BSD-3-Clause", vec![LicenseKind::BSD3]),
("ISC", vec![LicenseKind::ISC]),
("MIT OR Zlib OR Apache-2.0", vec![LicenseKind::Apache2]),
("Zlib OR Apache-2.0 OR MIT", vec![LicenseKind::Apache2]),
("0BSD OR MIT OR Apache-2.0", vec![LicenseKind::Apache2]),
(
"(MIT OR Apache-2.0) AND Unicode-3.0",
vec![LicenseKind::Apache2, LicenseKind::Unicode3],
),
("MIT AND (MIT OR Apache-2.0)", vec![LicenseKind::Apache2]),
("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT", vec![LicenseKind::Apache2]),
("BSD-2-Clause OR Apache-2.0 OR MIT", vec![LicenseKind::Apache2]),
("BSD-2-Clause OR Apache-2.0 OR MIT", vec![LicenseKind::Apache2]),
("BSD-2-Clause OR MIT OR Apache-2.0", vec![LicenseKind::Apache2]),
("BSD-3-Clause OR MIT OR Apache-2.0", vec![LicenseKind::Apache2]),
("Unicode-3.0", vec![LicenseKind::Unicode3]),
("Zlib", vec![LicenseKind::Zlib]),
("BSL-1.0", vec![LicenseKind::BSL]),
])
});
static LICENSE_KIND_TO_LICENSE_FILES: LazyLock<HashMap<LicenseKind, Vec<String>>> =
LazyLock::new(|| {
const PREFIX: &str = "LICENSE";
const EXTENSIONS: [&str; 3] = ["", ".md", ".txt"];
// This block generates a map with the most common license file types, in order
// of priority.
let mut map = HashMap::new();
for kind in LicenseKind::iter() {
// The suffix for the license file name is taken from the Display
// implementation. E.g. "Apache-2.0" becomes "APACHE"
let license_suffix = kind.to_string().split("-").next().unwrap().to_uppercase();
let mut license_files = vec![];
// License types with the license-specific suffix are higher priority.
for ext in EXTENSIONS {
license_files.push(format!("{PREFIX}-{license_suffix}{ext}"));
}
// License types that are common to all licenses are lower priority.
for ext in EXTENSIONS {
license_files.push(format!("{PREFIX}{ext}"));
}
map.insert(kind, license_files);
}
// Special cases for specific license types. If your license has a case
// that is not covered already in the map generated above, add it here.
let apache = map.get_mut(&LicenseKind::Apache2).unwrap();
apache.insert(0, "license-apache-2.0".to_string());
apache.insert(0, "LICENSE-Apache".to_string());
map
});
/// Converts a license string from Cargo.toml into a Vec of LicenseKinds.
fn parse_license_string(pkg_license: &str) -> Result<Vec<LicenseKind>> {
LICENSE_STRING_TO_LICENSE_KIND.get(pkg_license).cloned().ok_or_else(|| {
format_err!(
"License '{pkg_license}' not found in the `LICENSE_STRING_TO_LICENSE_KIND` \
map. Please consider teaching `gnrt` about this license kind \
by editing //tools/crates/gnrt/lib/readme.rs` and adding the \
license to `enum LicenseKinds`, `LICENSE_STRING_TO_LICENSE_KIND`, \
and `LICENSE_KIND_TO_LICENSE_FILES`. Note that this will require \
an additional review whether the new license kind can be used in \
Chromium - for more details see \
`//tools/crates/gnrt/lib/readme.rs-third-party-license-review.md`."
)
})
}
/// Converts a slice of LicenseKinds into a comma-separated string.
fn license_kinds_to_string(license_kinds: &[LicenseKind]) -> String {
license_kinds.iter().join(", ")
}
/// Finds license files for the given license kinds in the crate directory.
fn find_license_files_for_kinds(
license_kinds: &[LicenseKind],
crate_vendor_dir: &Path,
) -> Result<Vec<String>> {
let mut found_files = Vec::new();
for kind in license_kinds {
// Safe to unwrap because if a LicenseKind isn't in
// LICENSE_KIND_TO_LICENSE_FILES, it's a bug in gnrt's implementation.
let possible_files = LICENSE_KIND_TO_LICENSE_FILES.get(kind).unwrap_or_else(|| {
panic!("Bug in gnrt: License kind {kind:?} not in LICENSE_KIND_TO_LICENSE_FILES")
});
// Try each possible file in priority order.
for file in possible_files {
let path = crate_vendor_dir.join(file);
if does_license_file_exist(&path)? {
let normalized_path = format!("//{}", paths::normalize_unix_path_separator(&path));
found_files.push(normalized_path);
break; // Found highest priority file for this license kind.
}
}
}
// Check for duplicates using itertools.
if found_files.iter().duplicates().count() > 0 {
bail!("Duplicate license files found: {:?}", found_files);
}
Ok(found_files)
}
fn does_license_file_exist(path: &Path) -> Result<bool> {
path.try_exists()
.with_context(|| format!("Failed to check if a license file exists at {}", path.display()))
}