blob: 6606d8e744a726acd7ad18eebc98d5159cb0e9d0 [file] [log] [blame] [edit]
// 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(())
}