// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! GN build file generation.
use crate::config::BuildConfig;
use crate::crates::CrateFiles;
use crate::crates::{Epoch, NormalizedName, VendoredCrate, Visibility};
use crate::deps::{self, DepOfDep};
use crate::group::Group;
use crate::paths;
use crate::platforms;
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use anyhow::{bail, Result};
use itertools::Itertools;
use serde::Serialize;
/// Describes a BUILD.gn file for a single crate epoch. Each file may have
/// multiple rules, including:
/// * A :lib target for normal dependents
/// * A :test_support target for first-party testonly dependents
/// * A :cargo_tests_support target for building third-party tests
/// * A :buildrs_support target for third-party build script dependents
/// * Binary targets for crate executables
#[derive(Default, Serialize)]
pub struct BuildFile {
pub rules: Vec<Rule>,
}
/// Identifies a package version. A package's dependency list uses this to refer
/// to other targets.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct PackageId {
/// Package name in normalized form, as used in GN target and path names.
pub name: String,
/// Package epoch if relevant (i.e. when needed as part of target paths).
pub epoch: Option<String>,
}
/// Defines what other GN targets can depend on this one.
#[derive(Debug, Default, Serialize)]
pub struct GnVisibility {
pub testonly: bool,
/// Controls the visibility constraint on the GN target. If this is true, no
/// visibility constraint is generated. If false, it's defined so that only
/// other third party Rust crates can depend on this target.
pub public: bool,
}
/// A GN rule in a generated build file.
#[derive(Debug, Serialize)]
pub struct Rule {
/// The GN rule name, which can be unrelated to the Cargo package name.
pub name: String,
pub gn_visibility: GnVisibility,
pub detail: RuleDetail,
}
/// A concrete build rule. Refer to //build/rust/cargo_crate.gni for fields
/// undocumented here.
#[derive(Clone, Debug, Default, Serialize)]
pub struct RuleDetail {
pub crate_name: Option<String>,
pub epoch: Option<Epoch>,
pub crate_type: String,
pub crate_root: String,
pub sources: Vec<String>,
pub inputs: Vec<String>,
pub edition: String,
pub cargo_pkg_version: String,
pub cargo_pkg_authors: Option<String>,
pub cargo_pkg_name: String,
pub cargo_pkg_description: Option<String>,
pub deps: Vec<DepGroup>,
pub build_deps: Vec<DepGroup>,
pub aliased_deps: Vec<(String, String)>,
pub features: Vec<String>,
pub build_root: Option<String>,
pub build_script_sources: Vec<String>,
pub build_script_inputs: Vec<String>,
pub build_script_outputs: Vec<String>,
pub native_libs: Vec<String>,
/// Data passed unchanged from gnrt_config.toml to the build file template.
pub extra_kv: HashMap<String, serde_json::Value>,
/// Whether this rule depends on the main lib target in its group (e.g. a
/// bin target alongside a lib inside a package).
pub dep_on_lib: bool,
}
/// Set of rule dependencies with a shared condition.
#[derive(Clone, Debug, Serialize)]
pub struct DepGroup {
/// `if` condition for GN, or `None` for unconditional deps.
cond: Option<Condition>,
/// Packages to depend on. The build file template determines the exact name
/// based on the identified package and context.
packages: Vec<PackageId>,
}
/// Extra metadata influencing GN output for a particular crate.
#[derive(Clone, Debug, Default)]
pub struct PerCrateMetadata {
/// Names of files the build.rs script may output.
pub build_script_outputs: Vec<String>,
/// Extra GN code pasted literally into the build rule.
pub gn_variables: Option<String>,
/// GN target visibility.
pub visibility: Visibility,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NameLibStyle {
PackageName,
LibLiteral,
}
pub fn build_file_from_deps<'a, 'b, Iter, GetFiles>(
deps: Iter,
paths: &'b paths::ChromiumPaths,
extra_config: &'b BuildConfig,
name_lib_style: NameLibStyle,
get_files: GetFiles,
) -> Result<BuildFile>
where
Iter: IntoIterator<Item = &'a deps::Package>,
GetFiles: Fn(&VendoredCrate) -> &'b CrateFiles,
{
let mut b = BuildFile { rules: Vec::new() };
for dep in deps {
let crate_id = dep.crate_id();
b.rules.extend(build_rule_from_dep(
dep,
paths,
get_files(&crate_id),
extra_config,
name_lib_style,
)?)
}
Ok(b)
}
pub fn build_rule_from_dep(
dep: &deps::Package,
paths: &paths::ChromiumPaths,
details: &CrateFiles,
extra_config: &BuildConfig,
name_lib_style: NameLibStyle,
) -> Result<Vec<Rule>> {
let cargo_pkg_authors =
if dep.authors.is_empty() { None } else { Some(dep.authors.join(", ")) };
let per_crate_config = extra_config.per_crate_config.get(&*dep.package_name);
let normalized_crate_name = NormalizedName::from_crate_name(&dep.package_name);
let crate_epoch = Epoch::from_version(&dep.version);
// Get deps to exclude from resolved deps.
let exclude_deps: Vec<String> = per_crate_config
.iter()
.flat_map(|c| &c.exclude_deps_in_gn)
.chain(&extra_config.all_config.exclude_deps_in_gn)
.cloned()
.collect();
// Get the config's extra (key, value) pairs, which are passed as-is to the
// build file template engine.
let mut extra_kv = extra_config.all_config.extra_kv.clone();
if let Some(per_crate) = per_crate_config {
extra_kv.extend(per_crate.extra_kv.iter().map(|(k, v)| (k.clone(), v.clone())));
}
let allow_first_party_usage = match extra_kv.get("allow_first_party_usage") {
Some(serde_json::Value::Bool(b)) => *b,
_ => dep.is_toplevel_dep,
};
let mut detail_template = RuleDetail {
edition: dep.edition.clone(),
cargo_pkg_version: dep.version.to_string(),
cargo_pkg_authors,
cargo_pkg_name: dep.package_name.to_string(),
cargo_pkg_description: dep.description.as_ref().map(|s| s.trim_end().to_string()),
extra_kv,
..Default::default()
};
// Add only normal and build dependencies: we don't run unit tests.
let normal_deps: Vec<&DepOfDep> = dep
.dependencies
.iter()
.filter(|d| !exclude_deps.iter().any(|e| e.as_str() == &*d.package_name))
.collect();
let build_deps: Vec<&DepOfDep> = dep
.build_dependencies
.iter()
.filter(|d| !exclude_deps.iter().any(|e| e.as_str() == &*d.package_name))
.collect();
let aliased_normal_deps = {
let mut aliases = Vec::new();
for dep in &normal_deps {
let target_name = NormalizedName::from_crate_name(&dep.package_name).to_string();
if target_name != dep.use_name {
aliases.push((dep.use_name.clone(), format!(":{target_name}")));
}
}
aliases.sort_unstable();
aliases.dedup();
aliases
};
// TODO(danakj): There is no support for `aliased_build_deps` in the
// `cargo_crate` GN template as there's been no usage needed. So we don't
// compute it here.
// Group the dependencies by condition, where the unconditional deps come
// first.
detail_template.deps = group_deps(&normal_deps, |d| PackageId {
name: NormalizedName::from_crate_name(&d.package_name).to_string(),
epoch: match name_lib_style {
// TODO(danakj): Separate this choice to another parameter option.
NameLibStyle::LibLiteral => Some(Epoch::from_version(&d.version).to_string()),
NameLibStyle::PackageName => None,
},
});
detail_template.build_deps = group_deps(&build_deps, |d| PackageId {
name: NormalizedName::from_crate_name(&d.package_name).to_string(),
epoch: match name_lib_style {
// TODO(danakj): Separate this choice to another parameter option.
NameLibStyle::LibLiteral => Some(Epoch::from_version(&d.version).to_string()),
NameLibStyle::PackageName => None,
},
});
detail_template.aliased_deps = aliased_normal_deps;
detail_template.sources =
details.sources.iter().map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap())).collect();
detail_template.inputs =
details.inputs.iter().map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap())).collect();
detail_template.native_libs = details
.native_libs
.iter()
.map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap()))
.collect();
let requested_features_for_normal = {
let mut features = dep
.dependency_kinds
.get(&deps::DependencyKind::Normal)
.map(|per_kind_info| per_kind_info.features.clone())
.unwrap_or_default();
features.sort_unstable();
features.dedup();
features
};
let requested_features_for_build = {
let mut features = dep
.dependency_kinds
.get(&deps::DependencyKind::Build)
.map(|per_kind_info| per_kind_info.features.clone())
.unwrap_or_default();
features.sort_unstable();
features.dedup();
features
};
let unexpected_features: Vec<&str> = {
let banned_features =
extra_config.get_combined_set(&*dep.package_name, |cfg| &cfg.ban_features);
let mut actual_features = HashSet::new();
actual_features.extend(requested_features_for_normal.iter().map(Deref::deref));
actual_features.extend(requested_features_for_build.iter().map(Deref::deref));
banned_features.intersection(&actual_features).map(|s| *s).sorted_unstable().collect()
};
if !unexpected_features.is_empty() {
bail!(
"The following crate features are enabled in crate `{}` \
despite being listed in `ban_features`: {}",
&*dep.package_name,
unexpected_features.iter().map(|f| format!("`{f}`")).join(", "),
);
}
if !per_crate_config.map(|config| config.remove_build_rs).unwrap_or(false) {
let build_script_from_src =
dep.build_script.as_ref().map(|p| paths.to_gn_abs_path(p).unwrap());
detail_template.build_root = build_script_from_src.as_ref().map(|p| format!("//{p}"));
detail_template.build_script_sources = build_script_from_src
.as_ref()
.map(|p| format!("//{p}"))
.into_iter()
.chain(
details
.build_script_sources
.iter()
.map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap())),
)
.collect();
detail_template.build_script_inputs = details
.build_script_inputs
.iter()
.map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap()))
.collect();
detail_template.build_script_outputs =
if let Some(outs) = per_crate_config.map(|config| &config.build_script_outputs) {
outs.iter().map(|path| path.display().to_string()).collect()
} else {
vec![]
};
}
let mut rules: Vec<Rule> = Vec::new();
// Generate rules for each binary the package provides.
for bin_target in &dep.bin_targets {
let bin_root_from_src = paths.to_gn_abs_path(&bin_target.root).unwrap();
let mut bin_detail = detail_template.clone();
bin_detail.crate_type = "bin".to_string();
bin_detail.crate_root = format!("//{bin_root_from_src}");
// Bins are not part of a build script, so they don't need build-script
// deps, only normal deps.
bin_detail.features = requested_features_for_normal.clone();
if dep.lib_target.is_some() {
bin_detail.dep_on_lib = true;
if bin_detail.deps.is_empty() {
bin_detail.deps.push(DepGroup { cond: None, packages: Vec::new() });
}
}
rules.push(Rule {
name: NormalizedName::from_crate_name(&bin_target.name).to_string(),
gn_visibility: GnVisibility { testonly: dep.group == Group::Test, public: true },
detail: bin_detail,
});
}
// Generate the rule for the main library target, if it exists.
if let Some(lib_target) = &dep.lib_target {
use deps::DependencyKind::*;
let lib_root_from_src = paths.to_gn_abs_path(&lib_target.root).unwrap();
// Generate the rules for each dependency kind. We use a stable
// order instead of the hashmap iteration order.
for dep_kind in [Normal, Build] {
if dep.dependency_kinds.get(&dep_kind).is_none() {
continue;
}
let lib_rule_name: String = match &dep_kind {
deps::DependencyKind::Normal => match name_lib_style {
NameLibStyle::PackageName => normalized_crate_name.to_string(),
NameLibStyle::LibLiteral => "lib".to_string(),
},
deps::DependencyKind::Build => "buildrs_support".to_string(),
_ => unreachable!(),
};
let (crate_name, epoch) = match name_lib_style {
NameLibStyle::PackageName => (None, None),
NameLibStyle::LibLiteral => {
(Some(normalized_crate_name.to_string()), Some(crate_epoch))
}
};
let crate_type = {
// The stdlib is a "dylib" crate but we only want rlibs.
let t = lib_target.lib_type.to_string();
if t == "dylib" { "rlib".to_string() } else { t }
};
let mut lib_detail = detail_template.clone();
lib_detail.crate_name = crate_name;
lib_detail.epoch = epoch;
lib_detail.crate_type = crate_type;
lib_detail.crate_root = format!("//{lib_root_from_src}");
lib_detail.features = match &dep_kind {
Normal => requested_features_for_normal.clone(),
Build => requested_features_for_build.clone(),
_ => unreachable!(), // The for loop here is over [Normal, Build].
};
// TODO(danakj): Crates in the 'sandbox' group should have their
// visibility restructed in some way. Possibly to an allowlist
// specified in the crate's config, and reviewed by security folks?
rules.push(Rule {
name: lib_rule_name.clone(),
gn_visibility: GnVisibility {
testonly: dep.group == Group::Test,
public: allow_first_party_usage,
},
detail: lib_detail,
});
}
}
Ok(rules)
}
/// Group dependencies by condition, with unconditional deps first.
///
/// If the returned list is non-empty, it will always have a group without a
/// condition, even if that group is empty. If there are no dependencies, then
/// the returned list is empty.
fn group_deps<F: Fn(&DepOfDep) -> PackageId>(deps: &[&DepOfDep], target_name: F) -> Vec<DepGroup>
where
F: Fn(&DepOfDep) -> PackageId,
{
let mut groups = HashMap::<Option<Condition>, Vec<_>>::new();
for dep in deps {
let cond = dep.platform.as_ref().map(platform_to_condition);
groups.entry(cond).or_default().push(target_name(dep));
}
if !groups.is_empty() {
groups.entry(None).or_default();
}
let mut groups: Vec<DepGroup> =
groups.into_iter().map(|(cond, rules)| DepGroup { cond, packages: rules }).collect();
for group in groups.iter_mut() {
group.packages.sort_unstable();
}
groups.sort_unstable_by(|l, r| l.cond.cmp(&r.cond));
groups
}
/// Describes a condition for some GN declaration.
#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Condition(pub String);
impl Condition {
pub fn from_platform_set(platforms: platforms::PlatformSet) -> Option<Self> {
let platforms = match platforms {
platforms::PlatformSet::All => return None,
platforms::PlatformSet::Platforms(platforms) => platforms,
};
Some(Condition(
platforms
.iter()
.map(|p| format!("({})", platform_to_condition(p).0))
.collect::<Vec<_>>()
.join(" || "),
))
}
}
/// Map a cargo `Platform` constraint to a GN conditional expression.
pub fn platform_to_condition(platform: &platforms::Platform) -> Condition {
Condition(match platform {
platforms::Platform::Name(triple) => triple_to_condition(triple).to_string(),
platforms::Platform::Cfg(cfg_expr) => cfg_expr_to_condition(cfg_expr),
})
}
pub fn cfg_expr_to_condition(cfg_expr: &cargo_platform::CfgExpr) -> String {
match cfg_expr {
cargo_platform::CfgExpr::Not(expr) => {
format!("!({})", cfg_expr_to_condition(expr))
}
cargo_platform::CfgExpr::All(exprs) => {
let mut conds = exprs
.iter()
.map(|expr| format!("({})", cfg_expr_to_condition(expr)))
.collect::<Vec<String>>();
conds.sort();
conds.dedup();
conds.join(" && ")
}
cargo_platform::CfgExpr::Any(exprs) => {
let mut conds = exprs
.iter()
.map(|expr| format!("({})", cfg_expr_to_condition(expr)))
.collect::<Vec<String>>();
conds.sort();
conds.dedup();
conds.join(" || ")
}
cargo_platform::CfgExpr::Value(cfg) => cfg_to_condition(cfg),
}
}
pub fn cfg_to_condition(cfg: &cargo_platform::Cfg) -> String {
match cfg {
cargo_platform::Cfg::Name(name) => match name.as_str() {
// Note that while Fuchsia is not a unix, rustc sets the unix cfg
// anyway. We must be consistent with rustc. This may change with
// https://github.com/rust-lang/rust/issues/58590
"unix" => "!is_win",
"windows" => "is_win",
_ => unreachable!(),
},
cargo_platform::Cfg::KeyPair(key, value) => match key.as_ref() {
"target_os" => target_os_to_condition(value),
"target_arch" => target_arch_to_condition(value),
_ => unreachable!("unknown key in cargo_platform::Cfg"),
},
}
.to_string()
}
fn triple_to_condition(triple: &str) -> &'static str {
for (t, c) in &[
("i686-linux-android", "is_android && target_cpu == \"x86\""),
("x86_64-linux-android", "is_android && target_cpu == \"x64\""),
("armv7-linux-android", "is_android && target_cpu == \"arm\""),
("aarch64-linux-android", "is_android && target_cpu == \"arm64\""),
("aarch64-fuchsia", "is_fuchsia && target_cpu == \"arm64\""),
("x86_64-fuchsia", "is_fuchsia && target_cpu == \"x64\""),
("aarch64-apple-ios", "is_ios && target_cpu == \"arm64\""),
("armv7-apple-ios", "is_ios && target_cpu == \"arm\""),
("x86_64-apple-ios", "is_ios && target_cpu == \"x64\""),
("i386-apple-ios", "is_ios && target_cpu == \"x86\""),
("i686-pc-windows-msvc", "is_win && target_cpu == \"x86\""),
("x86_64-pc-windows-msvc", "is_win && target_cpu == \"x64\""),
("i686-unknown-linux-gnu", "(is_linux || is_chromeos) && target_cpu == \"x86\""),
("x86_64-unknown-linux-gnu", "(is_linux || is_chromeos) && target_cpu == \"x64\""),
("x86_64-apple-darwin", "is_mac && target_cpu == \"x64\""),
("aarch64-apple-darwin", "is_mac && target_cpu == \"arm64\""),
] {
if *t == triple {
return c;
}
}
panic!("target triple {triple} not found")
}
fn target_os_to_condition(target_os: &str) -> &'static str {
for (t, c) in &[
("android", "is_android"),
("darwin", "is_mac"),
("fuchsia", "is_fuchsia"),
("ios", "is_ios"),
("linux", "is_linux || is_chromeos"),
("windows", "is_win"),
] {
if *t == target_os {
return c;
}
}
panic!("target os {target_os} not found")
}
fn target_arch_to_condition(target_arch: &str) -> &'static str {
for (t, c) in &[
("aarch64", "target_cpu == \"arm64\""),
("arm", "target_cpu == \"arm\""),
("x86", "target_cpu == \"x86\""),
("x86_64", "target_cpu == \"x64\""),
] {
if *t == target_arch {
return c;
}
}
panic!("target arch {target_arch} not found")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_to_condition() {
use crate::platforms::{Platform, PlatformSet};
use cargo_platform::CfgExpr;
use std::str::FromStr;
// Try an unconditional filter.
assert_eq!(Condition::from_platform_set(PlatformSet::one(None)), None);
// Try a target triple.
assert_eq!(
Condition::from_platform_set(PlatformSet::one(Some(Platform::Name(
"x86_64-pc-windows-msvc".to_string()
))))
.unwrap()
.0,
"(is_win && target_cpu == \"x64\")"
);
// Try a cfg expression.
assert_eq!(
Condition::from_platform_set(PlatformSet::one(Some(Platform::Cfg(
CfgExpr::from_str("any(windows, target_os = \"android\")").unwrap()
))))
.unwrap()
.0,
"((is_android) || (is_win))"
);
// Redundant cfg expression.
assert_eq!(
Condition::from_platform_set(PlatformSet::one(Some(Platform::Cfg(
CfgExpr::from_str("any(windows, windows)").unwrap()
))))
.unwrap()
.0,
"((is_win))"
);
// Try a PlatformSet with multiple filters.
let mut platform_set = PlatformSet::empty();
platform_set.add(Some(Platform::Name("armv7-linux-android".to_string())));
platform_set.add(Some(Platform::Cfg(CfgExpr::from_str("windows").unwrap())));
assert_eq!(
Condition::from_platform_set(platform_set).unwrap().0,
"(is_android && target_cpu == \"arm\") || (is_win)"
);
// A cfg expression on arch only.
assert_eq!(
Condition::from_platform_set(PlatformSet::one(Some(Platform::Cfg(
CfgExpr::from_str("target_arch = \"aarch64\"").unwrap()
))))
.unwrap()
.0,
"(target_cpu == \"arm64\")"
);
// A cfg expression on arch and OS (but not via the target triple string).
assert_eq!(
Condition::from_platform_set(PlatformSet::one(Some(Platform::Cfg(
CfgExpr::from_str("all(target_arch = \"aarch64\", unix)").unwrap()
))))
.unwrap()
.0,
"((!is_win) && (target_cpu == \"arm64\"))"
);
}
}