blob: 923fa1ee2efff0c7bd14a59f6dbcc4ffaf3091f5 [file] [log] [blame]
//! Crate specific information embedded into [crate::context::Context] objects.
use std::collections::{BTreeMap, BTreeSet};
use cargo_metadata::{Node, Package, PackageId};
use serde::{Deserialize, Serialize};
use crate::config::CrateId;
use crate::metadata::{CrateAnnotation, Dependency, PairredExtras, SourceAnnotation};
use crate::utils::sanitize_module_name;
use crate::utils::starlark::{Glob, SelectList, SelectMap, SelectStringDict, SelectStringList};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
pub struct CrateDependency {
/// The [CrateId] of the dependency
pub id: CrateId,
/// The target name of the dependency. Note this may differ from the
/// dependency's package name in cases such as build scripts.
pub target: String,
/// Some dependencies are assigned aliases. This is tracked here
#[serde(default, skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct TargetAttributes {
/// The module name of the crate (notably, not the package name).
pub crate_name: String,
/// The path to the crate's root source file, relative to the manifest.
pub crate_root: Option<String>,
/// A glob pattern of all source files required by the target
pub srcs: Glob,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
pub enum Rule {
/// `cargo_build_script`
BuildScript(TargetAttributes),
/// `rust_proc_macro`
ProcMacro(TargetAttributes),
/// `rust_library`
Library(TargetAttributes),
/// `rust_binary`
Binary(TargetAttributes),
}
/// A set of attributes common to most `rust_library`, `rust_proc_macro`, and other
/// [core rules of `rules_rust`](https://bazelbuild.github.io/rules_rust/defs.html).
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct CommonAttributes {
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub compile_data: SelectStringList,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub compile_data_glob: BTreeSet<String>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub crate_features: BTreeSet<String>,
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub data: SelectStringList,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub data_glob: BTreeSet<String>,
#[serde(skip_serializing_if = "SelectList::should_skip_serializing")]
pub deps: SelectList<CrateDependency>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub extra_deps: BTreeSet<String>,
#[serde(skip_serializing_if = "SelectList::should_skip_serializing")]
pub deps_dev: SelectList<CrateDependency>,
pub edition: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub linker_script: Option<String>,
#[serde(skip_serializing_if = "SelectList::should_skip_serializing")]
pub proc_macro_deps: SelectList<CrateDependency>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub extra_proc_macro_deps: BTreeSet<String>,
#[serde(skip_serializing_if = "SelectList::should_skip_serializing")]
pub proc_macro_deps_dev: SelectList<CrateDependency>,
#[serde(skip_serializing_if = "SelectStringDict::should_skip_serializing")]
pub rustc_env: SelectStringDict,
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub rustc_env_files: SelectStringList,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub rustc_flags: Vec<String>,
pub version: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
impl Default for CommonAttributes {
fn default() -> Self {
Self {
compile_data: Default::default(),
// Generated targets include all files in their package by default
compile_data_glob: BTreeSet::from(["**".to_owned()]),
crate_features: Default::default(),
data: Default::default(),
data_glob: Default::default(),
deps: Default::default(),
extra_deps: Default::default(),
deps_dev: Default::default(),
edition: Default::default(),
linker_script: Default::default(),
proc_macro_deps: Default::default(),
extra_proc_macro_deps: Default::default(),
proc_macro_deps_dev: Default::default(),
rustc_env: Default::default(),
rustc_env_files: Default::default(),
rustc_flags: Default::default(),
version: Default::default(),
tags: Default::default(),
}
}
}
// Build script attributes. See
// https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct BuildScriptAttributes {
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub compile_data: SelectStringList,
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub data: SelectStringList,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub data_glob: BTreeSet<String>,
#[serde(skip_serializing_if = "SelectList::should_skip_serializing")]
pub deps: SelectList<CrateDependency>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub extra_deps: BTreeSet<String>,
#[serde(skip_serializing_if = "SelectStringDict::should_skip_serializing")]
pub build_script_env: SelectStringDict,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub extra_proc_macro_deps: BTreeSet<String>,
#[serde(skip_serializing_if = "SelectList::should_skip_serializing")]
pub proc_macro_deps: SelectList<CrateDependency>,
#[serde(skip_serializing_if = "SelectStringDict::should_skip_serializing")]
pub rustc_env: SelectStringDict,
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub rustc_flags: SelectStringList,
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub rustc_env_files: SelectStringList,
#[serde(skip_serializing_if = "SelectStringList::should_skip_serializing")]
pub tools: SelectStringList,
#[serde(skip_serializing_if = "Option::is_none")]
pub links: Option<String>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub toolchains: BTreeSet<String>,
}
impl Default for BuildScriptAttributes {
fn default() -> Self {
Self {
compile_data: Default::default(),
data: Default::default(),
// Build scripts include all sources by default
data_glob: BTreeSet::from(["**".to_owned()]),
deps: Default::default(),
extra_deps: Default::default(),
build_script_env: Default::default(),
extra_proc_macro_deps: Default::default(),
proc_macro_deps: Default::default(),
rustc_env: Default::default(),
rustc_flags: Default::default(),
rustc_env_files: Default::default(),
tools: Default::default(),
links: Default::default(),
toolchains: Default::default(),
}
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct CrateContext {
/// The package name of the current crate
pub name: String,
/// The full version of the current crate
pub version: String,
/// Optional source annotations if they were discoverable in the
/// lockfile. Workspace Members will not have source annotations and
/// potentially others.
pub repository: Option<SourceAnnotation>,
/// A list of all targets (lib, proc-macro, bin) associated with this package
pub targets: Vec<Rule>,
/// The name of the crate's root library target. This is the target that a dependent
/// would get if they were to depend on `{crate_name}`.
pub library_target_name: Option<String>,
/// A set of attributes common to most [Rule] types or target types.
pub common_attrs: CommonAttributes,
/// Optional attributes for build scripts. This field is only populated if
/// a build script (`custom-build`) target is defined for the crate.
#[serde(skip_serializing_if = "Option::is_none")]
pub build_script_attrs: Option<BuildScriptAttributes>,
/// The license used by the crate
pub license: Option<String>,
/// Additional text to add to the generated BUILD file.
#[serde(skip_serializing_if = "Option::is_none")]
pub additive_build_file_content: Option<String>,
}
impl CrateContext {
pub fn new(
annotation: &CrateAnnotation,
packages: &BTreeMap<PackageId, Package>,
source_annotations: &BTreeMap<PackageId, SourceAnnotation>,
extras: &BTreeMap<CrateId, PairredExtras>,
include_build_scripts: bool,
) -> Self {
let package: &Package = &packages[&annotation.node.id];
let current_crate_id = CrateId::new(package.name.clone(), package.version.to_string());
let new_crate_dep = |dep: Dependency| -> CrateDependency {
let pkg = &packages[&dep.package_id];
// Unfortunately, The package graph and resolve graph of cargo metadata have different representations
// for the crate names (resolve graph sanitizes names to match module names) so to get the rest of this
// content to align when rendering, the dependency target needs to be explicitly sanitized.
let target = sanitize_module_name(&dep.target_name);
CrateDependency {
id: CrateId::new(pkg.name.clone(), pkg.version.to_string()),
target,
alias: dep.alias,
}
};
// Convert the dependencies into renderable strings
let deps = annotation.deps.normal_deps.clone().map(new_crate_dep);
let deps_dev = annotation.deps.normal_dev_deps.clone().map(new_crate_dep);
let proc_macro_deps = annotation.deps.proc_macro_deps.clone().map(new_crate_dep);
let proc_macro_deps_dev = annotation
.deps
.proc_macro_dev_deps
.clone()
.map(new_crate_dep);
// Gather all "common" attributes
let mut common_attrs = CommonAttributes {
crate_features: annotation.node.features.iter().cloned().collect(),
deps,
deps_dev,
edition: package.edition.clone(),
proc_macro_deps,
proc_macro_deps_dev,
version: package.version.to_string(),
..Default::default()
};
let include_build_scripts =
Self::crate_includes_build_script(package, extras, include_build_scripts);
// Iterate over each target and produce a Bazel target for all supported "kinds"
let targets = Self::collect_targets(&annotation.node, packages, include_build_scripts);
// Parse the library crate name from the set of included targets
let library_target_name = {
let lib_targets: Vec<&TargetAttributes> = targets
.iter()
.filter_map(|t| match t {
Rule::ProcMacro(attrs) => Some(attrs),
Rule::Library(attrs) => Some(attrs),
_ => None,
})
.collect();
// TODO: There should only be at most 1 library target. This case
// should be handled in a more intelligent way.
assert!(lib_targets.len() <= 1);
lib_targets
.iter()
.last()
.map(|attr| attr.crate_name.clone())
};
// Gather any build-script related attributes
let build_script_target = targets.iter().find_map(|r| match r {
Rule::BuildScript(attr) => Some(attr),
_ => None,
});
let build_script_attrs = if let Some(target) = build_script_target {
// Track the build script dependency
common_attrs.deps.insert(
CrateDependency {
id: current_crate_id,
target: target.crate_name.clone(),
alias: None,
},
None,
);
let build_deps = annotation.deps.build_deps.clone().map(new_crate_dep);
let build_proc_macro_deps = annotation
.deps
.build_proc_macro_deps
.clone()
.map(new_crate_dep);
Some(BuildScriptAttributes {
deps: build_deps,
proc_macro_deps: build_proc_macro_deps,
links: package.links.clone(),
..Default::default()
})
} else {
None
};
// Save the repository information for the current crate
let repository = source_annotations.get(&package.id).cloned();
// Identify the license type
let license = package.license.clone();
// Create the crate's context and apply extra settings
CrateContext {
name: package.name.clone(),
version: package.version.to_string(),
repository,
targets,
library_target_name,
common_attrs,
build_script_attrs,
license,
additive_build_file_content: None,
}
.with_overrides(extras)
}
fn with_overrides(mut self, extras: &BTreeMap<CrateId, PairredExtras>) -> Self {
let id = CrateId::new(self.name.clone(), self.version.clone());
// Insert all overrides/extras
if let Some(pairred_override) = extras.get(&id) {
let crate_extra = &pairred_override.crate_extra;
// Deps
if let Some(extra) = &crate_extra.deps {
self.common_attrs.extra_deps = extra.clone();
}
// Proc macro deps
if let Some(extra) = &crate_extra.proc_macro_deps {
self.common_attrs.extra_proc_macro_deps = extra.clone();
}
// Compile data
if let Some(extra) = &crate_extra.compile_data {
for data in extra.iter() {
self.common_attrs.compile_data.insert(data.clone(), None);
}
}
// Compile data glob
if let Some(extra) = &crate_extra.compile_data_glob {
self.common_attrs.compile_data_glob.extend(extra.clone());
}
// Crate features
if let Some(extra) = &crate_extra.crate_features {
for data in extra.iter() {
self.common_attrs.crate_features.insert(data.clone());
}
}
// Data
if let Some(extra) = &crate_extra.data {
for data in extra.iter() {
self.common_attrs.data.insert(data.clone(), None);
}
}
// Data glob
if let Some(extra) = &crate_extra.data_glob {
self.common_attrs.data_glob.extend(extra.clone());
}
// Rustc flags
if let Some(extra) = &crate_extra.rustc_flags {
self.common_attrs.rustc_flags.append(&mut extra.clone());
}
// Rustc env
if let Some(extra) = &crate_extra.rustc_env {
self.common_attrs.rustc_env.insert(extra.clone(), None);
}
// Rustc env files
if let Some(extra) = &crate_extra.rustc_env_files {
for data in extra.iter() {
self.common_attrs.rustc_env_files.insert(data.clone(), None);
}
}
// Build script Attributes
if let Some(attrs) = &mut self.build_script_attrs {
// Deps
if let Some(extra) = &crate_extra.build_script_deps {
attrs.extra_deps = extra.clone();
}
// Proc macro deps
if let Some(extra) = &crate_extra.build_script_proc_macro_deps {
attrs.extra_proc_macro_deps = extra.clone();
}
// Data
if let Some(extra) = &crate_extra.build_script_data {
for data in extra {
attrs.data.insert(data.clone(), None);
}
}
// Tools
if let Some(extra) = &crate_extra.build_script_tools {
for data in extra {
attrs.tools.insert(data.clone(), None);
}
}
// Toolchains
if let Some(extra) = &crate_extra.build_script_toolchains {
for data in extra {
attrs.toolchains.insert(data.clone());
}
}
// Data glob
if let Some(extra) = &crate_extra.build_script_data_glob {
attrs.data_glob.extend(extra.clone());
}
// Rustc env
if let Some(extra) = &crate_extra.build_script_rustc_env {
attrs.rustc_env.insert(extra.clone(), None);
}
// Build script env
if let Some(extra) = &crate_extra.build_script_env {
attrs.build_script_env.insert(extra.clone(), None);
}
}
// Extra build contents
self.additive_build_file_content = crate_extra
.additive_build_file_content
.as_ref()
.map(|content| {
// For prettier rendering, dedent the build contents
textwrap::dedent(content)
});
// Git shallow_since
if let Some(SourceAnnotation::Git { shallow_since, .. }) = &mut self.repository {
*shallow_since = crate_extra.shallow_since.clone()
}
// Patch attributes
if let Some(repository) = &mut self.repository {
match repository {
SourceAnnotation::Git {
patch_args,
patch_tool,
patches,
..
} => {
*patch_args = crate_extra.patch_args.clone();
*patch_tool = crate_extra.patch_tool.clone();
*patches = crate_extra.patches.clone();
}
SourceAnnotation::Http {
patch_args,
patch_tool,
patches,
..
} => {
*patch_args = crate_extra.patch_args.clone();
*patch_tool = crate_extra.patch_tool.clone();
*patches = crate_extra.patches.clone();
}
}
}
}
self
}
/// Determine whether or not a crate __should__ include a build script
/// (build.rs) if it happens to have one.
fn crate_includes_build_script(
package: &Package,
overrides: &BTreeMap<CrateId, PairredExtras>,
default_generate_build_script: bool,
) -> bool {
// Locate extra settings for the current package.
let settings = overrides
.iter()
.find(|(_, settings)| settings.package_id == package.id);
// If the crate has extra settings, which explicitly set `gen_build_script`, always use
// this value, otherwise, fallback to the provided default.
settings
.and_then(|(_, settings)| settings.crate_extra.gen_build_script)
.unwrap_or(default_generate_build_script)
}
/// Collect all Bazel targets that should be generated for a particular Package
fn collect_targets(
node: &Node,
packages: &BTreeMap<PackageId, Package>,
include_build_scripts: bool,
) -> Vec<Rule> {
let package = &packages[&node.id];
let package_root = package
.manifest_path
.as_std_path()
.parent()
.expect("Every manifest should have a parent directory");
package
.targets
.iter()
.flat_map(|target| {
target
.kind
.iter()
.filter_map(|kind| {
// Unfortunately, The package graph and resolve graph of cargo metadata have different representations
// for the crate names (resolve graph sanitizes names to match module names) so to get the rest of this
// content to align when rendering, the package target names are always sanitized.
let crate_name = sanitize_module_name(&target.name);
// Locate the crate's root source file relative to the package root normalized for unix
let crate_root = pathdiff::diff_paths(&target.src_path, package_root).map(
// Normalize the path so that it always renders the same regardless of platform
|root| root.to_string_lossy().replace('\\', "/"),
);
// Conditionally check to see if the dependencies is a build-script target
if include_build_scripts && kind == "custom-build" {
return Some(Rule::BuildScript(TargetAttributes {
crate_name,
crate_root,
srcs: Glob::new_rust_srcs(),
}));
}
// Check to see if the dependencies is a proc-macro target
if kind == "proc-macro" {
return Some(Rule::ProcMacro(TargetAttributes {
crate_name,
crate_root,
srcs: Glob::new_rust_srcs(),
}));
}
// Check to see if the dependencies is a library target
if ["lib", "rlib"].contains(&kind.as_str()) {
return Some(Rule::Library(TargetAttributes {
crate_name,
crate_root,
srcs: Glob::new_rust_srcs(),
}));
}
// Check to see if the dependencies is a library target
if kind == "bin" {
return Some(Rule::Binary(TargetAttributes {
crate_name: target.name.clone(),
crate_root,
srcs: Glob::new_rust_srcs(),
}));
}
None
})
.collect::<Vec<Rule>>()
})
.collect()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::CrateAnnotations;
use crate::metadata::Annotations;
fn common_annotations() -> Annotations {
Annotations::new(
crate::test::metadata::common(),
crate::test::lockfile::common(),
crate::config::Config::default(),
)
.unwrap()
}
#[test]
fn new_context() {
let annotations = common_annotations();
let crate_annotation = &annotations.metadata.crates[&PackageId {
repr: "common 0.1.0 (path+file://{TEMP_DIR}/common)".to_owned(),
}];
let context = CrateContext::new(
crate_annotation,
&annotations.metadata.packages,
&annotations.lockfile.crates,
&annotations.pairred_extras,
false,
);
assert_eq!(context.name, "common");
assert_eq!(
context.targets,
vec![
Rule::Library(TargetAttributes {
crate_name: "common".to_owned(),
crate_root: Some("lib.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
}),
Rule::Binary(TargetAttributes {
crate_name: "common-bin".to_owned(),
crate_root: Some("main.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
}),
]
);
}
#[test]
fn context_with_overrides() {
let annotations = common_annotations();
let package_id = PackageId {
repr: "common 0.1.0 (path+file://{TEMP_DIR}/common)".to_owned(),
};
let crate_annotation = &annotations.metadata.crates[&package_id];
let mut pairred_extras = BTreeMap::new();
pairred_extras.insert(
CrateId::new("common".to_owned(), "0.1.0".to_owned()),
PairredExtras {
package_id,
crate_extra: CrateAnnotations {
data_glob: Some(BTreeSet::from(["**/data_glob/**".to_owned()])),
..CrateAnnotations::default()
},
},
);
let context = CrateContext::new(
crate_annotation,
&annotations.metadata.packages,
&annotations.lockfile.crates,
&pairred_extras,
false,
);
assert_eq!(context.name, "common");
assert_eq!(
context.targets,
vec![
Rule::Library(TargetAttributes {
crate_name: "common".to_owned(),
crate_root: Some("lib.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
}),
Rule::Binary(TargetAttributes {
crate_name: "common-bin".to_owned(),
crate_root: Some("main.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
}),
]
);
assert_eq!(
context.common_attrs.data_glob,
BTreeSet::from(["**/data_glob/**".to_owned()])
);
}
fn build_script_annotations() -> Annotations {
Annotations::new(
crate::test::metadata::build_scripts(),
crate::test::lockfile::build_scripts(),
crate::config::Config::default(),
)
.unwrap()
}
fn crate_type_annotations() -> Annotations {
Annotations::new(
crate::test::metadata::crate_types(),
crate::test::lockfile::crate_types(),
crate::config::Config::default(),
)
.unwrap()
}
#[test]
fn context_with_build_script() {
let annotations = build_script_annotations();
let package_id = PackageId {
repr: "openssl-sys 0.9.72 (registry+https://github.com/rust-lang/crates.io-index)"
.to_owned(),
};
let crate_annotation = &annotations.metadata.crates[&package_id];
let context = CrateContext::new(
crate_annotation,
&annotations.metadata.packages,
&annotations.lockfile.crates,
&annotations.pairred_extras,
true,
);
assert_eq!(context.name, "openssl-sys");
assert!(context.build_script_attrs.is_some());
assert_eq!(
context.targets,
vec![
Rule::Library(TargetAttributes {
crate_name: "openssl_sys".to_owned(),
crate_root: Some("src/lib.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
}),
Rule::BuildScript(TargetAttributes {
crate_name: "build_script_main".to_owned(),
crate_root: Some("build/main.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
})
]
);
// Cargo build scripts should include all sources
assert!(context.build_script_attrs.unwrap().data_glob.contains("**"));
}
#[test]
fn context_disabled_build_script() {
let annotations = build_script_annotations();
let package_id = PackageId {
repr: "openssl-sys 0.9.72 (registry+https://github.com/rust-lang/crates.io-index)"
.to_owned(),
};
let crate_annotation = &annotations.metadata.crates[&package_id];
let context = CrateContext::new(
crate_annotation,
&annotations.metadata.packages,
&annotations.lockfile.crates,
&annotations.pairred_extras,
false,
);
assert_eq!(context.name, "openssl-sys");
assert!(context.build_script_attrs.is_none());
assert_eq!(
context.targets,
vec![Rule::Library(TargetAttributes {
crate_name: "openssl_sys".to_owned(),
crate_root: Some("src/lib.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
})],
);
}
#[test]
fn context_rlib_crate_type() {
let annotations = crate_type_annotations();
let package_id = PackageId {
repr: "sysinfo 0.22.5 (registry+https://github.com/rust-lang/crates.io-index)"
.to_owned(),
};
let crate_annotation = &annotations.metadata.crates[&package_id];
let context = CrateContext::new(
crate_annotation,
&annotations.metadata.packages,
&annotations.lockfile.crates,
&annotations.pairred_extras,
false,
);
assert_eq!(context.name, "sysinfo");
assert!(context.build_script_attrs.is_none());
assert_eq!(
context.targets,
vec![Rule::Library(TargetAttributes {
crate_name: "sysinfo".to_owned(),
crate_root: Some("src/lib.rs".to_owned()),
srcs: Glob::new_rust_srcs(),
})],
);
}
}