blob: 2872851dcc794d58d4b79af6c6c9e91c69392db1 [file]
//! 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 std::sync::Arc;
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::metadata::SourceAnnotation;
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, CargoTomlEnvVars, 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<String, BTreeSet<String>>;
/// A single hub alias — the `alias()` target and the `alias_rule` it uses.
struct HubAlias {
alias_rule: AliasRule,
alias: Alias,
}
/// Every top-level alias the hub repository would emit, split by source so
/// the renderer can preserve the existing "# Workspace Member Dependencies"
/// / "# Binaries" comment sections when rendering into the flat root BUILD.
struct HubAliases {
workspace_member: Vec<HubAlias>,
binaries: Vec<HubAlias>,
}
/// The renderer's output. `files` is the map ready for [`write_outputs`];
/// `hub_packages` lists the per-alias subpackage names so the bzlmod
/// extension can read each `<name>/BUILD.bazel` from disk via a sidecar
/// (it cannot enumerate directories).
pub(crate) struct RenderedHub {
pub(crate) files: BTreeMap<PathBuf, String>,
pub(crate) hub_packages: Vec<String>,
}
pub(crate) struct Renderer {
config: Arc<RenderConfig>,
supported_platform_triples: Arc<BTreeSet<TargetTriple>>,
}
impl Renderer {
pub(crate) fn new(
config: Arc<RenderConfig>,
supported_platform_triples: Arc<BTreeSet<TargetTriple>>,
) -> Self {
Self {
config,
supported_platform_triples,
}
}
pub(crate) fn render(
&self,
context: &Context,
generator: Option<Label>,
) -> Result<BTreeMap<PathBuf, String>> {
Ok(self.render_hub(context, generator)?.files)
}
/// Like [`render`], but also returns the names of the per-alias hub
/// subpackages produced. Tests usually want just the files and call
/// [`render`].
pub(crate) fn render_hub(
&self,
context: &Context,
generator: Option<Label>,
) -> Result<RenderedHub> {
let conditions = Arc::new(context.conditions.clone());
let engine = self.create_engine(Arc::clone(&conditions));
let aliases = self.collect_hub_aliases(context)?;
let mut files = BTreeMap::new();
let platforms = self.render_platform_labels(conditions);
files.extend(self.render_build_files(&engine, context, &platforms)?);
files.extend(self.render_crates_module(&engine, context, &platforms, generator, &aliases)?);
let hub_packages = aliases
.workspace_member
.iter()
.chain(aliases.binaries.iter())
.map(|entry| entry.alias.name.clone())
.collect();
Ok(RenderedHub {
files,
hub_packages,
})
}
pub(crate) fn create_engine(
&self,
conditions: Arc<BTreeMap<String, BTreeSet<TargetTriple>>>,
) -> TemplateEngine {
TemplateEngine::new(
Arc::clone(&self.config),
Arc::clone(&self.supported_platform_triples),
Arc::clone(&conditions),
)
}
pub(crate) fn render_platform_labels(
&self,
conditions: Arc<BTreeMap<String, BTreeSet<TargetTriple>>>,
) -> BTreeMap<String, BTreeSet<String>> {
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,
engine: &TemplateEngine,
context: &Context,
platforms: &Platforms,
generator: Option<Label>,
aliases: &HubAliases,
) -> Result<BTreeMap<PathBuf, String>> {
let path = |name: &str| -> Result<PathBuf> {
let label = render_module_label(&self.config.crates_module_template, name)
.context("Failed to resolve string to module file label")?;
Ok(Renderer::label_to_path(&label))
};
let mut map = BTreeMap::from([
(
path("crates.bzl")?,
engine.render_crates_bzl(context, platforms, generator)?,
),
(path("defs.bzl")?, engine.render_defs_bzl_shim()?),
(
path("BUILD.bazel")?,
self.render_module_build_file(engine, aliases)?,
),
(
path("alias_rules.bzl")?,
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/rendering/verbatim/alias_rules.bzl"
))
.to_owned(),
),
]);
// Per-alias subpackage `BUILD.bazel`s let consumers migrate to
// `@<repo>//<alias>` independently of the hub owner flipping
// `incompatible_no_root_alias_targets`. Skip in local-vendor mode,
// where the per-crate paths `<output_pkg>/<name>-<version>/BUILD.bazel`
// are already occupied by the vendored `rust_library` `BUILD.bazel`s.
if self.config.vendor_mode != Some(VendorMode::Local) {
map.extend(self.render_alias_subpackages(engine, aliases)?);
}
Ok(map)
}
/// Render one `<alias>/BUILD.bazel` per hub alias. Each subpackage holds
/// a single `alias()` (plus any `load()` its `alias_rule` requires) so
/// that `@<repo>//<alias>` resolves to that alias as the package's
/// default target.
fn render_alias_subpackages(
&self,
engine: &TemplateEngine,
aliases: &HubAliases,
) -> Result<BTreeMap<PathBuf, String>> {
let header = engine.render_header()?;
let mut map = BTreeMap::new();
for entry in aliases
.workspace_member
.iter()
.chain(aliases.binaries.iter())
{
let mut starlark = vec![Starlark::Verbatim(header.clone())];
if let Some(bzl) = entry.alias_rule.bzl() {
starlark.push(Starlark::Load(Load {
bzl,
items: BTreeSet::from([entry.alias_rule.rule()]),
}));
}
starlark.push(Starlark::Package(Package::default_visibility_public(
BTreeSet::new(),
)));
starlark.push(Starlark::Alias(entry.alias.clone()));
let subpackage_label = render_module_label(
&self.config.crates_module_template,
&format!("{}/BUILD.bazel", entry.alias.name),
)
.context("Failed to resolve subpackage BUILD file label")?;
map.insert(
Renderer::label_to_path(&subpackage_label),
starlark::serialize(&starlark)?,
);
}
Ok(map)
}
fn render_module_build_file(
&self,
engine: &TemplateEngine,
aliases: &HubAliases,
) -> Result<String> {
let mut starlark = Vec::new();
// Banner comment for top of the file.
let header = engine.render_header()?;
starlark.push(Starlark::Verbatim(header));
// Load any `alias_rule`s referenced by the hub aliases. Per-alias
// subpackage `BUILD.bazel`s each carry their own scoped load, so when
// `incompatible_no_root_alias_targets` is on the root needs no
// alias-rule loads.
if !self.config.incompatible_no_root_alias_targets {
let mut loads: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for alias_rule in std::iter::once(&self.config.default_alias_rule).chain(
aliases
.workspace_member
.iter()
.map(|entry| &entry.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 exports_files = ExportsFiles {
paths: BTreeSet::from([
"cargo-bazel.json".to_owned(),
"crates.bzl".to_owned(),
"defs.bzl".to_owned(),
]),
globs: Glob {
allow_empty: true,
include: BTreeSet::from(["*.bazel".to_owned()]),
exclude: BTreeSet::new(),
},
};
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));
// `incompatible_no_root_alias_targets` drops the duplicate root-level
// `alias()` rules. Subpackage `BUILD.bazel`s always exist, so consumers
// can still use `@<repo>//<name>` — the flag just stops emitting the
// parallel `@<repo>//:<name>` form.
if !self.config.incompatible_no_root_alias_targets {
if !aliases.workspace_member.is_empty() {
let comment = "# Workspace Member Dependencies".to_owned();
starlark.push(Starlark::Verbatim(comment));
starlark.extend(
aliases
.workspace_member
.iter()
.map(|entry| Starlark::Alias(entry.alias.clone())),
);
}
if !aliases.binaries.is_empty() {
let comment = "# Binaries".to_owned();
starlark.push(Starlark::Verbatim(comment));
starlark.extend(
aliases
.binaries
.iter()
.map(|entry| Starlark::Alias(entry.alias.clone())),
);
}
}
let starlark = starlark::serialize(&starlark)?;
Ok(starlark)
}
/// Collect every alias the hub repository would emit (workspace member
/// deps, version-disambiguated, renames, `extra_aliased_targets`, and
/// binary aliases).
fn collect_hub_aliases(&self, context: &Context) -> Result<HubAliases> {
let mut workspace_member = 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)
.clone();
if let Some(library_target_name) = &krate.library_target_name {
// Avoid adding the <crate_name>-<version> alias if there are
// more than 1 dependency referencing the same crate at the
// same version, but one of them is aliased.
//
// Without this check we would add duplicate aliases in
// scenarios like the following:
//
// itertools = "0.11.24"
// itertools_other = { version = "0.11.24", package = "itertools" }
//
let add_primary_alias = dep.alias.is_none()
|| !context.has_duplicate_workspace_member_dep_by_version(&dep);
if add_primary_alias {
workspace_member.push(HubAlias {
alias_rule: alias_rule.clone(),
alias: Alias {
rule: alias_rule.rule(),
name: format!("{}-{}", krate.name, krate.version),
actual: self.crate_label(
&krate.name,
&krate.version.to_string(),
library_target_name,
),
tags: BTreeSet::from(["manual".to_owned()]),
},
});
}
let shorthand = if let Some(rename) = dep.alias.as_ref() {
// when the alias is the same as the crate name, don't create the alias
if krate.name != *rename {
workspace_member.push(HubAlias {
alias_rule: alias_rule.clone(),
alias: Alias {
rule: alias_rule.rule(),
name: format!("{}-{}", rename, krate.version),
actual: self.crate_label(
&krate.name,
&krate.version.to_string(),
library_target_name,
),
tags: BTreeSet::from(["manual".to_owned()]),
},
});
}
rename
} else {
&krate.name
};
// Add a shorthand for crate names as long as there isn't a duplicate
// entry. Shorthands for duplicate entries would lead to ambiguous
// dependencies.
if !context.has_duplicate_workspace_member_dep_by_alias(&dep) {
workspace_member.push(HubAlias {
alias_rule: alias_rule.clone(),
alias: Alias {
rule: alias_rule.rule(),
name: shorthand.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 {
workspace_member.push(HubAlias {
alias_rule: alias_rule.clone(),
alias: 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 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(HubAlias {
alias_rule: AliasRule::default(),
alias: 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()]),
},
});
}
}
}
let duplicates: Vec<_> = workspace_member
.iter()
.chain(binaries.iter())
.map(|entry| &entry.alias.name)
.duplicates()
.sorted()
.collect();
assert!(
duplicates.is_empty(),
"Found duplicate aliases that must be changed (Check your `extra_aliased_targets`): {:#?}",
duplicates
);
Ok(HubAliases {
workspace_member,
binaries,
})
}
fn render_build_files(
&self,
engine: &TemplateEngine,
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.to_string(),
) {
Ok(label) => label,
Err(e) => bail!(e),
};
let filename = match &context.crates[id].repository {
Some(SourceAnnotation::Path { path }) => path.join("BUILD.bazel").into(),
_ => Renderer::label_to_path(&label),
};
let content = self.render_one_build_file(engine, platforms, &context.crates[id])?;
Ok((filename, content))
})
.collect()
}
pub(crate) fn render_one_build_file(
&self,
engine: &TemplateEngine,
platforms: &Platforms,
krate: &CrateContext,
) -> Result<String> {
let mut krate = krate.clone();
if self.config.generate_cargo_toml_env_vars {
krate
.common_attrs
.rustc_env_files
.insert(":cargo_toml_env_vars".to_owned(), None);
if let Some(ref mut build_script_attrs) = &mut krate.build_script_attrs {
build_script_attrs
.rustc_env_files
.insert(":cargo_toml_env_vars".to_owned(), None);
build_script_attrs
.build_script_env_files
.insert(":cargo_toml_env_vars".to_owned(), None);
}
}
let mut starlark = Vec::new();
// Banner comment for top of the file.
let header = 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<String, BTreeSet<String>> = 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));
}
if self.config.generate_cargo_toml_env_vars {
load("@rules_rust//cargo:defs.bzl", "cargo_toml_env_vars");
starlark.push(Starlark::CargoTomlEnvVars(CargoTomlEnvVars {
name: "cargo_toml_env_vars".to_owned(),
src: "Cargo.toml".to_owned(),
}));
}
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<CargoBuildScript> {
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,
),
use_default_shell_env: attrs.and_then(|a| a.use_default_shell_env),
use_cc_toolchain: attrs.and_then(|a| a.use_cc_toolchain),
compile_data: make_data_with_exclude(
platforms,
attrs
.map(|attrs| attrs.compile_data_glob.clone())
.unwrap_or_default(),
attrs
.map(|attrs| attrs.compile_data_glob_excludes.clone())
.unwrap_or_default(),
attrs
.map(|attrs| attrs.compile_data.clone())
.unwrap_or_default(),
),
exec_properties: SelectDict::new(
attrs
.map(|attrs| attrs.exec_properties.clone())
.unwrap_or_default(),
platforms,
),
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(),
Default::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,
),
// Match Cargo's default: registry/git crates are quiet unless the
// build fails or the user passes `-vv`.
emit_warnings: false,
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<RustProcMacro> {
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<RustLibrary> {
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,
),
link_deps: SelectSet::new(krate.common_attrs.extra_link_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<RustBinary> {
// If the crate's library target is a proc-macro, the binary must list
// it in `proc_macro_deps` rather than `deps`; rules_rust validates this.
let lib_is_proc_macro = krate
.targets
.iter()
.any(|rule| matches!(rule, Rule::ProcMacro(_)));
let mut deps = self.make_deps(
krate.common_attrs.deps.clone(),
krate.common_attrs.extra_deps.clone(),
);
let mut proc_macro_deps = self.make_deps(
krate.common_attrs.proc_macro_deps.clone(),
krate.common_attrs.extra_proc_macro_deps.clone(),
);
if let Some(library_target_name) = &krate.library_target_name {
let lib_label = Label::from_str(&format!(":{library_target_name}")).unwrap();
if lib_is_proc_macro {
proc_macro_deps.insert(lib_label, None);
} else {
deps.insert(lib_label, None);
}
}
Ok(RustBinary {
name: format!("{}__bin", target.crate_name),
deps: SelectSet::new(deps, platforms),
proc_macro_deps: SelectSet::new(proc_macro_deps, platforms),
link_deps: SelectSet::new(krate.common_attrs.extra_link_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<CommonAttrs> {
Ok(CommonAttrs {
compile_data: make_data(
platforms,
krate.common_attrs.compile_data_glob.clone(),
krate.common_attrs.compile_data_glob_excludes.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(),
Default::default(),
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<BTreeMap<Label, String>> {
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<BTreeMap<Label, String>> = 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<BTreeSet<CrateDependency>>,
extra_deps: Select<BTreeSet<Label>>,
) -> Select<BTreeSet<Label>> {
Select::merge(
deps.map(|dep| {
match (dep.local_path, self.config.vendor_mode) {
// In local vendor mode, we use paths within the the repo.
(Some(path), Some(VendorMode::Local)) => {
Label::from_str(&format!("//{}:{}", path, dep.target)).unwrap()
}
// If we're not vendoring source, or don't have a path for the dep, construct the label we expect.
_ => self.crate_label(&dep.id.name, &dep.id.version.to_string(), &dep.target),
}
}),
extra_deps,
)
}
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<PathBuf, String>, 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<Label> {
Label::from_str(&template.replace("{file}", name))
}
/// Render the Bazel label of a platform triple
fn render_platform_constraint_label(template: &str, target_triple: &TargetTriple) -> String {
template.replace("{triple}", &target_triple.to_bazel())
}
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_with_exclude(
platforms: &Platforms,
include: BTreeSet<String>,
exclude: BTreeSet<String>,
select: Select<BTreeSet<Label>>,
) -> Data {
const COMMON_GLOB_EXCLUDES: &[&str] = &[
"**/* *",
"BUILD.bazel",
"BUILD",
"WORKSPACE.bazel",
"WORKSPACE",
".tmp_git_root/**/*",
];
Data {
glob: Glob {
allow_empty: true,
include,
exclude: COMMON_GLOB_EXCLUDES
.iter()
.map(|&glob| glob.to_owned())
.chain(exclude)
.collect(),
},
select: SelectSet::new(select, platforms),
}
}
fn make_data(
platforms: &Platforms,
glob: BTreeSet<String>,
excludes: BTreeSet<String>,
select: Select<BTreeSet<Label>>,
) -> Data {
make_data_with_exclude(platforms, glob, excludes, select)
}
#[cfg(test)]
mod test {
use super::*;
use camino::Utf8Path;
use indoc::indoc;
use crate::config::{Config, CrateId};
use crate::context::{BuildScriptAttributes, CommonAttributes};
use crate::metadata::Annotations;
use crate::test;
use crate::utils::normalize_cargo_file_paths;
const VERSION_ZERO_ONE_ZERO: semver::Version = semver::Version::new(0, 1, 0);
fn mock_target_attributes() -> TargetAttributes {
TargetAttributes {
crate_name: "mock_crate".to_owned(),
crate_root: Some("src/root.rs".to_owned()),
..TargetAttributes::default()
}
}
fn mock_render_config(vendor_mode: Option<VendorMode>) -> Arc<RenderConfig> {
Arc::new(RenderConfig {
repository_name: "test_rendering".to_owned(),
regen_command: "cargo_bazel_regen_command".to_owned(),
vendor_mode,
..RenderConfig::default()
})
}
fn mock_supported_platform_triples() -> Arc<BTreeSet<TargetTriple>> {
Arc::new(BTreeSet::from([
TargetTriple::from_bazel("aarch64-apple-darwin".to_owned()),
TargetTriple::from_bazel("aarch64-apple-ios".to_owned()),
TargetTriple::from_bazel("aarch64-linux-android".to_owned()),
TargetTriple::from_bazel("aarch64-pc-windows-msvc".to_owned()),
TargetTriple::from_bazel("aarch64-unknown-linux-gnu".to_owned()),
TargetTriple::from_bazel("arm-unknown-linux-gnueabi".to_owned()),
TargetTriple::from_bazel("armv7-unknown-linux-gnueabi".to_owned()),
TargetTriple::from_bazel("i686-apple-darwin".to_owned()),
TargetTriple::from_bazel("i686-linux-android".to_owned()),
TargetTriple::from_bazel("i686-pc-windows-msvc".to_owned()),
TargetTriple::from_bazel("i686-unknown-freebsd".to_owned()),
TargetTriple::from_bazel("i686-unknown-linux-gnu".to_owned()),
TargetTriple::from_bazel("powerpc-unknown-linux-gnu".to_owned()),
TargetTriple::from_bazel("s390x-unknown-linux-gnu".to_owned()),
TargetTriple::from_bazel("wasm32-unknown-unknown".to_owned()),
TargetTriple::from_bazel("wasm32-wasip1".to_owned()),
TargetTriple::from_bazel("x86_64-apple-darwin".to_owned()),
TargetTriple::from_bazel("x86_64-apple-ios".to_owned()),
TargetTriple::from_bazel("x86_64-linux-android".to_owned()),
TargetTriple::from_bazel("x86_64-pc-windows-msvc".to_owned()),
TargetTriple::from_bazel("x86_64-unknown-freebsd".to_owned()),
TargetTriple::from_bazel("x86_64-unknown-linux-gnu".to_owned()),
]))
}
#[test]
fn render_rust_library() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).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\""));
assert!(build_file_content.contains("\"crate-name=mock_crate\""));
}
#[test]
fn test_disable_pipelining() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: true,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output
.get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
.unwrap();
assert!(build_file_content.contains("disable_pipelining = True"));
}
#[test]
fn render_cargo_build_script() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
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.
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: Some(BuildScriptAttributes::default()),
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).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("),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("name = \"build_script_build\""),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("\"crate-name=mock_crate\""),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("compile_data = glob("),
"```\n{}```\n",
build_file_content
);
assert!(
!build_file_content.contains("use_default_shell_env ="),
"```\n{}```\n",
build_file_content
);
assert!(
!build_file_content.contains("use_cc_toolchain ="),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("emit_warnings = False"),
"```\n{}```\n",
build_file_content
);
// Ensure `cargo_build_script` requirements are met
assert!(build_file_content.contains("name = \"_bs\""));
}
#[test]
fn render_cargo_build_script_complex() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
let attrs = BuildScriptAttributes {
use_default_shell_env: Some(1),
use_cc_toolchain: Some(0),
exec_properties: Select::from_value(BTreeMap::from([
("OSFamily".to_owned(), "Linux".to_owned()),
("container-image".to_owned(), "docker://my-image".to_owned()),
])),
..BuildScriptAttributes::default()
};
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
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.
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: Some(attrs),
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).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("),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("name = \"build_script_build\""),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("\"crate-name=mock_crate\""),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("compile_data = glob("),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("use_default_shell_env = 1"),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("use_cc_toolchain = 0"),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("exec_properties = {"),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("\"OSFamily\": \"Linux\""),
"```\n{}```\n",
build_file_content
);
assert!(
build_file_content.contains("\"container-image\": \"docker://my-image\""),
"```\n{}```\n",
build_file_content
);
// Ensure `cargo_build_script` requirements are met
assert!(build_file_content.contains("name = \"_bs\""));
}
#[test]
fn render_proc_macro() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::ProcMacro(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).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\""));
assert!(build_file_content.contains("\"crate-name=mock_crate\""));
}
#[test]
fn render_binary() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Binary(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).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\""));
assert!(build_file_content.contains("\"crate-name=mock_crate\""));
}
#[test]
fn render_additive_build_contents() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Binary(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: Some(
"# Hello World from additive section!".to_owned(),
),
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).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(),
&None,
test::lockfile::alias(),
config,
Utf8Path::new("/tmp/bazelworkspace"),
)
.unwrap();
let context = Context::new(annotations, false).unwrap();
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).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(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
// `crates.bzl` is the source of truth; `defs.bzl` is a
// back-compat re-export shim that only loads + re-exports.
let crates_module = output.get(&PathBuf::from("crates.bzl")).unwrap();
assert!(crates_module.contains("def crate_repositories():"));
let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap();
assert!(defs_module.contains("_crate_repositories = \"crate_repositories\""));
assert!(!defs_module.contains("def crate_repositories"));
}
#[test]
fn render_crate_edition() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context
.workspace_members
.insert(crate_id.clone(), "mock_crate".to_owned());
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes {
edition: "2021".to_owned(),
..CommonAttributes::default()
},
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let crates_module = output.get(&PathBuf::from("crates.bzl")).unwrap();
assert!(crates_module.contains("def crate_edition("));
assert!(crates_module.contains(r#""mock_crate": "2021","#));
}
#[test]
fn remote_remote_vendor_mode() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
// Enable remote vendor mode
let renderer = Renderer::new(
mock_render_config(Some(VendorMode::Remote)),
mock_supported_platform_triples(),
);
let output = renderer.render(&context, None).unwrap();
// Source of truth lives in `crates.bzl`; `defs.bzl` is a
// back-compat shim that loads + re-exports from `crates.bzl`.
let crates_module = output.get(&PathBuf::from("crates.bzl")).unwrap();
assert!(crates_module.contains("def crate_repositories():"));
let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap();
assert!(defs_module.contains("_crate_repositories = \"crate_repositories\""));
assert!(!defs_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(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
// Enable local vendor mode
let renderer = Renderer::new(
mock_render_config(Some(VendorMode::Local)),
mock_supported_platform_triples(),
);
let output = renderer.render(&context, None).unwrap();
// Local vendoring does not produce a `crate_repositories` macro.
let crates_module = output.get(&PathBuf::from("crates.bzl")).unwrap();
assert!(!crates_module.contains("def crate_repositories():"));
// `defs.bzl` shim is always rendered as a back-compat re-export.
let defs_module = output.get(&PathBuf::from("defs.bzl")).unwrap();
assert!(defs_module.contains("_aliases = \"aliases\""));
}
#[test]
fn duplicate_rustc_flags() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
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,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes {
rustc_flags: Select::from_value(rustc_flags.clone()),
..CommonAttributes::default()
},
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
// Enable local vendor mode
let renderer = Renderer::new(
mock_render_config(Some(VendorMode::Local)),
mock_supported_platform_triples(),
);
let output = renderer.render(&context, None).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": {
"generate_cargo_toml_env_vars": true,
"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,
&None,
lockfile,
config.clone(),
Utf8Path::new("/tmp/bazelworkspace"),
)
.unwrap();
let context = Context::new(annotations, false).unwrap();
let renderer = Renderer::new(
Arc::new(config.rendering),
Arc::new(config.supported_platform_triples),
);
let output = renderer.render(&context, None).unwrap();
let build_file_content = output
.get(&PathBuf::from("BUILD.cpufeatures-0.2.7.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", # cfg(all(target_arch = "aarch64", target_vendor = "apple"))
],
"@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,
);
}
#[test]
fn crate_features_by_target() {
let mut context = Context {
conditions: mock_supported_platform_triples()
.iter()
.map(|platform| (platform.to_bazel(), BTreeSet::from([platform.clone()])))
.collect(),
..Context::default()
};
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
let mut crate_features: Select<BTreeSet<String>> = Select::default();
crate_features.insert("foo".to_owned(), Some("aarch64-apple-darwin".to_owned()));
crate_features.insert("bar".to_owned(), None);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes {
crate_features,
..CommonAttributes::default()
},
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output
.get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
.unwrap();
let expected = indoc! {r#"
crate_features = [
"bar",
] + select({
"@rules_rust//rust/platform:aarch64-apple-darwin": [
"foo", # aarch64-apple-darwin
],
"//conditions:default": [],
}),
"#};
assert!(build_file_content
.replace(' ', "")
.contains(&expected.replace(' ', "")));
}
#[test]
fn crate_package_metadata_without_license_ids() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: Some("http://www.mock_crate.com/".to_owned()),
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let mut render_config = mock_render_config(None);
Arc::get_mut(&mut render_config)
.unwrap()
.generate_rules_license_metadata = true;
let renderer = Renderer::new(render_config, mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output
.get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
.unwrap();
let expected = indoc! {r#"
package(
default_package_metadata = [":package_info"],
default_visibility = ["//visibility:public"],
)
package_info(
name = "package_info",
package_name = "mock_crate",
package_version = "0.1.0",
package_url = "http://www.mock_crate.com/",
)
"#};
assert!(build_file_content
.replace(' ', "")
.contains(&expected.replace(' ', "")));
}
#[test]
fn crate_package_metadata_with_license_ids() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: Some("http://www.mock_crate.com/".to_owned()),
license_ids: BTreeSet::from(["Apache-2.0".to_owned(), "MIT".to_owned()]),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
repository: None,
license: None,
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let mut render_config = mock_render_config(None);
Arc::get_mut(&mut render_config)
.unwrap()
.generate_rules_license_metadata = true;
let renderer = Renderer::new(render_config, mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output
.get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
.unwrap();
let expected = indoc! {r#"
package(
default_package_metadata = [
":license",
":package_info",
],
default_visibility = ["//visibility:public"],
)
package_info(
name = "package_info",
package_name = "mock_crate",
package_version = "0.1.0",
package_url = "http://www.mock_crate.com/",
)
license(
name = "license",
license_kinds = [
"@rules_license//licenses/spdx:Apache-2.0",
"@rules_license//licenses/spdx:MIT",
],
)
"#};
assert!(build_file_content
.replace(' ', "")
.contains(&expected.replace(' ', "")));
}
#[test]
fn crate_package_metadata_with_license_ids_and_file() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: Some("http://www.mock_crate.com/".to_owned()),
license_ids: BTreeSet::from(["Apache-2.0".to_owned(), "MIT".to_owned()]),
license_file: Some("LICENSE.txt".to_owned()),
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
repository: None,
license: None,
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let mut render_config = mock_render_config(None);
Arc::get_mut(&mut render_config)
.unwrap()
.generate_rules_license_metadata = true;
let renderer = Renderer::new(render_config, mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output
.get(&PathBuf::from("BUILD.mock_crate-0.1.0.bazel"))
.unwrap();
let expected = indoc! {r#"
package(
default_package_metadata = [
":license",
":package_info",
],
default_visibility = ["//visibility:public"],
)
package_info(
name = "package_info",
package_name = "mock_crate",
package_version = "0.1.0",
package_url = "http://www.mock_crate.com/",
)
license(
name = "license",
license_kinds = [
"@rules_license//licenses/spdx:Apache-2.0",
"@rules_license//licenses/spdx:MIT",
],
license_text = "LICENSE.txt",
)
"#};
assert!(build_file_content
.replace(' ', "")
.contains(&expected.replace(' ', "")));
}
#[test]
fn write_outputs_semver_metadata() {
let mut context = Context::default();
// generate crate for libbpf-sys-1.4.0-v1.4.0
let mut version = semver::Version::new(1, 4, 0);
version.build = semver::BuildMetadata::new("v1.4.0").unwrap();
// ensure metadata has a +
assert!(version.to_string().contains('+'));
let crate_id = CrateId::new("libbpf-sys".to_owned(), version);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name,
version: crate_id.version,
package_url: None,
repository: None,
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: None,
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let mut config = mock_render_config(Some(VendorMode::Local));
// change templates so it matches local vendor
Arc::get_mut(&mut config).unwrap().build_file_template =
"//{name}-{version}:BUILD.bazel".into();
// Enable local vendor mode
let renderer = Renderer::new(config, mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
eprintln!("output before {:?}", output.keys());
// Local vendoring does not produce a `crate_repositories` macro.
let crates_module = output.get(&PathBuf::from("crates.bzl")).unwrap();
assert!(!crates_module.contains("def crate_repositories():"));
// create tempdir to write to
let outdir = tempfile::tempdir().unwrap();
// create dir to mimic cargo vendor
let _ = std::fs::create_dir_all(outdir.path().join("libbpf-sys-1.4.0+v1.4.0"));
let normalized_outputs = normalize_cargo_file_paths(output, outdir.path());
eprintln!(
"Normalized outputs are {:?}",
normalized_outputs.clone().into_keys()
);
write_outputs(normalized_outputs, false).unwrap();
let expected = outdir.path().join("libbpf-sys-1.4.0-v1.4.0");
let mut found = false;
// ensure no files paths have a + sign
for entry in fs::read_dir(outdir.path()).unwrap() {
let path_str = entry.as_ref().unwrap().path().to_str().unwrap().to_string();
if entry.unwrap().path() == expected {
found = true;
}
assert!(!path_str.contains('+'));
}
assert!(found);
}
/// Tests a situation where we identical aliases to the crate's name on the
/// package's deps
#[test]
fn crate_with_ambiguous_rename() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context
.workspace_members
.insert(crate_id.clone(), "mock_crate".into());
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name.clone(),
version: crate_id.version.clone(),
package_url: Some("http://www.mock_crate.com/".to_owned()),
license_ids: BTreeSet::from(["Apache-2.0".to_owned(), "MIT".to_owned()]),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: Some("library_name".into()),
common_attrs: CommonAttributes {
deps: Select::from_value(BTreeSet::from([CrateDependency {
id: crate_id,
target: "target".into(),
// this is identical to what we have in the `name` attribute
// which creates conflict in `render_module_build_file`
alias: Some("mock_crate".into()),
local_path: None,
}])),
..Default::default()
},
build_script_attrs: None,
repository: None,
license: None,
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let mut render_config = mock_render_config(None);
Arc::get_mut(&mut render_config)
.unwrap()
.generate_rules_license_metadata = true;
let renderer = Renderer::new(render_config, mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output.get(&PathBuf::from("BUILD.bazel")).unwrap();
println!("{build_file_content}");
let expected = indoc! {r#"
###############################################################################
# @generated
# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To
# regenerate this file, run the following:
#
# cargo_bazel_regen_command
###############################################################################
package(default_visibility = ["//visibility:public"])
exports_files(
[
"cargo-bazel.json",
"crates.bzl",
"defs.bzl",
] + glob(
allow_empty = True,
include = ["*.bazel"],
),
)
filegroup(
name = "srcs",
srcs = glob(
allow_empty = True,
include = [
"*.bazel",
"*.bzl",
],
),
)
# Workspace Member Dependencies
alias(
name = "mock_crate-0.1.0",
actual = "@test_rendering__mock_crate-0.1.0//:library_name",
tags = ["manual"],
)
alias(
name = "mock_crate",
actual = "@test_rendering__mock_crate-0.1.0//:library_name",
tags = ["manual"],
)
"#};
assert!(build_file_content
.replace(' ', "")
.contains(&expected.replace(' ', "")));
}
/// Tests a situation where there are two dependencies referencing the same
/// crate at the same version, but with different aliases.
#[test]
fn crate_with_ambiguous_alias_dependencies() {
let mut context = Context::default();
let crate_id = CrateId::new("mock_crate".to_owned(), VERSION_ZERO_ONE_ZERO);
context
.workspace_members
.insert(crate_id.clone(), "mock_crate".into());
let dependency_id = CrateId::new("my_dependency".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
dependency_id.clone(),
CrateContext {
name: dependency_id.name.clone(),
version: dependency_id.version.clone(),
package_url: Some("http://www.my_dependency.com/".to_owned()),
license_ids: BTreeSet::from(["Apache-2.0".to_owned(), "MIT".to_owned()]),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: Some("my_dependency".into()),
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
repository: None,
license: None,
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name.clone(),
version: crate_id.version.clone(),
package_url: Some("http://www.mock_crate.com/".to_owned()),
license_ids: BTreeSet::from(["Apache-2.0".to_owned(), "MIT".to_owned()]),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
targets: BTreeSet::from([Rule::Library(mock_target_attributes())]),
library_target_name: Some("library_name".into()),
common_attrs: CommonAttributes {
deps: Select::from_value(BTreeSet::from([
// These two dependencies are identical, except one is aliased.
CrateDependency {
id: dependency_id.clone(),
target: "my_dependency".into(),
alias: None,
local_path: None,
},
CrateDependency {
id: dependency_id,
target: "my_dependency".into(),
alias: Some("my_dependency_other".into()),
local_path: None,
},
])),
..Default::default()
},
build_script_attrs: None,
repository: None,
license: None,
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let mut render_config = mock_render_config(None);
Arc::get_mut(&mut render_config)
.unwrap()
.generate_rules_license_metadata = true;
let renderer = Renderer::new(render_config, mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output.get(&PathBuf::from("BUILD.bazel")).unwrap();
println!("{build_file_content}");
let expected = indoc! {r#"
###############################################################################
# @generated
# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To
# regenerate this file, run the following:
#
# cargo_bazel_regen_command
###############################################################################
package(default_visibility = ["//visibility:public"])
exports_files(
[
"cargo-bazel.json",
"crates.bzl",
"defs.bzl",
] + glob(
allow_empty = True,
include = ["*.bazel"],
),
)
filegroup(
name = "srcs",
srcs = glob(
allow_empty = True,
include = [
"*.bazel",
"*.bzl",
],
),
)
# Workspace Member Dependencies
alias(
name = "my_dependency-0.1.0",
actual = "@test_rendering__my_dependency-0.1.0//:my_dependency",
tags = ["manual"],
)
alias(
name = "my_dependency",
actual = "@test_rendering__my_dependency-0.1.0//:my_dependency",
tags = ["manual"],
)
alias(
name = "my_dependency_other-0.1.0",
actual = "@test_rendering__my_dependency-0.1.0//:my_dependency",
tags = ["manual"],
)
alias(
name = "my_dependency_other",
actual = "@test_rendering__my_dependency-0.1.0//:my_dependency",
tags = ["manual"],
)
"#};
assert!(build_file_content
.replace(' ', "")
.contains(&expected.replace(' ', "")));
}
/// Binary targets of a proc-macro crate must list the proc-macro lib in
/// `proc_macro_deps`, not `deps`. rules_rust validates this strictly.
#[test]
fn binary_of_proc_macro_crate_uses_proc_macro_deps() {
let mut context = Context::default();
let crate_id = CrateId::new("my_proc_macro".to_owned(), VERSION_ZERO_ONE_ZERO);
context.crates.insert(
crate_id.clone(),
CrateContext {
name: crate_id.name.clone(),
version: crate_id.version.clone(),
package_url: None,
targets: BTreeSet::from([
Rule::ProcMacro(TargetAttributes {
crate_name: "my_proc_macro".to_owned(),
crate_root: Some("src/lib.rs".to_owned()),
..TargetAttributes::default()
}),
Rule::Binary(TargetAttributes {
crate_name: "my_bin".to_owned(),
crate_root: Some("src/main.rs".to_owned()),
..TargetAttributes::default()
}),
]),
library_target_name: Some("my_proc_macro".to_owned()),
common_attrs: CommonAttributes::default(),
build_script_attrs: None,
repository: None,
license: None,
license_ids: BTreeSet::default(),
license_file: None,
additive_build_file_content: None,
disable_pipelining: false,
extra_aliased_targets: BTreeMap::default(),
alias_rule: None,
override_targets: BTreeMap::default(),
},
);
let renderer = Renderer::new(mock_render_config(None), mock_supported_platform_triples());
let output = renderer.render(&context, None).unwrap();
let build_file_content = output
.get(&PathBuf::from("BUILD.my_proc_macro-0.1.0.bazel"))
.unwrap();
// Find the rust_binary section.
let binary_start = build_file_content
.find("rust_binary(")
.expect("should contain rust_binary");
let binary_section = &build_file_content[binary_start..];
// When the crate's lib is a proc-macro, deps will be empty and therefore
// omitted from the rendered output. Only proc_macro_deps should be present.
let proc_macro_deps_pos = binary_section
.find(" proc_macro_deps = ")
.expect("binary should have proc_macro_deps attribute");
assert!(
!binary_section[..proc_macro_deps_pos].contains(":my_proc_macro"),
"proc-macro lib must not appear before proc_macro_deps:\n{binary_section}"
);
assert!(
binary_section[proc_macro_deps_pos..].contains(":my_proc_macro"),
"proc-macro lib must appear in proc_macro_deps:\n{binary_section}"
);
}
}