| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use crate::build::BuildScript; |
| use crate::graph::GnBuildGraph; |
| use crate::target::GnTarget; |
| use crate::types::*; |
| use anyhow::{Context, Result}; |
| use argh::FromArgs; |
| use camino::Utf8PathBuf; |
| use cargo_metadata::{CargoOpt, DependencyKind, Package}; |
| use serde_derive::{Deserialize, Serialize}; |
| use std::collections::{BTreeMap, HashMap, HashSet}; |
| use std::fs::File; |
| use std::io::{self, Read, Write}; |
| use std::path::PathBuf; |
| use std::process::Command; |
| |
| mod build; |
| mod cfg; |
| mod gn; |
| mod graph; |
| mod target; |
| mod types; |
| |
| #[derive(FromArgs, Debug)] |
| /// Generate a GN manifest for your vendored Cargo dependencies. |
| pub struct Opt { |
| /// cargo manifest path |
| #[argh(option)] |
| manifest_path: PathBuf, |
| |
| /// root of GN project |
| #[argh(option)] |
| project_root: PathBuf, |
| |
| /// cargo binary to use (for vendored toolchains) |
| #[argh(option)] |
| cargo: Option<PathBuf>, |
| |
| /// already generated configs from cargo build scripts |
| #[argh(option, short = 'p')] |
| // TODO(https://fxbug.dev/42165549) |
| #[allow(unused)] |
| cargo_configs: Option<PathBuf>, |
| |
| /// location of GN file |
| #[argh(option, short = 'o')] |
| output: PathBuf, |
| |
| /// location of JSON file with crate metadata |
| #[argh(option)] |
| emit_metadata: Option<PathBuf>, |
| |
| /// location of GN binary to use for formating. |
| /// If no path is provided, no format will be run. |
| #[argh(option)] |
| gn_bin: Option<PathBuf>, |
| |
| /// don't generate a target for the root crate |
| #[argh(switch)] |
| skip_root: bool, |
| |
| /// run cargo with `--all-features` |
| #[argh(switch)] |
| all_features: bool, |
| |
| /// run cargo with `--no-default-features` |
| #[argh(switch)] |
| no_default_features: bool, |
| |
| /// run cargo with `--features <FEATURES>` |
| #[argh(option)] |
| features: Vec<String>, |
| |
| /// directory to emit Fuchsia SDK metadata atoms into |
| #[argh(option)] |
| output_fuchsia_sdk_metadata: Option<PathBuf>, |
| } |
| |
| type PackageName = String; |
| type TargetName = String; |
| type Version = String; |
| |
| /// Per-target metadata in the Cargo.toml for Rust crates that |
| /// require extra information to in the BUILD.gn |
| #[derive(Default, Clone, Serialize, Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| pub struct TargetCfg { |
| /// Config flags for rustc. Ex: --cfg=std |
| rustflags: Option<Vec<String>>, |
| /// Environment variables. These are usually from Cargo or the |
| /// build.rs file in the crate. |
| env_vars: Option<Vec<String>>, |
| /// GN Targets that this crate should depend on. Generally for |
| /// crates that build C libraries and link against. |
| deps: Option<Vec<String>>, |
| /// GN Configs that this crate should depend on. Used to add |
| /// crate-specific configs. |
| configs: Option<Vec<String>>, |
| /// GN Visibility that controls which targets can depend on this target. |
| visibility: Option<Vec<String>>, |
| |
| // Whether the package uses the Fuchsia license. |
| uses_fuchsia_license: Option<bool>, |
| } |
| |
| /// Configuration for a single GN executable target to generate from a Cargo binary target. |
| #[derive(Clone, Serialize, Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| pub struct BinaryCfg { |
| /// Name to use as both the top-level GN group target and the executable's output name. |
| output_name: String, |
| /// Binary target configuration for all platforms. |
| #[serde(default, flatten)] |
| default_cfg: TargetCfg, |
| /// Per-platform binary target configuration. |
| #[serde(default)] |
| #[serde(rename = "platform")] |
| platform_cfg: HashMap<Platform, TargetCfg>, |
| } |
| |
| /// Visibility list to use for a forwarding group |
| #[derive(Clone, Serialize, Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| pub struct GroupVisibility { |
| /// .gni file to import which defines the variable |
| import: String, |
| /// Name of variable defined by the gni file containing the visibility list to use |
| variable: String, |
| } |
| |
| /// Defines a per-target rule for rule renaming. |
| /// |
| /// Some external crates require additional post-processing. For those we define custom rules, |
| /// and force a rename of the rule and the group so that post-processing can be done by GN. |
| #[derive(Clone, Serialize, Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| pub struct RuleRenaming { |
| /// The label of the `.gni` file used to make the group and target renaming |
| /// available. |
| /// |
| /// Only a single file can be imported. This is on purpose so as |
| /// not to pollute the generated file with many imports. The same file will |
| /// be imported only once. |
| import: String, |
| /// If set, this name will be used instead of `group` for the top level group. |
| group_name: Option<String>, |
| /// If set, this rule name will be used instead of the name suggested by |
| /// the target type (e.g. `my_rule` instead of `rust_library`). |
| rule_name: Option<String>, |
| } |
| |
| /// Configuration for a Cargo package. Contains configuration for its (single) library target at the |
| /// top level and optionally zero or more binaries to generate. |
| #[derive(Default, Clone, Serialize, Deserialize, Debug)] |
| #[serde(default, deny_unknown_fields)] |
| pub struct PackageCfg { |
| /// Library target configuration for all platforms. |
| #[serde(flatten)] |
| default_cfg: TargetCfg, |
| /// Per-platform library target configuration. |
| #[serde(rename = "platform")] |
| platform_cfg: HashMap<Platform, TargetCfg>, |
| /// Configuration for GN binary targets to generate from one of the package's binary targets. |
| /// The map key identifies the cargo target name within this cargo package. |
| binary: HashMap<TargetName, BinaryCfg>, |
| /// List of cargo features which have been code reviewed for this cargo package |
| /// |
| /// Must be set if require_feature_reviews mentions this package. |
| reviewed_features: Option<Vec<String>>, |
| /// Visibility list to use for the forwarding group, for use with fixits which seek to remove |
| /// the use of a specific crate from the tree. |
| group_visibility: Option<GroupVisibility>, |
| /// Whether or not this target my only be used in tests. |
| testonly: Option<bool>, |
| /// Build tests for this target. |
| tests: bool, |
| /// Used to rename the group and target names used to bring this particular package |
| /// in. Normally, `cargo-gnaw` will use `group` and `rust_*` target name, but this |
| /// allows us to define custom replacement targets. An import is required, but the |
| /// feature is deliberately limited to just a single definition and a limited number |
| /// of renames, as it should be used in very special cases only. |
| target_renaming: Option<RuleRenaming>, |
| } |
| |
| /// Configs added to all GN targets in the BUILD.gn |
| #[derive(Serialize, Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| pub struct GlobalTargetCfgs { |
| remove_cfgs: Vec<String>, |
| add_cfgs: Vec<String>, |
| /// When true, crates must have license files. |
| require_licenses: Option<bool>, |
| } |
| |
| /// Extra metadata in the Cargo.toml file that feeds into the |
| /// BUILD.gn file. |
| #[derive(Serialize, Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| struct GnBuildMetadata { |
| /// global configs |
| config: Option<GlobalTargetCfgs>, |
| /// packages for which only some features will be code reviewed |
| #[serde(default)] |
| require_feature_reviews: HashSet<PackageName>, |
| /// map of per-Cargo package configuration |
| package: HashMap<PackageName, HashMap<Version, PackageCfg>>, |
| } |
| |
| impl GnBuildMetadata { |
| fn find_package<'a>(&'a self, pkg: &Package) -> Option<&'a PackageCfg> { |
| self.package.get(&pkg.name).and_then(|p| p.get(&pkg.version.to_string())) |
| } |
| } |
| |
| #[derive(Serialize, Deserialize, Debug)] |
| struct BuildMetadata { |
| gn: Option<GnBuildMetadata>, |
| } |
| |
| /// Used for identifying 3p owners via reverse dependencies. Ties together several pieces of |
| /// metadata needed to associate a GN target with an OWNERS file and vice versa. |
| #[derive(Clone, Debug, Deserialize, Eq, PartialEq, PartialOrd, Ord, Serialize)] |
| #[serde(deny_unknown_fields)] |
| pub struct CrateOutputMetadata { |
| /// Name of the crate as specified in Cargo.toml. |
| pub name: String, |
| |
| /// Version of the crate, used for disambiguating between duplicated transitive deps. |
| pub version: String, |
| |
| /// Full GN target for depending on the crate. |
| /// |
| /// For example, Rust targets all have a canonical target like |
| /// `//third_party/rust_crates:foo-v1_0_0`. |
| pub canonical_target: String, |
| |
| /// Shorthand GN target for depending on the crate. |
| /// |
| /// For example, Rust targets listed in `third_party/rust_crates/Cargo.toml` have a |
| /// shortcut target like `//third_party/rust_crates:foo`. |
| pub shortcut_target: Option<String>, |
| |
| /// Filesystem path to the directory containing `Cargo.toml`. |
| pub path: Utf8PathBuf, |
| } |
| |
| // Use BTreeMap so that iteration over platforms is stable. |
| type CombinedTargetCfg<'a> = BTreeMap<Option<&'a Platform>, &'a TargetCfg>; |
| |
| /// Render options for binary. |
| pub struct BinaryRenderOptions<'a> { |
| /// Name of the binary. |
| binary_name: &'a str, |
| /// If true, this binary target is a test target. |
| tests_enabled: bool, |
| } |
| |
| macro_rules! define_combined_cfg { |
| ($t:ty) => { |
| impl $t { |
| fn combined_target_cfg(&self) -> CombinedTargetCfg<'_> { |
| let mut combined: CombinedTargetCfg<'_> = |
| self.platform_cfg.iter().map(|(k, v)| (Some(k), v)).collect(); |
| assert!( |
| combined.insert(None, &self.default_cfg).is_none(), |
| "Default platform (None) already present in combined cfg" |
| ); |
| combined |
| } |
| } |
| }; |
| } |
| define_combined_cfg!(PackageCfg); |
| define_combined_cfg!(BinaryCfg); |
| |
| /// Writes out an import to the provided `output`. |
| /// |
| /// The import is written out only the first time it appears. `imported_files` |
| /// is updated so that the next appearance of the same import does not result |
| /// in a repeated import. |
| fn write_import_once<W: io::Write>( |
| mut output: &mut W, |
| imported_files: &mut HashSet<String>, |
| import: &String, |
| ) -> Result<()> { |
| if !imported_files.contains(import) { |
| gn::write_import(&mut output, import).with_context(|| "writing import")?; |
| imported_files.insert(import.clone()); |
| } |
| Ok(()) |
| } |
| |
| pub fn generate_from_manifest<W: io::Write>(mut output: &mut W, opt: &Opt) -> Result<()> { |
| let manifest_path = &opt.manifest_path; |
| let path_from_root_to_generated = opt |
| .output |
| .parent() |
| .unwrap() |
| .strip_prefix(&opt.project_root) |
| .expect("--project-root must be a parent of --output"); |
| let fuchsia_sdk_metadata_paths = |
| opt.output_fuchsia_sdk_metadata.as_ref().map(|output_fuchsia_sdk_metadata| { |
| let path_from_root_to_generated = output_fuchsia_sdk_metadata |
| .strip_prefix(opt.output.parent().unwrap()) |
| .expect("--output_fuchsia_sdk_metadata must be in the same directory as --output"); |
| (output_fuchsia_sdk_metadata, path_from_root_to_generated) |
| }); |
| let mut emitted_metadata: Vec<CrateOutputMetadata> = Vec::new(); |
| let mut top_level_metadata: HashSet<String> = HashSet::new(); |
| let mut imported_files: HashSet<String> = HashSet::new(); |
| |
| // generate cargo metadata |
| let mut cmd = cargo_metadata::MetadataCommand::new(); |
| let parent_dir = manifest_path |
| .parent() |
| .with_context(|| format!("while parsing parent path: {}", manifest_path.display()))?; |
| cmd.current_dir(parent_dir); |
| cmd.manifest_path(manifest_path); |
| if let Some(ref cargo_path) = opt.cargo { |
| cmd.cargo_path(cargo_path); |
| } |
| if opt.all_features { |
| cmd.features(CargoOpt::AllFeatures); |
| } |
| if opt.no_default_features { |
| cmd.features(CargoOpt::NoDefaultFeatures); |
| } |
| if !opt.features.is_empty() { |
| cmd.features(CargoOpt::SomeFeatures(opt.features.clone())); |
| } |
| cmd.other_options([String::from("--frozen")]); |
| let metadata = cmd.exec().with_context(|| { |
| format!("while running cargo metadata: supplied cargo binary: {:?}", &opt.cargo) |
| })?; |
| |
| // read out custom gn commands from the toml file |
| let mut file = File::open(&manifest_path) |
| .with_context(|| format!("opening {}", manifest_path.display()))?; |
| let mut contents = String::new(); |
| file.read_to_string(&mut contents) |
| .with_context(|| format!("while reading manifest: {}", manifest_path.display()))?; |
| let metadata_configs: BuildMetadata = |
| toml::from_str(&contents).context("parsing manifest toml")?; |
| |
| gn::write_header(&mut output, manifest_path).context("writing header")?; |
| |
| if opt.output_fuchsia_sdk_metadata.is_some() { |
| gn::write_fuchsia_sdk_metadata_header(&mut output) |
| .context("writing Fuchsia SDK metadata header")?; |
| } |
| |
| // Construct a build graph of all the targets for GN |
| let mut build_graph = GnBuildGraph::new(&metadata); |
| match metadata.resolve.as_ref() { |
| Some(resolve) => { |
| let top_level_id = |
| resolve.root.as_ref().expect("the Cargo.toml file must define a package"); |
| if opt.skip_root { |
| let top_level_node = resolve |
| .nodes |
| .iter() |
| .find(|node| node.id == *top_level_id) |
| .expect("top level node not in node graph"); |
| for dep in &top_level_node.deps { |
| build_graph |
| .add_cargo_package(dep.pkg.clone()) |
| .context("adding cargo package")?; |
| for kinds in dep.dep_kinds.iter() { |
| if kinds.kind == DependencyKind::Normal { |
| let platform = kinds.target.as_ref().map(|t| format!("{}", t)); |
| let package = &metadata[&dep.pkg]; |
| top_level_metadata.insert(package.name.to_owned()); |
| let cfg = metadata_configs |
| .gn |
| .as_ref() |
| .and_then(|cfg| cfg.find_package(package)); |
| |
| let visibility = cfg.and_then(|cfg| cfg.group_visibility.as_ref()); |
| if let Some(visibility) = visibility { |
| write_import_once( |
| &mut output, |
| &mut imported_files, |
| &visibility.import, |
| )?; |
| } |
| |
| let target_renaming = cfg.and_then(|cfg| cfg.target_renaming.as_ref()); |
| if let Some(target_renaming) = target_renaming { |
| write_import_once( |
| &mut output, |
| &mut imported_files, |
| &target_renaming.import, |
| )?; |
| } |
| |
| gn::write_top_level_rule( |
| &mut output, |
| platform.as_deref(), |
| package, |
| visibility, |
| target_renaming, |
| cfg.and_then(|cfg| cfg.testonly).unwrap_or(false), |
| cfg.map(|c| c.tests).unwrap_or(false), |
| ) |
| .with_context(|| { |
| format!("while writing top level rule for package: {}", &dep.pkg) |
| }) |
| .context("writing top level rule")?; |
| |
| if let Some((abs_path, rel_path)) = &fuchsia_sdk_metadata_paths { |
| gn::write_fuchsia_sdk_metadata( |
| &mut output, |
| platform.as_deref(), |
| package, |
| abs_path, |
| rel_path, |
| ) |
| .with_context(|| { |
| format!( |
| "while writing top level rule for package: {}", |
| &dep.pkg |
| ) |
| }) |
| .context("writing Fuchsia SDK metadata for top level rule")?; |
| } |
| } |
| } |
| } |
| } else { |
| build_graph |
| .add_cargo_package(top_level_id.clone()) |
| .with_context(|| "could not add cargo package")?; |
| let package = &metadata[&top_level_id]; |
| top_level_metadata.insert(package.name.to_owned()); |
| let cfg = metadata_configs.gn.as_ref().and_then(|cfg| cfg.find_package(package)); |
| |
| let visibility = cfg.and_then(|cfg| cfg.group_visibility.as_ref()); |
| if let Some(visibility) = visibility { |
| write_import_once(&mut output, &mut imported_files, &visibility.import)?; |
| } |
| |
| let target_renaming = cfg.and_then(|cfg| cfg.target_renaming.as_ref()); |
| if let Some(target_renaming) = target_renaming { |
| write_import_once(&mut output, &mut imported_files, &target_renaming.import)?; |
| } |
| |
| gn::write_top_level_rule( |
| &mut output, |
| None, |
| package, |
| visibility, |
| target_renaming, |
| cfg.and_then(|cfg| cfg.testonly).unwrap_or(false), |
| cfg.map(|c| c.tests).unwrap_or(false), |
| ) |
| .with_context(|| "writing top level rule")?; |
| |
| if let Some((abs_path, rel_path)) = &fuchsia_sdk_metadata_paths { |
| gn::write_fuchsia_sdk_metadata(&mut output, None, package, abs_path, rel_path) |
| .with_context(|| "writing Fuchsia SDK metadata for top level rule")?; |
| } |
| } |
| } |
| None => anyhow::bail!("Failed to resolve a build graph for the package tree"), |
| } |
| |
| // Sort targets for stable output to minimize diff churn |
| let mut graph_targets: Vec<&GnTarget<'_>> = build_graph.targets().collect(); |
| graph_targets.sort(); |
| |
| let global_config = match metadata_configs.gn { |
| Some(ref gn_configs) => gn_configs.config.as_ref(), |
| None => None, |
| }; |
| |
| let empty_hash_set = &HashSet::new(); |
| let require_feature_reviews = metadata_configs |
| .gn |
| .as_ref() |
| .map(|gn| &gn.require_feature_reviews) |
| .unwrap_or(empty_hash_set); |
| |
| // Grab the per-package configs. |
| let gn_pkg_cfgs = metadata_configs.gn.as_ref().map(|i| &i.package); |
| |
| // Iterate through the target configs, verifying that the build graph contains the configured |
| // targets, then save off a mapping of GnTarget to the target config. |
| let mut target_cfgs = HashMap::<&GnTarget<'_>, CombinedTargetCfg<'_>>::new(); |
| let mut target_binaries = HashMap::<&GnTarget<'_>, BinaryRenderOptions<'_>>::new(); |
| let mut testonly_targets = HashSet::<&GnTarget<'_>>::new(); |
| let mut targets_with_tests = HashSet::<&GnTarget<'_>>::new(); |
| let mut reviewed_features_map = HashMap::<&GnTarget<'_>, Option<&[String]>>::new(); |
| // An entry for each target that needs a renamed rule. There is no connection between |
| // PackageCfg and GnTarget, so to use the setting from PackageCfg, we need to build this map as |
| // we iterate through targets. |
| let mut renamed_rules = HashMap::<&GnTarget<'_>, &'_ str>::new(); |
| let mut unused_configs = String::new(); |
| if let Some(gn_pkg_cfgs) = gn_pkg_cfgs { |
| for (pkg_name, versions) in gn_pkg_cfgs { |
| for (pkg_version, pkg_cfg) in versions { |
| // Search the build graph for the library target. |
| if let Some(target) = build_graph.find_library_target(pkg_name, pkg_version) { |
| assert!( |
| target_cfgs.insert(target, pkg_cfg.combined_target_cfg()).is_none(), |
| "Duplicate library config somehow specified" |
| ); |
| assert!( |
| reviewed_features_map |
| .insert(target, pkg_cfg.reviewed_features.as_deref()) |
| .is_none(), |
| "Duplicate library config somehow specified" |
| ); |
| |
| if let Some(renamed_rule) = pkg_cfg |
| .target_renaming |
| .as_ref() |
| .and_then(|r| r.rule_name.as_ref()) |
| .map(|x| x.as_str()) |
| { |
| renamed_rules.insert(target, renamed_rule); |
| } |
| |
| if pkg_cfg.testonly == Some(true) { |
| testonly_targets.insert(target); |
| } |
| |
| if pkg_cfg.tests { |
| targets_with_tests.insert(target); |
| } |
| } else { |
| unused_configs.push_str(&format!( |
| "library crate, package {} version {}\n", |
| pkg_name, pkg_version |
| )); |
| } |
| |
| // Handle binaries that should be built for this package, similarly searching the |
| // build graph for the binary targets. |
| for (bin_cargo_target, bin_cfg) in &pkg_cfg.binary { |
| if let Some(target) = |
| build_graph.find_binary_target(pkg_name, pkg_version, bin_cargo_target) |
| { |
| if let Some(old_options) = target_binaries.insert( |
| target, |
| BinaryRenderOptions { |
| binary_name: &bin_cfg.output_name, |
| tests_enabled: pkg_cfg.tests, |
| }, |
| ) { |
| anyhow::bail!( |
| "A given binary target ({} in package {} version {}) can only be \ |
| used for a single GN target, but multiple exist, including {} \ |
| and {}", |
| bin_cargo_target, |
| pkg_name, |
| pkg_version, |
| &bin_cfg.output_name, |
| old_options.binary_name, |
| ); |
| } |
| assert!( |
| target_cfgs.insert(target, bin_cfg.combined_target_cfg()).is_none(), |
| "Should have bailed above" |
| ); |
| |
| if pkg_cfg.tests { |
| targets_with_tests.insert(target); |
| } |
| } else { |
| unused_configs.push_str(&format!( |
| "binary crate {}, package {} version {}\n", |
| bin_cargo_target, pkg_name, pkg_version |
| )); |
| } |
| } |
| } |
| } |
| } |
| if !unused_configs.is_empty() { |
| anyhow::bail!( |
| "GNaw config exists for crates that were not found in the Cargo build graph:\n\n{}", |
| unused_configs |
| ); |
| } |
| |
| // Write the top-level GN rules for binaries. Verify that the names are unique, otherwise a |
| // build failure will result. |
| { |
| let mut names = HashSet::new(); |
| for (target, options) in &target_binaries { |
| if !names.insert(options.binary_name) { |
| anyhow::bail!( |
| "Multiple targets are configured to generate executables named \"{}\"", |
| options.binary_name |
| ); |
| } |
| |
| emitted_metadata.push(CrateOutputMetadata { |
| name: options.binary_name.to_string(), |
| version: target.version(), |
| canonical_target: format!( |
| "//{}:{}", |
| path_from_root_to_generated.display(), |
| target.gn_target_name() |
| ), |
| shortcut_target: Some(format!( |
| "//{}:{}", |
| path_from_root_to_generated.display(), |
| options.binary_name |
| )), |
| path: target.package_root(), |
| }); |
| gn::write_binary_top_level_rule(&mut output, None, target, options) |
| .context(format!("writing binary top level rule: {}", target.name()))?; |
| } |
| } |
| |
| // Write out a GN rule for each target in the build graph |
| for target in graph_targets { |
| // Check whether we should generate a target if this is a binary. |
| let binary_name = if let GnRustType::Binary = target.target_type { |
| let name = target_binaries.get(target).map(|opt| opt.binary_name); |
| if name.is_none() { |
| continue; |
| } |
| name |
| } else { |
| None |
| }; |
| |
| let target_cfg = target_cfgs.get(target); |
| if target.has_build_script && target_cfg.is_none() { |
| let build_output = BuildScript::execute(target); |
| match build_output { |
| Ok(rules) => { |
| anyhow::bail!( |
| "Add this to your Cargo.toml located at {}:\n\ |
| [gn.package.{}.\"{}\"]\n\ |
| rustflags = [{}]\n\ |
| rustenv = [{}]", |
| manifest_path.display(), |
| target.name(), |
| target.version(), |
| rules.rustflags.join(", "), |
| rules.rustenv.join(", ") |
| ); |
| } |
| Err(err) => anyhow::bail!( |
| "{} {} uses a build script but no section defined in the GN section \ |
| nor can we automatically generate the appropriate rules:\n{}", |
| target.name(), |
| target.version(), |
| err, |
| ), |
| } |
| } |
| |
| // Check to see if we need to review individual features for this crate |
| let reviewed_features = reviewed_features_map.get(target); |
| if require_feature_reviews.contains(&target.name()) { |
| match reviewed_features { |
| Some(Some(_)) => {} |
| _ => { |
| anyhow::bail!( |
| "{name} {version} requires feature review but reviewed features not found.\n\n\ |
| Make sure to conduct code review assuming the following features are enabled, \ |
| and then add this to your Cargo.toml located at {manifest_path}:\n\ |
| [gn.package.{name}.\"{version}\"]\n\ |
| reviewed_features = {features}", |
| manifest_path = manifest_path.display(), |
| name = target.name(), |
| version = target.version(), |
| features = toml::to_string(target.features).unwrap() |
| ); |
| } |
| } |
| } else if let Some(Some(_)) = reviewed_features { |
| anyhow::bail!( |
| "{name} {version} sets reviewed_features but {name} was not found in \ |
| require_feature_reviews.\n\n\ |
| Make sure to add it there so that reviewed_features is not accidentally \ |
| removed during future crate version bumps.", |
| name = target.name(), |
| version = target.version(), |
| ); |
| } |
| |
| if let Some(&Some(reviewed_features)) = reviewed_features { |
| let reviewed_features_set = |
| reviewed_features.iter().map(|s| s.as_str()).collect::<HashSet<_>>(); |
| let unreviewed_features = target |
| .features |
| .iter() |
| .filter(|f| !reviewed_features_set.contains(f.as_str())) |
| .collect::<Vec<_>>(); |
| if !unreviewed_features.is_empty() { |
| anyhow::bail!( |
| "{name} {version} is included with unreviewed features {unreviewed:?}\n\n\ |
| Make sure to additionally review code gated by these features, then add them \ |
| to reviewed_features under [gn.package.{name}.\"{version}\"] in {manifest_path}", |
| name = target.name(), |
| version = target.version(), |
| unreviewed = unreviewed_features, |
| manifest_path = manifest_path.display(), |
| ) |
| } |
| } |
| |
| let package_root = target.package_root(); |
| package_root.strip_prefix(&opt.project_root).unwrap_or_else(|e| { |
| panic!( |
| "{}: {} is not under the project_root ({})", |
| e, |
| target.package_root(), |
| opt.project_root.display() |
| ) |
| }); |
| |
| let shortcut_target = if top_level_metadata.contains(target.pkg_name) { |
| Some(format!("//{}:{}", path_from_root_to_generated.display(), target.pkg_name)) |
| } else { |
| None |
| }; |
| |
| emitted_metadata.push(CrateOutputMetadata { |
| name: target.name(), |
| version: target.version(), |
| canonical_target: format!( |
| "//{}:{}", |
| path_from_root_to_generated.display(), |
| target.gn_target_name() |
| ), |
| shortcut_target, |
| path: package_root.to_owned(), |
| }); |
| |
| gn::write_rule( |
| &mut output, |
| target, |
| &opt.project_root, |
| global_config, |
| target_cfg, |
| binary_name, |
| testonly_targets.contains(target), |
| false, |
| renamed_rules.get(target).copied(), |
| false, // Pigweed GN has no defined format for licenses |
| ) |
| .with_context(|| format!("writing rule for: {} {}", target.name(), target.version()))?; |
| |
| if targets_with_tests.contains(target) { |
| gn::write_rule( |
| &mut output, |
| target, |
| &opt.project_root, |
| global_config, |
| target_cfg, |
| binary_name, |
| true, |
| true, |
| renamed_rules.get(&target).copied(), |
| false, // Pigweed GN has no defined format for licenses |
| ) |
| .with_context(|| format!("writing rule for: {} {}", target.name(), target.version()))?; |
| } |
| } |
| |
| if let Some(metadata_path) = &opt.emit_metadata { |
| eprintln!("Emitting external crate metadata: {}", metadata_path.display()); |
| emitted_metadata.sort(); |
| let metadata_json = serde_json::to_string_pretty(&emitted_metadata) |
| .context("serializing metadata to json")?; |
| std::fs::create_dir_all( |
| metadata_path |
| .parent() |
| .expect("The metadata path must include a valid parent directory"), |
| )?; |
| std::fs::write(metadata_path, &metadata_json).context("writing metadata file")?; |
| } |
| |
| Ok(()) |
| } |
| |
| pub fn run(args: &[impl AsRef<str>]) -> Result<()> { |
| // Check if running through cargo or stand-alone before arg parsing |
| let mut strs: Vec<&str> = args.iter().map(|s| s.as_ref()).collect(); |
| if strs.get(1) == Some(&"gnaw") { |
| // If the second command is "gnaw" this likely invoked by `cargo gnaw` |
| // shift all args by one. |
| strs = strs[1..].to_vec() |
| } |
| let opt = match Opt::from_args(&[strs[0]], &strs[1..]) { |
| Ok(opt) => opt, |
| Err(e) if e.status.is_ok() => { |
| println!("{}", e.output); |
| return Ok(()); |
| } |
| Err(e) => return Err(anyhow::Error::msg(e.output)), |
| }; |
| |
| eprintln!("Generating GN file from {}", opt.manifest_path.to_string_lossy()); |
| |
| // redirect to stdout if no GN output file specified |
| // Stores data in a buffer in-case to prevent creating bad BUILD.gn |
| let mut gn_output_buffer = vec![]; |
| generate_from_manifest(&mut gn_output_buffer, &opt).context("generating manifest")?; |
| |
| // Write the file buffer to an actual file |
| File::create(&opt.output) |
| .context("creating output file")? |
| .write_all(&gn_output_buffer) |
| .context("writing output contents")?; |
| |
| if let Some(gn_bin) = opt.gn_bin { |
| eprintln!("Formatting output file: {}", opt.output.display()); |
| let status = Command::new(&gn_bin) |
| .arg("format") |
| .arg(opt.output) |
| .status() |
| .with_context(|| format!("could not spawn GN: {}", gn_bin.display()))?; |
| if !status.success() { |
| anyhow::bail!("GN format command failed:{:?}", status); |
| } |
| } |
| |
| Ok(()) |
| } |