xref: /aosp_15_r20/external/bazelbuild-rules_rust/crate_universe/src/splicing/splicer.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 //! Utility for creating valid Cargo workspaces
2 
3 use std::collections::{BTreeMap, BTreeSet};
4 use std::fs;
5 use std::path::{Path, PathBuf};
6 
7 use anyhow::{bail, Context, Result};
8 use cargo_toml::{Dependency, Manifest};
9 use normpath::PathExt;
10 
11 use crate::config::CrateId;
12 use crate::splicing::{Cargo, SplicedManifest, SplicingManifest};
13 use crate::utils::starlark::Label;
14 use crate::utils::symlink::{remove_symlink, symlink};
15 
16 use super::{read_manifest, DirectPackageManifest, WorkspaceMetadata};
17 
18 /// The core splicer implementation. Each style of Bazel workspace should be represented
19 /// here and a splicing implementation defined.
20 pub(crate) enum SplicerKind<'a> {
21     /// Splice a manifest which is represented by a Cargo workspace
22     Workspace {
23         path: &'a PathBuf,
24         manifest: &'a Manifest,
25         splicing_manifest: &'a SplicingManifest,
26     },
27     /// Splice a manifest for a single package. This includes cases where
28     /// were defined directly in Bazel.
29     Package {
30         path: &'a PathBuf,
31         manifest: &'a Manifest,
32         splicing_manifest: &'a SplicingManifest,
33     },
34     /// Splice a manifest from multiple disjoint Cargo manifests.
35     MultiPackage {
36         manifests: &'a BTreeMap<PathBuf, Manifest>,
37         splicing_manifest: &'a SplicingManifest,
38     },
39 }
40 
41 /// A list of files or directories to ignore when when symlinking
42 const IGNORE_LIST: &[&str] = &[".git", "bazel-*", ".svn"];
43 
44 impl<'a> SplicerKind<'a> {
new( manifests: &'a BTreeMap<PathBuf, Manifest>, splicing_manifest: &'a SplicingManifest, cargo_bin: &Cargo, ) -> Result<Self>45     pub(crate) fn new(
46         manifests: &'a BTreeMap<PathBuf, Manifest>,
47         splicing_manifest: &'a SplicingManifest,
48         cargo_bin: &Cargo,
49     ) -> Result<Self> {
50         // First check for any workspaces in the provided manifests
51         let workspace_owned: BTreeMap<&PathBuf, &Manifest> = manifests
52             .iter()
53             .filter(|(_, manifest)| is_workspace_owned(manifest))
54             .collect();
55 
56         let mut root_workspace_pair: Option<(&PathBuf, &Manifest)> = None;
57 
58         if !workspace_owned.is_empty() {
59             // Filter for the root workspace manifest info
60             let (workspace_roots, workspace_packages): (
61                 BTreeMap<&PathBuf, &Manifest>,
62                 BTreeMap<&PathBuf, &Manifest>,
63             ) = workspace_owned
64                 .into_iter()
65                 .partition(|(_, manifest)| is_workspace_root(manifest));
66 
67             if workspace_roots.len() > 1 {
68                 bail!("When splicing manifests, there can only be 1 root workspace manifest");
69             }
70 
71             // This is an error case - we've detected some manifests are in a workspace, but can't
72             // find it.
73             // This block is just for trying to give as useful an error message as possible in this
74             // case.
75             if workspace_roots.is_empty() {
76                 let sorted_manifests: BTreeSet<_> = manifests.keys().collect();
77                 for manifest_path in sorted_manifests {
78                     let metadata_result = cargo_bin
79                         .metadata_command_with_options(manifest_path, Vec::new())?
80                         .no_deps()
81                         .exec();
82                     if let Ok(metadata) = metadata_result {
83                         let label = Label::from_absolute_path(
84                             metadata.workspace_root.join("Cargo.toml").as_std_path(),
85                         );
86                         if let Ok(label) = label {
87                             bail!("Missing root workspace manifest. Please add the following label to the `manifests` key: \"{}\"", label);
88                         }
89                     }
90                 }
91                 bail!("Missing root workspace manifest. Please add the label of the workspace root to the `manifests` key");
92             }
93 
94             // Ensure all workspace owned manifests are members of the one workspace root
95             // UNWRAP: Safe because we've checked workspace_roots isn't empty.
96             let (root_manifest_path, root_manifest) = workspace_roots.into_iter().next().unwrap();
97             let external_workspace_members: BTreeSet<String> = workspace_packages
98                 .into_iter()
99                 .filter(|(manifest_path, _)| {
100                     !is_workspace_member(root_manifest, root_manifest_path, manifest_path)
101                 })
102                 .map(|(path, _)| path.to_string_lossy().to_string())
103                 .collect();
104 
105             if !external_workspace_members.is_empty() {
106                 bail!("A package was provided that appears to be a part of another workspace.\nworkspace root: '{}'\nexternal packages: {:#?}", root_manifest_path.display(), external_workspace_members)
107             }
108 
109             // UNWRAP: Safe because a Cargo.toml file must have a parent directory.
110             let root_manifest_dir = root_manifest_path.parent().unwrap();
111             let missing_manifests = Self::find_missing_manifests(
112                 root_manifest,
113                 root_manifest_dir,
114                 &manifests
115                     .keys()
116                     .map(|p| {
117                         p.normalize()
118                             .with_context(|| format!("Failed to normalize path {p:?}"))
119                     })
120                     .collect::<Result<_, _>>()?,
121             )
122             .context("Identifying missing manifests")?;
123             if !missing_manifests.is_empty() {
124                 bail!("Some manifests are not being tracked. Please add the following labels to the `manifests` key: {:#?}", missing_manifests);
125             }
126 
127             root_workspace_pair = Some((root_manifest_path, root_manifest));
128         }
129 
130         if let Some((path, manifest)) = root_workspace_pair {
131             Ok(Self::Workspace {
132                 path,
133                 manifest,
134                 splicing_manifest,
135             })
136         } else if manifests.len() == 1 {
137             let (path, manifest) = manifests.iter().last().unwrap();
138             Ok(Self::Package {
139                 path,
140                 manifest,
141                 splicing_manifest,
142             })
143         } else {
144             Ok(Self::MultiPackage {
145                 manifests,
146                 splicing_manifest,
147             })
148         }
149     }
150 
find_missing_manifests( root_manifest: &Manifest, root_manifest_dir: &Path, known_manifest_paths: &BTreeSet<normpath::BasePathBuf>, ) -> Result<BTreeSet<String>>151     fn find_missing_manifests(
152         root_manifest: &Manifest,
153         root_manifest_dir: &Path,
154         known_manifest_paths: &BTreeSet<normpath::BasePathBuf>,
155     ) -> Result<BTreeSet<String>> {
156         let workspace_manifest_paths = root_manifest
157             .workspace
158             .as_ref()
159             .unwrap()
160             .members
161             .iter()
162             .map(|member| {
163                 let path = root_manifest_dir.join(member).join("Cargo.toml");
164                 path.normalize()
165                     .with_context(|| format!("Failed to normalize path {path:?}"))
166             })
167             .collect::<Result<BTreeSet<normpath::BasePathBuf>, _>>()?;
168 
169         // Ensure all workspace members are present for the given workspace
170         workspace_manifest_paths
171             .into_iter()
172             .filter(|workspace_manifest_path| {
173                 !known_manifest_paths.contains(workspace_manifest_path)
174             })
175             .map(|workspace_manifest_path| {
176                 let label = Label::from_absolute_path(workspace_manifest_path.as_path())
177                     .with_context(|| {
178                         format!("Failed to identify label for path {workspace_manifest_path:?}")
179                     })?;
180                 Ok(label.to_string())
181             })
182             .collect()
183     }
184 
185     /// Performs splicing based on the current variant.
186     #[tracing::instrument(skip_all)]
splice(&self, workspace_dir: &Path) -> Result<SplicedManifest>187     pub(crate) fn splice(&self, workspace_dir: &Path) -> Result<SplicedManifest> {
188         match self {
189             SplicerKind::Workspace {
190                 path,
191                 manifest,
192                 splicing_manifest,
193             } => Self::splice_workspace(workspace_dir, path, manifest, splicing_manifest),
194             SplicerKind::Package {
195                 path,
196                 manifest,
197                 splicing_manifest,
198             } => Self::splice_package(workspace_dir, path, manifest, splicing_manifest),
199             SplicerKind::MultiPackage {
200                 manifests,
201                 splicing_manifest,
202             } => Self::splice_multi_package(workspace_dir, manifests, splicing_manifest),
203         }
204     }
205 
206     /// Implementation for splicing Cargo workspaces
207     #[tracing::instrument(skip_all)]
splice_workspace( workspace_dir: &Path, path: &&PathBuf, manifest: &&Manifest, splicing_manifest: &&SplicingManifest, ) -> Result<SplicedManifest>208     fn splice_workspace(
209         workspace_dir: &Path,
210         path: &&PathBuf,
211         manifest: &&Manifest,
212         splicing_manifest: &&SplicingManifest,
213     ) -> Result<SplicedManifest> {
214         let mut manifest = (*manifest).clone();
215         let manifest_dir = path
216             .parent()
217             .expect("Every manifest should havee a parent directory");
218 
219         // Link the sources of the root manifest into the new workspace
220         symlink_roots(manifest_dir, workspace_dir, Some(IGNORE_LIST))?;
221 
222         // Optionally install the cargo config after contents have been symlinked
223         Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir)?;
224 
225         // Add any additional depeendencies to the root package
226         if !splicing_manifest.direct_packages.is_empty() {
227             Self::inject_direct_packages(&mut manifest, &splicing_manifest.direct_packages)?;
228         }
229 
230         let root_manifest_path = workspace_dir.join("Cargo.toml");
231         let member_manifests = BTreeMap::from([(*path, String::new())]);
232 
233         // Write the generated metadata to the manifest
234         let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, member_manifests)?;
235         workspace_metadata.inject_into(&mut manifest)?;
236 
237         // Write the root manifest
238         write_root_manifest(&root_manifest_path, manifest)?;
239 
240         Ok(SplicedManifest::Workspace(root_manifest_path))
241     }
242 
243     /// Implementation for splicing individual Cargo packages
244     #[tracing::instrument(skip_all)]
splice_package( workspace_dir: &Path, path: &&PathBuf, manifest: &&Manifest, splicing_manifest: &&SplicingManifest, ) -> Result<SplicedManifest>245     fn splice_package(
246         workspace_dir: &Path,
247         path: &&PathBuf,
248         manifest: &&Manifest,
249         splicing_manifest: &&SplicingManifest,
250     ) -> Result<SplicedManifest> {
251         let manifest_dir = path
252             .parent()
253             .expect("Every manifest should havee a parent directory");
254 
255         // Link the sources of the root manifest into the new workspace
256         symlink_roots(manifest_dir, workspace_dir, Some(IGNORE_LIST))?;
257 
258         // Optionally install the cargo config after contents have been symlinked
259         Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir)?;
260 
261         // Ensure the root package manifest has a populated `workspace` member
262         let mut manifest = (*manifest).clone();
263         if manifest.workspace.is_none() {
264             manifest.workspace =
265                 default_cargo_workspace_manifest(&splicing_manifest.resolver_version).workspace
266         }
267 
268         // Add any additional dependencies to the root package
269         if !splicing_manifest.direct_packages.is_empty() {
270             Self::inject_direct_packages(&mut manifest, &splicing_manifest.direct_packages)?;
271         }
272 
273         let root_manifest_path = workspace_dir.join("Cargo.toml");
274         let member_manifests = BTreeMap::from([(*path, String::new())]);
275 
276         // Write the generated metadata to the manifest
277         let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, member_manifests)?;
278         workspace_metadata.inject_into(&mut manifest)?;
279 
280         // Write the root manifest
281         write_root_manifest(&root_manifest_path, manifest)?;
282 
283         Ok(SplicedManifest::Package(root_manifest_path))
284     }
285 
286     /// Implementation for splicing together multiple Cargo packages/workspaces
287     #[tracing::instrument(skip_all)]
splice_multi_package( workspace_dir: &Path, manifests: &&BTreeMap<PathBuf, Manifest>, splicing_manifest: &&SplicingManifest, ) -> Result<SplicedManifest>288     fn splice_multi_package(
289         workspace_dir: &Path,
290         manifests: &&BTreeMap<PathBuf, Manifest>,
291         splicing_manifest: &&SplicingManifest,
292     ) -> Result<SplicedManifest> {
293         let mut manifest = default_cargo_workspace_manifest(&splicing_manifest.resolver_version);
294 
295         // Optionally install a cargo config file into the workspace root.
296         Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir)?;
297 
298         let installations =
299             Self::inject_workspace_members(&mut manifest, manifests, workspace_dir)?;
300 
301         // Collect all patches from the manifests provided
302         for (_, sub_manifest) in manifests.iter() {
303             Self::inject_patches(&mut manifest, &sub_manifest.patch).with_context(|| {
304                 format!(
305                     "Duplicate `[patch]` entries detected in {:#?}",
306                     manifests
307                         .keys()
308                         .map(|p| p.display().to_string())
309                         .collect::<Vec<String>>()
310                 )
311             })?;
312         }
313 
314         // Write the generated metadata to the manifest
315         let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, installations)?;
316         workspace_metadata.inject_into(&mut manifest)?;
317 
318         // Add any additional depeendencies to the root package
319         if !splicing_manifest.direct_packages.is_empty() {
320             Self::inject_direct_packages(&mut manifest, &splicing_manifest.direct_packages)?;
321         }
322 
323         // Write the root manifest
324         let root_manifest_path = workspace_dir.join("Cargo.toml");
325         write_root_manifest(&root_manifest_path, manifest)?;
326 
327         Ok(SplicedManifest::MultiPackage(root_manifest_path))
328     }
329 
330     /// A helper for installing Cargo config files into the spliced workspace while also
331     /// ensuring no other linked config file is available
setup_cargo_config(cargo_config_path: &Option<PathBuf>, workspace_dir: &Path) -> Result<()>332     fn setup_cargo_config(cargo_config_path: &Option<PathBuf>, workspace_dir: &Path) -> Result<()> {
333         // If the `.cargo` dir is a symlink, we'll need to relink it and ensure
334         // a Cargo config file is omitted
335         let dot_cargo_dir = workspace_dir.join(".cargo");
336         if dot_cargo_dir.exists() {
337             let is_symlink = dot_cargo_dir
338                 .symlink_metadata()
339                 .map(|m| m.file_type().is_symlink())
340                 .unwrap_or(false);
341             if is_symlink {
342                 let real_path = dot_cargo_dir.canonicalize()?;
343                 remove_symlink(&dot_cargo_dir).with_context(|| {
344                     format!(
345                         "Failed to remove existing symlink {}",
346                         dot_cargo_dir.display()
347                     )
348                 })?;
349                 fs::create_dir(&dot_cargo_dir)?;
350                 symlink_roots(&real_path, &dot_cargo_dir, Some(&["config", "config.toml"]))?;
351             } else {
352                 for config in [
353                     dot_cargo_dir.join("config"),
354                     dot_cargo_dir.join("config.toml"),
355                 ] {
356                     if config.exists() {
357                         remove_symlink(&config).with_context(|| {
358                             format!(
359                                 "Failed to delete existing cargo config: {}",
360                                 config.display()
361                             )
362                         })?;
363                     }
364                 }
365             }
366         }
367 
368         // Make sure no other config files exist
369         for config in [
370             workspace_dir.join("config"),
371             workspace_dir.join("config.toml"),
372             dot_cargo_dir.join("config"),
373             dot_cargo_dir.join("config.toml"),
374         ] {
375             if config.exists() {
376                 remove_symlink(&config).with_context(|| {
377                     format!(
378                         "Failed to delete existing cargo config: {}",
379                         config.display()
380                     )
381                 })?;
382             }
383         }
384 
385         // Ensure no parent directory also has a cargo config
386         let mut current_parent = workspace_dir.parent();
387         while let Some(parent) = current_parent {
388             let dot_cargo_dir = parent.join(".cargo");
389             for config in [
390                 dot_cargo_dir.join("config.toml"),
391                 dot_cargo_dir.join("config"),
392             ] {
393                 if config.exists() {
394                     bail!(
395                         "A Cargo config file was found in a parent directory to the current workspace. This is not allowed because these settings will leak into your Bazel build but will not be reproducible on other machines.\nWorkspace = {}\nCargo config = {}",
396                         workspace_dir.display(),
397                         config.display(),
398                     )
399                 }
400             }
401             current_parent = parent.parent()
402         }
403 
404         // Install the new config file after having removed all others
405         if let Some(cargo_config_path) = cargo_config_path {
406             if !dot_cargo_dir.exists() {
407                 fs::create_dir_all(&dot_cargo_dir)?;
408             }
409 
410             fs::copy(cargo_config_path, dot_cargo_dir.join("config.toml"))?;
411         }
412 
413         Ok(())
414     }
415 
416     /// Update the newly generated manifest to include additional packages as
417     /// Cargo workspace members.
inject_workspace_members<'b>( root_manifest: &mut Manifest, manifests: &'b BTreeMap<PathBuf, Manifest>, workspace_dir: &Path, ) -> Result<BTreeMap<&'b PathBuf, String>>418     fn inject_workspace_members<'b>(
419         root_manifest: &mut Manifest,
420         manifests: &'b BTreeMap<PathBuf, Manifest>,
421         workspace_dir: &Path,
422     ) -> Result<BTreeMap<&'b PathBuf, String>> {
423         manifests
424             .iter()
425             .map(|(path, manifest)| {
426                 let package_name = &manifest
427                     .package
428                     .as_ref()
429                     .expect("Each manifest should have a root package")
430                     .name;
431 
432                 root_manifest
433                     .workspace
434                     .as_mut()
435                     .expect("The root manifest is expected to always have a workspace")
436                     .members
437                     .push(package_name.clone());
438 
439                 let manifest_dir = path
440                     .parent()
441                     .expect("Every manifest should havee a parent directory");
442 
443                 let dest_package_dir = workspace_dir.join(package_name);
444 
445                 match symlink_roots(manifest_dir, &dest_package_dir, Some(IGNORE_LIST)) {
446                     Ok(_) => Ok((path, package_name.clone())),
447                     Err(e) => Err(e),
448                 }
449             })
450             .collect()
451     }
452 
inject_direct_packages( manifest: &mut Manifest, direct_packages_manifest: &DirectPackageManifest, ) -> Result<()>453     fn inject_direct_packages(
454         manifest: &mut Manifest,
455         direct_packages_manifest: &DirectPackageManifest,
456     ) -> Result<()> {
457         // Ensure there's a root package to satisfy Cargo requirements
458         if manifest.package.is_none() {
459             let new_manifest = default_cargo_package_manifest();
460             manifest.package = new_manifest.package;
461             if manifest.lib.is_none() {
462                 manifest.lib = new_manifest.lib;
463             }
464         }
465 
466         // Check for any duplicates
467         let duplicates: Vec<&String> = manifest
468             .dependencies
469             .keys()
470             .filter(|k| direct_packages_manifest.contains_key(*k))
471             .collect();
472         if !duplicates.is_empty() {
473             bail!(
474                 "Duplications detected between manifest dependencies and direct dependencies: {:?}",
475                 duplicates
476             )
477         }
478 
479         // Add the dependencies
480         for (name, details) in direct_packages_manifest.iter() {
481             manifest.dependencies.insert(
482                 name.clone(),
483                 cargo_toml::Dependency::Detailed(Box::new(details.clone())),
484             );
485         }
486 
487         Ok(())
488     }
489 
inject_patches(manifest: &mut Manifest, patches: &cargo_toml::PatchSet) -> Result<()>490     fn inject_patches(manifest: &mut Manifest, patches: &cargo_toml::PatchSet) -> Result<()> {
491         for (registry, new_patches) in patches.iter() {
492             // If there is an existing patch entry it will need to be merged
493             if let Some(existing_patches) = manifest.patch.get_mut(registry) {
494                 // Error out if there are duplicate patches
495                 existing_patches.extend(
496                     new_patches
497                         .iter()
498                         .map(|(pkg, info)| {
499                             if let Some(existing_info) = existing_patches.get(pkg) {
500                                 // Only error if the patches are not identical
501                                 if existing_info != info {
502                                     bail!(
503                                         "Duplicate patches were found for `[patch.{}] {}`",
504                                         registry,
505                                         pkg
506                                     );
507                                 }
508                             }
509                             Ok((pkg.clone(), info.clone()))
510                         })
511                         .collect::<Result<cargo_toml::DepsSet>>()?,
512                 );
513             } else {
514                 manifest.patch.insert(registry.clone(), new_patches.clone());
515             }
516         }
517 
518         Ok(())
519     }
520 }
521 
522 pub(crate) struct Splicer {
523     workspace_dir: PathBuf,
524     manifests: BTreeMap<PathBuf, Manifest>,
525     splicing_manifest: SplicingManifest,
526 }
527 
528 impl Splicer {
new(workspace_dir: PathBuf, splicing_manifest: SplicingManifest) -> Result<Self>529     pub(crate) fn new(workspace_dir: PathBuf, splicing_manifest: SplicingManifest) -> Result<Self> {
530         // Load all manifests
531         let manifests = splicing_manifest
532             .manifests
533             .keys()
534             .map(|path| {
535                 let m = read_manifest(path)
536                     .with_context(|| format!("Failed to read manifest at {}", path.display()))?;
537                 Ok((path.clone(), m))
538             })
539             .collect::<Result<BTreeMap<PathBuf, Manifest>>>()?;
540 
541         Ok(Self {
542             workspace_dir,
543             manifests,
544             splicing_manifest,
545         })
546     }
547 
548     /// Build a new workspace root
splice_workspace(&self, cargo: &Cargo) -> Result<SplicedManifest>549     pub(crate) fn splice_workspace(&self, cargo: &Cargo) -> Result<SplicedManifest> {
550         SplicerKind::new(&self.manifests, &self.splicing_manifest, cargo)?
551             .splice(&self.workspace_dir)
552     }
553 }
554 const DEFAULT_SPLICING_PACKAGE_NAME: &str = "direct-cargo-bazel-deps";
555 const DEFAULT_SPLICING_PACKAGE_VERSION: &str = "0.0.1";
556 
default_cargo_package_manifest() -> cargo_toml::Manifest557 pub(crate) fn default_cargo_package_manifest() -> cargo_toml::Manifest {
558     // A manifest is generated with a fake workspace member so the [cargo_toml::Manifest::Workspace]
559     // member is deseralized and is not `None`.
560     cargo_toml::Manifest::from_str(
561         &toml::toml! {
562             [package]
563             name = DEFAULT_SPLICING_PACKAGE_NAME
564             version = DEFAULT_SPLICING_PACKAGE_VERSION
565             edition = "2018"
566 
567             // A fake target used to satisfy requirements of Cargo.
568             [lib]
569             name = "direct_cargo_bazel_deps"
570             path = ".direct_cargo_bazel_deps.rs"
571         }
572         .to_string(),
573     )
574     .unwrap()
575 }
576 
default_splicing_package_crate_id() -> CrateId577 pub(crate) fn default_splicing_package_crate_id() -> CrateId {
578     CrateId::new(
579         DEFAULT_SPLICING_PACKAGE_NAME.to_string(),
580         semver::Version::parse(DEFAULT_SPLICING_PACKAGE_VERSION)
581             .expect("Known good version didn't parse"),
582     )
583 }
584 
default_cargo_workspace_manifest( resolver_version: &cargo_toml::Resolver, ) -> cargo_toml::Manifest585 pub(crate) fn default_cargo_workspace_manifest(
586     resolver_version: &cargo_toml::Resolver,
587 ) -> cargo_toml::Manifest {
588     // A manifest is generated with a fake workspace member so the [cargo_toml::Manifest::Workspace]
589     // member is deseralized and is not `None`.
590     let mut manifest = cargo_toml::Manifest::from_str(&textwrap::dedent(&format!(
591         r#"
592             [workspace]
593             resolver = "{resolver_version}"
594         "#,
595     )))
596     .unwrap();
597 
598     // Drop the temp workspace member
599     manifest.workspace.as_mut().unwrap().members.pop();
600 
601     manifest
602 }
603 
604 /// Determine whtether or not the manifest is a workspace root
is_workspace_root(manifest: &Manifest) -> bool605 pub(crate) fn is_workspace_root(manifest: &Manifest) -> bool {
606     // Anything with any workspace data is considered a workspace
607     manifest.workspace.is_some()
608 }
609 
610 /// Evaluates whether or not a manifest is considered a "workspace" manifest.
611 /// See [Cargo workspaces](https://doc.rust-lang.org/cargo/reference/workspaces.html).
is_workspace_owned(manifest: &Manifest) -> bool612 pub(crate) fn is_workspace_owned(manifest: &Manifest) -> bool {
613     if is_workspace_root(manifest) {
614         return true;
615     }
616 
617     // Additionally, anything that contains path dependencies is also considered a workspace
618     manifest.dependencies.iter().any(|(_, dep)| match dep {
619         Dependency::Detailed(dep) => dep.path.is_some(),
620         _ => false,
621     })
622 }
623 
624 /// Determines whether or not a particular manifest is a workspace member to a given root manifest
is_workspace_member( root_manifest: &Manifest, root_manifest_path: &Path, manifest_path: &Path, ) -> bool625 pub(crate) fn is_workspace_member(
626     root_manifest: &Manifest,
627     root_manifest_path: &Path,
628     manifest_path: &Path,
629 ) -> bool {
630     let members = match root_manifest.workspace.as_ref() {
631         Some(workspace) => &workspace.members,
632         None => return false,
633     };
634 
635     let root_parent = root_manifest_path
636         .parent()
637         .expect("All manifest paths should have a parent");
638     let manifest_abs_path = root_parent.join(manifest_path);
639 
640     members.iter().any(|member| {
641         let member_manifest_path = root_parent.join(member).join("Cargo.toml");
642         member_manifest_path == manifest_abs_path
643     })
644 }
645 
write_root_manifest(path: &Path, manifest: cargo_toml::Manifest) -> Result<()>646 pub(crate) fn write_root_manifest(path: &Path, manifest: cargo_toml::Manifest) -> Result<()> {
647     // Remove the file in case one exists already, preventing symlinked files
648     // from having their contents overwritten.
649     if path.exists() {
650         fs::remove_file(path)?;
651     }
652 
653     // Ensure the directory exists
654     if let Some(parent) = path.parent() {
655         fs::create_dir_all(parent)?;
656     }
657 
658     // Write an intermediate manifest so we can run `cargo metadata` to list all the transitive proc-macros.
659     write_manifest(path, &manifest)?;
660 
661     Ok(())
662 }
663 
write_manifest(path: &Path, manifest: &cargo_toml::Manifest) -> Result<()>664 pub(crate) fn write_manifest(path: &Path, manifest: &cargo_toml::Manifest) -> Result<()> {
665     // TODO(https://gitlab.com/crates.rs/cargo_toml/-/issues/3)
666     let value = toml::Value::try_from(manifest)?;
667     let content = toml::to_string(&value)?;
668     tracing::debug!(
669         "Writing Cargo manifest '{}':\n```toml\n{}```",
670         path.display(),
671         content
672     );
673     fs::write(path, content).context(format!("Failed to write manifest to {}", path.display()))
674 }
675 
676 /// Symlinks the root contents of a source directory into a destination directory
symlink_roots( source: &Path, dest: &Path, ignore_list: Option<&[&str]>, ) -> Result<()>677 pub(crate) fn symlink_roots(
678     source: &Path,
679     dest: &Path,
680     ignore_list: Option<&[&str]>,
681 ) -> Result<()> {
682     // Ensure the source exists and is a directory
683     if !source.is_dir() {
684         bail!("Source path is not a directory: {}", source.display());
685     }
686 
687     // Only check if the dest is a directory if it already exists
688     if dest.exists() && !dest.is_dir() {
689         bail!("Dest path is not a directory: {}", dest.display());
690     }
691 
692     fs::create_dir_all(dest)?;
693 
694     // Link each directory entry from the source dir to the dest
695     for entry in (source.read_dir()?).flatten() {
696         let basename = entry.file_name();
697 
698         // Ignore certain directories that may lead to confusion
699         if let Some(base_str) = basename.to_str() {
700             if let Some(list) = ignore_list {
701                 for item in list.iter() {
702                     // Handle optional glob patterns here. This allows us to ignore `bazel-*` patterns.
703                     if item.ends_with('*') && base_str.starts_with(item.trim_end_matches('*')) {
704                         continue;
705                     }
706 
707                     // Finally, simply compare the string
708                     if *item == base_str {
709                         continue;
710                     }
711                 }
712             }
713         }
714 
715         let link_src = source.join(&basename);
716         let link_dest = dest.join(&basename);
717         symlink(&link_src, &link_dest).context(format!(
718             "Failed to create symlink: {} -> {}",
719             link_src.display(),
720             link_dest.display()
721         ))?;
722     }
723 
724     Ok(())
725 }
726 
727 #[cfg(test)]
728 mod test {
729     use super::*;
730 
731     use std::fs::File;
732     use std::str::FromStr;
733 
734     use cargo_metadata::PackageId;
735     use maplit::btreeset;
736 
737     use crate::splicing::Cargo;
738 
739     /// Clone and compare two items after calling `.sort()` on them.
740     macro_rules! assert_sort_eq {
741         ($left:expr, $right:expr $(,)?) => {
742             let mut left = $left.clone();
743             left.sort();
744             let mut right = $right.clone();
745             right.sort();
746             assert_eq!(left, right);
747         };
748     }
749 
should_skip_network_test() -> bool750     fn should_skip_network_test() -> bool {
751         // Some test cases require network access to build pull crate metadata
752         // so that we can actually run `cargo tree`. However, RBE (and perhaps
753         // other environments) disallow or don't support this. In those cases,
754         // we just skip this test case.
755         use std::net::ToSocketAddrs;
756         if "github.com:443".to_socket_addrs().is_err() {
757             eprintln!("This test case requires network access.");
758             true
759         } else {
760             false
761         }
762     }
763 
764     /// Get cargo and rustc binaries the Bazel way
765     #[cfg(not(feature = "cargo"))]
get_cargo_and_rustc_paths() -> (PathBuf, PathBuf)766     fn get_cargo_and_rustc_paths() -> (PathBuf, PathBuf) {
767         let r = runfiles::Runfiles::create().unwrap();
768         let cargo_path = runfiles::rlocation!(r, concat!("rules_rust/", env!("CARGO")));
769         let rustc_path = runfiles::rlocation!(r, concat!("rules_rust/", env!("RUSTC")));
770 
771         (cargo_path, rustc_path)
772     }
773 
774     /// Get cargo and rustc binaries the Cargo way
775     #[cfg(feature = "cargo")]
get_cargo_and_rustc_paths() -> (PathBuf, PathBuf)776     fn get_cargo_and_rustc_paths() -> (PathBuf, PathBuf) {
777         (PathBuf::from("cargo"), PathBuf::from("rustc"))
778     }
779 
cargo() -> Cargo780     fn cargo() -> Cargo {
781         let (cargo, rustc) = get_cargo_and_rustc_paths();
782         Cargo::new(cargo, rustc)
783     }
784 
generate_metadata(manifest_path: &Path) -> cargo_metadata::Metadata785     fn generate_metadata(manifest_path: &Path) -> cargo_metadata::Metadata {
786         cargo()
787             .metadata_command_with_options(manifest_path, vec!["--offline".to_owned()])
788             .unwrap()
789             .exec()
790             .unwrap()
791     }
792 
mock_cargo_toml(path: &Path, name: &str) -> cargo_toml::Manifest793     fn mock_cargo_toml(path: &Path, name: &str) -> cargo_toml::Manifest {
794         mock_cargo_toml_with_dependencies(path, name, &[])
795     }
796 
mock_cargo_toml_with_dependencies( path: &Path, name: &str, deps: &[&str], ) -> cargo_toml::Manifest797     fn mock_cargo_toml_with_dependencies(
798         path: &Path,
799         name: &str,
800         deps: &[&str],
801     ) -> cargo_toml::Manifest {
802         let manifest = cargo_toml::Manifest::from_str(&textwrap::dedent(&format!(
803             r#"
804             [package]
805             name = "{name}"
806             version = "0.0.1"
807 
808             [lib]
809             path = "lib.rs"
810 
811             [dependencies]
812             {dependencies}
813             "#,
814             name = name,
815             dependencies = deps.join("\n")
816         )))
817         .unwrap();
818 
819         fs::create_dir_all(path.parent().unwrap()).unwrap();
820         fs::write(path, toml::to_string(&manifest).unwrap()).unwrap();
821 
822         manifest
823     }
824 
mock_workspace_metadata( include_extra_member: bool, workspace_prefix: Option<&str>, ) -> serde_json::Value825     fn mock_workspace_metadata(
826         include_extra_member: bool,
827         workspace_prefix: Option<&str>,
828     ) -> serde_json::Value {
829         let mut obj = if include_extra_member {
830             serde_json::json!({
831                 "cargo-bazel": {
832                     "package_prefixes": {},
833                     "sources": {
834                         "extra_pkg 0.0.1": {
835                             "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
836                             "url": "https://crates.io/"
837                         }
838                     },
839                     "tree_metadata": {}
840                 }
841             })
842         } else {
843             serde_json::json!({
844                 "cargo-bazel": {
845                     "package_prefixes": {},
846                     "sources": {},
847                     "tree_metadata": {}
848                 }
849             })
850         };
851         if let Some(workspace_prefix) = workspace_prefix {
852             obj.as_object_mut().unwrap()["cargo-bazel"]
853                 .as_object_mut()
854                 .unwrap()
855                 .insert("workspace_prefix".to_owned(), workspace_prefix.into());
856         }
857         obj
858     }
859 
mock_splicing_manifest_with_workspace() -> (SplicingManifest, tempfile::TempDir)860     fn mock_splicing_manifest_with_workspace() -> (SplicingManifest, tempfile::TempDir) {
861         let mut splicing_manifest = SplicingManifest::default();
862         let cache_dir = tempfile::tempdir().unwrap();
863 
864         // Write workspace members
865         for pkg in &["sub_pkg_a", "sub_pkg_b"] {
866             let manifest_path = cache_dir
867                 .as_ref()
868                 .join("root_pkg")
869                 .join(pkg)
870                 .join("Cargo.toml");
871             let deps = if pkg == &"sub_pkg_b" {
872                 vec![r#"sub_pkg_a = { path = "../sub_pkg_a" }"#]
873             } else {
874                 vec![]
875             };
876             mock_cargo_toml_with_dependencies(&manifest_path, pkg, &deps);
877 
878             splicing_manifest.manifests.insert(
879                 manifest_path,
880                 Label::from_str(&format!("//{pkg}:Cargo.toml")).unwrap(),
881             );
882         }
883 
884         // Create the root package with a workspace definition
885         let manifest: cargo_toml::Manifest = toml::toml! {
886             [workspace]
887             members = [
888                 "sub_pkg_a",
889                 "sub_pkg_b",
890             ]
891             [package]
892             name = "root_pkg"
893             version = "0.0.1"
894 
895             [lib]
896             path = "lib.rs"
897         }
898         .try_into()
899         .unwrap();
900 
901         let workspace_root = cache_dir.as_ref();
902         {
903             File::create(workspace_root.join("WORKSPACE.bazel")).unwrap();
904         }
905         let root_pkg = workspace_root.join("root_pkg");
906         let manifest_path = root_pkg.join("Cargo.toml");
907         fs::create_dir_all(manifest_path.parent().unwrap()).unwrap();
908         fs::write(&manifest_path, toml::to_string(&manifest).unwrap()).unwrap();
909         {
910             File::create(root_pkg.join("BUILD.bazel")).unwrap();
911         }
912 
913         splicing_manifest.manifests.insert(
914             manifest_path,
915             Label::from_str("//root_pkg:Cargo.toml").unwrap(),
916         );
917 
918         for sub_pkg in ["sub_pkg_a", "sub_pkg_b"] {
919             let sub_pkg_path = root_pkg.join(sub_pkg);
920             fs::create_dir_all(&sub_pkg_path).unwrap();
921             File::create(sub_pkg_path.join("BUILD.bazel")).unwrap();
922         }
923 
924         (splicing_manifest, cache_dir)
925     }
926 
mock_splicing_manifest_with_workspace_in_root() -> (SplicingManifest, tempfile::TempDir)927     fn mock_splicing_manifest_with_workspace_in_root() -> (SplicingManifest, tempfile::TempDir) {
928         let mut splicing_manifest = SplicingManifest::default();
929         let cache_dir = tempfile::tempdir().unwrap();
930 
931         // Write workspace members
932         for pkg in &["sub_pkg_a", "sub_pkg_b"] {
933             let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml");
934             mock_cargo_toml(&manifest_path, pkg);
935 
936             splicing_manifest.manifests.insert(
937                 manifest_path,
938                 Label::from_str(&format!("//{pkg}:Cargo.toml")).unwrap(),
939             );
940         }
941 
942         // Create the root package with a workspace definition
943         let manifest: cargo_toml::Manifest = toml::toml! {
944             [workspace]
945             members = [
946                 "sub_pkg_a",
947                 "sub_pkg_b",
948             ]
949             [package]
950             name = "root_pkg"
951             version = "0.0.1"
952 
953             [lib]
954             path = "lib.rs"
955         }
956         .try_into()
957         .unwrap();
958 
959         let workspace_root = cache_dir.as_ref();
960         {
961             File::create(workspace_root.join("WORKSPACE.bazel")).unwrap();
962         }
963         let manifest_path = workspace_root.join("Cargo.toml");
964         fs::create_dir_all(manifest_path.parent().unwrap()).unwrap();
965         fs::write(&manifest_path, toml::to_string(&manifest).unwrap()).unwrap();
966 
967         splicing_manifest
968             .manifests
969             .insert(manifest_path, Label::from_str("//:Cargo.toml").unwrap());
970 
971         for sub_pkg in ["sub_pkg_a", "sub_pkg_b"] {
972             let sub_pkg_path = workspace_root.join(sub_pkg);
973             fs::create_dir_all(&sub_pkg_path).unwrap();
974             File::create(sub_pkg_path.join("BUILD.bazel")).unwrap();
975         }
976 
977         (splicing_manifest, cache_dir)
978     }
979 
mock_splicing_manifest_with_package() -> (SplicingManifest, tempfile::TempDir)980     fn mock_splicing_manifest_with_package() -> (SplicingManifest, tempfile::TempDir) {
981         let mut splicing_manifest = SplicingManifest::default();
982         let cache_dir = tempfile::tempdir().unwrap();
983 
984         // Add an additional package
985         let manifest_path = cache_dir.as_ref().join("root_pkg").join("Cargo.toml");
986         mock_cargo_toml(&manifest_path, "root_pkg");
987         splicing_manifest
988             .manifests
989             .insert(manifest_path, Label::from_str("//:Cargo.toml").unwrap());
990 
991         (splicing_manifest, cache_dir)
992     }
993 
mock_splicing_manifest_with_multi_package() -> (SplicingManifest, tempfile::TempDir)994     fn mock_splicing_manifest_with_multi_package() -> (SplicingManifest, tempfile::TempDir) {
995         let mut splicing_manifest = SplicingManifest::default();
996         let cache_dir = tempfile::tempdir().unwrap();
997 
998         // Add an additional package
999         for pkg in &["pkg_a", "pkg_b", "pkg_c"] {
1000             let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml");
1001             mock_cargo_toml(&manifest_path, pkg);
1002             splicing_manifest
1003                 .manifests
1004                 .insert(manifest_path, Label::from_str("//:Cargo.toml").unwrap());
1005         }
1006 
1007         (splicing_manifest, cache_dir)
1008     }
1009 
new_package_id( name: &str, workspace_root: &Path, is_root: bool, cargo: &Cargo, ) -> PackageId1010     fn new_package_id(
1011         name: &str,
1012         workspace_root: &Path,
1013         is_root: bool,
1014         cargo: &Cargo,
1015     ) -> PackageId {
1016         let mut workspace_root = workspace_root.display().to_string();
1017 
1018         // On windows, make sure we normalize the path to match what Cargo would
1019         // otherwise use to populate metadata.
1020         if cfg!(target_os = "windows") {
1021             workspace_root = format!("/{}", workspace_root.replace('\\', "/"))
1022         };
1023 
1024         // Cargo updated the way package id's are represented. We should make sure
1025         // to render the correct version based on the current cargo binary.
1026         let use_format_v2 = cargo.uses_new_package_id_format().expect(
1027             "Tests should have a fully controlled environment and consistent access to cargo.",
1028         );
1029 
1030         if is_root {
1031             PackageId {
1032                 repr: if use_format_v2 {
1033                     format!("path+file://{workspace_root}#{name}@0.0.1")
1034                 } else {
1035                     format!("{name} 0.0.1 (path+file://{workspace_root})")
1036                 },
1037             }
1038         } else {
1039             PackageId {
1040                 repr: if use_format_v2 {
1041                     format!("path+file://{workspace_root}/{name}#0.0.1")
1042                 } else {
1043                     format!("{name} 0.0.1 (path+file://{workspace_root}/{name})")
1044                 },
1045             }
1046         }
1047     }
1048 
1049     #[test]
splice_workspace()1050     fn splice_workspace() {
1051         let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root();
1052 
1053         // Splice the workspace
1054         let workspace_root = tempfile::tempdir().unwrap();
1055         let workspace_manifest =
1056             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1057                 .unwrap()
1058                 .splice_workspace(&cargo())
1059                 .unwrap();
1060 
1061         // Locate cargo
1062         let cargo = cargo();
1063 
1064         // Ensure metadata is valid
1065         let metadata = generate_metadata(workspace_manifest.as_path_buf());
1066         assert_sort_eq!(
1067             metadata.workspace_members,
1068             vec![
1069                 new_package_id("sub_pkg_a", workspace_root.as_ref(), false, &cargo),
1070                 new_package_id("sub_pkg_b", workspace_root.as_ref(), false, &cargo),
1071                 new_package_id("root_pkg", workspace_root.as_ref(), true, &cargo),
1072             ]
1073         );
1074 
1075         // Ensure the workspace metadata annotations are populated
1076         assert_eq!(
1077             metadata.workspace_metadata,
1078             mock_workspace_metadata(false, None)
1079         );
1080 
1081         // Since no direct packages were added to the splicing manifest, the cargo_bazel
1082         // deps target should __not__ have been injected into the manifest.
1083         assert!(!metadata
1084             .packages
1085             .iter()
1086             .any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME));
1087 
1088         // Ensure lockfile was successfully spliced
1089         cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap();
1090     }
1091 
1092     #[test]
splice_workspace_in_root()1093     fn splice_workspace_in_root() {
1094         let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root();
1095 
1096         // Splice the workspace
1097         let workspace_root = tempfile::tempdir().unwrap();
1098         let workspace_manifest =
1099             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1100                 .unwrap()
1101                 .splice_workspace(&cargo())
1102                 .unwrap();
1103 
1104         // Locate cargo
1105         let cargo = cargo();
1106 
1107         // Ensure metadata is valid
1108         let metadata = generate_metadata(workspace_manifest.as_path_buf());
1109         assert_sort_eq!(
1110             metadata.workspace_members,
1111             vec![
1112                 new_package_id("sub_pkg_a", workspace_root.as_ref(), false, &cargo),
1113                 new_package_id("sub_pkg_b", workspace_root.as_ref(), false, &cargo),
1114                 new_package_id("root_pkg", workspace_root.as_ref(), true, &cargo),
1115             ]
1116         );
1117 
1118         // Ensure the workspace metadata annotations are populated
1119         assert_eq!(
1120             metadata.workspace_metadata,
1121             mock_workspace_metadata(false, None)
1122         );
1123 
1124         // Since no direct packages were added to the splicing manifest, the cargo_bazel
1125         // deps target should __not__ have been injected into the manifest.
1126         assert!(!metadata
1127             .packages
1128             .iter()
1129             .any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME));
1130 
1131         // Ensure lockfile was successfully spliced
1132         cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap();
1133     }
1134 
1135     #[test]
splice_workspace_report_missing_members()1136     fn splice_workspace_report_missing_members() {
1137         let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace();
1138 
1139         // Remove everything but the root manifest
1140         splicing_manifest
1141             .manifests
1142             .retain(|_, label| *label == Label::from_str("//root_pkg:Cargo.toml").unwrap());
1143         assert_eq!(splicing_manifest.manifests.len(), 1);
1144 
1145         // Splice the workspace
1146         let workspace_root = tempfile::tempdir().unwrap();
1147         let workspace_manifest =
1148             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1149                 .unwrap()
1150                 .splice_workspace(&cargo());
1151 
1152         assert!(workspace_manifest.is_err());
1153 
1154         // Ensure both the missing manifests are mentioned in the error string
1155         let err_str = format!("{:?}", &workspace_manifest);
1156         assert!(
1157             err_str.contains("Some manifests are not being tracked")
1158                 && err_str.contains("//root_pkg/sub_pkg_a:Cargo.toml")
1159                 && err_str.contains("//root_pkg/sub_pkg_b:Cargo.toml")
1160         );
1161     }
1162 
1163     #[test]
splice_workspace_report_missing_root()1164     fn splice_workspace_report_missing_root() {
1165         let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace();
1166 
1167         // Remove everything but the root manifest
1168         splicing_manifest
1169             .manifests
1170             .retain(|_, label| *label != Label::from_str("//root_pkg:Cargo.toml").unwrap());
1171         assert_eq!(splicing_manifest.manifests.len(), 2);
1172 
1173         // Splice the workspace
1174         let workspace_root = tempfile::tempdir().unwrap();
1175         let workspace_manifest =
1176             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1177                 .unwrap()
1178                 .splice_workspace(&cargo());
1179 
1180         assert!(workspace_manifest.is_err());
1181 
1182         // Ensure both the missing manifests are mentioned in the error string
1183         let err_str = format!("{:?}", &workspace_manifest);
1184         assert!(
1185             err_str.contains("Missing root workspace manifest")
1186                 && err_str.contains("//root_pkg:Cargo.toml")
1187         );
1188     }
1189 
1190     #[test]
splice_workspace_report_external_workspace_members()1191     fn splice_workspace_report_external_workspace_members() {
1192         let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace();
1193 
1194         // Add a new package from an existing external workspace
1195         let external_workspace_root = tempfile::tempdir().unwrap();
1196         let external_manifest = external_workspace_root
1197             .as_ref()
1198             .join("external_workspace_member")
1199             .join("Cargo.toml");
1200         fs::create_dir_all(external_manifest.parent().unwrap()).unwrap();
1201         fs::write(
1202             &external_manifest,
1203             textwrap::dedent(
1204                 r#"
1205                 [package]
1206                 name = "external_workspace_member"
1207                 version = "0.0.1"
1208 
1209                 [lib]
1210                 path = "lib.rs"
1211 
1212                 [dependencies]
1213                 neighbor = { path = "../neighbor" }
1214                 "#,
1215             ),
1216         )
1217         .unwrap();
1218 
1219         splicing_manifest.manifests.insert(
1220             external_manifest.clone(),
1221             Label::from_str("@remote_dep//external_workspace_member:Cargo.toml").unwrap(),
1222         );
1223 
1224         // Splice the workspace
1225         let workspace_root = tempfile::tempdir().unwrap();
1226         let workspace_manifest =
1227             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1228                 .unwrap()
1229                 .splice_workspace(&cargo());
1230 
1231         assert!(workspace_manifest.is_err());
1232 
1233         // Ensure both the external workspace member
1234         let err_str = format!("{:?}", &workspace_manifest);
1235         let bytes_str = format!("{:?}", external_manifest.to_string_lossy());
1236         assert!(
1237             err_str
1238                 .contains("A package was provided that appears to be a part of another workspace.")
1239                 && err_str.contains(&bytes_str)
1240         );
1241     }
1242 
1243     #[test]
splice_workspace_no_root_pkg()1244     fn splice_workspace_no_root_pkg() {
1245         let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_workspace_in_root();
1246 
1247         // Modify the root manifest to remove the rendered package
1248         fs::write(
1249             cache_dir.as_ref().join("Cargo.toml"),
1250             textwrap::dedent(
1251                 r#"
1252                 [workspace]
1253                 members = [
1254                     "sub_pkg_a",
1255                     "sub_pkg_b",
1256                 ]
1257                 "#,
1258             ),
1259         )
1260         .unwrap();
1261 
1262         // Splice the workspace
1263         let workspace_root = tempfile::tempdir().unwrap();
1264         let workspace_manifest =
1265             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1266                 .unwrap()
1267                 .splice_workspace(&cargo())
1268                 .unwrap();
1269 
1270         let metadata = generate_metadata(workspace_manifest.as_path_buf());
1271 
1272         // Since no direct packages were added to the splicing manifest, the cargo_bazel
1273         // deps target should __not__ have been injected into the manifest.
1274         assert!(!metadata
1275             .packages
1276             .iter()
1277             .any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME));
1278 
1279         // Ensure lockfile was successfully spliced
1280         cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap();
1281     }
1282 
1283     #[test]
splice_package()1284     fn splice_package() {
1285         let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_package();
1286 
1287         // Splice the workspace
1288         let workspace_root = tempfile::tempdir().unwrap();
1289         let workspace_manifest =
1290             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1291                 .unwrap()
1292                 .splice_workspace(&cargo())
1293                 .unwrap();
1294 
1295         // Locate cargo
1296         let cargo = cargo();
1297 
1298         // Ensure metadata is valid
1299         let metadata = generate_metadata(workspace_manifest.as_path_buf());
1300         assert_sort_eq!(
1301             metadata.workspace_members,
1302             vec![new_package_id(
1303                 "root_pkg",
1304                 workspace_root.as_ref(),
1305                 true,
1306                 &cargo
1307             )]
1308         );
1309 
1310         // Ensure the workspace metadata annotations are not populated
1311         assert_eq!(
1312             metadata.workspace_metadata,
1313             mock_workspace_metadata(false, None)
1314         );
1315 
1316         // Ensure lockfile was successfully spliced
1317         cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap();
1318     }
1319 
1320     #[test]
splice_multi_package()1321     fn splice_multi_package() {
1322         let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_multi_package();
1323 
1324         // Splice the workspace
1325         let workspace_root = tempfile::tempdir().unwrap();
1326         let workspace_manifest =
1327             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1328                 .unwrap()
1329                 .splice_workspace(&cargo())
1330                 .unwrap();
1331 
1332         // Check the default resolver version
1333         let cargo_manifest = cargo_toml::Manifest::from_str(
1334             &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
1335         )
1336         .unwrap();
1337         assert!(cargo_manifest.workspace.is_some());
1338         assert_eq!(
1339             cargo_manifest.workspace.unwrap().resolver,
1340             Some(cargo_toml::Resolver::V1)
1341         );
1342 
1343         // Locate cargo
1344         let cargo = cargo();
1345 
1346         // Ensure metadata is valid
1347         let metadata = generate_metadata(workspace_manifest.as_path_buf());
1348         assert_sort_eq!(
1349             metadata.workspace_members,
1350             vec![
1351                 new_package_id("pkg_a", workspace_root.as_ref(), false, &cargo),
1352                 new_package_id("pkg_b", workspace_root.as_ref(), false, &cargo),
1353                 new_package_id("pkg_c", workspace_root.as_ref(), false, &cargo),
1354             ]
1355         );
1356 
1357         // Ensure the workspace metadata annotations are populated
1358         assert_eq!(
1359             metadata.workspace_metadata,
1360             mock_workspace_metadata(false, None)
1361         );
1362 
1363         // Ensure lockfile was successfully spliced
1364         cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap();
1365     }
1366 
1367     #[test]
splice_multi_package_with_resolver()1368     fn splice_multi_package_with_resolver() {
1369         let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_multi_package();
1370 
1371         // Update the resolver version
1372         splicing_manifest.resolver_version = cargo_toml::Resolver::V2;
1373 
1374         // Splice the workspace
1375         let workspace_root = tempfile::tempdir().unwrap();
1376         let workspace_manifest =
1377             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1378                 .unwrap()
1379                 .splice_workspace(&cargo())
1380                 .unwrap();
1381 
1382         // Check the specified resolver version
1383         let cargo_manifest = cargo_toml::Manifest::from_str(
1384             &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
1385         )
1386         .unwrap();
1387         assert!(cargo_manifest.workspace.is_some());
1388         assert_eq!(
1389             cargo_manifest.workspace.unwrap().resolver,
1390             Some(cargo_toml::Resolver::V2)
1391         );
1392 
1393         // Locate cargo
1394         let cargo = cargo();
1395 
1396         // Ensure metadata is valid
1397         let metadata = generate_metadata(workspace_manifest.as_path_buf());
1398         assert_sort_eq!(
1399             metadata.workspace_members,
1400             vec![
1401                 new_package_id("pkg_a", workspace_root.as_ref(), false, &cargo),
1402                 new_package_id("pkg_b", workspace_root.as_ref(), false, &cargo),
1403                 new_package_id("pkg_c", workspace_root.as_ref(), false, &cargo),
1404             ]
1405         );
1406 
1407         // Ensure the workspace metadata annotations are populated
1408         assert_eq!(
1409             metadata.workspace_metadata,
1410             mock_workspace_metadata(false, None)
1411         );
1412 
1413         // Ensure lockfile was successfully spliced
1414         cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap();
1415     }
1416 
1417     #[test]
splice_multi_package_with_direct_deps()1418     fn splice_multi_package_with_direct_deps() {
1419         if should_skip_network_test() {
1420             return;
1421         }
1422 
1423         let (mut splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package();
1424 
1425         // Add a "direct dependency" entry
1426         splicing_manifest.direct_packages.insert(
1427             "syn".to_owned(),
1428             cargo_toml::DependencyDetail {
1429                 version: Some("1.0.109".to_owned()),
1430                 ..syn_dependency_detail()
1431             },
1432         );
1433 
1434         // Splice the workspace
1435         let workspace_root = tempfile::tempdir().unwrap();
1436         let workspace_manifest =
1437             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1438                 .unwrap()
1439                 .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned()))
1440                 .unwrap();
1441 
1442         // Check the default resolver version
1443         let cargo_manifest = cargo_toml::Manifest::from_str(
1444             &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
1445         )
1446         .unwrap();
1447 
1448         // Due to the addition of direct deps for splicing, this package should have been added to the root manfiest.
1449         assert!(cargo_manifest.package.unwrap().name == DEFAULT_SPLICING_PACKAGE_NAME);
1450     }
1451 
1452     #[test]
splice_multi_package_with_patch()1453     fn splice_multi_package_with_patch() {
1454         if should_skip_network_test() {
1455             return;
1456         }
1457 
1458         let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package();
1459 
1460         // Generate a patch entry
1461         let expected = cargo_toml::PatchSet::from([(
1462             "crates-io".to_owned(),
1463             BTreeMap::from([(
1464                 "syn".to_owned(),
1465                 cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())),
1466             )]),
1467         )]);
1468 
1469         // Insert the patch entry to the manifests
1470         let manifest_path = cache_dir.as_ref().join("pkg_a").join("Cargo.toml");
1471         let mut manifest =
1472             cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap()).unwrap();
1473         manifest.patch.extend(expected.clone());
1474         fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap();
1475 
1476         // Splice the workspace
1477         let workspace_root = tempfile::tempdir().unwrap();
1478         let workspace_manifest =
1479             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1480                 .unwrap()
1481                 .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned()))
1482                 .unwrap();
1483 
1484         // Ensure the patches match the expected value
1485         let cargo_manifest = cargo_toml::Manifest::from_str(
1486             &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
1487         )
1488         .unwrap();
1489         assert_eq!(expected, cargo_manifest.patch);
1490     }
1491 
1492     #[test]
splice_multi_package_with_merged_patch_registries()1493     fn splice_multi_package_with_merged_patch_registries() {
1494         if should_skip_network_test() {
1495             return;
1496         }
1497 
1498         let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package();
1499 
1500         let expected = cargo_toml::PatchSet::from([(
1501             "crates-io".to_owned(),
1502             cargo_toml::DepsSet::from([
1503                 (
1504                     "syn".to_owned(),
1505                     cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())),
1506                 ),
1507                 (
1508                     "lazy_static".to_owned(),
1509                     cargo_toml::Dependency::Detailed(Box::new(lazy_static_dependency_detail())),
1510                 ),
1511             ]),
1512         )]);
1513 
1514         for pkg in ["pkg_a", "pkg_b"] {
1515             // Generate a patch entry
1516             let mut map = BTreeMap::new();
1517             if pkg == "pkg_a" {
1518                 map.insert(
1519                     "syn".to_owned(),
1520                     cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())),
1521                 );
1522             } else {
1523                 map.insert(
1524                     "lazy_static".to_owned(),
1525                     cargo_toml::Dependency::Detailed(Box::new(lazy_static_dependency_detail())),
1526                 );
1527             }
1528             let new_patch = cargo_toml::PatchSet::from([("crates-io".to_owned(), map)]);
1529 
1530             // Insert the patch entry to the manifests
1531             let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml");
1532             let mut manifest =
1533                 cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap())
1534                     .unwrap();
1535             manifest.patch.extend(new_patch);
1536             fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap();
1537         }
1538 
1539         // Splice the workspace
1540         let workspace_root = tempfile::tempdir().unwrap();
1541         let workspace_manifest =
1542             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1543                 .unwrap()
1544                 .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned()))
1545                 .unwrap();
1546 
1547         // Ensure the patches match the expected value
1548         let cargo_manifest = cargo_toml::Manifest::from_str(
1549             &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
1550         )
1551         .unwrap();
1552         assert_eq!(expected, cargo_manifest.patch);
1553     }
1554 
1555     #[test]
splice_multi_package_with_merged_identical_patch_registries()1556     fn splice_multi_package_with_merged_identical_patch_registries() {
1557         if should_skip_network_test() {
1558             return;
1559         }
1560 
1561         let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package();
1562 
1563         let expected = cargo_toml::PatchSet::from([(
1564             "crates-io".to_owned(),
1565             cargo_toml::DepsSet::from([(
1566                 "syn".to_owned(),
1567                 cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())),
1568             )]),
1569         )]);
1570 
1571         for pkg in ["pkg_a", "pkg_b"] {
1572             // Generate a patch entry
1573             let new_patch = cargo_toml::PatchSet::from([(
1574                 "crates-io".to_owned(),
1575                 BTreeMap::from([(
1576                     "syn".to_owned(),
1577                     cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())),
1578                 )]),
1579             )]);
1580 
1581             // Insert the patch entry to the manifests
1582             let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml");
1583             let mut manifest =
1584                 cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap())
1585                     .unwrap();
1586             manifest.patch.extend(new_patch);
1587             fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap();
1588         }
1589 
1590         // Splice the workspace
1591         let workspace_root = tempfile::tempdir().unwrap();
1592         let workspace_manifest =
1593             Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1594                 .unwrap()
1595                 .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned()))
1596                 .unwrap();
1597 
1598         // Ensure the patches match the expected value
1599         let cargo_manifest = cargo_toml::Manifest::from_str(
1600             &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
1601         )
1602         .unwrap();
1603         assert_eq!(expected, cargo_manifest.patch);
1604     }
1605 
1606     #[test]
splice_multi_package_with_conflicting_patch()1607     fn splice_multi_package_with_conflicting_patch() {
1608         let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package();
1609 
1610         let mut patch = 3;
1611         for pkg in ["pkg_a", "pkg_b"] {
1612             // Generate a patch entry
1613             let new_patch = cargo_toml::PatchSet::from([(
1614                 "registry".to_owned(),
1615                 BTreeMap::from([(
1616                     "foo".to_owned(),
1617                     cargo_toml::Dependency::Simple(format!("1.2.{patch}")),
1618                 )]),
1619             )]);
1620 
1621             // Increment the patch semver to make the patch info unique.
1622             patch += 1;
1623 
1624             // Insert the patch entry to the manifests
1625             let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml");
1626             let mut manifest =
1627                 cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap())
1628                     .unwrap();
1629             manifest.patch.extend(new_patch);
1630             fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap();
1631         }
1632 
1633         // Splice the workspace
1634         let workspace_root = tempfile::tempdir().unwrap();
1635         let result = Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1636             .unwrap()
1637             .splice_workspace(&cargo());
1638 
1639         // Confirm conflicting patches have been detected
1640         assert!(result.is_err());
1641         let err_str = result.err().unwrap().to_string();
1642         assert!(err_str.starts_with("Duplicate `[patch]` entries detected in"));
1643     }
1644 
1645     #[test]
cargo_config_setup()1646     fn cargo_config_setup() {
1647         let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root();
1648 
1649         // Write a cargo config
1650         let temp_dir = tempfile::tempdir().unwrap();
1651         let external_config = temp_dir.as_ref().join("config.toml");
1652         fs::write(&external_config, "# Cargo configuration file").unwrap();
1653         splicing_manifest.cargo_config = Some(external_config);
1654 
1655         // Splice the workspace
1656         let workspace_root = tempfile::tempdir().unwrap();
1657         Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1658             .unwrap()
1659             .splice_workspace(&cargo())
1660             .unwrap();
1661 
1662         let cargo_config = workspace_root.as_ref().join(".cargo").join("config.toml");
1663         assert!(cargo_config.exists());
1664         assert_eq!(
1665             fs::read_to_string(cargo_config).unwrap().trim(),
1666             "# Cargo configuration file"
1667         );
1668     }
1669 
1670     #[test]
unregistered_cargo_config_replaced()1671     fn unregistered_cargo_config_replaced() {
1672         let (mut splicing_manifest, cache_dir) = mock_splicing_manifest_with_workspace_in_root();
1673 
1674         // Generate a cargo config that is not tracked by the splicing manifest
1675         fs::create_dir_all(cache_dir.as_ref().join(".cargo")).unwrap();
1676         fs::write(
1677             cache_dir.as_ref().join(".cargo").join("config.toml"),
1678             "# Untracked Cargo configuration file",
1679         )
1680         .unwrap();
1681 
1682         // Write a cargo config
1683         let temp_dir = tempfile::tempdir().unwrap();
1684         let external_config = temp_dir.as_ref().join("config.toml");
1685         fs::write(&external_config, "# Cargo configuration file").unwrap();
1686         splicing_manifest.cargo_config = Some(external_config);
1687 
1688         // Splice the workspace
1689         let workspace_root = tempfile::tempdir().unwrap();
1690         Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest)
1691             .unwrap()
1692             .splice_workspace(&cargo())
1693             .unwrap();
1694 
1695         let cargo_config = workspace_root.as_ref().join(".cargo").join("config.toml");
1696         assert!(cargo_config.exists());
1697         assert_eq!(
1698             fs::read_to_string(cargo_config).unwrap().trim(),
1699             "# Cargo configuration file"
1700         );
1701     }
1702 
1703     #[test]
error_on_cargo_config_in_parent()1704     fn error_on_cargo_config_in_parent() {
1705         let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root();
1706 
1707         // Write a cargo config
1708         let temp_dir = tempfile::tempdir().unwrap();
1709         let dot_cargo_dir = temp_dir.as_ref().join(".cargo");
1710         fs::create_dir_all(&dot_cargo_dir).unwrap();
1711         let external_config = dot_cargo_dir.join("config.toml");
1712         fs::write(&external_config, "# Cargo configuration file").unwrap();
1713         splicing_manifest.cargo_config = Some(external_config.clone());
1714 
1715         // Splice the workspace
1716         let workspace_root = temp_dir.as_ref().join("workspace_root");
1717         let splicing_result = Splicer::new(workspace_root.clone(), splicing_manifest)
1718             .unwrap()
1719             .splice_workspace(&cargo());
1720 
1721         // Ensure cargo config files in parent directories lead to errors
1722         assert!(splicing_result.is_err());
1723         let err_str = splicing_result.err().unwrap().to_string();
1724         assert!(err_str.starts_with("A Cargo config file was found in a parent directory"));
1725         assert!(err_str.contains(&format!("Workspace = {}", workspace_root.display())));
1726         assert!(err_str.contains(&format!("Cargo config = {}", external_config.display())));
1727     }
1728 
1729     #[test]
find_missing_manifests_correct_without_root()1730     fn find_missing_manifests_correct_without_root() {
1731         let temp_dir = tempfile::tempdir().unwrap();
1732         let root_manifest_dir = temp_dir.path();
1733         touch(&root_manifest_dir.join("WORKSPACE.bazel"));
1734         touch(&root_manifest_dir.join("BUILD.bazel"));
1735         touch(&root_manifest_dir.join("Cargo.toml"));
1736         touch(&root_manifest_dir.join("foo").join("Cargo.toml"));
1737         touch(&root_manifest_dir.join("bar").join("BUILD.bazel"));
1738         touch(&root_manifest_dir.join("bar").join("Cargo.toml"));
1739 
1740         let known_manifest_paths = btreeset![
1741             root_manifest_dir
1742                 .join("foo")
1743                 .join("Cargo.toml")
1744                 .normalize()
1745                 .unwrap(),
1746             root_manifest_dir
1747                 .join("bar")
1748                 .join("Cargo.toml")
1749                 .normalize()
1750                 .unwrap(),
1751         ];
1752 
1753         let root_manifest: cargo_toml::Manifest = toml::toml! {
1754             [workspace]
1755             members = [
1756                 "foo",
1757                 "bar",
1758             ]
1759             [package]
1760             name = "root_pkg"
1761             version = "0.0.1"
1762 
1763             [lib]
1764             path = "lib.rs"
1765         }
1766         .try_into()
1767         .unwrap();
1768         let missing_manifests = SplicerKind::find_missing_manifests(
1769             &root_manifest,
1770             root_manifest_dir,
1771             &known_manifest_paths,
1772         )
1773         .unwrap();
1774         assert_eq!(missing_manifests, btreeset![]);
1775     }
1776 
1777     #[test]
find_missing_manifests_correct_with_root()1778     fn find_missing_manifests_correct_with_root() {
1779         let temp_dir = tempfile::tempdir().unwrap();
1780         let root_manifest_dir = temp_dir.path();
1781         touch(&root_manifest_dir.join("WORKSPACE.bazel"));
1782         touch(&root_manifest_dir.join("BUILD.bazel"));
1783         touch(&root_manifest_dir.join("Cargo.toml"));
1784         touch(&root_manifest_dir.join("foo").join("Cargo.toml"));
1785         touch(&root_manifest_dir.join("bar").join("BUILD.bazel"));
1786         touch(&root_manifest_dir.join("bar").join("Cargo.toml"));
1787 
1788         let known_manifest_paths = btreeset![
1789             root_manifest_dir.join("Cargo.toml").normalize().unwrap(),
1790             root_manifest_dir
1791                 .join("foo")
1792                 .join("Cargo.toml")
1793                 .normalize()
1794                 .unwrap(),
1795             root_manifest_dir
1796                 .join("bar")
1797                 .join("Cargo.toml")
1798                 .normalize()
1799                 .unwrap(),
1800         ];
1801 
1802         let root_manifest: cargo_toml::Manifest = toml::toml! {
1803             [workspace]
1804             members = [
1805                 ".",
1806                 "foo",
1807                 "bar",
1808             ]
1809             [package]
1810             name = "root_pkg"
1811             version = "0.0.1"
1812 
1813             [lib]
1814             path = "lib.rs"
1815         }
1816         .try_into()
1817         .unwrap();
1818         let missing_manifests = SplicerKind::find_missing_manifests(
1819             &root_manifest,
1820             root_manifest_dir,
1821             &known_manifest_paths,
1822         )
1823         .unwrap();
1824         assert_eq!(missing_manifests, btreeset![]);
1825     }
1826 
1827     #[test]
find_missing_manifests_missing_root()1828     fn find_missing_manifests_missing_root() {
1829         let temp_dir = tempfile::tempdir().unwrap();
1830         let root_manifest_dir = temp_dir.path();
1831         touch(&root_manifest_dir.join("WORKSPACE.bazel"));
1832         touch(&root_manifest_dir.join("BUILD.bazel"));
1833         touch(&root_manifest_dir.join("Cargo.toml"));
1834         touch(&root_manifest_dir.join("foo").join("Cargo.toml"));
1835         touch(&root_manifest_dir.join("bar").join("BUILD.bazel"));
1836         touch(&root_manifest_dir.join("bar").join("Cargo.toml"));
1837 
1838         let known_manifest_paths = btreeset![
1839             root_manifest_dir
1840                 .join("foo")
1841                 .join("Cargo.toml")
1842                 .normalize()
1843                 .unwrap(),
1844             root_manifest_dir
1845                 .join("bar")
1846                 .join("Cargo.toml")
1847                 .normalize()
1848                 .unwrap(),
1849         ];
1850 
1851         let root_manifest: cargo_toml::Manifest = toml::toml! {
1852             [workspace]
1853             members = [
1854                 ".",
1855                 "foo",
1856                 "bar",
1857             ]
1858             [package]
1859             name = "root_pkg"
1860             version = "0.0.1"
1861 
1862             [lib]
1863             path = "lib.rs"
1864         }
1865         .try_into()
1866         .unwrap();
1867         let missing_manifests = SplicerKind::find_missing_manifests(
1868             &root_manifest,
1869             root_manifest_dir,
1870             &known_manifest_paths,
1871         )
1872         .unwrap();
1873         assert_eq!(missing_manifests, btreeset![String::from("//:Cargo.toml")]);
1874     }
1875 
1876     #[test]
find_missing_manifests_missing_nonroot()1877     fn find_missing_manifests_missing_nonroot() {
1878         let temp_dir = tempfile::tempdir().unwrap();
1879         let root_manifest_dir = temp_dir.path();
1880         touch(&root_manifest_dir.join("WORKSPACE.bazel"));
1881         touch(&root_manifest_dir.join("BUILD.bazel"));
1882         touch(&root_manifest_dir.join("Cargo.toml"));
1883         touch(&root_manifest_dir.join("foo").join("Cargo.toml"));
1884         touch(&root_manifest_dir.join("bar").join("BUILD.bazel"));
1885         touch(&root_manifest_dir.join("bar").join("Cargo.toml"));
1886         touch(&root_manifest_dir.join("baz").join("BUILD.bazel"));
1887         touch(&root_manifest_dir.join("baz").join("Cargo.toml"));
1888 
1889         let known_manifest_paths = btreeset![
1890             root_manifest_dir
1891                 .join("foo")
1892                 .join("Cargo.toml")
1893                 .normalize()
1894                 .unwrap(),
1895             root_manifest_dir
1896                 .join("bar")
1897                 .join("Cargo.toml")
1898                 .normalize()
1899                 .unwrap(),
1900         ];
1901 
1902         let root_manifest: cargo_toml::Manifest = toml::toml! {
1903             [workspace]
1904             members = [
1905                 "foo",
1906                 "bar",
1907                 "baz",
1908             ]
1909             [package]
1910             name = "root_pkg"
1911             version = "0.0.1"
1912 
1913             [lib]
1914             path = "lib.rs"
1915         }
1916         .try_into()
1917         .unwrap();
1918         let missing_manifests = SplicerKind::find_missing_manifests(
1919             &root_manifest,
1920             root_manifest_dir,
1921             &known_manifest_paths,
1922         )
1923         .unwrap();
1924         assert_eq!(
1925             missing_manifests,
1926             btreeset![String::from("//baz:Cargo.toml")]
1927         );
1928     }
1929 
touch(path: &Path)1930     fn touch(path: &Path) {
1931         std::fs::create_dir_all(path.parent().unwrap()).unwrap();
1932         std::fs::write(path, []).unwrap();
1933     }
1934 
syn_dependency_detail() -> cargo_toml::DependencyDetail1935     fn syn_dependency_detail() -> cargo_toml::DependencyDetail {
1936         cargo_toml::DependencyDetail {
1937             git: Some("https://github.com/dtolnay/syn.git".to_owned()),
1938             tag: Some("1.0.109".to_owned()),
1939             ..cargo_toml::DependencyDetail::default()
1940         }
1941     }
1942 
lazy_static_dependency_detail() -> cargo_toml::DependencyDetail1943     fn lazy_static_dependency_detail() -> cargo_toml::DependencyDetail {
1944         cargo_toml::DependencyDetail {
1945             git: Some("https://github.com/rust-lang-nursery/lazy-static.rs.git".to_owned()),
1946             tag: Some("1.5.0".to_owned()),
1947             ..cargo_toml::DependencyDetail::default()
1948         }
1949     }
1950 }
1951