// 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; use anyhow::Result; 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, } /// 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, } /// 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, pub epoch: Option, pub crate_type: String, pub crate_root: String, pub sources: Vec, pub inputs: Vec, pub edition: String, pub cargo_pkg_version: String, pub cargo_pkg_authors: Option, pub cargo_pkg_name: String, pub cargo_pkg_description: Option, pub deps: Vec, pub build_deps: Vec, pub aliased_deps: Vec<(String, String)>, pub features: Vec, pub build_root: Option, pub build_script_sources: Vec, pub build_script_inputs: Vec, pub build_script_outputs: Vec, /// Data passed unchanged from gnrt_config.toml to the build file template. pub extra_kv: HashMap, /// 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, /// Packages to depend on. The build file template determines the exact name /// based on the identified package and context. packages: Vec, } /// 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, /// Extra GN code pasted literally into the build rule. pub gn_variables: Option, /// GN target visibility. pub visibility: Visibility, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum NameLibStyle { PackageName, LibLiteral, } pub fn build_file_from_std_deps<'a, 'b, Iter, GetFiles>( deps: Iter, paths: &'b paths::ChromiumPaths, extra_config: &'b BuildConfig, name_lib_style: NameLibStyle, get_files: GetFiles, ) -> Result where Iter: IntoIterator, 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_std_dep( dep, paths, get_files(&crate_id), extra_config, name_lib_style, )?) } Ok(b) } pub fn build_rule_from_std_dep( dep: &deps::Package, paths: &paths::ChromiumPaths, details: &CrateFiles, extra_config: &BuildConfig, name_lib_style: NameLibStyle, ) -> Result> { 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 = 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}__rlib"))); } } 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; 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 }; 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 = 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}"); bin_detail.sources = details .sources .iter() .map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap())) .collect(); bin_detail.inputs = details .inputs .iter() .map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap())) .collect(); // 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.sources = details .sources .iter() .map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap())) .collect(); lib_detail.inputs = details .inputs .iter() .map(|p| format!("//{}", paths.to_gn_abs_path(p).unwrap())) .collect(); 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 PackageId>(deps: &[&DepOfDep], target_name: F) -> Vec where F: Fn(&DepOfDep) -> PackageId, { let mut groups = HashMap::, 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 = 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 { 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::>() .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::>(); 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::>(); 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) => { assert_eq!(key, "target_os"); target_os_to_condition(value) } } .to_string() } fn triple_to_condition(triple: &str) -> &'static str { for (t, c) in TRIPLE_TO_GN_CONDITION { 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 TARGET_OS_TO_GN_CONDITION { if *t == target_os { return c; } } panic!("target os {target_os} not found") } static TRIPLE_TO_GN_CONDITION: &[(&str, &str)] = &[ ("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\""), ]; static TARGET_OS_TO_GN_CONDITION: &[(&str, &str)] = &[ ("android", "is_android"), ("darwin", "is_mac"), ("fuchsia", "is_fuchsia"), ("ios", "is_ios"), ("linux", "is_linux || is_chromeos"), ("windows", "is_win"), ]; #[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)" ); } }