//! A module for configuration information use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Formatter; use std::iter::Sum; use std::ops::Add; use std::path::Path; use std::str::FromStr; use std::{fmt, fs}; use anyhow::{Context, Result}; use cargo_lock::package::GitReference; use cargo_metadata::Package; use semver::VersionReq; use serde::de::value::SeqAccessDeserializer; use serde::de::{Deserializer, SeqAccess, Unexpected, Visitor}; use serde::{Deserialize, Serialize, Serializer}; use crate::select::{Select, Selectable}; use crate::utils::starlark::Label; use crate::utils::target_triple::TargetTriple; /// Representations of different kinds of crate vendoring into workspaces. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub(crate) enum VendorMode { /// Crates having full source being vendored into a workspace Local, /// Crates having only BUILD files with repository rules vendored into a workspace Remote, } impl std::fmt::Display for VendorMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt( match self { VendorMode::Local => "local", VendorMode::Remote => "remote", }, f, ) } } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub(crate) struct RenderConfig { /// The name of the repository being rendered pub(crate) repository_name: String, /// The pattern to use for BUILD file names. /// Eg. `//:BUILD.{name}-{version}.bazel` #[serde(default = "default_build_file_template")] pub(crate) build_file_template: String, /// The pattern to use for a crate target. /// Eg. `@{repository}__{name}-{version}//:{target}` #[serde(default = "default_crate_label_template")] pub(crate) crate_label_template: String, /// The pattern to use for the `defs.bzl` and `BUILD.bazel` /// file names used for the crates module. /// Eg. `//:{file}` #[serde(default = "default_crates_module_template")] pub(crate) crates_module_template: String, /// The pattern used for a crate's repository name. /// Eg. `{repository}__{name}-{version}` #[serde(default = "default_crate_repository_template")] pub(crate) crate_repository_template: String, /// Default alias rule to use for packages. Can be overridden by annotations. #[serde(default)] pub(crate) default_alias_rule: AliasRule, /// The default of the `package_name` parameter to use for the module macros like `all_crate_deps`. /// In general, this should be be unset to allow the macros to do auto-detection in the analysis phase. pub(crate) default_package_name: Option, /// Whether to generate `target_compatible_with` annotations on the generated BUILD files. This /// catches a `target_triple`being targeted that isn't declared in `supported_platform_triples`. #[serde(default = "default_generate_target_compatible_with")] pub(crate) generate_target_compatible_with: bool, /// The pattern to use for platform constraints. /// Eg. `@rules_rust//rust/platform:{triple}`. #[serde(default = "default_platforms_template")] pub(crate) platforms_template: String, /// The command to use for regenerating generated files. pub(crate) regen_command: String, /// An optional configuration for rendering content to be rendered into repositories. pub(crate) vendor_mode: Option, /// Whether to generate package metadata #[serde(default = "default_generate_rules_license_metadata")] pub(crate) generate_rules_license_metadata: bool, } // Default is manually implemented so that the default values match the default // values when deserializing, which involves calling the vairous `default_x()` // functions specified in `#[serde(default = "default_x")]`. impl Default for RenderConfig { fn default() -> Self { RenderConfig { repository_name: String::default(), build_file_template: default_build_file_template(), crate_label_template: default_crate_label_template(), crates_module_template: default_crates_module_template(), crate_repository_template: default_crate_repository_template(), default_alias_rule: AliasRule::default(), default_package_name: Option::default(), generate_target_compatible_with: default_generate_target_compatible_with(), platforms_template: default_platforms_template(), regen_command: String::default(), vendor_mode: Option::default(), generate_rules_license_metadata: default_generate_rules_license_metadata(), } } } impl RenderConfig { pub(crate) fn are_sources_present(&self) -> bool { self.vendor_mode == Some(VendorMode::Local) } } fn default_build_file_template() -> String { "//:BUILD.{name}-{version}.bazel".to_owned() } fn default_crates_module_template() -> String { "//:{file}".to_owned() } fn default_crate_label_template() -> String { "@{repository}__{name}-{version}//:{target}".to_owned() } fn default_crate_repository_template() -> String { "{repository}__{name}-{version}".to_owned() } fn default_platforms_template() -> String { "@rules_rust//rust/platform:{triple}".to_owned() } fn default_generate_target_compatible_with() -> bool { true } fn default_generate_rules_license_metadata() -> bool { false } /// A representation of some Git identifier used to represent the "revision" or "pin" of a checkout. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum Commitish { /// From a tag. Tag(String), /// From the HEAD of a branch. Branch(String), /// From a specific revision. Rev(String), } impl From for Commitish { fn from(git_ref: GitReference) -> Self { match git_ref { GitReference::Tag(v) => Self::Tag(v), GitReference::Branch(v) => Self::Branch(v), GitReference::Rev(v) => Self::Rev(v), } } } #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)] pub(crate) enum AliasRule { #[default] #[serde(rename = "alias")] Alias, #[serde(rename = "dbg")] Dbg, #[serde(rename = "fastbuild")] Fastbuild, #[serde(rename = "opt")] Opt, #[serde(untagged)] Custom { bzl: String, rule: String }, } impl AliasRule { pub(crate) fn bzl(&self) -> Option { match self { AliasRule::Alias => None, AliasRule::Dbg | AliasRule::Fastbuild | AliasRule::Opt => { Some("//:alias_rules.bzl".to_owned()) } AliasRule::Custom { bzl, .. } => Some(bzl.clone()), } } pub(crate) fn rule(&self) -> String { match self { AliasRule::Alias => "alias".to_owned(), AliasRule::Dbg => "transition_alias_dbg".to_owned(), AliasRule::Fastbuild => "transition_alias_fastbuild".to_owned(), AliasRule::Opt => "transition_alias_opt".to_owned(), AliasRule::Custom { rule, .. } => rule.clone(), } } } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct CrateAnnotations { /// Which subset of the crate's bins should get produced as `rust_binary` targets. pub(crate) gen_binaries: Option, /// Determins whether or not Cargo build scripts should be generated for the current package pub(crate) gen_build_script: Option, /// Additional data to pass to /// [deps](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-deps) attribute. pub(crate) deps: Option>>, /// Additional data to pass to /// [proc_macro_deps](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-proc_macro_deps) attribute. pub(crate) proc_macro_deps: Option>>, /// Additional data to pass to the target's /// [crate_features](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-crate_features) attribute. pub(crate) crate_features: Option>>, /// Additional data to pass to the target's /// [data](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-data) attribute. pub(crate) data: Option>>, /// An optional glob pattern to set on the /// [data](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-data) attribute. pub(crate) data_glob: Option>, /// Additional data to pass to /// [compile_data](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-compile_data) attribute. pub(crate) compile_data: Option>>, /// An optional glob pattern to set on the /// [compile_data](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-compile_data) attribute. pub(crate) compile_data_glob: Option>, /// If true, disables pipelining for library targets generated for this crate. pub(crate) disable_pipelining: bool, /// Additional data to pass to the target's /// [rustc_env](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-rustc_env) attribute. pub(crate) rustc_env: Option>>, /// Additional data to pass to the target's /// [rustc_env_files](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-rustc_env_files) attribute. pub(crate) rustc_env_files: Option>>, /// Additional data to pass to the target's /// [rustc_flags](https://bazelbuild.github.io/rules_rust/defs.html#rust_library-rustc_flags) attribute. pub(crate) rustc_flags: Option>>, /// Additional dependencies to pass to a build script's /// [deps](https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script-deps) attribute. pub(crate) build_script_deps: Option>>, /// Additional data to pass to a build script's /// [proc_macro_deps](https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script-proc_macro_deps) attribute. pub(crate) build_script_proc_macro_deps: Option>>, /// Additional data to pass to a build script's /// [build_script_data](https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script-data) attribute. pub(crate) build_script_data: Option>>, /// Additional data to pass to a build script's /// [tools](https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script-tools) attribute. pub(crate) build_script_tools: Option>>, /// An optional glob pattern to set on the /// [build_script_data](https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script-build_script_env) attribute. pub(crate) build_script_data_glob: Option>, /// Additional environment variables to pass to a build script's /// [build_script_env](https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script-rustc_env) attribute. pub(crate) build_script_env: Option>>, /// Additional rustc_env flags to pass to a build script's /// [rustc_env](https://bazelbuild.github.io/rules_rust/cargo.html#cargo_build_script-rustc_env) attribute. pub(crate) build_script_rustc_env: Option>>, /// Additional labels to pass to a build script's /// [toolchains](https://bazel.build/reference/be/common-definitions#common-attributes) attribute. pub(crate) build_script_toolchains: Option>, /// Directory to run the crate's build script in. If not set, will run in the manifest directory, otherwise a directory relative to the exec root. pub(crate) build_script_rundir: Option>, /// A scratch pad used to write arbitrary text to target BUILD files. pub(crate) additive_build_file_content: Option, /// For git sourced crates, this is a the /// [git_repository::shallow_since](https://docs.bazel.build/versions/main/repo/git.html#new_git_repository-shallow_since) attribute. pub(crate) shallow_since: Option, /// The `patch_args` attribute of a Bazel repository rule. See /// [http_archive.patch_args](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_args) pub(crate) patch_args: Option>, /// The `patch_tool` attribute of a Bazel repository rule. See /// [http_archive.patch_tool](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_tool) pub(crate) patch_tool: Option, /// The `patches` attribute of a Bazel repository rule. See /// [http_archive.patches](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patches) pub(crate) patches: Option>, /// Extra targets the should be aliased during rendering. pub(crate) extra_aliased_targets: Option>, /// Transition rule to use instead of `native.alias()`. pub(crate) alias_rule: Option, /// The crates to use instead of the generated one. pub(crate) override_targets: Option>, } macro_rules! joined_extra_member { ($lhs:expr, $rhs:expr, $fn_new:expr, $fn_extend:expr) => { if let Some(lhs) = $lhs { if let Some(rhs) = $rhs { let mut new = $fn_new(); $fn_extend(&mut new, lhs); $fn_extend(&mut new, rhs); Some(new) } else { Some(lhs) } } else if $rhs.is_some() { $rhs } else { None } }; } impl Add for CrateAnnotations { type Output = CrateAnnotations; fn add(self, rhs: Self) -> Self::Output { fn select_merge(lhs: Option>, rhs: Option>) -> Option> where T: Selectable, { match (lhs, rhs) { (Some(lhs), Some(rhs)) => Some(Select::merge(lhs, rhs)), (Some(lhs), None) => Some(lhs), (None, Some(rhs)) => Some(rhs), (None, None) => None, } } let concat_string = |lhs: &mut String, rhs: String| { *lhs = format!("{lhs}{rhs}"); }; #[rustfmt::skip] let output = CrateAnnotations { gen_binaries: self.gen_binaries.or(rhs.gen_binaries), gen_build_script: self.gen_build_script.or(rhs.gen_build_script), deps: select_merge(self.deps, rhs.deps), proc_macro_deps: select_merge(self.proc_macro_deps, rhs.proc_macro_deps), crate_features: select_merge(self.crate_features, rhs.crate_features), data: select_merge(self.data, rhs.data), data_glob: joined_extra_member!(self.data_glob, rhs.data_glob, BTreeSet::new, BTreeSet::extend), disable_pipelining: self.disable_pipelining || rhs.disable_pipelining, compile_data: select_merge(self.compile_data, rhs.compile_data), compile_data_glob: joined_extra_member!(self.compile_data_glob, rhs.compile_data_glob, BTreeSet::new, BTreeSet::extend), rustc_env: select_merge(self.rustc_env, rhs.rustc_env), rustc_env_files: select_merge(self.rustc_env_files, rhs.rustc_env_files), rustc_flags: select_merge(self.rustc_flags, rhs.rustc_flags), build_script_deps: select_merge(self.build_script_deps, rhs.build_script_deps), build_script_proc_macro_deps: select_merge(self.build_script_proc_macro_deps, rhs.build_script_proc_macro_deps), build_script_data: select_merge(self.build_script_data, rhs.build_script_data), build_script_tools: select_merge(self.build_script_tools, rhs.build_script_tools), build_script_data_glob: joined_extra_member!(self.build_script_data_glob, rhs.build_script_data_glob, BTreeSet::new, BTreeSet::extend), build_script_env: select_merge(self.build_script_env, rhs.build_script_env), build_script_rustc_env: select_merge(self.build_script_rustc_env, rhs.build_script_rustc_env), build_script_toolchains: joined_extra_member!(self.build_script_toolchains, rhs.build_script_toolchains, BTreeSet::new, BTreeSet::extend), build_script_rundir: self.build_script_rundir.or(rhs.build_script_rundir), additive_build_file_content: joined_extra_member!(self.additive_build_file_content, rhs.additive_build_file_content, String::new, concat_string), shallow_since: self.shallow_since.or(rhs.shallow_since), patch_args: joined_extra_member!(self.patch_args, rhs.patch_args, Vec::new, Vec::extend), patch_tool: self.patch_tool.or(rhs.patch_tool), patches: joined_extra_member!(self.patches, rhs.patches, BTreeSet::new, BTreeSet::extend), extra_aliased_targets: joined_extra_member!(self.extra_aliased_targets, rhs.extra_aliased_targets, BTreeMap::new, BTreeMap::extend), alias_rule: self.alias_rule.or(rhs.alias_rule), override_targets: self.override_targets.or(rhs.override_targets), }; output } } impl Sum for CrateAnnotations { fn sum>(iter: I) -> Self { iter.fold(CrateAnnotations::default(), |a, b| a + b) } } /// A subset of `crate.annotation` that we allow packages to define in their /// free-form Cargo.toml metadata. /// /// ```toml /// [package.metadata.bazel] /// additive_build_file_contents = """ /// ... /// """ /// data = ["font.woff2"] /// extra_aliased_targets = { ... } /// gen_build_script = false /// ``` /// /// These are considered default values which apply if the Bazel workspace does /// not specify a different value for the same annotation in their /// crates_repository attributes. #[derive(Debug, Deserialize)] pub(crate) struct AnnotationsProvidedByPackage { pub(crate) gen_build_script: Option, pub(crate) data: Option>>, pub(crate) data_glob: Option>, pub(crate) deps: Option>>, pub(crate) compile_data: Option>>, pub(crate) compile_data_glob: Option>, pub(crate) rustc_env: Option>>, pub(crate) rustc_env_files: Option>>, pub(crate) rustc_flags: Option>>, pub(crate) build_script_env: Option>>, pub(crate) build_script_rustc_env: Option>>, pub(crate) build_script_rundir: Option>, pub(crate) additive_build_file_content: Option, pub(crate) extra_aliased_targets: Option>, } impl CrateAnnotations { pub(crate) fn apply_defaults_from_package_metadata( &mut self, pkg_metadata: &serde_json::Value, ) { #[deny(unused_variables)] let AnnotationsProvidedByPackage { gen_build_script, data, data_glob, deps, compile_data, compile_data_glob, rustc_env, rustc_env_files, rustc_flags, build_script_env, build_script_rustc_env, build_script_rundir, additive_build_file_content, extra_aliased_targets, } = match AnnotationsProvidedByPackage::deserialize(&pkg_metadata["bazel"]) { Ok(annotations) => annotations, // Ignore bad annotations. The set of supported annotations evolves // over time across different versions of crate_universe, and we // don't want a library to be impossible to import into Bazel for // having old or broken annotations. The Bazel workspace can specify // its own correct annotations. Err(_) => return, }; fn default(workspace_value: &mut Option, default_value: Option) { if workspace_value.is_none() { *workspace_value = default_value; } } default(&mut self.gen_build_script, gen_build_script); default(&mut self.gen_build_script, gen_build_script); default(&mut self.data, data); default(&mut self.data_glob, data_glob); default(&mut self.deps, deps); default(&mut self.compile_data, compile_data); default(&mut self.compile_data_glob, compile_data_glob); default(&mut self.rustc_env, rustc_env); default(&mut self.rustc_env_files, rustc_env_files); default(&mut self.rustc_flags, rustc_flags); default(&mut self.build_script_env, build_script_env); default(&mut self.build_script_rustc_env, build_script_rustc_env); default(&mut self.build_script_rundir, build_script_rundir); default( &mut self.additive_build_file_content, additive_build_file_content, ); default(&mut self.extra_aliased_targets, extra_aliased_targets); } } /// A unique identifier for Crates #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct CrateId { /// The name of the crate pub name: String, /// The crate's semantic version pub version: semver::Version, } impl CrateId { /// Construct a new [CrateId] pub(crate) fn new(name: String, version: semver::Version) -> Self { Self { name, version } } } impl From<&Package> for CrateId { fn from(package: &Package) -> Self { Self { name: package.name.clone(), version: package.version.clone(), } } } impl Serialize for CrateId { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&format!("{} {}", self.name, self.version)) } } struct CrateIdVisitor; impl<'de> Visitor<'de> for CrateIdVisitor { type Value = CrateId; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("Expected string value of `{name} {version}`.") } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { let (name, version_str) = v.rsplit_once(' ').ok_or_else(|| { E::custom(format!( "Expected string value of `{{name}} {{version}}`. Got '{v}'" )) })?; let version = semver::Version::parse(version_str).map_err(|err| { E::custom(format!( "Couldn't parse {version_str} as a semver::Version: {err}" )) })?; Ok(CrateId { name: name.to_string(), version, }) } } impl<'de> Deserialize<'de> for CrateId { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_str(CrateIdVisitor) } } impl std::fmt::Display for CrateId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&format!("{} {}", self.name, self.version), f) } } #[derive(Debug, Hash, Clone, PartialEq, Eq)] pub(crate) enum GenBinaries { All, Some(BTreeSet), } impl Default for GenBinaries { fn default() -> Self { GenBinaries::Some(BTreeSet::new()) } } impl Serialize for GenBinaries { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { GenBinaries::All => serializer.serialize_bool(true), GenBinaries::Some(set) if set.is_empty() => serializer.serialize_bool(false), GenBinaries::Some(set) => serializer.collect_seq(set), } } } impl<'de> Deserialize<'de> for GenBinaries { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserializer.deserialize_any(GenBinariesVisitor) } } struct GenBinariesVisitor; impl<'de> Visitor<'de> for GenBinariesVisitor { type Value = GenBinaries; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("boolean, or array of bin names") } fn visit_bool(self, gen_binaries: bool) -> Result { if gen_binaries { Ok(GenBinaries::All) } else { Ok(GenBinaries::Some(BTreeSet::new())) } } fn visit_seq(self, seq: A) -> Result where A: SeqAccess<'de>, { BTreeSet::deserialize(SeqAccessDeserializer::new(seq)).map(GenBinaries::Some) } } /// Workspace specific settings to control how targets are generated #[derive(Debug, Default, Serialize, Deserialize, Clone)] #[serde(deny_unknown_fields)] pub(crate) struct Config { /// Whether to generate `rust_binary` targets for all bins by default pub(crate) generate_binaries: bool, /// Whether or not to generate Cargo build scripts by default pub(crate) generate_build_scripts: bool, /// Additional settings to apply to generated crates #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub(crate) annotations: BTreeMap, /// Settings used to determine various render info pub(crate) rendering: RenderConfig, /// The contents of a Cargo configuration file pub(crate) cargo_config: Option, /// A set of platform triples to use in generated select statements #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] pub(crate) supported_platform_triples: BTreeSet, } impl Config { pub(crate) fn try_from_path>(path: T) -> Result { let data = fs::read_to_string(path)?; Ok(serde_json::from_str(&data)?) } } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct CrateNameAndVersionReq { /// The name of the crate pub name: String, version_req_string: VersionReqString, } impl Serialize for CrateNameAndVersionReq { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&format!( "{} {}", self.name, self.version_req_string.original )) } } struct CrateNameAndVersionReqVisitor; impl<'de> Visitor<'de> for CrateNameAndVersionReqVisitor { type Value = CrateNameAndVersionReq; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("Expected string value of `{name} {version}`.") } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { let (name, version) = v.rsplit_once(' ').ok_or_else(|| { E::custom(format!( "Expected string value of `{{name}} {{version}}`. Got '{v}'" )) })?; version .parse() .map(|version| CrateNameAndVersionReq { name: name.to_string(), version_req_string: version, }) .map_err(|err| E::custom(err.to_string())) } } impl<'de> Deserialize<'de> for CrateNameAndVersionReq { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_str(CrateNameAndVersionReqVisitor) } } /// A version requirement (i.e. a semver::VersionReq) which preserves the original string it was parsed from. /// This means that you can report back to the user whether they wrote `1` or `1.0.0` or `^1.0.0` or `>=1,<2`, /// and support exact round-trip serialization and deserialization. #[derive(Clone, Debug)] pub struct VersionReqString { original: String, parsed: VersionReq, } impl FromStr for VersionReqString { type Err = anyhow::Error; fn from_str(original: &str) -> Result { let parsed = VersionReq::parse(original) .context("VersionReqString must be a valid semver requirement")?; Ok(VersionReqString { original: original.to_owned(), parsed, }) } } impl PartialEq for VersionReqString { fn eq(&self, other: &Self) -> bool { self.original == other.original } } impl Eq for VersionReqString {} impl PartialOrd for VersionReqString { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for VersionReqString { fn cmp(&self, other: &Self) -> Ordering { Ord::cmp(&self.original, &other.original) } } impl Serialize for VersionReqString { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { serializer.serialize_str(&self.original) } } impl<'de> Deserialize<'de> for VersionReqString { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { struct StringVisitor; impl<'de> Visitor<'de> for StringVisitor { type Value = String; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { formatter.write_str("string of a semver requirement") } } let original = deserializer.deserialize_str(StringVisitor)?; let parsed = VersionReq::parse(&original).map_err(|_| { serde::de::Error::invalid_value( Unexpected::Str(&original), &"a valid semver requirement", ) })?; Ok(VersionReqString { original, parsed }) } } impl CrateNameAndVersionReq { #[cfg(test)] pub fn new(name: String, version_req_string: VersionReqString) -> CrateNameAndVersionReq { CrateNameAndVersionReq { name, version_req_string, } } /// Compares a [CrateNameAndVersionReq] against a [cargo_metadata::Package]. pub fn matches(&self, package: &Package) -> bool { // If the package name does not match, it's obviously // not the right package if self.name != "*" && self.name != package.name { return false; } // First see if the package version matches exactly if package.version.to_string() == self.version_req_string.original { return true; } // If the version provided is the wildcard "*", it matches. Do not // delegate to the semver crate in this case because semver does not // consider "*" to match prerelease packages. That's expected behavior // in the context of declaring package dependencies, but not in the // context of declaring which versions of preselected packages an // annotation applies to. if self.version_req_string.original == "*" { return true; } // Next, check to see if the version provided is a semver req and // check if the package matches the condition self.version_req_string.parsed.matches(&package.version) } } #[cfg(test)] mod test { use super::*; use crate::test::*; #[test] fn test_crate_id_serde() { let id: CrateId = serde_json::from_str("\"crate 0.1.0\"").unwrap(); assert_eq!( id, CrateId::new("crate".to_owned(), semver::Version::new(0, 1, 0)) ); assert_eq!(serde_json::to_string(&id).unwrap(), "\"crate 0.1.0\""); } #[test] fn test_crate_id_matches() { let mut package = mock_cargo_metadata_package(); let id = CrateNameAndVersionReq::new("mock-pkg".to_owned(), "0.1.0".parse().unwrap()); package.version = cargo_metadata::semver::Version::new(0, 1, 0); assert!(id.matches(&package)); package.version = cargo_metadata::semver::Version::new(1, 0, 0); assert!(!id.matches(&package)); } #[test] fn test_crate_name_and_version_req_serde() { let id: CrateNameAndVersionReq = serde_json::from_str("\"crate 0.1.0\"").unwrap(); assert_eq!( id, CrateNameAndVersionReq::new( "crate".to_owned(), VersionReqString::from_str("0.1.0").unwrap() ) ); assert_eq!(serde_json::to_string(&id).unwrap(), "\"crate 0.1.0\""); } #[test] fn test_crate_name_and_version_req_serde_semver() { let id: CrateNameAndVersionReq = serde_json::from_str("\"crate *\"").unwrap(); assert_eq!( id, CrateNameAndVersionReq::new( "crate".to_owned(), VersionReqString::from_str("*").unwrap() ) ); assert_eq!(serde_json::to_string(&id).unwrap(), "\"crate *\""); } #[test] fn test_crate_name_and_version_req_semver_matches() { let mut package = mock_cargo_metadata_package(); package.version = cargo_metadata::semver::Version::new(1, 0, 0); let id = CrateNameAndVersionReq::new("mock-pkg".to_owned(), "*".parse().unwrap()); assert!(id.matches(&package)); let mut prerelease = mock_cargo_metadata_package(); prerelease.version = cargo_metadata::semver::Version::parse("1.0.0-pre.0").unwrap(); assert!(id.matches(&prerelease)); let id = CrateNameAndVersionReq::new("mock-pkg".to_owned(), "<1".parse().unwrap()); assert!(!id.matches(&package)); } #[test] fn deserialize_config() { let runfiles = runfiles::Runfiles::create().unwrap(); let path = runfiles::rlocation!( runfiles, "rules_rust/crate_universe/test_data/serialized_configs/config.json" ); let content = std::fs::read_to_string(path).unwrap(); let config: Config = serde_json::from_str(&content).unwrap(); // Annotations let annotation = config .annotations .get(&CrateNameAndVersionReq::new( "rand".to_owned(), "0.8.5".parse().unwrap(), )) .unwrap(); assert_eq!( annotation.crate_features, Some(Select::from_value(BTreeSet::from(["small_rng".to_owned()]))) ); // Global settings assert!(config.cargo_config.is_none()); assert!(!config.generate_binaries); assert!(!config.generate_build_scripts); // Render Config assert_eq!( config.rendering.platforms_template, "//custom/platform:{triple}" ); } }