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