blob: a36eacf2aa33ac4afe841b0a479d7a37ce749a5f [file] [log] [blame]
// 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<String>,
pub epoch: Option<Epoch>,
pub crate_type: String,
pub testonly: bool,
pub crate_root: 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<RuleDep>,
pub dev_deps: Vec<RuleDep>,
pub build_deps: Vec<RuleDep>,
pub features: Vec<String>,
pub build_root: Option<String>,
pub build_script_outputs: Vec<String>,
/// 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<Item = &'a deps::ThirdPartyDep>>(
deps: Iter,
paths: &'b paths::ChromiumPaths,
metadata: &HashMap<ThirdPartyCrate, CargoPackage>,
build_script_outputs: &HashMap<ThirdPartyCrate, Vec<String>>,
pub_deps: &HashSet<ThirdPartyCrate>,
) -> HashMap<ThirdPartyCrate, BuildFile> {
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::ThirdPartyDep,
paths: &paths::ChromiumPaths,
metadata: &HashMap<ThirdPartyCrate, CargoPackage>,
build_script_outputs: &HashMap<ThirdPartyCrate, Vec<String>>,
pub_deps: &HashSet<ThirdPartyCrate>,
) -> Option<(ThirdPartyCrate, BuildFile)> {
let third_party_path_str = paths.third_party.to_str().unwrap();
let crate_id = ThirdPartyCrate { name: dep.package_name.clone(), epoch: dep.epoch };
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 rule = format!(
"//{third_party_path_str}/{}/{}:{target_name}",
dep_of_dep.normalized_name, dep_of_dep.epoch
);
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(dep.normalized_name.to_string());
lib_rule.epoch = Some(dep.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<W: fmt::Write>(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<W: fmt::Write>(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<W: fmt::Write>(mut writer: W, kind: &str, mut deps: Vec<RuleDep>) -> 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 (<cond>) { }" 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<W: fmt::Write, T: fmt::Display, I: IntoIterator<Item = T>>(
mut writer: W,
items: I,
) -> fmt::Result {
writeln!(writer, "[")?;
for item in items.into_iter() {
writeln!(writer, "\"{item}\",")?;
}
writeln!(writer, "]")
}
fn write_str_escaped<W: fmt::Write>(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<W: fmt::Write>(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<platforms::PlatformSet> 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::<Vec<String>>()
.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::<Vec<String>>()
.join(" && "),
cargo_platform::CfgExpr::Any(exprs) => exprs
.iter()
.map(|expr| format!("({})", cfg_expr_to_condition(expr)))
.collect::<Vec<String>>()
.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/*\" ]";