| //! Tools for rendering and writing BUILD and other Starlark files |
| |
| mod template_engine; |
| |
| use std::collections::{BTreeMap, BTreeSet}; |
| use std::fs; |
| use std::iter::FromIterator; |
| use std::path::{Path, PathBuf}; |
| use std::str::FromStr; |
| |
| use anyhow::{bail, Context as AnyhowContext, Result}; |
| use indoc::formatdoc; |
| |
| use crate::config::{RenderConfig, VendorMode}; |
| use crate::context::crate_context::{CrateContext, CrateDependency, Rule}; |
| use crate::context::{Context, TargetAttributes}; |
| use crate::rendering::template_engine::TemplateEngine; |
| use crate::splicing::default_splicing_package_crate_id; |
| use crate::utils::starlark::{ |
| self, Alias, CargoBuildScript, CommonAttrs, Data, ExportsFiles, Filegroup, Glob, Label, Load, |
| Package, RustBinary, RustLibrary, RustProcMacro, Select, SelectDict, SelectList, SelectMap, |
| Starlark, |
| }; |
| use crate::utils::{self, sanitize_repository_name}; |
| |
| // Configuration remapper used to convert from cfg expressions like "cfg(unix)" |
| // to platform labels like "@rules_rust//rust/platform:x86_64-unknown-linux-gnu". |
| type Platforms = BTreeMap<String, BTreeSet<String>>; |
| |
| pub struct Renderer { |
| config: RenderConfig, |
| engine: TemplateEngine, |
| } |
| |
| impl Renderer { |
| pub fn new(config: RenderConfig) -> Self { |
| let engine = TemplateEngine::new(&config); |
| Self { config, engine } |
| } |
| |
| pub fn render(&self, context: &Context) -> Result<BTreeMap<PathBuf, String>> { |
| let mut output = BTreeMap::new(); |
| |
| let platforms = self.render_platform_labels(context); |
| output.extend(self.render_build_files(context, &platforms)?); |
| output.extend(self.render_crates_module(context)?); |
| |
| if let Some(vendor_mode) = &self.config.vendor_mode { |
| match vendor_mode { |
| crate::config::VendorMode::Local => { |
| // Nothing to do for local vendor crate |
| } |
| crate::config::VendorMode::Remote => { |
| output.extend(self.render_vendor_support_files(context)?); |
| } |
| } |
| } |
| |
| Ok(output) |
| } |
| |
| fn render_platform_labels(&self, context: &Context) -> BTreeMap<String, BTreeSet<String>> { |
| context |
| .conditions |
| .iter() |
| .map(|(cfg, triples)| { |
| ( |
| cfg.clone(), |
| triples |
| .iter() |
| .map(|triple| { |
| render_platform_constraint_label( |
| &self.config.platforms_template, |
| triple, |
| ) |
| }) |
| .collect(), |
| ) |
| }) |
| .collect() |
| } |
| |
| fn render_crates_module(&self, context: &Context) -> Result<BTreeMap<PathBuf, String>> { |
| let module_label = render_module_label(&self.config.crates_module_template, "defs.bzl") |
| .context("Failed to resolve string to module file label")?; |
| let module_build_label = |
| render_module_label(&self.config.crates_module_template, "BUILD.bazel") |
| .context("Failed to resolve string to module file label")?; |
| |
| let mut map = BTreeMap::new(); |
| map.insert( |
| Renderer::label_to_path(&module_label), |
| self.engine.render_module_bzl(context)?, |
| ); |
| map.insert( |
| Renderer::label_to_path(&module_build_label), |
| self.render_module_build_file(context)?, |
| ); |
| |
| Ok(map) |
| } |
| |
| fn render_module_build_file(&self, context: &Context) -> Result<String> { |
| let mut starlark = Vec::new(); |
| |
| // Banner comment for top of the file. |
| let header = self.engine.render_header()?; |
| starlark.push(Starlark::Verbatim(header)); |
| |
| // Package visibility, exported bzl files. |
| let package = Package::default_visibility_public(); |
| starlark.push(Starlark::Package(package)); |
| |
| let mut exports_files = ExportsFiles { |
| paths: BTreeSet::from(["cargo-bazel.json".to_owned(), "defs.bzl".to_owned()]), |
| globs: Glob { |
| include: BTreeSet::from(["*.bazel".to_owned()]), |
| exclude: BTreeSet::new(), |
| }, |
| }; |
| if let Some(VendorMode::Remote) = self.config.vendor_mode { |
| exports_files.paths.insert("crates.bzl".to_owned()); |
| } |
| starlark.push(Starlark::ExportsFiles(exports_files)); |
| |
| let filegroup = Filegroup { |
| name: "srcs".to_owned(), |
| srcs: Glob { |
| include: BTreeSet::from(["*.bazel".to_owned(), "*.bzl".to_owned()]), |
| exclude: BTreeSet::new(), |
| }, |
| }; |
| starlark.push(Starlark::Filegroup(filegroup)); |
| |
| // An `alias` for each direct dependency of a workspace member crate. |
| let mut dependencies = Vec::new(); |
| for dep in context.workspace_member_deps() { |
| let krate = &context.crates[&dep.id]; |
| if let Some(library_target_name) = &krate.library_target_name { |
| let rename = dep.alias.as_ref().unwrap_or(&krate.name); |
| dependencies.push(Alias { |
| // If duplicates exist, include version to disambiguate them. |
| name: if context.has_duplicate_workspace_member_dep(dep) { |
| format!("{}-{}", rename, krate.version) |
| } else { |
| rename.clone() |
| }, |
| actual: self.crate_label(&krate.name, &krate.version, library_target_name), |
| tags: BTreeSet::from(["manual".to_owned()]), |
| }); |
| } |
| } |
| if !dependencies.is_empty() { |
| let comment = "# Workspace Member Dependencies".to_owned(); |
| starlark.push(Starlark::Verbatim(comment)); |
| starlark.extend(dependencies.into_iter().map(Starlark::Alias)); |
| } |
| |
| // An `alias` for each binary dependency. |
| let mut binaries = Vec::new(); |
| for crate_id in &context.binary_crates { |
| let krate = &context.crates[crate_id]; |
| for rule in &krate.targets { |
| if let Rule::Binary(bin) = rule { |
| binaries.push(Alias { |
| // If duplicates exist, include version to disambiguate them. |
| name: if context.has_duplicate_binary_crate(crate_id) { |
| format!("{}-{}__{}", krate.name, krate.version, bin.crate_name) |
| } else { |
| format!("{}__{}", krate.name, bin.crate_name) |
| }, |
| actual: self.crate_label( |
| &krate.name, |
| &krate.version, |
| &format!("{}__bin", bin.crate_name), |
| ), |
| tags: BTreeSet::from(["manual".to_owned()]), |
| }); |
| } |
| } |
| } |
| if !binaries.is_empty() { |
| let comment = "# Binaries".to_owned(); |
| starlark.push(Starlark::Verbatim(comment)); |
| starlark.extend(binaries.into_iter().map(Starlark::Alias)); |
| } |
| |
| let starlark = starlark::serialize(&starlark)?; |
| Ok(starlark) |
| } |
| |
| fn render_build_files( |
| &self, |
| context: &Context, |
| platforms: &Platforms, |
| ) -> Result<BTreeMap<PathBuf, String>> { |
| let default_splicing_package_id = default_splicing_package_crate_id(); |
| context |
| .crates |
| .keys() |
| // Do not render the default splicing package |
| .filter(|id| *id != &default_splicing_package_id) |
| // Do not render local packages |
| .filter(|id| !context.workspace_members.contains_key(id)) |
| .map(|id| { |
| let label = match render_build_file_template( |
| &self.config.build_file_template, |
| &id.name, |
| &id.version, |
| ) { |
| Ok(label) => label, |
| Err(e) => bail!(e), |
| }; |
| |
| let filename = Renderer::label_to_path(&label); |
| let content = self.render_one_build_file(platforms, &context.crates[id])?; |
| Ok((filename, content)) |
| }) |
| .collect() |
| } |
| |
| fn render_one_build_file(&self, platforms: &Platforms, krate: &CrateContext) -> Result<String> { |
| let mut starlark = Vec::new(); |
| |
| // Banner comment for top of the file. |
| let header = self.engine.render_header()?; |
| starlark.push(Starlark::Verbatim(header)); |
| |
| // Loads. |
| starlark.push(Starlark::Load(Load { |
| bzl: "@bazel_skylib//lib:dicts.bzl".to_owned(), |
| items: BTreeSet::from(["dicts".to_owned()]), |
| })); |
| starlark.push(Starlark::Load(Load { |
| bzl: "@rules_rust//cargo:defs.bzl".to_owned(), |
| items: BTreeSet::from(["cargo_build_script".to_owned()]), |
| })); |
| starlark.push(Starlark::Load(Load { |
| bzl: "@rules_rust//rust:defs.bzl".to_owned(), |
| items: BTreeSet::from([ |
| "rust_binary".to_owned(), |
| "rust_library".to_owned(), |
| "rust_proc_macro".to_owned(), |
| ]), |
| })); |
| let disable_visibility = "# buildifier: disable=bzl-visibility".to_owned(); |
| starlark.push(Starlark::Verbatim(disable_visibility)); |
| starlark.push(Starlark::Load(Load { |
| bzl: "@rules_rust//crate_universe/private:selects.bzl".to_owned(), |
| items: BTreeSet::from(["selects".to_owned()]), |
| })); |
| |
| // Package visibility. |
| let package = Package::default_visibility_public(); |
| starlark.push(Starlark::Package(package)); |
| |
| if let Some(license) = &krate.license { |
| starlark.push(Starlark::Verbatim(formatdoc! {r#" |
| # licenses([ |
| # "TODO", # {license} |
| # ]) |
| "#})); |
| } |
| |
| for rule in &krate.targets { |
| match rule { |
| Rule::BuildScript(target) => { |
| let cargo_build_script = |
| self.make_cargo_build_script(platforms, krate, target)?; |
| starlark.push(Starlark::CargoBuildScript(cargo_build_script)); |
| starlark.push(Starlark::Alias(Alias { |
| name: target.crate_name.clone(), |
| actual: format!("{}_build_script", krate.name), |
| tags: BTreeSet::from(["manual".to_owned()]), |
| })); |
| } |
| Rule::ProcMacro(target) => { |
| let rust_proc_macro = self.make_rust_proc_macro(platforms, krate, target)?; |
| starlark.push(Starlark::RustProcMacro(rust_proc_macro)); |
| } |
| Rule::Library(target) => { |
| let rust_library = self.make_rust_library(platforms, krate, target)?; |
| starlark.push(Starlark::RustLibrary(rust_library)); |
| } |
| Rule::Binary(target) => { |
| let rust_binary = self.make_rust_binary(platforms, krate, target)?; |
| starlark.push(Starlark::RustBinary(rust_binary)); |
| } |
| } |
| } |
| |
| if let Some(additive_build_file_content) = &krate.additive_build_file_content { |
| let comment = "# Additive BUILD file content".to_owned(); |
| starlark.push(Starlark::Verbatim(comment)); |
| starlark.push(Starlark::Verbatim(additive_build_file_content.clone())); |
| } |
| |
| let starlark = starlark::serialize(&starlark)?; |
| Ok(starlark) |
| } |
| |
| fn make_cargo_build_script( |
| &self, |
| platforms: &Platforms, |
| krate: &CrateContext, |
| target: &TargetAttributes, |
| ) -> Result<CargoBuildScript> { |
| let empty_set = BTreeSet::<String>::new(); |
| let empty_list = SelectList::<String>::default(); |
| let empty_deps = SelectList::<CrateDependency>::default(); |
| let attrs = krate.build_script_attrs.as_ref(); |
| |
| Ok(CargoBuildScript { |
| // Because `cargo_build_script` does some invisible target name |
| // mutating to determine the package and crate name for a build |
| // script, the Bazel target name of any build script cannot be the |
| // Cargo canonical name of "cargo_build_script" without losing out |
| // on having certain Cargo environment variables set. |
| // |
| // Do not change this name to "cargo_build_script". |
| name: format!("{}_build_script", krate.name), |
| aliases: self |
| .make_aliases(krate, true, false) |
| .remap_configurations(platforms), |
| build_script_env: attrs |
| .map_or_else(SelectDict::default, |attrs| attrs.build_script_env.clone()) |
| .remap_configurations(platforms), |
| compile_data: make_data( |
| platforms, |
| &empty_set, |
| attrs.map_or(&empty_list, |attrs| &attrs.compile_data), |
| ), |
| crate_features: krate.common_attrs.crate_features.clone(), |
| crate_name: utils::sanitize_module_name(&target.crate_name), |
| crate_root: target.crate_root.clone(), |
| data: make_data( |
| platforms, |
| attrs.map_or(&empty_set, |attrs| &attrs.data_glob), |
| attrs.map_or(&empty_list, |attrs| &attrs.data), |
| ), |
| deps: self |
| .make_deps( |
| attrs.map_or(&empty_deps, |attrs| &attrs.deps), |
| attrs.map_or(&empty_set, |attrs| &attrs.extra_deps), |
| ) |
| .remap_configurations(platforms), |
| edition: krate.common_attrs.edition.clone(), |
| linker_script: krate.common_attrs.linker_script.clone(), |
| links: attrs.and_then(|attrs| attrs.links.clone()), |
| proc_macro_deps: self |
| .make_deps( |
| attrs.map_or(&empty_deps, |attrs| &attrs.proc_macro_deps), |
| attrs.map_or(&empty_set, |attrs| &attrs.extra_proc_macro_deps), |
| ) |
| .remap_configurations(platforms), |
| rustc_env: attrs |
| .map_or_else(SelectDict::default, |attrs| attrs.rustc_env.clone()) |
| .remap_configurations(platforms), |
| rustc_env_files: attrs |
| .map_or_else(SelectList::default, |attrs| attrs.rustc_env_files.clone()) |
| .remap_configurations(platforms), |
| rustc_flags: { |
| let mut rustc_flags = |
| attrs.map_or_else(SelectList::default, |attrs| attrs.rustc_flags.clone()); |
| // In most cases, warnings in 3rd party crates are not |
| // interesting as they're out of the control of consumers. The |
| // flag here silences warnings. For more details see: |
| // https://doc.rust-lang.org/rustc/lints/levels.html |
| rustc_flags.insert("--cap-lints=allow".to_owned(), None); |
| rustc_flags.remap_configurations(platforms) |
| }, |
| srcs: target.srcs.clone(), |
| tags: { |
| let mut tags = BTreeSet::from_iter(krate.common_attrs.tags.iter().cloned()); |
| tags.insert("cargo-bazel".to_owned()); |
| tags.insert("manual".to_owned()); |
| tags.insert("noclippy".to_owned()); |
| tags.insert("norustfmt".to_owned()); |
| tags |
| }, |
| tools: attrs |
| .map_or_else(SelectList::default, |attrs| attrs.tools.clone()) |
| .remap_configurations(platforms), |
| toolchains: attrs.map_or_else(BTreeSet::new, |attrs| attrs.toolchains.clone()), |
| version: krate.common_attrs.version.clone(), |
| visibility: BTreeSet::from(["//visibility:private".to_owned()]), |
| }) |
| } |
| |
| fn make_rust_proc_macro( |
| &self, |
| platforms: &Platforms, |
| krate: &CrateContext, |
| target: &TargetAttributes, |
| ) -> Result<RustProcMacro> { |
| Ok(RustProcMacro { |
| name: target.crate_name.clone(), |
| deps: self |
| .make_deps(&krate.common_attrs.deps, &krate.common_attrs.extra_deps) |
| .remap_configurations(platforms), |
| proc_macro_deps: self |
| .make_deps( |
| &krate.common_attrs.proc_macro_deps, |
| &krate.common_attrs.extra_proc_macro_deps, |
| ) |
| .remap_configurations(platforms), |
| aliases: self |
| .make_aliases(krate, false, false) |
| .remap_configurations(platforms), |
| common: self.make_common_attrs(platforms, krate, target)?, |
| }) |
| } |
| |
| fn make_rust_library( |
| &self, |
| platforms: &Platforms, |
| krate: &CrateContext, |
| target: &TargetAttributes, |
| ) -> Result<RustLibrary> { |
| Ok(RustLibrary { |
| name: target.crate_name.clone(), |
| deps: self |
| .make_deps(&krate.common_attrs.deps, &krate.common_attrs.extra_deps) |
| .remap_configurations(platforms), |
| proc_macro_deps: self |
| .make_deps( |
| &krate.common_attrs.proc_macro_deps, |
| &krate.common_attrs.extra_proc_macro_deps, |
| ) |
| .remap_configurations(platforms), |
| aliases: self |
| .make_aliases(krate, false, false) |
| .remap_configurations(platforms), |
| common: self.make_common_attrs(platforms, krate, target)?, |
| }) |
| } |
| |
| fn make_rust_binary( |
| &self, |
| platforms: &Platforms, |
| krate: &CrateContext, |
| target: &TargetAttributes, |
| ) -> Result<RustBinary> { |
| Ok(RustBinary { |
| name: format!("{}__bin", target.crate_name), |
| deps: { |
| let mut deps = |
| self.make_deps(&krate.common_attrs.deps, &krate.common_attrs.extra_deps); |
| if let Some(library_target_name) = &krate.library_target_name { |
| deps.insert(format!(":{library_target_name}"), None); |
| } |
| deps.remap_configurations(platforms) |
| }, |
| proc_macro_deps: self |
| .make_deps( |
| &krate.common_attrs.proc_macro_deps, |
| &krate.common_attrs.extra_proc_macro_deps, |
| ) |
| .remap_configurations(platforms), |
| aliases: self |
| .make_aliases(krate, false, false) |
| .remap_configurations(platforms), |
| common: self.make_common_attrs(platforms, krate, target)?, |
| }) |
| } |
| |
| fn make_common_attrs( |
| &self, |
| platforms: &Platforms, |
| krate: &CrateContext, |
| target: &TargetAttributes, |
| ) -> Result<CommonAttrs> { |
| Ok(CommonAttrs { |
| compile_data: make_data( |
| platforms, |
| &krate.common_attrs.compile_data_glob, |
| &krate.common_attrs.compile_data, |
| ), |
| crate_features: krate.common_attrs.crate_features.clone(), |
| crate_root: target.crate_root.clone(), |
| data: make_data( |
| platforms, |
| &krate.common_attrs.data_glob, |
| &krate.common_attrs.data, |
| ), |
| edition: krate.common_attrs.edition.clone(), |
| linker_script: krate.common_attrs.linker_script.clone(), |
| rustc_env: krate |
| .common_attrs |
| .rustc_env |
| .clone() |
| .remap_configurations(platforms), |
| rustc_env_files: krate |
| .common_attrs |
| .rustc_env_files |
| .clone() |
| .remap_configurations(platforms), |
| rustc_flags: { |
| let mut rustc_flags = krate.common_attrs.rustc_flags.clone(); |
| // In most cases, warnings in 3rd party crates are not |
| // interesting as they're out of the control of consumers. The |
| // flag here silences warnings. For more details see: |
| // https://doc.rust-lang.org/rustc/lints/levels.html |
| rustc_flags.insert(0, "--cap-lints=allow".to_owned()); |
| rustc_flags |
| }, |
| srcs: target.srcs.clone(), |
| tags: { |
| let mut tags = BTreeSet::from_iter(krate.common_attrs.tags.iter().cloned()); |
| tags.insert("cargo-bazel".to_owned()); |
| tags.insert("manual".to_owned()); |
| tags.insert("noclippy".to_owned()); |
| tags.insert("norustfmt".to_owned()); |
| tags |
| }, |
| version: krate.common_attrs.version.clone(), |
| }) |
| } |
| |
| /// Filter a crate's dependencies to only ones with aliases |
| fn make_aliases( |
| &self, |
| krate: &CrateContext, |
| build: bool, |
| include_dev: bool, |
| ) -> SelectDict<String> { |
| let mut dep_lists = Vec::new(); |
| if build { |
| if let Some(build_script_attrs) = &krate.build_script_attrs { |
| dep_lists.push(&build_script_attrs.deps); |
| dep_lists.push(&build_script_attrs.proc_macro_deps); |
| } |
| } else { |
| dep_lists.push(&krate.common_attrs.deps); |
| dep_lists.push(&krate.common_attrs.proc_macro_deps); |
| if include_dev { |
| dep_lists.push(&krate.common_attrs.deps_dev); |
| dep_lists.push(&krate.common_attrs.proc_macro_deps_dev); |
| } |
| } |
| |
| let mut aliases = SelectDict::default(); |
| for (dep, conf) in dep_lists.into_iter().flat_map(|deps| { |
| deps.configurations().into_iter().flat_map(move |conf| { |
| deps.get_iter(conf) |
| .expect("Iterating over known keys should never panic") |
| .map(move |dep| (dep, conf)) |
| }) |
| }) { |
| if let Some(alias) = &dep.alias { |
| let label = self.crate_label(&dep.id.name, &dep.id.version, &dep.target); |
| aliases.insert(label, alias.clone(), conf.cloned()); |
| } |
| } |
| aliases |
| } |
| |
| fn make_deps( |
| &self, |
| deps: &SelectList<CrateDependency>, |
| extra_deps: &BTreeSet<String>, |
| ) -> SelectList<String> { |
| let mut deps = deps |
| .clone() |
| .map(|dep| self.crate_label(&dep.id.name, &dep.id.version, &dep.target)); |
| for extra_dep in extra_deps { |
| deps.insert(extra_dep.clone(), None); |
| } |
| deps |
| } |
| |
| fn render_vendor_support_files(&self, context: &Context) -> Result<BTreeMap<PathBuf, String>> { |
| let module_label = render_module_label(&self.config.crates_module_template, "crates.bzl") |
| .context("Failed to resolve string to module file label")?; |
| |
| let mut map = BTreeMap::new(); |
| map.insert( |
| Renderer::label_to_path(&module_label), |
| self.engine.render_vendor_module_file(context)?, |
| ); |
| |
| Ok(map) |
| } |
| |
| fn label_to_path(label: &Label) -> PathBuf { |
| match &label.package { |
| Some(package) => PathBuf::from(format!("{}/{}", package, label.target)), |
| None => PathBuf::from(&label.target), |
| } |
| } |
| |
| fn crate_label(&self, name: &str, version: &str, target: &str) -> String { |
| sanitize_repository_name(&render_crate_bazel_label( |
| &self.config.crate_label_template, |
| &self.config.repository_name, |
| name, |
| version, |
| target, |
| )) |
| } |
| } |
| |
| /// Write a set of [CrateContext][crate::context::CrateContext] to disk. |
| pub fn write_outputs( |
| outputs: BTreeMap<PathBuf, String>, |
| out_dir: &Path, |
| dry_run: bool, |
| ) -> Result<()> { |
| let outputs: BTreeMap<PathBuf, String> = outputs |
| .into_iter() |
| .map(|(path, content)| (out_dir.join(path), content)) |
| .collect(); |
| |
| if dry_run { |
| for (path, content) in outputs { |
| println!( |
| "===============================================================================" |
| ); |
| println!("{}", path.display()); |
| println!( |
| "===============================================================================" |
| ); |
| println!("{content}\n"); |
| } |
| } else { |
| for (path, content) in outputs { |
| // Ensure the output directory exists |
| fs::create_dir_all( |
| path.parent() |
| .expect("All file paths should have valid directories"), |
| )?; |
| |
| fs::write(&path, content.as_bytes()) |
| .context(format!("Failed to write file to disk: {}", path.display()))?; |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Render the Bazel label of a crate |
| pub fn render_crate_bazel_label( |
| template: &str, |
| repository_name: &str, |
| name: &str, |
| version: &str, |
| target: &str, |
| ) -> String { |
| template |
| .replace("{repository}", repository_name) |
| .replace("{name}", name) |
| .replace("{version}", version) |
| .replace("{target}", target) |
| } |
| |
| /// Render the Bazel label of a crate |
| pub fn render_crate_bazel_repository( |
| template: &str, |
| repository_name: &str, |
| name: &str, |
| version: &str, |
| ) -> String { |
| template |
| .replace("{repository}", repository_name) |
| .replace("{name}", name) |
| .replace("{version}", version) |
| } |
| |
| /// Render the Bazel label of a crate |
| pub fn render_crate_build_file(template: &str, name: &str, version: &str) -> String { |
| template |
| .replace("{name}", name) |
| .replace("{version}", version) |
| } |
| |
| /// Render the Bazel label of a vendor module label |
| pub fn render_module_label(template: &str, name: &str) -> Result<Label> { |
| Label::from_str(&template.replace("{file}", name)) |
| } |
| |
| /// Render the Bazel label of a platform triple |
| fn render_platform_constraint_label(template: &str, triple: &str) -> String { |
| template.replace("{triple}", triple) |
| } |
| |
| fn render_build_file_template(template: &str, name: &str, version: &str) -> Result<Label> { |
| Label::from_str( |
| &template |
| .replace("{name}", name) |
| .replace("{version}", version), |
| ) |
| } |
| |
| fn make_data(platforms: &Platforms, glob: &BTreeSet<String>, select: &SelectList<String>) -> Data { |
| const COMMON_GLOB_EXCLUDES: &[&str] = &[ |
| "**/* *", |
| "BUILD.bazel", |
| "BUILD", |
| "WORKSPACE.bazel", |
| "WORKSPACE", |
| ]; |
| |
| Data { |
| glob: Glob { |
| include: glob.clone(), |
| exclude: COMMON_GLOB_EXCLUDES |
| .iter() |
| .map(|&glob| glob.to_owned()) |
| .collect(), |
| }, |
| select: select.clone().remap_configurations(platforms), |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| use indoc::indoc; |
| |
| use crate::config::{Config, CrateId, VendorMode}; |
| use crate::context::crate_context::{CrateContext, Rule}; |
| use crate::context::{BuildScriptAttributes, CommonAttributes, Context, TargetAttributes}; |
| use crate::metadata::Annotations; |
| use crate::test; |
| |
| fn mock_render_config() -> RenderConfig { |
| serde_json::from_value(serde_json::json!({ |
| "repository_name": "test_rendering", |
| "regen_command": "cargo_bazel_regen_command", |
| })) |
| .unwrap() |
| } |
| |
| fn mock_target_attributes() -> TargetAttributes { |
| TargetAttributes { |
| crate_name: "mock_crate".to_owned(), |
| crate_root: Some("src/root.rs".to_owned()), |
| ..TargetAttributes::default() |
| } |
| } |
| |
| #[test] |
| fn render_rust_library() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::Library(mock_target_attributes())]), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| let renderer = Renderer::new(mock_render_config()); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output |
| .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel")) |
| .unwrap(); |
| |
| assert!(build_file_content.contains("rust_library(")); |
| assert!(build_file_content.contains("name = \"mock_crate\"")); |
| } |
| |
| #[test] |
| fn render_cargo_build_script() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::BuildScript(TargetAttributes { |
| crate_name: "build_script_build".to_owned(), |
| crate_root: Some("build.rs".to_owned()), |
| ..TargetAttributes::default() |
| })]), |
| // Build script attributes are required. |
| build_script_attrs: Some(BuildScriptAttributes::default()), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| let renderer = Renderer::new(mock_render_config()); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output |
| .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel")) |
| .unwrap(); |
| |
| assert!(build_file_content.contains("cargo_build_script(")); |
| assert!(build_file_content.contains("name = \"build_script_build\"")); |
| |
| // Ensure `cargo_build_script` requirements are met |
| assert!(build_file_content.contains("name = \"mock_crate_build_script\"")); |
| } |
| |
| #[test] |
| fn render_proc_macro() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::ProcMacro(mock_target_attributes())]), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| let renderer = Renderer::new(mock_render_config()); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output |
| .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel")) |
| .unwrap(); |
| |
| assert!(build_file_content.contains("rust_proc_macro(")); |
| assert!(build_file_content.contains("name = \"mock_crate\"")); |
| } |
| |
| #[test] |
| fn render_binary() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::Binary(mock_target_attributes())]), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| let renderer = Renderer::new(mock_render_config()); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output |
| .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel")) |
| .unwrap(); |
| |
| assert!(build_file_content.contains("rust_binary(")); |
| assert!(build_file_content.contains("name = \"mock_crate__bin\"")); |
| } |
| |
| #[test] |
| fn render_additive_build_contents() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::Binary(mock_target_attributes())]), |
| additive_build_file_content: Some( |
| "# Hello World from additive section!".to_owned(), |
| ), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| let renderer = Renderer::new(mock_render_config()); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output |
| .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel")) |
| .unwrap(); |
| |
| assert!(build_file_content.contains("# Hello World from additive section!")); |
| } |
| |
| #[test] |
| fn render_aliases() { |
| let config = Config { |
| generate_binaries: true, |
| ..Config::default() |
| }; |
| let annotations = |
| Annotations::new(test::metadata::alias(), test::lockfile::alias(), config).unwrap(); |
| let context = Context::new(annotations).unwrap(); |
| |
| let renderer = Renderer::new(mock_render_config()); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output.get(&PathBuf::from("BUILD.bazel")).unwrap(); |
| |
| assert!(build_file_content.contains(r#"name = "names-0.12.1-dev__names","#)); |
| assert!(build_file_content.contains(r#"name = "names-0.13.0__names","#)); |
| } |
| |
| #[test] |
| fn render_crate_repositories() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::Library(mock_target_attributes())]), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| let renderer = Renderer::new(mock_render_config()); |
| let output = renderer.render(&context).unwrap(); |
| |
| let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap(); |
| |
| assert!(defs_module.contains("def crate_repositories():")); |
| } |
| |
| #[test] |
| fn remote_remote_vendor_mode() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::Library(mock_target_attributes())]), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| // Enable remote vendor mode |
| let config = RenderConfig { |
| vendor_mode: Some(VendorMode::Remote), |
| ..mock_render_config() |
| }; |
| |
| let renderer = Renderer::new(config); |
| let output = renderer.render(&context).unwrap(); |
| |
| let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap(); |
| assert!(defs_module.contains("def crate_repositories():")); |
| |
| let crates_module = output.get(&PathBuf::from("crates.bzl")).unwrap(); |
| assert!(crates_module.contains("def crate_repositories():")); |
| } |
| |
| #[test] |
| fn remote_local_vendor_mode() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::Library(mock_target_attributes())]), |
| ..CrateContext::default() |
| }, |
| ); |
| |
| // Enable local vendor mode |
| let config = RenderConfig { |
| vendor_mode: Some(VendorMode::Local), |
| ..mock_render_config() |
| }; |
| |
| let renderer = Renderer::new(config); |
| let output = renderer.render(&context).unwrap(); |
| |
| // Local vendoring does not produce a `crate_repositories` macro |
| let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap(); |
| assert!(!defs_module.contains("def crate_repositories():")); |
| |
| // Local vendoring does not produce a `crates.bzl` file. |
| assert!(output.get(&PathBuf::from("crates.bzl")).is_none()); |
| } |
| |
| #[test] |
| fn duplicate_rustc_flags() { |
| let mut context = Context::default(); |
| let crate_id = CrateId::new("mock_crate".to_owned(), "0.1.0".to_owned()); |
| |
| let rustc_flags = vec![ |
| "-l".to_owned(), |
| "dylib=ssl".to_owned(), |
| "-l".to_owned(), |
| "dylib=crypto".to_owned(), |
| ]; |
| |
| context.crates.insert( |
| crate_id.clone(), |
| CrateContext { |
| name: crate_id.name, |
| version: crate_id.version, |
| targets: BTreeSet::from([Rule::Library(mock_target_attributes())]), |
| common_attrs: CommonAttributes { |
| rustc_flags: rustc_flags.clone(), |
| ..CommonAttributes::default() |
| }, |
| ..CrateContext::default() |
| }, |
| ); |
| |
| // Enable local vendor mode |
| let config = RenderConfig { |
| vendor_mode: Some(VendorMode::Local), |
| ..mock_render_config() |
| }; |
| |
| let renderer = Renderer::new(config); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output |
| .get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel")) |
| .unwrap(); |
| |
| // Strip all spaces from the generated BUILD file and ensure it has the flags |
| // represented by `rustc_flags` in the same order. |
| assert!(build_file_content.replace(' ', "").contains( |
| &rustc_flags |
| .iter() |
| .map(|s| format!("\"{s}\",")) |
| .collect::<Vec<String>>() |
| .join("\n") |
| )); |
| } |
| |
| #[test] |
| fn test_render_build_file_deps() { |
| let config: Config = serde_json::from_value(serde_json::json!({ |
| "generate_binaries": false, |
| "generate_build_scripts": false, |
| "rendering": { |
| "repository_name": "multi_cfg_dep", |
| "regen_command": "bazel test //crate_universe:unit_test", |
| }, |
| "supported_platform_triples": [ |
| "x86_64-apple-darwin", |
| "x86_64-unknown-linux-gnu", |
| "aarch64-apple-darwin", |
| "aarch64-unknown-linux-gnu", |
| ], |
| })) |
| .unwrap(); |
| let metadata = test::metadata::multi_cfg_dep(); |
| let lockfile = test::lockfile::multi_cfg_dep(); |
| |
| let annotations = Annotations::new(metadata, lockfile, config.clone()).unwrap(); |
| let context = Context::new(annotations).unwrap(); |
| |
| let renderer = Renderer::new(config.rendering); |
| let output = renderer.render(&context).unwrap(); |
| |
| let build_file_content = output |
| .get(&PathBuf::from("BUILD.cpufeatures-0.2.1.bazel")) |
| .unwrap(); |
| |
| // This is unfortunately somewhat brittle. Alas. Ultimately we wish to demonstrate that the |
| // original cfg(...) strings are preserved in the `deps` list for ease of debugging. |
| let expected = indoc! {r#" |
| deps = select({ |
| "@rules_rust//rust/platform:aarch64-apple-darwin": [ |
| "@multi_cfg_dep__libc-0.2.117//:libc", # aarch64-apple-darwin |
| ], |
| "@rules_rust//rust/platform:aarch64-unknown-linux-gnu": [ |
| "@multi_cfg_dep__libc-0.2.117//:libc", # cfg(all(target_arch = "aarch64", target_os = "linux")) |
| ], |
| "//conditions:default": [], |
| }), |
| "#}; |
| |
| assert!( |
| build_file_content.contains(&expected.replace('\n', "\n ")), |
| "{}", |
| build_file_content, |
| ); |
| } |
| } |