1 //! Collect and store information from Cargo metadata specific to Bazel's needs
2
3 use std::collections::{BTreeMap, BTreeSet};
4 use std::convert::TryFrom;
5 use std::path::PathBuf;
6
7 use anyhow::{bail, Result};
8 use cargo_metadata::{Node, Package, PackageId};
9 use hex::ToHex;
10 use serde::{Deserialize, Serialize};
11
12 use crate::config::{Commitish, Config, CrateAnnotations, CrateId};
13 use crate::metadata::dependency::DependencySet;
14 use crate::select::Select;
15 use crate::splicing::{SourceInfo, WorkspaceMetadata};
16
17 pub(crate) type CargoMetadata = cargo_metadata::Metadata;
18 pub(crate) type CargoLockfile = cargo_lock::Lockfile;
19
20 /// Additional information about a crate relative to other crates in a dependency graph.
21 #[derive(Debug, Serialize, Deserialize)]
22 pub(crate) struct CrateAnnotation {
23 /// The crate's node in the Cargo "resolve" graph.
24 pub(crate) node: Node,
25
26 /// The crate's sorted dependencies.
27 pub(crate) deps: DependencySet,
28 }
29
30 /// Additional information about a Cargo workspace's metadata.
31 #[derive(Debug, Default, Serialize, Deserialize)]
32 pub(crate) struct MetadataAnnotation {
33 /// All packages found within the Cargo metadata
34 pub(crate) packages: BTreeMap<PackageId, Package>,
35
36 /// All [CrateAnnotation]s for all packages
37 pub(crate) crates: BTreeMap<PackageId, CrateAnnotation>,
38
39 /// All packages that are workspace members
40 pub(crate) workspace_members: BTreeSet<PackageId>,
41
42 /// The path to the directory containing the Cargo workspace that produced the metadata.
43 pub(crate) workspace_root: PathBuf,
44
45 /// Information on the Cargo workspace.
46 pub(crate) workspace_metadata: WorkspaceMetadata,
47 }
48
49 impl MetadataAnnotation {
new(metadata: CargoMetadata) -> MetadataAnnotation50 pub(crate) fn new(metadata: CargoMetadata) -> MetadataAnnotation {
51 // UNWRAP: The workspace metadata should be written by a controlled process. This should not return a result
52 let workspace_metadata = find_workspace_metadata(&metadata).unwrap_or_default();
53
54 let resolve = metadata
55 .resolve
56 .as_ref()
57 .expect("The metadata provided requires a resolve graph")
58 .clone();
59
60 let is_node_workspace_member = |node: &Node, metadata: &CargoMetadata| -> bool {
61 metadata.workspace_members.iter().any(|pkg| pkg == &node.id)
62 };
63
64 let workspace_members: BTreeSet<PackageId> = resolve
65 .nodes
66 .iter()
67 .filter(|node| is_node_workspace_member(node, &metadata))
68 .map(|node| node.id.clone())
69 .collect();
70
71 let crates = resolve
72 .nodes
73 .iter()
74 .map(|node| {
75 (
76 node.id.clone(),
77 Self::annotate_crate(node.clone(), &metadata),
78 )
79 })
80 .collect();
81
82 let packages = metadata
83 .packages
84 .into_iter()
85 .map(|pkg| (pkg.id.clone(), pkg))
86 .collect();
87
88 MetadataAnnotation {
89 packages,
90 crates,
91 workspace_members,
92 workspace_root: PathBuf::from(metadata.workspace_root.as_std_path()),
93 workspace_metadata,
94 }
95 }
96
annotate_crate(node: Node, metadata: &CargoMetadata) -> CrateAnnotation97 fn annotate_crate(node: Node, metadata: &CargoMetadata) -> CrateAnnotation {
98 // Gather all dependencies
99 let deps = DependencySet::new_for_node(&node, metadata);
100
101 CrateAnnotation { node, deps }
102 }
103 }
104
105 /// Additional information about how and where to acquire a crate's source code from.
106 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
107 pub(crate) enum SourceAnnotation {
108 Git {
109 /// The Git url where to clone the source from.
110 remote: String,
111
112 /// The revision information for the git repository. This is used for
113 /// [git_repository::commit](https://docs.bazel.build/versions/main/repo/git.html#git_repository-commit),
114 /// [git_repository::tag](https://docs.bazel.build/versions/main/repo/git.html#git_repository-tag), or
115 /// [git_repository::branch](https://docs.bazel.build/versions/main/repo/git.html#git_repository-branch).
116 commitish: Commitish,
117
118 /// See [git_repository::shallow_since](https://docs.bazel.build/versions/main/repo/git.html#git_repository-shallow_since)
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 shallow_since: Option<String>,
121
122 /// See [git_repository::strip_prefix](https://docs.bazel.build/versions/main/repo/git.html#git_repository-strip_prefix)
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 strip_prefix: Option<String>,
125
126 /// See [git_repository::patch_args](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patch_args)
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 patch_args: Option<Vec<String>>,
129
130 /// See [git_repository::patch_tool](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patch_tool)
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 patch_tool: Option<String>,
133
134 /// See [git_repository::patches](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patches)
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 patches: Option<BTreeSet<String>>,
137 },
138 Http {
139 /// See [http_archive::url](https://docs.bazel.build/versions/main/repo/http.html#http_archive-url)
140 url: String,
141
142 /// See [http_archive::sha256](https://docs.bazel.build/versions/main/repo/http.html#http_archive-sha256)
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 sha256: Option<String>,
145
146 /// See [http_archive::patch_args](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_args)
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 patch_args: Option<Vec<String>>,
149
150 /// See [http_archive::patch_tool](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_tool)
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 patch_tool: Option<String>,
153
154 /// See [http_archive::patches](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patches)
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 patches: Option<BTreeSet<String>>,
157 },
158 }
159
160 /// Additional information related to [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html)
161 /// data used for improved determinism.
162 #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
163 pub(crate) struct LockfileAnnotation {
164 /// A mapping of crates/packages to additional source (network location) information.
165 pub(crate) crates: BTreeMap<PackageId, SourceAnnotation>,
166 }
167
168 impl LockfileAnnotation {
new(lockfile: CargoLockfile, metadata: &CargoMetadata) -> Result<Self>169 pub(crate) fn new(lockfile: CargoLockfile, metadata: &CargoMetadata) -> Result<Self> {
170 let workspace_metadata = find_workspace_metadata(metadata).unwrap_or_default();
171
172 let nodes: Vec<&Node> = metadata
173 .resolve
174 .as_ref()
175 .expect("Metadata is expected to have a resolve graph")
176 .nodes
177 .iter()
178 .filter(|node| !is_workspace_member(&node.id, metadata))
179 .collect();
180
181 // Produce source annotations for each crate in the resolve graph
182 let crates = nodes
183 .iter()
184 .map(|node| {
185 Ok((
186 node.id.clone(),
187 Self::collect_source_annotations(
188 node,
189 metadata,
190 &lockfile,
191 &workspace_metadata,
192 )?,
193 ))
194 })
195 .collect::<Result<BTreeMap<PackageId, SourceAnnotation>>>()?;
196
197 Ok(Self { crates })
198 }
199
200 /// Resolve all URLs and checksum-like data for each package
collect_source_annotations( node: &Node, metadata: &CargoMetadata, lockfile: &CargoLockfile, workspace_metadata: &WorkspaceMetadata, ) -> Result<SourceAnnotation>201 fn collect_source_annotations(
202 node: &Node,
203 metadata: &CargoMetadata,
204 lockfile: &CargoLockfile,
205 workspace_metadata: &WorkspaceMetadata,
206 ) -> Result<SourceAnnotation> {
207 let pkg = &metadata[&node.id];
208
209 // Locate the matching lock package for the current crate
210 let lock_pkg = match cargo_meta_pkg_to_locked_pkg(pkg, &lockfile.packages) {
211 Some(lock_pkg) => lock_pkg,
212 None => bail!(
213 "Could not find lockfile entry matching metadata package '{}'",
214 pkg.name
215 ),
216 };
217
218 // Check for spliced information about a crate's network source.
219 let spliced_source_info = Self::find_source_annotation(lock_pkg, workspace_metadata);
220
221 // Parse it's source info. The check above should prevent a panic
222 let source = match lock_pkg.source.as_ref() {
223 Some(source) => source,
224 None => match spliced_source_info {
225 Some(info) => {
226 return Ok(SourceAnnotation::Http {
227 url: info.url,
228 sha256: Some(info.sha256),
229 patch_args: None,
230 patch_tool: None,
231 patches: None,
232 })
233 }
234 None => bail!(
235 "The package '{:?} {:?}' has no source info so no annotation can be made",
236 lock_pkg.name,
237 lock_pkg.version
238 ),
239 },
240 };
241
242 // Handle any git repositories
243 if let Some(git_ref) = source.git_reference() {
244 let strip_prefix = Self::extract_git_strip_prefix(pkg)?;
245
246 return Ok(SourceAnnotation::Git {
247 remote: source.url().to_string(),
248 commitish: source
249 .precise()
250 .map(|rev| Commitish::Rev(rev.to_string()))
251 .unwrap_or(Commitish::from(git_ref.clone())),
252 shallow_since: None,
253 strip_prefix,
254 patch_args: None,
255 patch_tool: None,
256 patches: None,
257 });
258 }
259
260 // One of the last things that should be checked is the spliced source information as
261 // other sources may more accurately represent where a crate should be downloaded.
262 if let Some(info) = spliced_source_info {
263 return Ok(SourceAnnotation::Http {
264 url: info.url,
265 sha256: Some(info.sha256),
266 patch_args: None,
267 patch_tool: None,
268 patches: None,
269 });
270 }
271
272 // Finally, In the event that no spliced source information was included in the
273 // metadata the raw source info is used for registry crates and `crates.io` is
274 // assumed to be the source.
275 if source.is_registry() {
276 return Ok(SourceAnnotation::Http {
277 url: format!(
278 "https://crates.io/api/v1/crates/{}/{}/download",
279 lock_pkg.name, lock_pkg.version,
280 ),
281 sha256: lock_pkg
282 .checksum
283 .as_ref()
284 .and_then(|sum| {
285 if sum.is_sha256() {
286 sum.as_sha256()
287 } else {
288 None
289 }
290 })
291 .map(|sum| sum.encode_hex::<String>()),
292 patch_args: None,
293 patch_tool: None,
294 patches: None,
295 });
296 }
297
298 bail!(
299 "Unable to determine source annotation for '{:?} {:?}",
300 lock_pkg.name,
301 lock_pkg.version
302 )
303 }
304
find_source_annotation( package: &cargo_lock::Package, metadata: &WorkspaceMetadata, ) -> Option<SourceInfo>305 fn find_source_annotation(
306 package: &cargo_lock::Package,
307 metadata: &WorkspaceMetadata,
308 ) -> Option<SourceInfo> {
309 let crate_id = CrateId::new(package.name.to_string(), package.version.clone());
310 metadata.sources.get(&crate_id).cloned()
311 }
312
extract_git_strip_prefix(pkg: &Package) -> Result<Option<String>>313 fn extract_git_strip_prefix(pkg: &Package) -> Result<Option<String>> {
314 // {CARGO_HOME}/git/checkouts/name-hash/short-sha/[strip_prefix...]/Cargo.toml
315 let components = pkg
316 .manifest_path
317 .components()
318 .map(|v| v.to_string())
319 .collect::<Vec<_>>();
320 for (i, _) in components.iter().enumerate() {
321 let possible_components = &components[i..];
322 if possible_components.len() < 5 {
323 continue;
324 }
325 if possible_components[0] != "git"
326 || possible_components[1] != "checkouts"
327 || possible_components[possible_components.len() - 1] != "Cargo.toml"
328 {
329 continue;
330 }
331 if possible_components.len() == 5 {
332 return Ok(None);
333 }
334 return Ok(Some(
335 possible_components[4..(possible_components.len() - 1)].join("/"),
336 ));
337 }
338 bail!("Expected git package to have a manifest path of pattern {{CARGO_HOME}}/git/checkouts/[name]-[hash]/[short-sha]/.../Cargo.toml but {:?} had manifest path {}", pkg.id, pkg.manifest_path);
339 }
340 }
341
342 /// A pairing of a crate's package identifier to its annotations.
343 #[derive(Debug)]
344 pub(crate) struct PairedExtras {
345 /// The crate's package identifier
346 pub(crate) package_id: cargo_metadata::PackageId,
347
348 /// The crate's annotations
349 pub(crate) crate_extra: CrateAnnotations,
350 }
351
352 /// A collection of data which has been processed for optimal use in generating Bazel targets.
353 #[derive(Debug, Default)]
354 pub(crate) struct Annotations {
355 /// Annotated Cargo metadata
356 pub(crate) metadata: MetadataAnnotation,
357
358 /// Annotated Cargo lockfile
359 pub(crate) lockfile: LockfileAnnotation,
360
361 /// The current workspace's configuration settings
362 pub(crate) config: Config,
363
364 /// Pairred crate annotations
365 pub(crate) pairred_extras: BTreeMap<CrateId, PairedExtras>,
366
367 /// Feature set for each target triplet and crate.
368 pub(crate) crate_features: BTreeMap<CrateId, Select<BTreeSet<String>>>,
369 }
370
371 impl Annotations {
new( cargo_metadata: CargoMetadata, cargo_lockfile: CargoLockfile, config: Config, ) -> Result<Self>372 pub(crate) fn new(
373 cargo_metadata: CargoMetadata,
374 cargo_lockfile: CargoLockfile,
375 config: Config,
376 ) -> Result<Self> {
377 let lockfile_annotation = LockfileAnnotation::new(cargo_lockfile, &cargo_metadata)?;
378
379 // Annotate the cargo metadata
380 let metadata_annotation = MetadataAnnotation::new(cargo_metadata);
381
382 let mut unused_extra_annotations = config.annotations.clone();
383
384 // Ensure each override matches a particular package
385 let pairred_extras = metadata_annotation
386 .packages
387 .iter()
388 .filter_map(|(pkg_id, pkg)| {
389 let mut crate_extra: CrateAnnotations = config
390 .annotations
391 .iter()
392 .filter(|(id, _)| id.matches(pkg))
393 .map(|(id, extra)| {
394 // Mark that an annotation has been consumed
395 unused_extra_annotations.remove(id);
396
397 // Filter out the annotation
398 extra
399 })
400 .cloned()
401 .sum();
402
403 crate_extra.apply_defaults_from_package_metadata(&pkg.metadata);
404
405 if crate_extra == CrateAnnotations::default() {
406 None
407 } else {
408 Some((
409 CrateId::new(pkg.name.clone(), pkg.version.clone()),
410 PairedExtras {
411 package_id: pkg_id.clone(),
412 crate_extra,
413 },
414 ))
415 }
416 })
417 .collect();
418
419 // Alert on any unused annotations
420 if !unused_extra_annotations.is_empty() {
421 bail!(
422 "Unused annotations were provided. Please remove them: {:?}",
423 unused_extra_annotations.keys()
424 );
425 }
426
427 let crate_features = metadata_annotation.workspace_metadata.features.clone();
428
429 // Annotate metadata
430 Ok(Annotations {
431 metadata: metadata_annotation,
432 lockfile: lockfile_annotation,
433 config,
434 pairred_extras,
435 crate_features,
436 })
437 }
438 }
439
find_workspace_metadata(cargo_metadata: &CargoMetadata) -> Option<WorkspaceMetadata>440 fn find_workspace_metadata(cargo_metadata: &CargoMetadata) -> Option<WorkspaceMetadata> {
441 WorkspaceMetadata::try_from(cargo_metadata.workspace_metadata.clone()).ok()
442 }
443
444 /// Determines whether or not a package is a workspace member. This follows
445 /// the Cargo definition of a workspace memeber with one exception where
446 /// "extra workspace members" are *not* treated as workspace members
is_workspace_member(id: &PackageId, cargo_metadata: &CargoMetadata) -> bool447 fn is_workspace_member(id: &PackageId, cargo_metadata: &CargoMetadata) -> bool {
448 if cargo_metadata.workspace_members.contains(id) {
449 if let Some(data) = find_workspace_metadata(cargo_metadata) {
450 let pkg = &cargo_metadata[id];
451 let crate_id = CrateId::new(pkg.name.clone(), pkg.version.clone());
452
453 !data.sources.contains_key(&crate_id)
454 } else {
455 true
456 }
457 } else {
458 false
459 }
460 }
461
462 /// Match a [cargo_metadata::Package] to a [cargo_lock::Package].
cargo_meta_pkg_to_locked_pkg<'a>( pkg: &Package, lock_packages: &'a [cargo_lock::Package], ) -> Option<&'a cargo_lock::Package>463 fn cargo_meta_pkg_to_locked_pkg<'a>(
464 pkg: &Package,
465 lock_packages: &'a [cargo_lock::Package],
466 ) -> Option<&'a cargo_lock::Package> {
467 lock_packages
468 .iter()
469 .find(|lock_pkg| lock_pkg.name.as_str() == pkg.name && lock_pkg.version == pkg.version)
470 }
471
472 #[cfg(test)]
473 mod test {
474 use super::*;
475 use crate::config::CrateNameAndVersionReq;
476
477 use crate::test::*;
478
479 #[test]
test_cargo_meta_pkg_to_locked_pkg()480 fn test_cargo_meta_pkg_to_locked_pkg() {
481 let pkg = mock_cargo_metadata_package();
482 let lock_pkg = mock_cargo_lock_package();
483
484 assert!(cargo_meta_pkg_to_locked_pkg(&pkg, &vec![lock_pkg]).is_some())
485 }
486
487 #[test]
annotate_metadata_with_aliases()488 fn annotate_metadata_with_aliases() {
489 let annotations = MetadataAnnotation::new(test::metadata::alias());
490 let log_crates: BTreeMap<&PackageId, &CrateAnnotation> = annotations
491 .crates
492 .iter()
493 .filter(|(id, _)| {
494 let pkg = &annotations.packages[*id];
495 pkg.name == "log"
496 })
497 .collect();
498
499 assert_eq!(log_crates.len(), 2);
500 }
501
502 #[test]
annotate_lockfile_with_aliases()503 fn annotate_lockfile_with_aliases() {
504 LockfileAnnotation::new(test::lockfile::alias(), &test::metadata::alias()).unwrap();
505 }
506
507 #[test]
annotate_metadata_with_build_scripts()508 fn annotate_metadata_with_build_scripts() {
509 MetadataAnnotation::new(test::metadata::build_scripts());
510 }
511
512 #[test]
annotate_lockfile_with_build_scripts()513 fn annotate_lockfile_with_build_scripts() {
514 LockfileAnnotation::new(
515 test::lockfile::build_scripts(),
516 &test::metadata::build_scripts(),
517 )
518 .unwrap();
519 }
520
521 #[test]
annotate_metadata_with_no_deps()522 fn annotate_metadata_with_no_deps() {}
523
524 #[test]
annotate_lockfile_with_no_deps()525 fn annotate_lockfile_with_no_deps() {
526 LockfileAnnotation::new(test::lockfile::no_deps(), &test::metadata::no_deps()).unwrap();
527 }
528
529 #[test]
detects_strip_prefix_for_git_repo()530 fn detects_strip_prefix_for_git_repo() {
531 let crates =
532 LockfileAnnotation::new(test::lockfile::git_repos(), &test::metadata::git_repos())
533 .unwrap()
534 .crates;
535 let tracing_core = crates
536 .iter()
537 .find(|(k, _)| k.repr.starts_with("tracing-core "))
538 .map(|(_, v)| v)
539 .unwrap();
540 match tracing_core {
541 SourceAnnotation::Git {
542 strip_prefix: Some(strip_prefix),
543 ..
544 } if strip_prefix == "tracing-core" => {
545 // Matched correctly.
546 }
547 other => {
548 panic!("Wanted SourceAnnotation::Git with strip_prefix == Some(\"tracing-core\"), got: {:?}", other);
549 }
550 }
551 }
552
553 #[test]
resolves_commit_from_branches_and_tags()554 fn resolves_commit_from_branches_and_tags() {
555 let crates =
556 LockfileAnnotation::new(test::lockfile::git_repos(), &test::metadata::git_repos())
557 .unwrap()
558 .crates;
559
560 let package_id = PackageId { repr: "tracing 0.2.0 (git+https://github.com/tokio-rs/tracing.git?branch=master#1e09e50e8d15580b5929adbade9c782a6833e4a0)".into() };
561 let annotation = crates.get(&package_id).unwrap();
562
563 let commitish = match annotation {
564 SourceAnnotation::Git { commitish, .. } => commitish,
565 _ => panic!("Unexpected annotation type"),
566 };
567
568 assert_eq!(
569 *commitish,
570 Commitish::Rev("1e09e50e8d15580b5929adbade9c782a6833e4a0".into())
571 );
572 }
573
574 #[test]
detect_unused_annotation()575 fn detect_unused_annotation() {
576 // Create a config with some random annotation
577 let mut config = Config::default();
578 config.annotations.insert(
579 CrateNameAndVersionReq::new("mock-crate".to_owned(), "0.1.0".parse().unwrap()),
580 CrateAnnotations::default(),
581 );
582
583 let result = Annotations::new(test::metadata::no_deps(), test::lockfile::no_deps(), config);
584 assert!(result.is_err());
585
586 let result_str = format!("{result:?}");
587 assert!(result_str.contains("Unused annotations were provided. Please remove them"));
588 assert!(result_str.contains("mock-crate"));
589 }
590
591 #[test]
defaults_from_package_metadata()592 fn defaults_from_package_metadata() {
593 let crate_id = CrateId::new(
594 "has_package_metadata".to_owned(),
595 semver::Version::new(0, 0, 0),
596 );
597 let crate_name_and_version_req = CrateNameAndVersionReq::new(
598 "has_package_metadata".to_owned(),
599 "0.0.0".parse().unwrap(),
600 );
601 let annotations = CrateAnnotations {
602 rustc_env: Some(Select::from_value(BTreeMap::from([(
603 "BAR".to_owned(),
604 "bar is set".to_owned(),
605 )]))),
606 ..CrateAnnotations::default()
607 };
608
609 let mut config = Config::default();
610 config
611 .annotations
612 .insert(crate_name_and_version_req, annotations.clone());
613
614 // Combine the above annotations with default values provided by the
615 // crate author in package metadata.
616 let combined_annotations = Annotations::new(
617 test::metadata::has_package_metadata(),
618 test::lockfile::has_package_metadata(),
619 config,
620 )
621 .unwrap();
622
623 let extras = &combined_annotations.pairred_extras[&crate_id].crate_extra;
624 let expected = CrateAnnotations {
625 // This comes from has_package_metadata's [package.metadata.bazel].
626 additive_build_file_content: Some("genrule(**kwargs)\n".to_owned()),
627 // The package metadata defines a default rustc_env containing FOO,
628 // but it is superseded by a rustc_env annotation containing only
629 // BAR. These dictionaries are intentionally not merged together.
630 ..annotations
631 };
632 assert_eq!(*extras, expected);
633 }
634 }
635