// 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::crates::*; use crate::deps; use crate::manifest::CargoPackage; use crate::paths; use crate::platforms; use std::collections::{HashMap, HashSet}; use std::convert::From; use std::fmt; use std::path::Path; /// 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 testonly dependents /// * A :buildrs_support target for build script dependents /// * Binary targets for crate executables pub struct BuildFile { pub rules: Vec<(String, Rule)>, } impl BuildFile { /// Return a `fmt::Display` instance for the build file. Formatting this /// will write an entire valid BUILD.gn file. pub fn display(&self) -> impl '_ + fmt::Display { BuildFileFormatter { build_file: self } } } /// Describes a single GN build rule for a crate configuration. Each field /// corresponds directly to a argument to the `cargo_crate()` template defined /// in build/rust/cargo_crate.gni. /// /// For undocumented fields, refer to the docs in the above file. #[derive(Clone, Debug)] pub struct Rule { pub crate_name: Option, pub epoch: Option, pub crate_type: String, pub testonly: bool, pub crate_root: String, 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 dev_deps: Vec, pub build_deps: Vec, pub features: Vec, pub build_root: Option, pub build_script_outputs: Vec, /// 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_visibility: bool, } /// A (possibly conditional) dependency on another GN rule. /// /// Has an `Ord` instance based on an arbitrary ordering of `Condition`s so that /// `RuleDep`s can be easily grouped by condition. Unconditional dependencies /// are always ordered first #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct RuleDep { cond: Condition, rule: String, } impl RuleDep { pub fn construct_for_testing(cond: Condition, rule: String) -> RuleDep { RuleDep { cond, rule } } } /// Generate `BuildFile` descriptions for each third party crate in the /// dependency graph. /// /// * `deps` is the result of dependency resolution from the `deps` module. /// * `metadata` contains the package metadata for each third party crate. /// * `build_script_outputs` is the list of files generated by the build.rs /// script for each package. /// * `pub_deps` is the list of packages that should be available outside of /// third-party code. pub fn build_files_from_deps<'a, 'b, Iter: IntoIterator>( deps: Iter, paths: &'b paths::ChromiumPaths, metadata: &HashMap, build_script_outputs: &HashMap>, pub_deps: &HashSet, ) -> HashMap { deps.into_iter() .filter_map(|dep| { make_build_file_for_dep(dep, paths, metadata, build_script_outputs, pub_deps) }) .collect() } /// Generate the `BuildFile` for `dep`, or return `None` if no rules would be /// present. fn make_build_file_for_dep( dep: &deps::Package, paths: &paths::ChromiumPaths, metadata: &HashMap, build_script_outputs: &HashMap>, pub_deps: &HashSet, ) -> Option<(ChromiumVendoredCrate, BuildFile)> { let third_party_path_str = paths.third_party.to_str().unwrap(); let crate_id = dep.third_party_crate_id(); let crate_abs_path = paths.root.join(paths.third_party.join(crate_id.build_path())); let to_gn_path = |abs_path: &Path| { abs_path.strip_prefix(&crate_abs_path).unwrap().to_string_lossy().into_owned() }; let package_metadata = metadata.get(&crate_id).unwrap(); let cargo_pkg_description = package_metadata.description.clone(); let cargo_pkg_authors = if package_metadata.authors.is_empty() { None } else { Some(package_metadata.authors.join(", ")) }; // Template for all the rules in a build file. Several fields are // the same for all a package's rules. let mut rule_template = Rule { crate_name: None, epoch: None, crate_type: String::new(), testonly: false, crate_root: String::new(), edition: package_metadata.edition.0.clone(), cargo_pkg_version: package_metadata.version.to_string(), cargo_pkg_authors: cargo_pkg_authors, cargo_pkg_name: package_metadata.name.clone(), cargo_pkg_description, deps: Vec::new(), dev_deps: Vec::new(), build_deps: Vec::new(), features: Vec::new(), build_root: dep.build_script.as_ref().map(|p| to_gn_path(p.as_path())), build_script_outputs: build_script_outputs.get(&crate_id).cloned().unwrap_or_default(), public_visibility: pub_deps.contains(&crate_id), }; // Enumerate the dependencies of each kind for the package. for (target_name, gn_deps, cargo_deps) in [ ("lib", &mut rule_template.deps, &dep.dependencies), ("lib", &mut rule_template.dev_deps, &dep.dev_dependencies), ("buildrs_support", &mut rule_template.build_deps, &dep.build_dependencies), ] { for dep_of_dep in cargo_deps { let cond = match &dep_of_dep.platform { None => Condition::Always, Some(p) => Condition::If(platform_to_condition(p)), }; let crate_id = dep_of_dep.third_party_crate_id(); let normalized_name = crate_id.normalized_name(); let epoch = crate_id.epoch; let rule = format!("//{third_party_path_str}/{normalized_name}/{epoch}:{target_name}"); gn_deps.push(RuleDep { cond, rule }); } } let mut rules: Vec<(String, Rule)> = Vec::new(); // Generate rules for each binary the package provides. for bin_target in &dep.bin_targets { let mut bin_rule = rule_template.clone(); bin_rule.crate_type = "bin".to_string(); bin_rule.crate_root = to_gn_path(bin_target.root.as_path()); bin_rule.features = match dep.dependency_kinds.get(&deps::DependencyKind::Normal) { Some(per_kind_info) => per_kind_info.features.clone(), // As a hack, fill in empty feature set. This happens // because binary-only workspace members aren't the target // of any edge in the dependency graph: so, they have no // requested features. // // TODO(crbug.com/1291994): find a way to specify features // for these deps in third_party.toml. None => Vec::new(), }; if dep.lib_target.is_some() { bin_rule.deps.push(RuleDep { cond: Condition::Always, rule: ":lib".to_string() }); } rules.push((NormalizedName::from_crate_name(&bin_target.name).to_string(), bin_rule)); } // Generate the rule for the main library target, if it exists. if let Some(lib_target) = &dep.lib_target { use deps::DependencyKind::*; // Generate the rules for each dependency kind. We use a stable // order instead of the hashmap iteration order. for dep_kind in [Normal, Build, Development] { let per_kind_info = match dep.dependency_kinds.get(&dep_kind) { Some(x) => x, None => continue, }; let rule_name = match dep_kind { deps::DependencyKind::Normal => "lib", deps::DependencyKind::Development => "test_support", deps::DependencyKind::Build => "buildrs_support", _ => unreachable!(), } .to_string(); let mut lib_rule = rule_template.clone(); lib_rule.crate_name = Some(crate_id.normalized_name().to_string()); lib_rule.epoch = Some(crate_id.epoch); lib_rule.crate_type = lib_target.lib_type.to_string(); lib_rule.testonly = dep_kind == deps::DependencyKind::Development; lib_rule.crate_root = to_gn_path(lib_target.root.as_path()); lib_rule.features = per_kind_info.features.clone(); rules.push((rule_name, lib_rule)); } } if rules.is_empty() { None } else { Some((crate_id, BuildFile { rules })) } } /// `BuildFile` wrapper with a `Display` impl. Displays the `BuildFile` as a GN /// file. struct BuildFileFormatter<'a> { build_file: &'a BuildFile, } impl<'a> fmt::Display for BuildFileFormatter<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write_build_file(f, self.build_file) } } fn write_build_file(mut writer: W, build_file: &BuildFile) -> fmt::Result { writeln!(writer, "{COPYRIGHT_HEADER}\n")?; writeln!(writer, r#"import("//build/rust/cargo_crate.gni")"#)?; writeln!(writer, "")?; for (name, rule) in &build_file.rules { // Don't use writeln!, each rule adds a trailing newline. write!(writer, "{}", RuleFormatter { name: &name, rule: &rule })?; } Ok(()) } /// `Rule` wrapper with a `Display` impl. Displays the `Rule` as a GN rule. struct RuleFormatter<'a> { name: &'a str, rule: &'a Rule, } impl<'a> fmt::Display for RuleFormatter<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write_rule(f, self.name, self.rule) } } fn write_rule(mut writer: W, name: &str, rule: &Rule) -> fmt::Result { writeln!(writer, "cargo_crate(\"{name}\") {{")?; if let Some(name) = &rule.crate_name { writeln!(writer, "crate_name = \"{name}\"")?; } if let Some(epoch) = rule.epoch { writeln!(writer, "epoch = \"{}\"", epoch.to_version_string())?; } writeln!(writer, "crate_type = \"{}\"", rule.crate_type)?; if rule.testonly { writeln!(writer, "testonly = true")?; } if !rule.public_visibility { writeln!(writer, "\n{VISIBILITY_CONSTRAINT}")?; } writeln!(writer, "crate_root = \"{}\"", rule.crate_root)?; // TODO(crbug.com/1291994): actually support unit test generation. writeln!(writer, "\n# Unit tests skipped. Generate with --with-tests to include them.")?; writeln!(writer, "build_native_rust_unit_tests = false")?; writeln!(writer, "sources = [ \"{}\" ]", rule.crate_root)?; writeln!(writer, "edition = \"{}\"", rule.edition)?; writeln!(writer, "cargo_pkg_version = \"{}\"", rule.cargo_pkg_version)?; if let Some(authors) = &rule.cargo_pkg_authors { writeln!(writer, "cargo_pkg_authors = \"{authors}\"")?; } writeln!(writer, "cargo_pkg_name = \"{}\"", rule.cargo_pkg_name)?; if let Some(description) = &rule.cargo_pkg_description { write!(writer, "cargo_pkg_description = \"")?; write_str_escaped(&mut writer, description)?; writeln!(writer, "\"")?; } if !rule.deps.is_empty() { write_deps(&mut writer, "deps", rule.deps.clone())?; } if !rule.build_deps.is_empty() { write_deps(&mut writer, "build_deps", rule.build_deps.clone())?; } if !rule.features.is_empty() { write!(writer, "features = ")?; write_list(&mut writer, &rule.features)?; } if let Some(build_root) = &rule.build_root { writeln!(writer, "build_root = \"{build_root}\"")?; writeln!(writer, "build_sources = [ \"{build_root}\" ]")?; if !rule.build_script_outputs.is_empty() { write!(writer, "build_script_outputs = ")?; write_list(&mut writer, &rule.build_script_outputs)?; } } writeln!(writer, "}}") } fn write_deps(mut writer: W, kind: &str, mut deps: Vec) -> fmt::Result { // Group dependencies by platform condition via sorting. deps.sort(); // Get the index of the first non-conditional dependency. This may be 0. let unconditional_end = deps.partition_point(|dep| dep.cond == Condition::Always); // Write the unconditional deps. Or, if there are none, but there are // conditional deps, write "deps = []". if !deps.is_empty() { write!(writer, "{kind} = ")?; write_list(&mut writer, deps[..unconditional_end].iter().map(|dep| &dep.rule))?; } // Loop through the groups of deps by condition, writing the lists wrapped // in "if () { }" blocks. let mut tail = &deps[unconditional_end..]; while !tail.is_empty() { let RuleDep { cond: group_cond, rule: _ } = &tail[0]; let cond_end = tail.partition_point(|dep| dep.cond == *group_cond); let group = &tail[..cond_end]; let if_expr = match group_cond { Condition::Always => unreachable!(), Condition::If(string) => string, }; write!(writer, "if ({if_expr}) {{\n{kind} += ")?; write_list(&mut writer, group.iter().map(|dep| &dep.rule))?; writeln!(writer, "}}")?; tail = &tail[cond_end..]; } Ok(()) } fn write_list>( mut writer: W, items: I, ) -> fmt::Result { writeln!(writer, "[")?; for item in items.into_iter() { writeln!(writer, "\"{item}\",")?; } writeln!(writer, "]") } fn write_str_escaped(mut writer: W, s: &str) -> fmt::Result { // This escaping isn't entirely correct; it misses some characters that // should be escaped and unnecessarily changes " to '. See // https://gn.googlesource.com/gn/+/main/docs/language.md#strings // // We keep the crates.py behavior for now to keep build file output as // similar as possible. // // TODO(https://crbug.com/1291994): do escaping as specified in GN docs. for c in s.chars() { let mut buf = [0u8; 4]; let s = match c { // Skip newlines to match crates.py behavior. '\n' => continue, '"' => r#"'"#, c => c.encode_utf8(&mut buf), }; writer.write_str(s)?; } Ok(()) } pub fn write_str_escaped_for_testing(writer: W, s: &str) -> fmt::Result { write_str_escaped(writer, s) } /// Describes a condition for some GN declaration. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] pub enum Condition { /// The associated GN declarations are unconditional: they will not be /// wrapped in an if condition. Always, /// The association GN declaration is wrapped in an if condition. The /// string is the conditional expression. If(String), } impl Condition { /// Get the conditional expression, or `None` if it's unconditional. pub fn get_if(&self) -> Option<&str> { match self { Condition::If(cond) => Some(cond), _ => None, } } } impl From for Condition { fn from(platform_set: platforms::PlatformSet) -> Self { let platforms = match platform_set { platforms::PlatformSet::All => return Condition::Always, platforms::PlatformSet::Platforms(platforms) => platforms, }; Condition::If( platforms .iter() .map(|platform| format!("({})", platform_to_condition(platform))) .collect::>() .join(" || "), ) } } /// Map a cargo `Platform` constraint to a GN conditional expression. pub fn platform_to_condition(platform: &platforms::Platform) -> String { 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) => exprs .iter() .map(|expr| format!("({})", cfg_expr_to_condition(expr))) .collect::>() .join(" && "), cargo_platform::CfgExpr::Any(exprs) => exprs .iter() .map(|expr| format!("({})", cfg_expr_to_condition(expr))) .collect::>() .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: &'static [(&'static str, &'static 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: &'static [(&'static str, &'static str)] = &[ ("android", "is_android"), ("darwin", "is_mac"), ("fuchsia", "is_fuchsia"), ("ios", "is_ios"), ("linux", "is_linux || is_chromeos"), ("windows", "is_win"), ]; static COPYRIGHT_HEADER: &'static str = "# 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."; static VISIBILITY_CONSTRAINT: &'static str = "# Only for usage from third-party crates. Add the crate to # third_party.toml to use it from first-party code. visibility = [ \"//third_party/rust/*\" ]";