// 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::crates; use crate::group::Group; use crate::paths; use anyhow::{format_err, Result}; use semver::Version; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; #[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, revision: Option, } /// 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, paths: &paths::ChromiumPaths, extra_config: &BuildConfig, mut find_group: impl FnMut(&'a cargo_metadata::PackageId) -> Group, mut find_security_critical: impl FnMut(&'a cargo_metadata::PackageId) -> Option, mut find_shipped: impl FnMut(&'a cargo_metadata::PackageId) -> Option, ) -> Result> { 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, )?; map.insert(dir, readme); } Ok(map) } pub fn readme_file_from_package<'a>( package: &'a cargo_metadata::Package, paths: &paths::ChromiumPaths, extra_config: &BuildConfig, find_group: &mut dyn FnMut(&'a cargo_metadata::PackageId) -> Group, find_security_critical: &mut dyn FnMut(&'a cargo_metadata::PackageId) -> Option, find_shipped: &mut dyn FnMut(&'a cargo_metadata::PackageId) -> Option, ) -> Result<(PathBuf, ReadmeFile)> { let epoch = crates::Epoch::from_version(&package.version); let dir = paths .third_party .join(crates::NormalizedName::from_crate_name(&package.name).to_string()) .join(epoch.to_string()); let crate_config = extra_config.per_crate_config.get(&package.name); let crate_dir = paths .third_party_cargo_root .join("vendor") .join(format!("{}-{}", package.name, package.version)); 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 { // Map to something in ALLOWED_LICENSES. if let Some(mapped_license) = ALLOWED_LICENSES .iter() .find(|(allowed_license, _)| pkg_license == *allowed_license) .map(|(_, mapped_license)| *mapped_license) { mapped_license.to_owned() } else { return Err(format_err!( "License '{}' in Cargo.toml for {} crate is not in ALLOWED_LICENSES", pkg_license, package.name, )); } } else { return Err(format_err!( "No license field found in Cargo.toml for {} crate", package.name )); } }; let path_if_exists = |path: &'a Path| -> Result> { if crate_dir.join(path).try_exists()? { Ok(Some(path)) } else { Ok(None) } }; let to_crate_dir_string = |path: &Path| -> String { format!("//{}", paths::normalize_unix_path_separator(&crate_dir.join(path))) }; let license_files: Vec = { if let Some(config_license_files) = crate_config.and_then(|config| { if config.license_files.is_empty() { None } else { Some(config.license_files.iter().map(Path::new)) } }) { config_license_files.map(to_crate_dir_string).collect() } else if let Some(file) = &package.license_file { path_if_exists(file.as_std_path())?.into_iter().map(to_crate_dir_string).collect() } else { EXPECTED_LICENSE_FILE .iter() .filter_map(|(l, path)| { if license == **l { path_if_exists(Path::new(path)).unwrap_or(None).map(to_crate_dir_string) } else { None } }) .collect() } }; if license_files.is_empty() && shipped { log::warn!( "License file not found for crate {name}.\n Crates that are \ marked `shipped` must specify a License File.\n You can specify \ the `license_files` in [crate.{name}] relative to the crate's root \ directory.", name = package.name ); } let revision = { if let Ok(file) = std::fs::File::open( paths .third_party_cargo_root .join("vendor") .join(format!("{}-{}", 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.clone(), url: format!("https://crates.io/crates/{}", package.name), description: package.description.clone().unwrap_or_default(), version: package.version.clone(), security_critical: if security_critical { "yes" } else { "no" }, shipped: if shipped { "yes" } else { "no" }, license, license_files, revision, }; Ok((dir, readme)) } // Allowed licenses, in the format they are specified in Cargo.toml files from // crates.io, and the format to write to README.chromium. static ALLOWED_LICENSES: [(&str, &str); 20] = [ // ("Cargo.toml string", "License for README.chromium") ("Apache-2.0", "Apache 2.0"), ("MIT OR Apache-2.0", "Apache 2.0"), ("MIT/Apache-2.0", "Apache 2.0"), ("MIT / Apache-2.0", "Apache 2.0"), ("Apache-2.0 / MIT", "Apache 2.0"), ("Apache-2.0 OR MIT", "Apache 2.0"), ("Apache-2.0/MIT", "Apache 2.0"), ("(Apache-2.0 OR MIT) AND BSD-3-Clause", "Apache 2.0 | BSD 3-Clause"), ("MIT OR Apache-2.0 OR Zlib", "Apache 2.0"), ("MIT", "MIT"), ("Unlicense OR MIT", "MIT"), ("Unlicense/MIT", "MIT"), ("Apache-2.0 OR BSL-1.0", "Apache 2.0"), ("BSD-3-Clause", "BSD 3-Clause"), ("ISC", "ISC"), ("MIT OR Zlib OR Apache-2.0", "Apache 2.0"), ("Zlib OR Apache-2.0 OR MIT", "Apache 2.0"), ("0BSD OR MIT OR Apache-2.0", "Apache 2.0"), ( "(MIT OR Apache-2.0) AND Unicode-DFS-2016", "Apache 2.0 AND Unicode License Agreement - Data Files and Software (2016)", ), ("Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT", "Apache 2.0"), ]; static EXPECTED_LICENSE_FILE: [(&str, &str); 15] = [ ("Apache 2.0", "LICENSE-APACHE"), ("Apache 2.0", "LICENSE-APACHE.txt"), ("Apache 2.0", "LICENSE-APACHE.md"), ("Apache 2.0", "LICENSE"), ("MIT", "LICENSE"), ("MIT", "LICENSE-MIT"), ("MIT", "LICENSE-MIT.txt"), ("MIT", "LICENSE-MIT.md"), ("BSD 3-Clause", "LICENSE"), ("BSD 3-Clause", "LICENSE-BSD"), ("BSD 3-Clause", "LICENSE-BSD.txt"), ("BSD 3-Clause", "LICENSE-BSD.md"), ("ISC", "LICENSE"), ("ISC", "LICENSE-ISC"), ("Apache 2.0 | BSD 3-Clause", "LICENSE"), ];