//! Tools for rendering and writing BUILD and other Starlark files mod template_engine; use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::PathBuf; use std::str::FromStr; use anyhow::{bail, Context as AnyhowContext, Result}; use itertools::Itertools; use crate::config::{AliasRule, RenderConfig, VendorMode}; use crate::context::crate_context::{CrateContext, CrateDependency, Rule}; use crate::context::{Context, TargetAttributes}; use crate::rendering::template_engine::TemplateEngine; use crate::select::Select; 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, SelectDict, SelectList, SelectScalar, SelectSet, Starlark, TargetCompatibleWith, }; use crate::utils::target_triple::TargetTriple; 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". pub(crate) type Platforms = BTreeMap>; pub(crate) struct Renderer { config: RenderConfig, supported_platform_triples: BTreeSet, engine: TemplateEngine, } impl Renderer { pub(crate) fn new( config: RenderConfig, supported_platform_triples: BTreeSet, ) -> Self { let engine = TemplateEngine::new(&config); Self { config, supported_platform_triples, engine, } } pub(crate) fn render(&self, context: &Context) -> Result> { 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, &platforms)?); 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> { context .conditions .iter() .map(|(cfg, target_triples)| { ( cfg.clone(), target_triples .iter() .map(|target_triple| { render_platform_constraint_label( &self.config.platforms_template, target_triple, ) }) .collect(), ) }) .collect() } fn render_crates_module( &self, context: &Context, platforms: &Platforms, ) -> Result> { 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 module_alias_rules_label = render_module_label(&self.config.crates_module_template, "alias_rules.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_module_bzl(context, platforms)?, ); map.insert( Renderer::label_to_path(&module_build_label), self.render_module_build_file(context)?, ); map.insert( Renderer::label_to_path(&module_alias_rules_label), include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/src/rendering/verbatim/alias_rules.bzl" )) .to_owned(), ); Ok(map) } fn render_module_build_file(&self, context: &Context) -> Result { let mut starlark = Vec::new(); // Banner comment for top of the file. let header = self.engine.render_header()?; starlark.push(Starlark::Verbatim(header)); // Load any `alias_rule`s. let mut loads: BTreeMap> = BTreeMap::new(); for alias_rule in Iterator::chain( std::iter::once(&self.config.default_alias_rule), context .workspace_member_deps() .iter() .flat_map(|dep| &context.crates[&dep.id].alias_rule), ) { if let Some(bzl) = alias_rule.bzl() { loads.entry(bzl).or_default().insert(alias_rule.rule()); } } for (bzl, items) in loads { starlark.push(Starlark::Load(Load { bzl, items })) } // Package visibility, exported bzl files. let package = Package::default_visibility_public(BTreeSet::new()); starlark.push(Starlark::Package(package)); let mut exports_files = ExportsFiles { paths: BTreeSet::from(["cargo-bazel.json".to_owned(), "defs.bzl".to_owned()]), globs: Glob { allow_empty: true, 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 { allow_empty: true, 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]; let alias_rule = krate .alias_rule .as_ref() .unwrap_or(&self.config.default_alias_rule); if let Some(library_target_name) = &krate.library_target_name { let rename = dep.alias.as_ref().unwrap_or(&krate.name); dependencies.push(Alias { rule: alias_rule.rule(), // 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.to_string(), library_target_name, ), tags: BTreeSet::from(["manual".to_owned()]), }); } for (alias, target) in &krate.extra_aliased_targets { dependencies.push(Alias { rule: alias_rule.rule(), name: alias.clone(), actual: self.crate_label(&krate.name, &krate.version.to_string(), target), tags: BTreeSet::from(["manual".to_owned()]), }); } } let duplicates: Vec<_> = dependencies .iter() .map(|alias| &alias.name) .duplicates() .sorted() .collect(); assert!( duplicates.is_empty(), "Found duplicate aliases that must be changed (Check your `extra_aliased_targets`): {:#?}", duplicates ); 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 { rule: AliasRule::default().rule(), // 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.to_string(), &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> { 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.to_string(), ) { 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 { let mut starlark = Vec::new(); // Banner comment for top of the file. let header = self.engine.render_header()?; starlark.push(Starlark::Verbatim(header)); // Loads: map of bzl file to set of items imported from that file. These // get inserted into `starlark` at the bottom of this function. let mut loads: BTreeMap> = BTreeMap::new(); let mut load = |bzl: &str, item: &str| { loads .entry(bzl.to_owned()) .or_default() .insert(item.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()]), })); if self.config.generate_rules_license_metadata { let has_license_ids = !krate.license_ids.is_empty(); let mut package_metadata = BTreeSet::from([Label::Relative { target: "package_info".to_owned(), }]); starlark.push(Starlark::Load(Load { bzl: "@rules_license//rules:package_info.bzl".to_owned(), items: BTreeSet::from(["package_info".to_owned()]), })); if has_license_ids { starlark.push(Starlark::Load(Load { bzl: "@rules_license//rules:license.bzl".to_owned(), items: BTreeSet::from(["license".to_owned()]), })); package_metadata.insert(Label::Relative { target: "license".to_owned(), }); } let package = Package::default_visibility_public(package_metadata); starlark.push(Starlark::Package(package)); starlark.push(Starlark::PackageInfo(starlark::PackageInfo { name: "package_info".to_owned(), package_name: krate.name.clone(), package_url: krate.package_url.clone().unwrap_or_default(), package_version: krate.version.to_string(), })); if has_license_ids { let mut license_kinds = BTreeSet::new(); krate.license_ids.clone().into_iter().for_each(|lic| { license_kinds.insert("@rules_license//licenses/spdx:".to_owned() + &lic); }); starlark.push(Starlark::License(starlark::License { name: "license".to_owned(), license_kinds, license_text: krate.license_file.clone().unwrap_or_default(), })); } } else { // Package visibility. let package = Package::default_visibility_public(BTreeSet::new()); starlark.push(Starlark::Package(package)); } for rule in &krate.targets { if let Some(override_target) = krate.override_targets.get(rule.override_target_key()) { starlark.push(Starlark::Alias(Alias { rule: AliasRule::default().rule(), name: rule.crate_name().to_owned(), actual: override_target.clone(), tags: BTreeSet::from(["manual".to_owned()]), })); } else { match rule { Rule::BuildScript(target) => { load("@rules_rust//cargo:defs.bzl", "cargo_build_script"); let cargo_build_script = self.make_cargo_build_script(platforms, krate, target)?; starlark.push(Starlark::CargoBuildScript(cargo_build_script)); starlark.push(Starlark::Alias(Alias { rule: AliasRule::default().rule(), name: target.crate_name.clone(), actual: Label::from_str("_bs").unwrap(), tags: BTreeSet::from(["manual".to_owned()]), })); } Rule::ProcMacro(target) => { load("@rules_rust//rust:defs.bzl", "rust_proc_macro"); let rust_proc_macro = self.make_rust_proc_macro(platforms, krate, target)?; starlark.push(Starlark::RustProcMacro(rust_proc_macro)); } Rule::Library(target) => { load("@rules_rust//rust:defs.bzl", "rust_library"); let rust_library = self.make_rust_library(platforms, krate, target)?; starlark.push(Starlark::RustLibrary(rust_library)); } Rule::Binary(target) => { load("@rules_rust//rust:defs.bzl", "rust_binary"); 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())); } // Insert all the loads immediately after the header banner comment. let loads = loads .into_iter() .map(|(bzl, items)| Starlark::Load(Load { bzl, items })); starlark.splice(1..1, loads); let starlark = starlark::serialize(&starlark)?; Ok(starlark) } fn make_cargo_build_script( &self, platforms: &Platforms, krate: &CrateContext, target: &TargetAttributes, ) -> Result { 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". // // This is set to a short name to avoid long path name issues on windows. name: "_bs".to_string(), aliases: SelectDict::new(self.make_aliases(krate, true, false), platforms), build_script_env: SelectDict::new( attrs .map(|attrs| attrs.build_script_env.clone()) .unwrap_or_default(), platforms, ), compile_data: make_data( platforms, Default::default(), attrs .map(|attrs| attrs.compile_data.clone()) .unwrap_or_default(), ), crate_features: SelectSet::new(krate.common_attrs.crate_features.clone(), platforms), crate_name: utils::sanitize_module_name(&target.crate_name), crate_root: target.crate_root.clone(), data: make_data( platforms, attrs .map(|attrs| attrs.data_glob.clone()) .unwrap_or_default(), attrs.map(|attrs| attrs.data.clone()).unwrap_or_default(), ), deps: SelectSet::new( self.make_deps( attrs.map(|attrs| attrs.deps.clone()).unwrap_or_default(), attrs .map(|attrs| attrs.extra_deps.clone()) .unwrap_or_default(), ), platforms, ), link_deps: SelectSet::new( self.make_deps( attrs .map(|attrs| attrs.link_deps.clone()) .unwrap_or_default(), attrs .map(|attrs| attrs.extra_link_deps.clone()) .unwrap_or_default(), ), platforms, ), edition: krate.common_attrs.edition.clone(), linker_script: krate.common_attrs.linker_script.clone(), links: attrs.and_then(|attrs| attrs.links.clone()), pkg_name: Some(krate.name.clone()), proc_macro_deps: SelectSet::new( self.make_deps( attrs .map(|attrs| attrs.proc_macro_deps.clone()) .unwrap_or_default(), attrs .map(|attrs| attrs.extra_proc_macro_deps.clone()) .unwrap_or_default(), ), platforms, ), rundir: SelectScalar::new( attrs.map(|attrs| attrs.rundir.clone()).unwrap_or_default(), platforms, ), rustc_env: SelectDict::new( attrs .map(|attrs| attrs.rustc_env.clone()) .unwrap_or_default(), platforms, ), rustc_env_files: SelectSet::new( attrs .map(|attrs| attrs.rustc_env_files.clone()) .unwrap_or_default(), platforms, ), rustc_flags: SelectList::new( // 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 Select::merge( Select::from_value(Vec::from(["--cap-lints=allow".to_owned()])), attrs .map(|attrs| attrs.rustc_flags.clone()) .unwrap_or_default(), ), 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.insert(format!("crate-name={}", krate.name)); tags }, tools: SelectSet::new( attrs.map(|attrs| attrs.tools.clone()).unwrap_or_default(), 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 { Ok(RustProcMacro { name: target.crate_name.clone(), deps: SelectSet::new( self.make_deps( krate.common_attrs.deps.clone(), krate.common_attrs.extra_deps.clone(), ), platforms, ), proc_macro_deps: SelectSet::new( self.make_deps( krate.common_attrs.proc_macro_deps.clone(), krate.common_attrs.extra_proc_macro_deps.clone(), ), platforms, ), aliases: SelectDict::new(self.make_aliases(krate, false, false), platforms), common: self.make_common_attrs(platforms, krate, target)?, }) } fn make_rust_library( &self, platforms: &Platforms, krate: &CrateContext, target: &TargetAttributes, ) -> Result { Ok(RustLibrary { name: target.crate_name.clone(), deps: SelectSet::new( self.make_deps( krate.common_attrs.deps.clone(), krate.common_attrs.extra_deps.clone(), ), platforms, ), proc_macro_deps: SelectSet::new( self.make_deps( krate.common_attrs.proc_macro_deps.clone(), krate.common_attrs.extra_proc_macro_deps.clone(), ), platforms, ), aliases: SelectDict::new(self.make_aliases(krate, false, false), platforms), common: self.make_common_attrs(platforms, krate, target)?, disable_pipelining: krate.disable_pipelining, }) } fn make_rust_binary( &self, platforms: &Platforms, krate: &CrateContext, target: &TargetAttributes, ) -> Result { Ok(RustBinary { name: format!("{}__bin", target.crate_name), deps: { let mut deps = self.make_deps( krate.common_attrs.deps.clone(), krate.common_attrs.extra_deps.clone(), ); if let Some(library_target_name) = &krate.library_target_name { deps.insert( Label::from_str(&format!(":{library_target_name}")).unwrap(), None, ); } SelectSet::new(deps, platforms) }, proc_macro_deps: SelectSet::new( self.make_deps( krate.common_attrs.proc_macro_deps.clone(), krate.common_attrs.extra_proc_macro_deps.clone(), ), platforms, ), aliases: SelectDict::new(self.make_aliases(krate, false, false), platforms), common: self.make_common_attrs(platforms, krate, target)?, }) } fn make_common_attrs( &self, platforms: &Platforms, krate: &CrateContext, target: &TargetAttributes, ) -> Result { Ok(CommonAttrs { compile_data: make_data( platforms, krate.common_attrs.compile_data_glob.clone(), krate.common_attrs.compile_data.clone(), ), crate_features: SelectSet::new(krate.common_attrs.crate_features.clone(), platforms), crate_root: target.crate_root.clone(), data: make_data( platforms, krate.common_attrs.data_glob.clone(), krate.common_attrs.data.clone(), ), edition: krate.common_attrs.edition.clone(), linker_script: krate.common_attrs.linker_script.clone(), rustc_env: SelectDict::new(krate.common_attrs.rustc_env.clone(), platforms), rustc_env_files: SelectSet::new(krate.common_attrs.rustc_env_files.clone(), platforms), rustc_flags: SelectList::new( // 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 Select::merge( Select::from_value(Vec::from(["--cap-lints=allow".to_owned()])), krate.common_attrs.rustc_flags.clone(), ), 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.insert(format!("crate-name={}", krate.name)); tags }, target_compatible_with: self.config.generate_target_compatible_with.then(|| { TargetCompatibleWith::new( self.supported_platform_triples .iter() .map(|target_triple| { render_platform_constraint_label( &self.config.platforms_template, target_triple, ) }) .collect(), ) }), 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, ) -> Select> { let mut dependency_selects = Vec::new(); if build { if let Some(build_script_attrs) = &krate.build_script_attrs { dependency_selects.push(&build_script_attrs.deps); dependency_selects.push(&build_script_attrs.proc_macro_deps); } } else { dependency_selects.push(&krate.common_attrs.deps); dependency_selects.push(&krate.common_attrs.proc_macro_deps); if include_dev { dependency_selects.push(&krate.common_attrs.deps_dev); dependency_selects.push(&krate.common_attrs.proc_macro_deps_dev); } } let mut aliases: Select> = Select::default(); for dependency_select in dependency_selects.iter() { for (configuration, dependency) in dependency_select.items().into_iter() { if let Some(alias) = &dependency.alias { let label = self.crate_label( &dependency.id.name, &dependency.id.version.to_string(), &dependency.target, ); aliases.insert((label, alias.clone()), configuration.clone()); } } } aliases } fn make_deps( &self, deps: Select>, extra_deps: Select>, ) -> Select> { Select::merge( deps.map(|dep| { self.crate_label(&dep.id.name, &dep.id.version.to_string(), &dep.target) }), extra_deps, ) } fn render_vendor_support_files(&self, context: &Context) -> Result> { 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) if !package.is_empty() => { PathBuf::from(format!("{}/{}", package, label.target())) } Some(_) | None => PathBuf::from(label.target()), } } fn crate_label(&self, name: &str, version: &str, target: &str) -> Label { Label::from_str(&sanitize_repository_name(&render_crate_bazel_label( &self.config.crate_label_template, &self.config.repository_name, name, version, target, ))) .unwrap() } } /// Write a set of [crate::context::crate_context::CrateContext] to disk. pub(crate) fn write_outputs(outputs: BTreeMap, dry_run: bool) -> Result<()> { 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(crate) 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(crate) 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(crate) 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(crate) fn render_module_label(template: &str, name: &str) -> Result