//! Convert annotated metadata into a renderable context pub(crate) mod crate_context; mod platforms; use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::{Path, PathBuf}; use anyhow::Result; use serde::{Deserialize, Serialize}; use crate::config::CrateId; use crate::context::platforms::resolve_cfg_platforms; use crate::lockfile::Digest; use crate::metadata::{Annotations, Dependency}; use crate::select::Select; use crate::utils::target_triple::TargetTriple; pub(crate) use self::crate_context::*; /// A struct containing information about a Cargo dependency graph in an easily to consume /// format for rendering reproducible Bazel targets. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct Context { /// The collective checksum of all inputs to the context pub(crate) checksum: Option, /// The collection of all crates that make up the dependency graph pub(crate) crates: BTreeMap, /// A subset of only crates with binary targets pub(crate) binary_crates: BTreeSet, /// A subset of workspace members mapping to their workspace /// path relative to the workspace root pub(crate) workspace_members: BTreeMap, /// A mapping of `cfg` flags to platform triples supporting the configuration pub(crate) conditions: BTreeMap>, /// A list of crates visible to any bazel module. pub(crate) direct_deps: BTreeSet, /// A list of crates visible to this bazel module. pub(crate) direct_dev_deps: BTreeSet, } impl Context { pub(crate) fn try_from_path>(path: T) -> Result { let data = fs::read_to_string(path.as_ref())?; Ok(serde_json::from_str(&data)?) } pub(crate) fn new(annotations: Annotations, sources_are_present: bool) -> Result { // Build a map of crate contexts let crates: BTreeMap = annotations .metadata .crates .values() .map(|annotation| { let context = CrateContext::new( annotation, &annotations.metadata.packages, &annotations.lockfile.crates, &annotations.pairred_extras, &annotations.metadata.workspace_metadata.tree_metadata, annotations.config.generate_binaries, annotations.config.generate_build_scripts, sources_are_present, ); let id = CrateId::new(context.name.clone(), context.version.clone()); (id, context) }) .collect(); // Filter for any crate that contains a binary let binary_crates: BTreeSet = crates .iter() .filter(|(_, ctx)| ctx.targets.iter().any(|t| matches!(t, Rule::Binary(..)))) // Only consider remote repositories (so non-workspace members). .filter(|(_, ctx)| ctx.repository.is_some()) .map(|(id, _)| id.clone()) .collect(); // Given a list of all conditional dependencies, build a set of platform // triples which satisfy the conditions. let conditions = resolve_cfg_platforms( crates.values().collect(), &annotations.config.supported_platform_triples, )?; // Generate a list of all workspace members let workspace_members = annotations .metadata .workspace_members .iter() .filter_map(|id| { let pkg = &annotations.metadata.packages[id]; let package_path_id = match Self::get_package_path_id( pkg, &annotations.metadata.workspace_root, &annotations.metadata.workspace_metadata.workspace_prefix, &annotations.metadata.workspace_metadata.package_prefixes, ) { Ok(id) => id, Err(e) => return Some(Err(e)), }; let crate_id = CrateId::from(pkg); // Crates that have repository information are not considered workspace members. // The assumpion is that they are "extra workspace members". match crates[&crate_id].repository { Some(_) => None, None => Some(Ok((crate_id, package_path_id))), } }) .collect::>>()?; let add_crate_ids = |crates: &mut BTreeSet, deps: &Select>| { for dep in deps.values() { crates.insert(CrateId::from( &annotations.metadata.packages[&dep.package_id], )); } }; let mut direct_deps: BTreeSet = BTreeSet::new(); let mut direct_dev_deps: BTreeSet = BTreeSet::new(); for id in &annotations.metadata.workspace_members { let deps = &annotations.metadata.crates[id].deps; add_crate_ids(&mut direct_deps, &deps.normal_deps); add_crate_ids(&mut direct_deps, &deps.proc_macro_deps); add_crate_ids(&mut direct_deps, &deps.build_deps); add_crate_ids(&mut direct_deps, &deps.build_link_deps); add_crate_ids(&mut direct_deps, &deps.build_proc_macro_deps); add_crate_ids(&mut direct_dev_deps, &deps.normal_dev_deps); add_crate_ids(&mut direct_dev_deps, &deps.proc_macro_dev_deps); } Ok(Self { checksum: None, crates, binary_crates, workspace_members, conditions, direct_dev_deps: direct_dev_deps.difference(&direct_deps).cloned().collect(), direct_deps, }) } // A helper function for locating the unique path in a workspace to a workspace member fn get_package_path_id( package: &cargo_metadata::Package, workspace_root: &Path, workspace_prefix: &Option, package_prefixes: &BTreeMap, ) -> Result { // Locate the package's manifest directory let manifest_dir = package .manifest_path .parent() .expect("Every manifest should have a parent") .as_std_path(); // Compare it with the root of the workspace let package_path_diff = pathdiff::diff_paths(manifest_dir, workspace_root) .expect("Every workspace member's manifest is a child of the workspace root"); // Ensure the package paths are adjusted in the macros according to the splicing results let package_path = match package_prefixes.get(&package.name) { // Any package prefix should be absolute and therefore always applied Some(prefix) => PathBuf::from(prefix).join(package_path_diff), // If no package prefix is present, attempt to apply the workspace prefix // since workspace members would not have shown up with their own label None => match workspace_prefix { Some(prefix) => PathBuf::from(prefix).join(package_path_diff), None => package_path_diff, }, }; // Sanitize the path for increased consistency let package_path_id = package_path .display() .to_string() .replace('\\', "/") .trim_matches('/') .to_owned(); Ok(package_path_id) } /// Create a set of all direct dependencies of workspace member crates. pub(crate) fn workspace_member_deps(&self) -> BTreeSet { self.workspace_members .keys() .map(move |id| &self.crates[id]) .flat_map(|ctx| { IntoIterator::into_iter([ &ctx.common_attrs.deps, &ctx.common_attrs.deps_dev, &ctx.common_attrs.proc_macro_deps, &ctx.common_attrs.proc_macro_deps_dev, ]) .flat_map(|deps| deps.values()) }) .collect() } pub(crate) fn has_duplicate_workspace_member_dep(&self, dep: &CrateDependency) -> bool { 1 < self .workspace_member_deps() .into_iter() .filter(|check| check.id.name == dep.id.name && check.alias == dep.alias) .count() } pub(crate) fn has_duplicate_binary_crate(&self, bin: &CrateId) -> bool { 1 < self .binary_crates .iter() .filter(|check| check.name == bin.name) .count() } } #[cfg(test)] mod test { use super::*; use semver::Version; use crate::config::Config; fn mock_context_common() -> Context { let annotations = Annotations::new( crate::test::metadata::common(), crate::test::lockfile::common(), Config::default(), ) .unwrap(); Context::new(annotations, false).unwrap() } fn mock_context_aliases() -> Context { let annotations = Annotations::new( crate::test::metadata::alias(), crate::test::lockfile::alias(), Config::default(), ) .unwrap(); Context::new(annotations, false).unwrap() } #[test] fn workspace_member_deps_collection() { let context = mock_context_common(); let workspace_member_deps = context.workspace_member_deps(); assert_eq! { workspace_member_deps .iter() .map(|dep| (&dep.id, context.has_duplicate_workspace_member_dep(dep))) .collect::>(), [ (&CrateId::new("bitflags".to_owned(), Version::new(1, 3, 2)), false), (&CrateId::new("cfg-if".to_owned(), Version::new(1, 0, 0)), false), ], } } #[test] fn workspace_member_deps_with_aliases() { let context = mock_context_aliases(); let workspace_member_deps = context.workspace_member_deps(); assert_eq! { workspace_member_deps .iter() .map(|dep| (&dep.id, context.has_duplicate_workspace_member_dep(dep))) .collect::>(), [ (&CrateId::new("log".to_owned(), Version::new(0, 3, 9)), false), (&CrateId::new("log".to_owned(), Version::new(0, 4, 21)), false), (&CrateId::new("names".to_owned(), Version::parse("0.12.1-dev").unwrap()), false), (&CrateId::new("names".to_owned(), Version::new(0, 13, 0)), false), (&CrateId::new("surrealdb".to_owned(), Version::new(1, 3, 1)), false), (&CrateId::new("value-bag".to_owned(), Version::parse("1.0.0-alpha.7").unwrap()), false), ], } } #[test] fn serialization() { let context = mock_context_aliases(); // Serialize and deserialize the context object let json_text = serde_json::to_string(&context).unwrap(); let deserialized_context: Context = serde_json::from_str(&json_text).unwrap(); // The data should be identical assert_eq!(context, deserialized_context); } }