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