| // 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::{ |
| cfg::{cfg_to_gn_conditional, target_to_gn_conditional}, |
| target::GnTarget, |
| types::*, |
| CombinedTargetCfg, GlobalTargetCfgs, GroupVisibility, |
| }, |
| anyhow::{Context, Error}, |
| cargo_metadata::Package, |
| std::borrow::Cow, |
| std::collections::BTreeMap, |
| std::fmt::Display, |
| std::io, |
| std::path::Path, |
| std::string::ToString, |
| }; |
| |
| /// Utility to add a version suffix to a GN target name. |
| pub fn add_version_suffix(prefix: &str, version: &impl ToString) -> String { |
| let mut accum = String::new(); |
| accum.push_str(&prefix); |
| accum.push_str("-v"); |
| accum.push_str(version.to_string().replace(".", "_").as_str()); |
| accum |
| } |
| |
| /// Write a header for the output GN file |
| pub fn write_header<W: io::Write>(output: &mut W, _cargo_file: &Path) -> Result<(), Error> { |
| writeln!( |
| output, |
| include_str!("../templates/gn_header.template"), |
| // TODO set this, but in a way that tests don't fail on Jan 1st |
| year = "2020", |
| ) |
| .map_err(Into::into) |
| } |
| |
| /// Write an import stament for the output GN file |
| pub fn write_import<W: io::Write>(output: &mut W, file_name: &str) -> Result<(), Error> { |
| writeln!(output, include_str!("../templates/gn_import.template"), file_name = file_name) |
| .map_err(Into::into) |
| } |
| |
| /// Writes rules at the top of the GN file that don't have the version appended |
| pub fn write_top_level_rule<'a, W: io::Write>( |
| output: &mut W, |
| platform: Option<String>, |
| pkg: &Package, |
| group_visibility: Option<&GroupVisibility>, |
| ) -> Result<(), Error> { |
| let target_name = if pkg.is_proc_macro() { |
| format!("{}($host_toolchain)", pkg.gn_name()) |
| } else { |
| pkg.gn_name() |
| }; |
| if let Some(ref platform) = platform { |
| writeln!( |
| output, |
| "if ({conditional}) {{\n", |
| conditional = target_to_gn_conditional(&platform)? |
| )?; |
| } |
| let optional_visibility = |
| group_visibility.map(|v| format!("visibility = {}", v.variable)).unwrap_or_default(); |
| writeln!( |
| output, |
| include_str!("../templates/top_level_gn_rule.template"), |
| group_name = pkg.name, |
| dep_name = target_name, |
| optional_visibility = optional_visibility, |
| )?; |
| if platform.is_some() { |
| writeln!(output, "}}\n")?; |
| } |
| Ok(()) |
| } |
| |
| /// Writes rules at the top of the GN file that don't have the version appended |
| pub fn write_binary_top_level_rule<'a, W: io::Write>( |
| output: &mut W, |
| platform: Option<String>, |
| rule_name: &str, |
| target: &GnTarget<'a>, |
| ) -> Result<(), Error> { |
| if let Some(ref platform) = platform { |
| writeln!( |
| output, |
| "if ({conditional}) {{\n", |
| conditional = cfg_to_gn_conditional(&platform)? |
| )?; |
| } |
| writeln!( |
| output, |
| include_str!("../templates/top_level_binary_gn_rule.template"), |
| group_name = rule_name, |
| dep_name = target.gn_target_name(), |
| )?; |
| if platform.is_some() { |
| writeln!(output, "}}\n")?; |
| } |
| Ok(()) |
| } |
| |
| struct GnField { |
| ty: String, |
| exists: bool, |
| // Use BTreeMap so that iteration over platforms is stable. |
| add_fields: BTreeMap<Option<Platform>, Vec<String>>, |
| remove_fields: BTreeMap<Option<Platform>, Vec<String>>, |
| } |
| impl GnField { |
| /// If defining a new field in the template |
| pub fn new(ty: &str) -> GnField { |
| GnField { |
| ty: ty.to_string(), |
| exists: false, |
| add_fields: BTreeMap::new(), |
| remove_fields: BTreeMap::new(), |
| } |
| } |
| |
| /// If the field already exists in the template |
| pub fn exists(ty: &str) -> GnField { |
| GnField { exists: true, ..Self::new(ty) } |
| } |
| |
| pub fn add_platform_cfg<T: AsRef<str> + Display>(&mut self, platform: Option<String>, cfg: T) { |
| let field = self.add_fields.entry(platform).or_insert(vec![]); |
| field.push(format!("\"{}\"", cfg)); |
| } |
| |
| pub fn remove_platform_cfg<T: AsRef<str> + Display>( |
| &mut self, |
| platform: Option<String>, |
| cfg: T, |
| ) { |
| let field = self.remove_fields.entry(platform).or_insert(vec![]); |
| field.push(format!("\"{}\"", cfg)); |
| } |
| |
| pub fn add_cfg<T: AsRef<str> + Display>(&mut self, cfg: T) { |
| self.add_platform_cfg(None, cfg) |
| } |
| |
| pub fn remove_cfg<T: AsRef<str> + Display>(&mut self, cfg: T) { |
| self.remove_platform_cfg(None, cfg) |
| } |
| |
| pub fn render_gn(&self) -> String { |
| let mut output = if self.exists { |
| // We don't create an empty [] if the field already exists |
| match self.add_fields.get(&None) { |
| Some(add_fields) => format!("{} += [{}]\n", self.ty, add_fields.join(",")), |
| None => "".to_string(), |
| } |
| } else { |
| format!("{} = [{}]\n", self.ty, self.add_fields.get(&None).unwrap_or(&vec![]).join(",")) |
| }; |
| |
| // remove platfrom independent configs |
| if let Some(rm_fields) = self.remove_fields.get(&None) { |
| output.push_str(format!("{} -= [{}]\n", self.ty, rm_fields.join(",")).as_str()); |
| } |
| |
| // Add logic for specific platforms |
| for platform in self.add_fields.keys().filter(|k| k.is_some()) { |
| output.push_str( |
| format!( |
| "if ({}) {{\n", |
| cfg_to_gn_conditional(&platform.as_ref().unwrap()).expect("valid cfg") |
| ) |
| .as_str(), |
| ); |
| output.push_str( |
| format!( |
| "{} += [{}]", |
| self.ty, |
| self.add_fields.get(platform).unwrap_or(&vec![]).join(",") |
| ) |
| .as_str(), |
| ); |
| output.push_str("}\n"); |
| } |
| |
| // Remove logic for specific platforms |
| for platform in self.remove_fields.keys().filter(|k| k.is_some()) { |
| output.push_str( |
| format!( |
| "if ({}) {{\n", |
| cfg_to_gn_conditional(&platform.as_ref().unwrap()).expect("valid cfg") |
| ) |
| .as_str(), |
| ); |
| output.push_str( |
| format!( |
| "{} -= [{}]", |
| self.ty, |
| self.remove_fields.get(platform).unwrap_or(&vec![]).join(",") |
| ) |
| .as_str(), |
| ); |
| output.push_str("}\n"); |
| } |
| output |
| } |
| } |
| |
| /// Write a Target to the GN file. Includes information from the build script. |
| pub fn write_rule<W: io::Write>( |
| output: &mut W, |
| target: &GnTarget<'_>, |
| project_root: &Path, |
| global_target_cfgs: Option<&GlobalTargetCfgs>, |
| custom_build: Option<&CombinedTargetCfg<'_>>, |
| output_name: Option<&str>, |
| ) -> Result<(), Error> { |
| // Generate a section for dependencies that is paramaterized on toolchain |
| let mut dependencies = String::from("deps = []\n"); |
| let mut aliased_deps = vec![]; |
| |
| // Stable output of platforms |
| let mut platform_deps: Vec<( |
| &Option<String>, |
| &Vec<(&cargo_metadata::Package, std::string::String)>, |
| )> = target.dependencies.iter().collect(); |
| platform_deps.sort_by(|p, p2| p.0.cmp(p2.0)); |
| |
| for (platform, deps) in platform_deps { |
| // sort for stable output |
| let mut deps = deps.clone(); |
| deps.sort_by(|a, b| (a.0).id.cmp(&(b.0).id)); |
| |
| // TODO(bwb) feed GN toolchain mapping in as a configuration to make more generic |
| match platform.as_ref().map(String::as_str) { |
| None => { |
| for pkg in deps { |
| dependencies.push_str(" deps += ["); |
| if pkg.0.is_proc_macro() { |
| dependencies.push_str( |
| format!("\":{}($host_toolchain)\"", pkg.0.gn_name()).as_str(), |
| ); |
| } else { |
| dependencies.push_str(format!("\":{}\"", pkg.0.gn_name()).as_str()); |
| } |
| dependencies.push_str("]\n"); |
| if pkg.0.name.replace("-", "_") != pkg.1 { |
| aliased_deps.push(format!("{} = \":{}\" ", pkg.1, pkg.0.gn_name())); |
| } |
| } |
| } |
| Some(platform) => { |
| dependencies.push_str( |
| format!("if ({}) {{\n", target_to_gn_conditional(platform)?).as_str(), |
| ); |
| for pkg in deps { |
| dependencies.push_str(" deps += ["); |
| if pkg.0.is_proc_macro() { |
| dependencies.push_str( |
| format!("\":{}($host_toolchain)\"", pkg.0.gn_name()).as_str(), |
| ); |
| } else { |
| dependencies.push_str(format!("\":{}\"", pkg.0.gn_name()).as_str()); |
| } |
| dependencies.push_str("]\n"); |
| |
| if pkg.0.name.replace("-", "_") != pkg.1 { |
| aliased_deps.push(format!("{} = \":{}\" ", pkg.1, pkg.0.gn_name())); |
| } |
| } |
| dependencies.push_str("}\n"); |
| } |
| } |
| } |
| |
| // write the features into the configs |
| let mut rustflags = GnField::new("rustflags"); |
| let mut rustenv = GnField::new("rustenv"); |
| let mut configs = GnField::exists("configs"); |
| |
| if let Some(global_cfg) = global_target_cfgs { |
| for cfg in &global_cfg.remove_cfgs { |
| configs.remove_cfg(cfg); |
| } |
| for cfg in &global_cfg.add_cfgs { |
| configs.add_cfg(cfg); |
| } |
| } |
| |
| // Associate unique metadata with this crate |
| rustflags.add_cfg("--cap-lints=allow"); |
| rustflags.add_cfg(format!("--edition={}", target.edition)); |
| rustflags.add_cfg(format!("-Cmetadata={}", target.metadata_hash())); |
| rustflags.add_cfg(format!("-Cextra-filename=-{}", target.metadata_hash())); |
| |
| // Aggregate feature flags |
| for feature in target.features { |
| rustflags.add_cfg(format!("--cfg=feature=\\\"{}\\\"", feature)); |
| } |
| |
| // From the gn custom configs, add flags, env vars, and visibility |
| let mut visibility = vec![]; |
| |
| if let Some(custom_build) = custom_build { |
| for (platform, cfg) in custom_build { |
| if let Some(ref deps) = cfg.deps { |
| for dep in deps { |
| // TODO: Respect dep.platform here. |
| dependencies.push_str(format!(" deps += [\"{}\"]", dep).as_str()); |
| } |
| } |
| if let Some(ref flags) = cfg.rustflags { |
| for flag in flags { |
| rustflags.add_platform_cfg(platform.cloned(), flag.to_string()); |
| } |
| } |
| if let Some(ref env_vars) = cfg.env_vars { |
| for flag in env_vars { |
| rustenv.add_platform_cfg(platform.cloned(), flag.to_string()); |
| } |
| } |
| if let Some(ref crate_configs) = cfg.configs { |
| for config in crate_configs { |
| configs.add_platform_cfg(platform.cloned(), config); |
| } |
| } |
| if let Some(ref vis) = cfg.visibility { |
| visibility.extend(vis.iter().map(|v| format!(" visibility += [\"{}\"]", v))); |
| } |
| } |
| } |
| |
| let visibility = if visibility.is_empty() { |
| String::from("visibility = [\":*\"]\n") |
| } else { |
| let mut v = String::from("visibility = []\n"); |
| v.extend(visibility); |
| v |
| }; |
| |
| // making the templates more readable. |
| let aliased_deps_str = if aliased_deps.len() == 0 { |
| String::from("") |
| } else { |
| format!("aliased_deps = {{{}}}", aliased_deps.join("\n")) |
| }; |
| |
| // GN root relative path |
| let root_relative_path = format!( |
| "//{}", |
| target |
| .crate_root |
| .strip_prefix(project_root) |
| .with_context(|| format!( |
| "{} is located outside of the project. Check your vendoring setup", |
| target.name() |
| ))? |
| .to_string() |
| ); |
| let output_name = output_name.map_or_else( |
| || Cow::Owned(format!("{}-{}", target.name().replace("-", "_"), target.metadata_hash())), |
| |n| Cow::Borrowed(n), |
| ); |
| writeln!( |
| output, |
| include_str!("../templates/gn_rule.template"), |
| gn_rule = target.gn_target_type(), |
| target_name = target.gn_target_name(), |
| crate_name = target.name().replace("-", "_"), |
| output_name = output_name, |
| root_path = root_relative_path, |
| aliased_deps = aliased_deps_str, |
| dependencies = dependencies, |
| cfgs = configs.render_gn(), |
| rustenv = rustenv.render_gn(), |
| rustflags = rustflags.render_gn(), |
| visibility = visibility, |
| ) |
| .map_err(Into::into) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use camino::Utf8Path; |
| use cargo_metadata::Version; |
| use std::collections::HashMap; |
| |
| #[test] |
| fn simple_target() { |
| let pkg_id = cargo_metadata::PackageId { repr: String::from("42") }; |
| let version = Version::new(0, 1, 0); |
| let target = GnTarget::new( |
| &pkg_id, |
| "test_target", |
| "test_package", |
| "2018", |
| Utf8Path::new("somewhere/over/the/rainbow.rs"), |
| &version, |
| GnRustType::Library, |
| &[], |
| None, |
| HashMap::new(), |
| ); |
| |
| let mut output = vec![]; |
| write_rule(&mut output, &target, Path::new("somewhere/over"), None, None, None).unwrap(); |
| let output = String::from_utf8(output).unwrap(); |
| assert_eq!( |
| output, |
| r#"rust_library("test_package-v0_1_0") { |
| crate_name = "test_target" |
| crate_root = "//the/rainbow.rs" |
| output_name = "test_target-c5bf97c44457465a" |
| |
| deps = [] |
| |
| rustenv = [] |
| |
| rustflags = ["--cap-lints=allow","--edition=2018","-Cmetadata=c5bf97c44457465a","-Cextra-filename=-c5bf97c44457465a"] |
| |
| |
| visibility = [":*"] |
| |
| } |
| |
| "# |
| ); |
| } |
| #[test] |
| fn binary_target() { |
| let pkg_id = cargo_metadata::PackageId { repr: String::from("42") }; |
| let version = Version::new(0, 1, 0); |
| let target = GnTarget::new( |
| &pkg_id, |
| "test_target", |
| "test_package", |
| "2018", |
| Utf8Path::new("somewhere/over/the/rainbow.rs"), |
| &version, |
| GnRustType::Binary, |
| &[], |
| None, |
| HashMap::new(), |
| ); |
| |
| let outname = Some("rainbow_binary"); |
| let mut output = vec![]; |
| write_rule(&mut output, &target, Path::new("somewhere/over"), None, None, outname).unwrap(); |
| let output = String::from_utf8(output).unwrap(); |
| assert_eq!( |
| output, |
| r#"executable("test_package-test_target-v0_1_0") { |
| crate_name = "test_target" |
| crate_root = "//the/rainbow.rs" |
| output_name = "rainbow_binary" |
| |
| deps = [] |
| |
| rustenv = [] |
| |
| rustflags = ["--cap-lints=allow","--edition=2018","-Cmetadata=bf8f4a806276c599","-Cextra-filename=-bf8f4a806276c599"] |
| |
| |
| visibility = [":*"] |
| |
| } |
| |
| "# |
| ); |
| } |
| } |