1 //! Convert annotated metadata into a renderable context 2 3 pub(crate) mod crate_context; 4 mod platforms; 5 6 use std::collections::{BTreeMap, BTreeSet}; 7 use std::fs; 8 use std::path::{Path, PathBuf}; 9 10 use anyhow::Result; 11 use serde::{Deserialize, Serialize}; 12 13 use crate::config::CrateId; 14 use crate::context::platforms::resolve_cfg_platforms; 15 use crate::lockfile::Digest; 16 use crate::metadata::{Annotations, Dependency}; 17 use crate::select::Select; 18 use crate::utils::target_triple::TargetTriple; 19 20 pub(crate) use self::crate_context::*; 21 22 /// A struct containing information about a Cargo dependency graph in an easily to consume 23 /// format for rendering reproducible Bazel targets. 24 #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] 25 pub(crate) struct Context { 26 /// The collective checksum of all inputs to the context 27 pub(crate) checksum: Option<Digest>, 28 29 /// The collection of all crates that make up the dependency graph 30 pub(crate) crates: BTreeMap<CrateId, CrateContext>, 31 32 /// A subset of only crates with binary targets 33 pub(crate) binary_crates: BTreeSet<CrateId>, 34 35 /// A subset of workspace members mapping to their workspace 36 /// path relative to the workspace root 37 pub(crate) workspace_members: BTreeMap<CrateId, String>, 38 39 /// A mapping of `cfg` flags to platform triples supporting the configuration 40 pub(crate) conditions: BTreeMap<String, BTreeSet<TargetTriple>>, 41 42 /// A list of crates visible to any bazel module. 43 pub(crate) direct_deps: BTreeSet<CrateId>, 44 45 /// A list of crates visible to this bazel module. 46 pub(crate) direct_dev_deps: BTreeSet<CrateId>, 47 } 48 49 impl Context { try_from_path<T: AsRef<Path>>(path: T) -> Result<Self>50 pub(crate) fn try_from_path<T: AsRef<Path>>(path: T) -> Result<Self> { 51 let data = fs::read_to_string(path.as_ref())?; 52 Ok(serde_json::from_str(&data)?) 53 } 54 new(annotations: Annotations, sources_are_present: bool) -> Result<Self>55 pub(crate) fn new(annotations: Annotations, sources_are_present: bool) -> Result<Self> { 56 // Build a map of crate contexts 57 let crates: BTreeMap<CrateId, CrateContext> = annotations 58 .metadata 59 .crates 60 .values() 61 .map(|annotation| { 62 let context = CrateContext::new( 63 annotation, 64 &annotations.metadata.packages, 65 &annotations.lockfile.crates, 66 &annotations.pairred_extras, 67 &annotations.metadata.workspace_metadata.tree_metadata, 68 annotations.config.generate_binaries, 69 annotations.config.generate_build_scripts, 70 sources_are_present, 71 ); 72 let id = CrateId::new(context.name.clone(), context.version.clone()); 73 (id, context) 74 }) 75 .collect(); 76 77 // Filter for any crate that contains a binary 78 let binary_crates: BTreeSet<CrateId> = crates 79 .iter() 80 .filter(|(_, ctx)| ctx.targets.iter().any(|t| matches!(t, Rule::Binary(..)))) 81 // Only consider remote repositories (so non-workspace members). 82 .filter(|(_, ctx)| ctx.repository.is_some()) 83 .map(|(id, _)| id.clone()) 84 .collect(); 85 86 // Given a list of all conditional dependencies, build a set of platform 87 // triples which satisfy the conditions. 88 let conditions = resolve_cfg_platforms( 89 crates.values().collect(), 90 &annotations.config.supported_platform_triples, 91 )?; 92 93 // Generate a list of all workspace members 94 let workspace_members = annotations 95 .metadata 96 .workspace_members 97 .iter() 98 .filter_map(|id| { 99 let pkg = &annotations.metadata.packages[id]; 100 let package_path_id = match Self::get_package_path_id( 101 pkg, 102 &annotations.metadata.workspace_root, 103 &annotations.metadata.workspace_metadata.workspace_prefix, 104 &annotations.metadata.workspace_metadata.package_prefixes, 105 ) { 106 Ok(id) => id, 107 Err(e) => return Some(Err(e)), 108 }; 109 let crate_id = CrateId::from(pkg); 110 111 // Crates that have repository information are not considered workspace members. 112 // The assumpion is that they are "extra workspace members". 113 match crates[&crate_id].repository { 114 Some(_) => None, 115 None => Some(Ok((crate_id, package_path_id))), 116 } 117 }) 118 .collect::<Result<BTreeMap<CrateId, String>>>()?; 119 120 let add_crate_ids = |crates: &mut BTreeSet<CrateId>, 121 deps: &Select<BTreeSet<Dependency>>| { 122 for dep in deps.values() { 123 crates.insert(CrateId::from( 124 &annotations.metadata.packages[&dep.package_id], 125 )); 126 } 127 }; 128 129 let mut direct_deps: BTreeSet<CrateId> = BTreeSet::new(); 130 let mut direct_dev_deps: BTreeSet<CrateId> = BTreeSet::new(); 131 for id in &annotations.metadata.workspace_members { 132 let deps = &annotations.metadata.crates[id].deps; 133 add_crate_ids(&mut direct_deps, &deps.normal_deps); 134 add_crate_ids(&mut direct_deps, &deps.proc_macro_deps); 135 add_crate_ids(&mut direct_deps, &deps.build_deps); 136 add_crate_ids(&mut direct_deps, &deps.build_link_deps); 137 add_crate_ids(&mut direct_deps, &deps.build_proc_macro_deps); 138 add_crate_ids(&mut direct_dev_deps, &deps.normal_dev_deps); 139 add_crate_ids(&mut direct_dev_deps, &deps.proc_macro_dev_deps); 140 } 141 142 Ok(Self { 143 checksum: None, 144 crates, 145 binary_crates, 146 workspace_members, 147 conditions, 148 direct_dev_deps: direct_dev_deps.difference(&direct_deps).cloned().collect(), 149 direct_deps, 150 }) 151 } 152 153 // A helper function for locating the unique path in a workspace to a workspace member get_package_path_id( package: &cargo_metadata::Package, workspace_root: &Path, workspace_prefix: &Option<String>, package_prefixes: &BTreeMap<String, String>, ) -> Result<String>154 fn get_package_path_id( 155 package: &cargo_metadata::Package, 156 workspace_root: &Path, 157 workspace_prefix: &Option<String>, 158 package_prefixes: &BTreeMap<String, String>, 159 ) -> Result<String> { 160 // Locate the package's manifest directory 161 let manifest_dir = package 162 .manifest_path 163 .parent() 164 .expect("Every manifest should have a parent") 165 .as_std_path(); 166 167 // Compare it with the root of the workspace 168 let package_path_diff = pathdiff::diff_paths(manifest_dir, workspace_root) 169 .expect("Every workspace member's manifest is a child of the workspace root"); 170 171 // Ensure the package paths are adjusted in the macros according to the splicing results 172 let package_path = match package_prefixes.get(&package.name) { 173 // Any package prefix should be absolute and therefore always applied 174 Some(prefix) => PathBuf::from(prefix).join(package_path_diff), 175 // If no package prefix is present, attempt to apply the workspace prefix 176 // since workspace members would not have shown up with their own label 177 None => match workspace_prefix { 178 Some(prefix) => PathBuf::from(prefix).join(package_path_diff), 179 None => package_path_diff, 180 }, 181 }; 182 183 // Sanitize the path for increased consistency 184 let package_path_id = package_path 185 .display() 186 .to_string() 187 .replace('\\', "/") 188 .trim_matches('/') 189 .to_owned(); 190 191 Ok(package_path_id) 192 } 193 194 /// Create a set of all direct dependencies of workspace member crates. workspace_member_deps(&self) -> BTreeSet<CrateDependency>195 pub(crate) fn workspace_member_deps(&self) -> BTreeSet<CrateDependency> { 196 self.workspace_members 197 .keys() 198 .map(move |id| &self.crates[id]) 199 .flat_map(|ctx| { 200 IntoIterator::into_iter([ 201 &ctx.common_attrs.deps, 202 &ctx.common_attrs.deps_dev, 203 &ctx.common_attrs.proc_macro_deps, 204 &ctx.common_attrs.proc_macro_deps_dev, 205 ]) 206 .flat_map(|deps| deps.values()) 207 }) 208 .collect() 209 } 210 has_duplicate_workspace_member_dep(&self, dep: &CrateDependency) -> bool211 pub(crate) fn has_duplicate_workspace_member_dep(&self, dep: &CrateDependency) -> bool { 212 1 < self 213 .workspace_member_deps() 214 .into_iter() 215 .filter(|check| check.id.name == dep.id.name && check.alias == dep.alias) 216 .count() 217 } 218 has_duplicate_binary_crate(&self, bin: &CrateId) -> bool219 pub(crate) fn has_duplicate_binary_crate(&self, bin: &CrateId) -> bool { 220 1 < self 221 .binary_crates 222 .iter() 223 .filter(|check| check.name == bin.name) 224 .count() 225 } 226 } 227 228 #[cfg(test)] 229 mod test { 230 use super::*; 231 use semver::Version; 232 233 use crate::config::Config; 234 mock_context_common() -> Context235 fn mock_context_common() -> Context { 236 let annotations = Annotations::new( 237 crate::test::metadata::common(), 238 crate::test::lockfile::common(), 239 Config::default(), 240 ) 241 .unwrap(); 242 243 Context::new(annotations, false).unwrap() 244 } 245 mock_context_aliases() -> Context246 fn mock_context_aliases() -> Context { 247 let annotations = Annotations::new( 248 crate::test::metadata::alias(), 249 crate::test::lockfile::alias(), 250 Config::default(), 251 ) 252 .unwrap(); 253 254 Context::new(annotations, false).unwrap() 255 } 256 257 #[test] workspace_member_deps_collection()258 fn workspace_member_deps_collection() { 259 let context = mock_context_common(); 260 let workspace_member_deps = context.workspace_member_deps(); 261 262 assert_eq! { 263 workspace_member_deps 264 .iter() 265 .map(|dep| (&dep.id, context.has_duplicate_workspace_member_dep(dep))) 266 .collect::<Vec<_>>(), 267 [ 268 (&CrateId::new("bitflags".to_owned(), Version::new(1, 3, 2)), false), 269 (&CrateId::new("cfg-if".to_owned(), Version::new(1, 0, 0)), false), 270 ], 271 } 272 } 273 274 #[test] workspace_member_deps_with_aliases()275 fn workspace_member_deps_with_aliases() { 276 let context = mock_context_aliases(); 277 let workspace_member_deps = context.workspace_member_deps(); 278 279 assert_eq! { 280 workspace_member_deps 281 .iter() 282 .map(|dep| (&dep.id, context.has_duplicate_workspace_member_dep(dep))) 283 .collect::<Vec<_>>(), 284 [ 285 (&CrateId::new("log".to_owned(), Version::new(0, 3, 9)), false), 286 (&CrateId::new("log".to_owned(), Version::new(0, 4, 21)), false), 287 (&CrateId::new("names".to_owned(), Version::parse("0.12.1-dev").unwrap()), false), 288 (&CrateId::new("names".to_owned(), Version::new(0, 13, 0)), false), 289 (&CrateId::new("surrealdb".to_owned(), Version::new(1, 3, 1)), false), 290 (&CrateId::new("value-bag".to_owned(), Version::parse("1.0.0-alpha.7").unwrap()), false), 291 ], 292 } 293 } 294 295 #[test] serialization()296 fn serialization() { 297 let context = mock_context_aliases(); 298 299 // Serialize and deserialize the context object 300 let json_text = serde_json::to_string(&context).unwrap(); 301 let deserialized_context: Context = serde_json::from_str(&json_text).unwrap(); 302 303 // The data should be identical 304 assert_eq!(context, deserialized_context); 305 } 306 } 307