// 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<String>,
revision: Option<String>,
}
/// 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 = &'a cargo_metadata::Package>,
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<bool>,
mut find_shipped: impl FnMut(&'a cargo_metadata::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,
)?;
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<bool>,
find_shipped: &mut dyn FnMut(&'a cargo_metadata::PackageId) -> Option<bool>,
) -> 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<Option<&'a Path>> {
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<String> = {
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); 21] = [
// ("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"),
("BSD-2-Clause OR Apache-2.0 OR MIT", "Apache 2.0"),
];
static EXPECTED_LICENSE_FILE: [(&str, &str); 20] = [
("Apache 2.0", "LICENSE"),
("Apache 2.0", "LICENSE.md"),
("Apache 2.0", "LICENSE-APACHE"),
("Apache 2.0", "LICENSE-APACHE.txt"),
("Apache 2.0", "LICENSE-APACHE.md"),
("MIT", "LICENSE"),
("MIT", "LICENSE.md"),
("MIT", "LICENSE-MIT"),
("MIT", "LICENSE-MIT.txt"),
("MIT", "LICENSE-MIT.md"),
("BSD 3-Clause", "LICENSE"),
("BSD 3-Clause", "LICENSE.md"),
("BSD 3-Clause", "LICENSE-BSD"),
("BSD 3-Clause", "LICENSE-BSD.txt"),
("BSD 3-Clause", "LICENSE-BSD.md"),
("ISC", "LICENSE"),
("ISC", "LICENSE.md"),
("ISC", "LICENSE-ISC"),
("Apache 2.0 | BSD 3-Clause", "LICENSE"),
("Apache 2.0 | BSD 3-Clause", "LICENSE.md"),
];