chromium/tools/crates/gnrt/lib/gn.rs

// 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\"))"
        );
    }
}