• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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