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