| // 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, graph::GnBuildGraph, target::GnTarget, types::*}, |
| anyhow::{Context, Error}, |
| argh::FromArgs, |
| camino::Utf8PathBuf, |
| cargo_metadata::{CargoOpt, DependencyKind, Package}, |
| serde_derive::{Deserialize, Serialize}, |
| std::collections::{BTreeMap, HashMap, HashSet}, |
| std::{ |
| fs::File, |
| io::{self, Read, Write}, |
| path::PathBuf, |
| 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(fxbug.dev/84729) |
| #[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>, |
| } |
| |
| 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)] |
| 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>>, |
| } |
| |
| /// Configuration for a single GN executable target to generate from a Cargo binary target. |
| #[derive(Clone, Serialize, Deserialize, Debug)] |
| 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)] |
| 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, |
| } |
| |
| // 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)] |
| 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>, |
| } |
| |
| /// Configs added to all GN targets in the BUILD.gn |
| #[derive(Serialize, Deserialize, Debug)] |
| pub struct GlobalTargetCfgs { |
| remove_cfgs: Vec<String>, |
| add_cfgs: Vec<String>, |
| } |
| |
| /// Extra metadata in the Cargo.toml file that feeds into the |
| /// BUILD.gn file. |
| #[derive(Serialize, Deserialize, Debug)] |
| 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)] |
| 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>; |
| |
| 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); |
| |
| pub fn generate_from_manifest<W: io::Write>(mut output: &mut W, opt: &Opt) -> Result<(), Error> { |
| 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 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")?; |
| |
| // 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 { |
| if !imported_files.contains(&visibility.import) { |
| gn::write_import(&mut output, &visibility.import) |
| .with_context(|| "writing import")?; |
| imported_files.insert(visibility.import.clone()); |
| } |
| } |
| gn::write_top_level_rule(&mut output, platform, package, visibility) |
| .with_context(|| { |
| format!( |
| "while writing top level rule for package: {}", |
| &dep.pkg |
| ) |
| }) |
| .context("writing 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 { |
| if !imported_files.contains(&visibility.import) { |
| gn::write_import(&mut output, &visibility.import) |
| .with_context(|| "writing import")?; |
| imported_files.insert(visibility.import.clone()); |
| } |
| } |
| |
| gn::write_top_level_rule(&mut output, None, package, visibility) |
| .with_context(|| "writing 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 binary_names = HashMap::<&GnTarget<'_>, &str>::new(); |
| let mut reviewed_features_map = HashMap::<&GnTarget<'_>, Option<&[String]>>::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" |
| ); |
| } 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_name) = binary_names.insert(target, &bin_cfg.output_name) { |
| 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_name |
| ); |
| } |
| assert!( |
| target_cfgs.insert(target, bin_cfg.combined_target_cfg()).is_none(), |
| "Should have bailed above" |
| ); |
| } else { |
| unused_configs.push_str(&format!( |
| "binary crate {}, package {} version {}\n", |
| bin_cargo_target, pkg_name, pkg_version |
| )); |
| } |
| } |
| } |
| } |
| } |
| if unused_configs.len() > 0 { |
| 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, bin_name) in &binary_names { |
| if !names.insert(bin_name) { |
| anyhow::bail!( |
| "Multiple targets are configured to generate executables named \"{}\"", |
| bin_name |
| ); |
| } |
| |
| emitted_metadata.push(CrateOutputMetadata { |
| name: bin_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(), |
| bin_name |
| )), |
| path: target.package_root(&opt.project_root), |
| }); |
| gn::write_binary_top_level_rule(&mut output, None, bin_name, target) |
| .context("writing binary top level rule")?; |
| } |
| } |
| |
| // 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 = binary_names.get(target).map(|s| *s); |
| if name.is_none() { |
| continue; |
| } |
| name |
| } else { |
| None |
| }; |
| |
| let target_cfg = target_cfgs.get(target); |
| if target.uses_build_script() && target_cfg.is_none() { |
| let build_output = BuildScript::compile(target).and_then(|s| s.execute()); |
| match build_output { |
| Ok(rules) => { |
| anyhow::bail!( |
| "Add this to your Cargo.toml located at {}:\n\ |
| [gn.package.{}.\"{}\"]\n\ |
| rustflags = [{}]", |
| manifest_path.display(), |
| target.name(), |
| target.version(), |
| rules.cfgs.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(&opt.project_root); |
| 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(), |
| }); |
| |
| let _ = gn::write_rule( |
| &mut output, |
| &target, |
| &opt.project_root, |
| global_config, |
| target_cfg, |
| binary_name, |
| ) |
| .context("writing rule")?; |
| } |
| |
| 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<(), Error> { |
| // 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 output = Command::new(&gn_bin) |
| .arg("format") |
| .arg(opt.output) |
| .output() |
| .with_context(|| format!("could not spawn GN: {}", gn_bin.display()))?; |
| if !output.status.success() { |
| anyhow::bail!("GN format command failed:\n{:?}", output); |
| } |
| } |
| |
| Ok(()) |
| } |