| //! Utility for creating valid Cargo workspaces |
| |
| use std::collections::{BTreeMap, BTreeSet}; |
| use std::fs; |
| use std::path::{Path, PathBuf}; |
| |
| use anyhow::{bail, Context, Result}; |
| use cargo_toml::{Dependency, Manifest}; |
| use normpath::PathExt; |
| |
| use crate::config::CrateId; |
| use crate::splicing::{Cargo, SplicedManifest, SplicingManifest}; |
| use crate::utils::starlark::Label; |
| use crate::utils::symlink::{remove_symlink, symlink}; |
| |
| use super::{read_manifest, DirectPackageManifest, WorkspaceMetadata}; |
| |
| /// The core splicer implementation. Each style of Bazel workspace should be represented |
| /// here and a splicing implementation defined. |
| pub(crate) enum SplicerKind<'a> { |
| /// Splice a manifest which is represented by a Cargo workspace |
| Workspace { |
| path: &'a PathBuf, |
| manifest: &'a Manifest, |
| splicing_manifest: &'a SplicingManifest, |
| }, |
| /// Splice a manifest for a single package. This includes cases where |
| /// were defined directly in Bazel. |
| Package { |
| path: &'a PathBuf, |
| manifest: &'a Manifest, |
| splicing_manifest: &'a SplicingManifest, |
| }, |
| /// Splice a manifest from multiple disjoint Cargo manifests. |
| MultiPackage { |
| manifests: &'a BTreeMap<PathBuf, Manifest>, |
| splicing_manifest: &'a SplicingManifest, |
| }, |
| } |
| |
| /// A list of files or directories to ignore when when symlinking |
| const IGNORE_LIST: &[&str] = &[".git", "bazel-*", ".svn"]; |
| |
| impl<'a> SplicerKind<'a> { |
| pub(crate) fn new( |
| manifests: &'a BTreeMap<PathBuf, Manifest>, |
| splicing_manifest: &'a SplicingManifest, |
| cargo_bin: &Cargo, |
| ) -> Result<Self> { |
| // First check for any workspaces in the provided manifests |
| let workspace_owned: BTreeMap<&PathBuf, &Manifest> = manifests |
| .iter() |
| .filter(|(_, manifest)| is_workspace_owned(manifest)) |
| .collect(); |
| |
| let mut root_workspace_pair: Option<(&PathBuf, &Manifest)> = None; |
| |
| if !workspace_owned.is_empty() { |
| // Filter for the root workspace manifest info |
| let (workspace_roots, workspace_packages): ( |
| BTreeMap<&PathBuf, &Manifest>, |
| BTreeMap<&PathBuf, &Manifest>, |
| ) = workspace_owned |
| .into_iter() |
| .partition(|(_, manifest)| is_workspace_root(manifest)); |
| |
| if workspace_roots.len() > 1 { |
| bail!("When splicing manifests, there can only be 1 root workspace manifest"); |
| } |
| |
| // This is an error case - we've detected some manifests are in a workspace, but can't |
| // find it. |
| // This block is just for trying to give as useful an error message as possible in this |
| // case. |
| if workspace_roots.is_empty() { |
| let sorted_manifests: BTreeSet<_> = manifests.keys().collect(); |
| for manifest_path in sorted_manifests { |
| let metadata_result = cargo_bin |
| .metadata_command_with_options(manifest_path, Vec::new())? |
| .no_deps() |
| .exec(); |
| if let Ok(metadata) = metadata_result { |
| let label = Label::from_absolute_path( |
| metadata.workspace_root.join("Cargo.toml").as_std_path(), |
| ); |
| if let Ok(label) = label { |
| bail!("Missing root workspace manifest. Please add the following label to the `manifests` key: \"{}\"", label); |
| } |
| } |
| } |
| bail!("Missing root workspace manifest. Please add the label of the workspace root to the `manifests` key"); |
| } |
| |
| // Ensure all workspace owned manifests are members of the one workspace root |
| // UNWRAP: Safe because we've checked workspace_roots isn't empty. |
| let (root_manifest_path, root_manifest) = workspace_roots.into_iter().next().unwrap(); |
| let external_workspace_members: BTreeSet<String> = workspace_packages |
| .into_iter() |
| .filter(|(manifest_path, _)| { |
| !is_workspace_member(root_manifest, root_manifest_path, manifest_path) |
| }) |
| .map(|(path, _)| path.to_string_lossy().to_string()) |
| .collect(); |
| |
| if !external_workspace_members.is_empty() { |
| 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) |
| } |
| |
| // UNWRAP: Safe because a Cargo.toml file must have a parent directory. |
| let root_manifest_dir = root_manifest_path.parent().unwrap(); |
| let missing_manifests = Self::find_missing_manifests( |
| root_manifest, |
| root_manifest_dir, |
| &manifests |
| .keys() |
| .map(|p| { |
| p.normalize() |
| .with_context(|| format!("Failed to normalize path {p:?}")) |
| }) |
| .collect::<Result<_, _>>()?, |
| ) |
| .context("Identifying missing manifests")?; |
| if !missing_manifests.is_empty() { |
| bail!("Some manifests are not being tracked. Please add the following labels to the `manifests` key: {:#?}", missing_manifests); |
| } |
| |
| root_workspace_pair = Some((root_manifest_path, root_manifest)); |
| } |
| |
| if let Some((path, manifest)) = root_workspace_pair { |
| Ok(Self::Workspace { |
| path, |
| manifest, |
| splicing_manifest, |
| }) |
| } else if manifests.len() == 1 { |
| let (path, manifest) = manifests.iter().last().unwrap(); |
| Ok(Self::Package { |
| path, |
| manifest, |
| splicing_manifest, |
| }) |
| } else { |
| Ok(Self::MultiPackage { |
| manifests, |
| splicing_manifest, |
| }) |
| } |
| } |
| |
| fn find_missing_manifests( |
| root_manifest: &Manifest, |
| root_manifest_dir: &Path, |
| known_manifest_paths: &BTreeSet<normpath::BasePathBuf>, |
| ) -> Result<BTreeSet<String>> { |
| let workspace_manifest_paths = root_manifest |
| .workspace |
| .as_ref() |
| .unwrap() |
| .members |
| .iter() |
| .map(|member| { |
| let path = root_manifest_dir.join(member).join("Cargo.toml"); |
| path.normalize() |
| .with_context(|| format!("Failed to normalize path {path:?}")) |
| }) |
| .collect::<Result<BTreeSet<normpath::BasePathBuf>, _>>()?; |
| |
| // Ensure all workspace members are present for the given workspace |
| workspace_manifest_paths |
| .into_iter() |
| .filter(|workspace_manifest_path| { |
| !known_manifest_paths.contains(workspace_manifest_path) |
| }) |
| .map(|workspace_manifest_path| { |
| let label = Label::from_absolute_path(workspace_manifest_path.as_path()) |
| .with_context(|| { |
| format!("Failed to identify label for path {workspace_manifest_path:?}") |
| })?; |
| Ok(label.to_string()) |
| }) |
| .collect() |
| } |
| |
| /// Performs splicing based on the current variant. |
| #[tracing::instrument(skip_all)] |
| pub(crate) fn splice(&self, workspace_dir: &Path) -> Result<SplicedManifest> { |
| match self { |
| SplicerKind::Workspace { |
| path, |
| manifest, |
| splicing_manifest, |
| } => Self::splice_workspace(workspace_dir, path, manifest, splicing_manifest), |
| SplicerKind::Package { |
| path, |
| manifest, |
| splicing_manifest, |
| } => Self::splice_package(workspace_dir, path, manifest, splicing_manifest), |
| SplicerKind::MultiPackage { |
| manifests, |
| splicing_manifest, |
| } => Self::splice_multi_package(workspace_dir, manifests, splicing_manifest), |
| } |
| } |
| |
| /// Implementation for splicing Cargo workspaces |
| #[tracing::instrument(skip_all)] |
| fn splice_workspace( |
| workspace_dir: &Path, |
| path: &&PathBuf, |
| manifest: &&Manifest, |
| splicing_manifest: &&SplicingManifest, |
| ) -> Result<SplicedManifest> { |
| let mut manifest = (*manifest).clone(); |
| let manifest_dir = path |
| .parent() |
| .expect("Every manifest should havee a parent directory"); |
| |
| // Link the sources of the root manifest into the new workspace |
| symlink_roots(manifest_dir, workspace_dir, Some(IGNORE_LIST))?; |
| |
| // Optionally install the cargo config after contents have been symlinked |
| Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir)?; |
| |
| // Add any additional depeendencies to the root package |
| if !splicing_manifest.direct_packages.is_empty() { |
| Self::inject_direct_packages(&mut manifest, &splicing_manifest.direct_packages)?; |
| } |
| |
| let root_manifest_path = workspace_dir.join("Cargo.toml"); |
| let member_manifests = BTreeMap::from([(*path, String::new())]); |
| |
| // Write the generated metadata to the manifest |
| let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, member_manifests)?; |
| workspace_metadata.inject_into(&mut manifest)?; |
| |
| // Write the root manifest |
| write_root_manifest(&root_manifest_path, manifest)?; |
| |
| Ok(SplicedManifest::Workspace(root_manifest_path)) |
| } |
| |
| /// Implementation for splicing individual Cargo packages |
| #[tracing::instrument(skip_all)] |
| fn splice_package( |
| workspace_dir: &Path, |
| path: &&PathBuf, |
| manifest: &&Manifest, |
| splicing_manifest: &&SplicingManifest, |
| ) -> Result<SplicedManifest> { |
| let manifest_dir = path |
| .parent() |
| .expect("Every manifest should havee a parent directory"); |
| |
| // Link the sources of the root manifest into the new workspace |
| symlink_roots(manifest_dir, workspace_dir, Some(IGNORE_LIST))?; |
| |
| // Optionally install the cargo config after contents have been symlinked |
| Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir)?; |
| |
| // Ensure the root package manifest has a populated `workspace` member |
| let mut manifest = (*manifest).clone(); |
| if manifest.workspace.is_none() { |
| manifest.workspace = |
| default_cargo_workspace_manifest(&splicing_manifest.resolver_version).workspace |
| } |
| |
| // Add any additional dependencies to the root package |
| if !splicing_manifest.direct_packages.is_empty() { |
| Self::inject_direct_packages(&mut manifest, &splicing_manifest.direct_packages)?; |
| } |
| |
| let root_manifest_path = workspace_dir.join("Cargo.toml"); |
| let member_manifests = BTreeMap::from([(*path, String::new())]); |
| |
| // Write the generated metadata to the manifest |
| let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, member_manifests)?; |
| workspace_metadata.inject_into(&mut manifest)?; |
| |
| // Write the root manifest |
| write_root_manifest(&root_manifest_path, manifest)?; |
| |
| Ok(SplicedManifest::Package(root_manifest_path)) |
| } |
| |
| /// Implementation for splicing together multiple Cargo packages/workspaces |
| #[tracing::instrument(skip_all)] |
| fn splice_multi_package( |
| workspace_dir: &Path, |
| manifests: &&BTreeMap<PathBuf, Manifest>, |
| splicing_manifest: &&SplicingManifest, |
| ) -> Result<SplicedManifest> { |
| let mut manifest = default_cargo_workspace_manifest(&splicing_manifest.resolver_version); |
| |
| // Optionally install a cargo config file into the workspace root. |
| Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir)?; |
| |
| let installations = |
| Self::inject_workspace_members(&mut manifest, manifests, workspace_dir)?; |
| |
| // Collect all patches from the manifests provided |
| for (_, sub_manifest) in manifests.iter() { |
| Self::inject_patches(&mut manifest, &sub_manifest.patch).with_context(|| { |
| format!( |
| "Duplicate `[patch]` entries detected in {:#?}", |
| manifests |
| .keys() |
| .map(|p| p.display().to_string()) |
| .collect::<Vec<String>>() |
| ) |
| })?; |
| } |
| |
| // Write the generated metadata to the manifest |
| let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, installations)?; |
| workspace_metadata.inject_into(&mut manifest)?; |
| |
| // Add any additional depeendencies to the root package |
| if !splicing_manifest.direct_packages.is_empty() { |
| Self::inject_direct_packages(&mut manifest, &splicing_manifest.direct_packages)?; |
| } |
| |
| // Write the root manifest |
| let root_manifest_path = workspace_dir.join("Cargo.toml"); |
| write_root_manifest(&root_manifest_path, manifest)?; |
| |
| Ok(SplicedManifest::MultiPackage(root_manifest_path)) |
| } |
| |
| /// A helper for installing Cargo config files into the spliced workspace while also |
| /// ensuring no other linked config file is available |
| fn setup_cargo_config(cargo_config_path: &Option<PathBuf>, workspace_dir: &Path) -> Result<()> { |
| // If the `.cargo` dir is a symlink, we'll need to relink it and ensure |
| // a Cargo config file is omitted |
| let dot_cargo_dir = workspace_dir.join(".cargo"); |
| if dot_cargo_dir.exists() { |
| let is_symlink = dot_cargo_dir |
| .symlink_metadata() |
| .map(|m| m.file_type().is_symlink()) |
| .unwrap_or(false); |
| if is_symlink { |
| let real_path = dot_cargo_dir.canonicalize()?; |
| remove_symlink(&dot_cargo_dir).with_context(|| { |
| format!( |
| "Failed to remove existing symlink {}", |
| dot_cargo_dir.display() |
| ) |
| })?; |
| fs::create_dir(&dot_cargo_dir)?; |
| symlink_roots(&real_path, &dot_cargo_dir, Some(&["config", "config.toml"]))?; |
| } else { |
| for config in [ |
| dot_cargo_dir.join("config"), |
| dot_cargo_dir.join("config.toml"), |
| ] { |
| if config.exists() { |
| remove_symlink(&config).with_context(|| { |
| format!( |
| "Failed to delete existing cargo config: {}", |
| config.display() |
| ) |
| })?; |
| } |
| } |
| } |
| } |
| |
| // Make sure no other config files exist |
| for config in [ |
| workspace_dir.join("config"), |
| workspace_dir.join("config.toml"), |
| dot_cargo_dir.join("config"), |
| dot_cargo_dir.join("config.toml"), |
| ] { |
| if config.exists() { |
| remove_symlink(&config).with_context(|| { |
| format!( |
| "Failed to delete existing cargo config: {}", |
| config.display() |
| ) |
| })?; |
| } |
| } |
| |
| // Ensure no parent directory also has a cargo config |
| let mut current_parent = workspace_dir.parent(); |
| while let Some(parent) = current_parent { |
| let dot_cargo_dir = parent.join(".cargo"); |
| for config in [ |
| dot_cargo_dir.join("config.toml"), |
| dot_cargo_dir.join("config"), |
| ] { |
| if config.exists() { |
| bail!( |
| "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 = {}", |
| workspace_dir.display(), |
| config.display(), |
| ) |
| } |
| } |
| current_parent = parent.parent() |
| } |
| |
| // Install the new config file after having removed all others |
| if let Some(cargo_config_path) = cargo_config_path { |
| if !dot_cargo_dir.exists() { |
| fs::create_dir_all(&dot_cargo_dir)?; |
| } |
| |
| fs::copy(cargo_config_path, dot_cargo_dir.join("config.toml"))?; |
| } |
| |
| Ok(()) |
| } |
| |
| /// Update the newly generated manifest to include additional packages as |
| /// Cargo workspace members. |
| fn inject_workspace_members<'b>( |
| root_manifest: &mut Manifest, |
| manifests: &'b BTreeMap<PathBuf, Manifest>, |
| workspace_dir: &Path, |
| ) -> Result<BTreeMap<&'b PathBuf, String>> { |
| manifests |
| .iter() |
| .map(|(path, manifest)| { |
| let package_name = &manifest |
| .package |
| .as_ref() |
| .expect("Each manifest should have a root package") |
| .name; |
| |
| root_manifest |
| .workspace |
| .as_mut() |
| .expect("The root manifest is expected to always have a workspace") |
| .members |
| .push(package_name.clone()); |
| |
| let manifest_dir = path |
| .parent() |
| .expect("Every manifest should havee a parent directory"); |
| |
| let dest_package_dir = workspace_dir.join(package_name); |
| |
| match symlink_roots(manifest_dir, &dest_package_dir, Some(IGNORE_LIST)) { |
| Ok(_) => Ok((path, package_name.clone())), |
| Err(e) => Err(e), |
| } |
| }) |
| .collect() |
| } |
| |
| fn inject_direct_packages( |
| manifest: &mut Manifest, |
| direct_packages_manifest: &DirectPackageManifest, |
| ) -> Result<()> { |
| // Ensure there's a root package to satisfy Cargo requirements |
| if manifest.package.is_none() { |
| let new_manifest = default_cargo_package_manifest(); |
| manifest.package = new_manifest.package; |
| if manifest.lib.is_none() { |
| manifest.lib = new_manifest.lib; |
| } |
| } |
| |
| // Check for any duplicates |
| let duplicates: Vec<&String> = manifest |
| .dependencies |
| .keys() |
| .filter(|k| direct_packages_manifest.contains_key(*k)) |
| .collect(); |
| if !duplicates.is_empty() { |
| bail!( |
| "Duplications detected between manifest dependencies and direct dependencies: {:?}", |
| duplicates |
| ) |
| } |
| |
| // Add the dependencies |
| for (name, details) in direct_packages_manifest.iter() { |
| manifest.dependencies.insert( |
| name.clone(), |
| cargo_toml::Dependency::Detailed(Box::new(details.clone())), |
| ); |
| } |
| |
| Ok(()) |
| } |
| |
| fn inject_patches(manifest: &mut Manifest, patches: &cargo_toml::PatchSet) -> Result<()> { |
| for (registry, new_patches) in patches.iter() { |
| // If there is an existing patch entry it will need to be merged |
| if let Some(existing_patches) = manifest.patch.get_mut(registry) { |
| // Error out if there are duplicate patches |
| existing_patches.extend( |
| new_patches |
| .iter() |
| .map(|(pkg, info)| { |
| if let Some(existing_info) = existing_patches.get(pkg) { |
| // Only error if the patches are not identical |
| if existing_info != info { |
| bail!( |
| "Duplicate patches were found for `[patch.{}] {}`", |
| registry, |
| pkg |
| ); |
| } |
| } |
| Ok((pkg.clone(), info.clone())) |
| }) |
| .collect::<Result<cargo_toml::DepsSet>>()?, |
| ); |
| } else { |
| manifest.patch.insert(registry.clone(), new_patches.clone()); |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| pub(crate) struct Splicer { |
| workspace_dir: PathBuf, |
| manifests: BTreeMap<PathBuf, Manifest>, |
| splicing_manifest: SplicingManifest, |
| } |
| |
| impl Splicer { |
| pub(crate) fn new(workspace_dir: PathBuf, splicing_manifest: SplicingManifest) -> Result<Self> { |
| // Load all manifests |
| let manifests = splicing_manifest |
| .manifests |
| .keys() |
| .map(|path| { |
| let m = read_manifest(path) |
| .with_context(|| format!("Failed to read manifest at {}", path.display()))?; |
| Ok((path.clone(), m)) |
| }) |
| .collect::<Result<BTreeMap<PathBuf, Manifest>>>()?; |
| |
| Ok(Self { |
| workspace_dir, |
| manifests, |
| splicing_manifest, |
| }) |
| } |
| |
| /// Build a new workspace root |
| pub(crate) fn splice_workspace(&self, cargo: &Cargo) -> Result<SplicedManifest> { |
| SplicerKind::new(&self.manifests, &self.splicing_manifest, cargo)? |
| .splice(&self.workspace_dir) |
| } |
| } |
| const DEFAULT_SPLICING_PACKAGE_NAME: &str = "direct-cargo-bazel-deps"; |
| const DEFAULT_SPLICING_PACKAGE_VERSION: &str = "0.0.1"; |
| |
| pub(crate) fn default_cargo_package_manifest() -> cargo_toml::Manifest { |
| // A manifest is generated with a fake workspace member so the [cargo_toml::Manifest::Workspace] |
| // member is deseralized and is not `None`. |
| cargo_toml::Manifest::from_str( |
| &toml::toml! { |
| [package] |
| name = DEFAULT_SPLICING_PACKAGE_NAME |
| version = DEFAULT_SPLICING_PACKAGE_VERSION |
| edition = "2018" |
| |
| // A fake target used to satisfy requirements of Cargo. |
| [lib] |
| name = "direct_cargo_bazel_deps" |
| path = ".direct_cargo_bazel_deps.rs" |
| } |
| .to_string(), |
| ) |
| .unwrap() |
| } |
| |
| pub(crate) fn default_splicing_package_crate_id() -> CrateId { |
| CrateId::new( |
| DEFAULT_SPLICING_PACKAGE_NAME.to_string(), |
| semver::Version::parse(DEFAULT_SPLICING_PACKAGE_VERSION) |
| .expect("Known good version didn't parse"), |
| ) |
| } |
| |
| pub(crate) fn default_cargo_workspace_manifest( |
| resolver_version: &cargo_toml::Resolver, |
| ) -> cargo_toml::Manifest { |
| // A manifest is generated with a fake workspace member so the [cargo_toml::Manifest::Workspace] |
| // member is deseralized and is not `None`. |
| let mut manifest = cargo_toml::Manifest::from_str(&textwrap::dedent(&format!( |
| r#" |
| [workspace] |
| resolver = "{resolver_version}" |
| "#, |
| ))) |
| .unwrap(); |
| |
| // Drop the temp workspace member |
| manifest.workspace.as_mut().unwrap().members.pop(); |
| |
| manifest |
| } |
| |
| /// Determine whtether or not the manifest is a workspace root |
| pub(crate) fn is_workspace_root(manifest: &Manifest) -> bool { |
| // Anything with any workspace data is considered a workspace |
| manifest.workspace.is_some() |
| } |
| |
| /// Evaluates whether or not a manifest is considered a "workspace" manifest. |
| /// See [Cargo workspaces](https://doc.rust-lang.org/cargo/reference/workspaces.html). |
| pub(crate) fn is_workspace_owned(manifest: &Manifest) -> bool { |
| if is_workspace_root(manifest) { |
| return true; |
| } |
| |
| // Additionally, anything that contains path dependencies is also considered a workspace |
| manifest.dependencies.iter().any(|(_, dep)| match dep { |
| Dependency::Detailed(dep) => dep.path.is_some(), |
| _ => false, |
| }) |
| } |
| |
| /// Determines whether or not a particular manifest is a workspace member to a given root manifest |
| pub(crate) fn is_workspace_member( |
| root_manifest: &Manifest, |
| root_manifest_path: &Path, |
| manifest_path: &Path, |
| ) -> bool { |
| let members = match root_manifest.workspace.as_ref() { |
| Some(workspace) => &workspace.members, |
| None => return false, |
| }; |
| |
| let root_parent = root_manifest_path |
| .parent() |
| .expect("All manifest paths should have a parent"); |
| let manifest_abs_path = root_parent.join(manifest_path); |
| |
| members.iter().any(|member| { |
| let member_manifest_path = root_parent.join(member).join("Cargo.toml"); |
| member_manifest_path == manifest_abs_path |
| }) |
| } |
| |
| pub(crate) fn write_root_manifest(path: &Path, manifest: cargo_toml::Manifest) -> Result<()> { |
| // Remove the file in case one exists already, preventing symlinked files |
| // from having their contents overwritten. |
| if path.exists() { |
| fs::remove_file(path)?; |
| } |
| |
| // Ensure the directory exists |
| if let Some(parent) = path.parent() { |
| fs::create_dir_all(parent)?; |
| } |
| |
| // Write an intermediate manifest so we can run `cargo metadata` to list all the transitive proc-macros. |
| write_manifest(path, &manifest)?; |
| |
| Ok(()) |
| } |
| |
| pub(crate) fn write_manifest(path: &Path, manifest: &cargo_toml::Manifest) -> Result<()> { |
| // TODO(https://gitlab.com/crates.rs/cargo_toml/-/issues/3) |
| let value = toml::Value::try_from(manifest)?; |
| let content = toml::to_string(&value)?; |
| tracing::debug!( |
| "Writing Cargo manifest '{}':\n```toml\n{}```", |
| path.display(), |
| content |
| ); |
| fs::write(path, content).context(format!("Failed to write manifest to {}", path.display())) |
| } |
| |
| /// Symlinks the root contents of a source directory into a destination directory |
| pub(crate) fn symlink_roots( |
| source: &Path, |
| dest: &Path, |
| ignore_list: Option<&[&str]>, |
| ) -> Result<()> { |
| // Ensure the source exists and is a directory |
| if !source.is_dir() { |
| bail!("Source path is not a directory: {}", source.display()); |
| } |
| |
| // Only check if the dest is a directory if it already exists |
| if dest.exists() && !dest.is_dir() { |
| bail!("Dest path is not a directory: {}", dest.display()); |
| } |
| |
| fs::create_dir_all(dest)?; |
| |
| // Link each directory entry from the source dir to the dest |
| for entry in (source.read_dir()?).flatten() { |
| let basename = entry.file_name(); |
| |
| // Ignore certain directories that may lead to confusion |
| if let Some(base_str) = basename.to_str() { |
| if let Some(list) = ignore_list { |
| for item in list.iter() { |
| // Handle optional glob patterns here. This allows us to ignore `bazel-*` patterns. |
| if item.ends_with('*') && base_str.starts_with(item.trim_end_matches('*')) { |
| continue; |
| } |
| |
| // Finally, simply compare the string |
| if *item == base_str { |
| continue; |
| } |
| } |
| } |
| } |
| |
| let link_src = source.join(&basename); |
| let link_dest = dest.join(&basename); |
| symlink(&link_src, &link_dest).context(format!( |
| "Failed to create symlink: {} -> {}", |
| link_src.display(), |
| link_dest.display() |
| ))?; |
| } |
| |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| use std::fs::File; |
| use std::str::FromStr; |
| |
| use cargo_metadata::PackageId; |
| use maplit::btreeset; |
| |
| use crate::splicing::Cargo; |
| |
| /// Clone and compare two items after calling `.sort()` on them. |
| macro_rules! assert_sort_eq { |
| ($left:expr, $right:expr $(,)?) => { |
| let mut left = $left.clone(); |
| left.sort(); |
| let mut right = $right.clone(); |
| right.sort(); |
| assert_eq!(left, right); |
| }; |
| } |
| |
| fn should_skip_network_test() -> bool { |
| // Some test cases require network access to build pull crate metadata |
| // so that we can actually run `cargo tree`. However, RBE (and perhaps |
| // other environments) disallow or don't support this. In those cases, |
| // we just skip this test case. |
| use std::net::ToSocketAddrs; |
| if "github.com:443".to_socket_addrs().is_err() { |
| eprintln!("This test case requires network access."); |
| true |
| } else { |
| false |
| } |
| } |
| |
| /// Get cargo and rustc binaries the Bazel way |
| #[cfg(not(feature = "cargo"))] |
| fn get_cargo_and_rustc_paths() -> (PathBuf, PathBuf) { |
| let r = runfiles::Runfiles::create().unwrap(); |
| let cargo_path = runfiles::rlocation!(r, concat!("rules_rust/", env!("CARGO"))).unwrap(); |
| let rustc_path = runfiles::rlocation!(r, concat!("rules_rust/", env!("RUSTC"))).unwrap(); |
| |
| (cargo_path, rustc_path) |
| } |
| |
| /// Get cargo and rustc binaries the Cargo way |
| #[cfg(feature = "cargo")] |
| fn get_cargo_and_rustc_paths() -> (PathBuf, PathBuf) { |
| (PathBuf::from("cargo"), PathBuf::from("rustc")) |
| } |
| |
| fn cargo() -> Cargo { |
| let (cargo, rustc) = get_cargo_and_rustc_paths(); |
| Cargo::new(cargo, rustc) |
| } |
| |
| fn generate_metadata(manifest_path: &Path) -> cargo_metadata::Metadata { |
| cargo() |
| .metadata_command_with_options(manifest_path, vec!["--offline".to_owned()]) |
| .unwrap() |
| .exec() |
| .unwrap() |
| } |
| |
| fn mock_cargo_toml(path: &Path, name: &str) -> cargo_toml::Manifest { |
| mock_cargo_toml_with_dependencies(path, name, &[]) |
| } |
| |
| fn mock_cargo_toml_with_dependencies( |
| path: &Path, |
| name: &str, |
| deps: &[&str], |
| ) -> cargo_toml::Manifest { |
| let manifest = cargo_toml::Manifest::from_str(&textwrap::dedent(&format!( |
| r#" |
| [package] |
| name = "{name}" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| |
| [dependencies] |
| {dependencies} |
| "#, |
| name = name, |
| dependencies = deps.join("\n") |
| ))) |
| .unwrap(); |
| |
| fs::create_dir_all(path.parent().unwrap()).unwrap(); |
| fs::write(path, toml::to_string(&manifest).unwrap()).unwrap(); |
| |
| manifest |
| } |
| |
| fn mock_workspace_metadata( |
| include_extra_member: bool, |
| workspace_prefix: Option<&str>, |
| ) -> serde_json::Value { |
| let mut obj = if include_extra_member { |
| serde_json::json!({ |
| "cargo-bazel": { |
| "package_prefixes": {}, |
| "sources": { |
| "extra_pkg 0.0.1": { |
| "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", |
| "url": "https://crates.io/" |
| } |
| }, |
| "tree_metadata": {} |
| } |
| }) |
| } else { |
| serde_json::json!({ |
| "cargo-bazel": { |
| "package_prefixes": {}, |
| "sources": {}, |
| "tree_metadata": {} |
| } |
| }) |
| }; |
| if let Some(workspace_prefix) = workspace_prefix { |
| obj.as_object_mut().unwrap()["cargo-bazel"] |
| .as_object_mut() |
| .unwrap() |
| .insert("workspace_prefix".to_owned(), workspace_prefix.into()); |
| } |
| obj |
| } |
| |
| fn mock_splicing_manifest_with_workspace() -> (SplicingManifest, tempfile::TempDir) { |
| let mut splicing_manifest = SplicingManifest::default(); |
| let cache_dir = tempfile::tempdir().unwrap(); |
| |
| // Write workspace members |
| for pkg in &["sub_pkg_a", "sub_pkg_b"] { |
| let manifest_path = cache_dir |
| .as_ref() |
| .join("root_pkg") |
| .join(pkg) |
| .join("Cargo.toml"); |
| let deps = if pkg == &"sub_pkg_b" { |
| vec![r#"sub_pkg_a = { path = "../sub_pkg_a" }"#] |
| } else { |
| vec![] |
| }; |
| mock_cargo_toml_with_dependencies(&manifest_path, pkg, &deps); |
| |
| splicing_manifest.manifests.insert( |
| manifest_path, |
| Label::from_str(&format!("//{pkg}:Cargo.toml")).unwrap(), |
| ); |
| } |
| |
| // Create the root package with a workspace definition |
| let manifest: cargo_toml::Manifest = toml::toml! { |
| [workspace] |
| members = [ |
| "sub_pkg_a", |
| "sub_pkg_b", |
| ] |
| [package] |
| name = "root_pkg" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| } |
| .try_into() |
| .unwrap(); |
| |
| let workspace_root = cache_dir.as_ref(); |
| { |
| File::create(workspace_root.join("WORKSPACE.bazel")).unwrap(); |
| } |
| let root_pkg = workspace_root.join("root_pkg"); |
| let manifest_path = root_pkg.join("Cargo.toml"); |
| fs::create_dir_all(manifest_path.parent().unwrap()).unwrap(); |
| fs::write(&manifest_path, toml::to_string(&manifest).unwrap()).unwrap(); |
| { |
| File::create(root_pkg.join("BUILD.bazel")).unwrap(); |
| } |
| |
| splicing_manifest.manifests.insert( |
| manifest_path, |
| Label::from_str("//root_pkg:Cargo.toml").unwrap(), |
| ); |
| |
| for sub_pkg in ["sub_pkg_a", "sub_pkg_b"] { |
| let sub_pkg_path = root_pkg.join(sub_pkg); |
| fs::create_dir_all(&sub_pkg_path).unwrap(); |
| File::create(sub_pkg_path.join("BUILD.bazel")).unwrap(); |
| } |
| |
| (splicing_manifest, cache_dir) |
| } |
| |
| fn mock_splicing_manifest_with_workspace_in_root() -> (SplicingManifest, tempfile::TempDir) { |
| let mut splicing_manifest = SplicingManifest::default(); |
| let cache_dir = tempfile::tempdir().unwrap(); |
| |
| // Write workspace members |
| for pkg in &["sub_pkg_a", "sub_pkg_b"] { |
| let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml"); |
| mock_cargo_toml(&manifest_path, pkg); |
| |
| splicing_manifest.manifests.insert( |
| manifest_path, |
| Label::from_str(&format!("//{pkg}:Cargo.toml")).unwrap(), |
| ); |
| } |
| |
| // Create the root package with a workspace definition |
| let manifest: cargo_toml::Manifest = toml::toml! { |
| [workspace] |
| members = [ |
| "sub_pkg_a", |
| "sub_pkg_b", |
| ] |
| [package] |
| name = "root_pkg" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| } |
| .try_into() |
| .unwrap(); |
| |
| let workspace_root = cache_dir.as_ref(); |
| { |
| File::create(workspace_root.join("WORKSPACE.bazel")).unwrap(); |
| } |
| let manifest_path = workspace_root.join("Cargo.toml"); |
| fs::create_dir_all(manifest_path.parent().unwrap()).unwrap(); |
| fs::write(&manifest_path, toml::to_string(&manifest).unwrap()).unwrap(); |
| |
| splicing_manifest |
| .manifests |
| .insert(manifest_path, Label::from_str("//:Cargo.toml").unwrap()); |
| |
| for sub_pkg in ["sub_pkg_a", "sub_pkg_b"] { |
| let sub_pkg_path = workspace_root.join(sub_pkg); |
| fs::create_dir_all(&sub_pkg_path).unwrap(); |
| File::create(sub_pkg_path.join("BUILD.bazel")).unwrap(); |
| } |
| |
| (splicing_manifest, cache_dir) |
| } |
| |
| fn mock_splicing_manifest_with_package() -> (SplicingManifest, tempfile::TempDir) { |
| let mut splicing_manifest = SplicingManifest::default(); |
| let cache_dir = tempfile::tempdir().unwrap(); |
| |
| // Add an additional package |
| let manifest_path = cache_dir.as_ref().join("root_pkg").join("Cargo.toml"); |
| mock_cargo_toml(&manifest_path, "root_pkg"); |
| splicing_manifest |
| .manifests |
| .insert(manifest_path, Label::from_str("//:Cargo.toml").unwrap()); |
| |
| (splicing_manifest, cache_dir) |
| } |
| |
| fn mock_splicing_manifest_with_multi_package() -> (SplicingManifest, tempfile::TempDir) { |
| let mut splicing_manifest = SplicingManifest::default(); |
| let cache_dir = tempfile::tempdir().unwrap(); |
| |
| // Add an additional package |
| for pkg in &["pkg_a", "pkg_b", "pkg_c"] { |
| let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml"); |
| mock_cargo_toml(&manifest_path, pkg); |
| splicing_manifest |
| .manifests |
| .insert(manifest_path, Label::from_str("//:Cargo.toml").unwrap()); |
| } |
| |
| (splicing_manifest, cache_dir) |
| } |
| |
| fn new_package_id( |
| name: &str, |
| workspace_root: &Path, |
| is_root: bool, |
| cargo: &Cargo, |
| ) -> PackageId { |
| let mut workspace_root = workspace_root.display().to_string(); |
| |
| // On windows, make sure we normalize the path to match what Cargo would |
| // otherwise use to populate metadata. |
| if cfg!(target_os = "windows") { |
| workspace_root = format!("/{}", workspace_root.replace('\\', "/")) |
| }; |
| |
| // Cargo updated the way package id's are represented. We should make sure |
| // to render the correct version based on the current cargo binary. |
| let use_format_v2 = cargo.uses_new_package_id_format().expect( |
| "Tests should have a fully controlled environment and consistent access to cargo.", |
| ); |
| |
| if is_root { |
| PackageId { |
| repr: if use_format_v2 { |
| format!("path+file://{workspace_root}#{name}@0.0.1") |
| } else { |
| format!("{name} 0.0.1 (path+file://{workspace_root})") |
| }, |
| } |
| } else { |
| PackageId { |
| repr: if use_format_v2 { |
| format!("path+file://{workspace_root}/{name}#0.0.1") |
| } else { |
| format!("{name} 0.0.1 (path+file://{workspace_root}/{name})") |
| }, |
| } |
| } |
| } |
| |
| #[test] |
| fn splice_workspace() { |
| let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root(); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| // Locate cargo |
| let cargo = cargo(); |
| |
| // Ensure metadata is valid |
| let metadata = generate_metadata(workspace_manifest.as_path_buf()); |
| assert_sort_eq!( |
| metadata.workspace_members, |
| vec![ |
| new_package_id("sub_pkg_a", workspace_root.as_ref(), false, &cargo), |
| new_package_id("sub_pkg_b", workspace_root.as_ref(), false, &cargo), |
| new_package_id("root_pkg", workspace_root.as_ref(), true, &cargo), |
| ] |
| ); |
| |
| // Ensure the workspace metadata annotations are populated |
| assert_eq!( |
| metadata.workspace_metadata, |
| mock_workspace_metadata(false, None) |
| ); |
| |
| // Since no direct packages were added to the splicing manifest, the cargo_bazel |
| // deps target should __not__ have been injected into the manifest. |
| assert!(!metadata |
| .packages |
| .iter() |
| .any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME)); |
| |
| // Ensure lockfile was successfully spliced |
| cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap(); |
| } |
| |
| #[test] |
| fn splice_workspace_in_root() { |
| let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root(); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| // Locate cargo |
| let cargo = cargo(); |
| |
| // Ensure metadata is valid |
| let metadata = generate_metadata(workspace_manifest.as_path_buf()); |
| assert_sort_eq!( |
| metadata.workspace_members, |
| vec![ |
| new_package_id("sub_pkg_a", workspace_root.as_ref(), false, &cargo), |
| new_package_id("sub_pkg_b", workspace_root.as_ref(), false, &cargo), |
| new_package_id("root_pkg", workspace_root.as_ref(), true, &cargo), |
| ] |
| ); |
| |
| // Ensure the workspace metadata annotations are populated |
| assert_eq!( |
| metadata.workspace_metadata, |
| mock_workspace_metadata(false, None) |
| ); |
| |
| // Since no direct packages were added to the splicing manifest, the cargo_bazel |
| // deps target should __not__ have been injected into the manifest. |
| assert!(!metadata |
| .packages |
| .iter() |
| .any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME)); |
| |
| // Ensure lockfile was successfully spliced |
| cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap(); |
| } |
| |
| #[test] |
| fn splice_workspace_report_missing_members() { |
| let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace(); |
| |
| // Remove everything but the root manifest |
| splicing_manifest |
| .manifests |
| .retain(|_, label| *label == Label::from_str("//root_pkg:Cargo.toml").unwrap()); |
| assert_eq!(splicing_manifest.manifests.len(), 1); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()); |
| |
| assert!(workspace_manifest.is_err()); |
| |
| // Ensure both the missing manifests are mentioned in the error string |
| let err_str = format!("{:?}", &workspace_manifest); |
| assert!( |
| err_str.contains("Some manifests are not being tracked") |
| && err_str.contains("//root_pkg/sub_pkg_a:Cargo.toml") |
| && err_str.contains("//root_pkg/sub_pkg_b:Cargo.toml") |
| ); |
| } |
| |
| #[test] |
| fn splice_workspace_report_missing_root() { |
| let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace(); |
| |
| // Remove everything but the root manifest |
| splicing_manifest |
| .manifests |
| .retain(|_, label| *label != Label::from_str("//root_pkg:Cargo.toml").unwrap()); |
| assert_eq!(splicing_manifest.manifests.len(), 2); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()); |
| |
| assert!(workspace_manifest.is_err()); |
| |
| // Ensure both the missing manifests are mentioned in the error string |
| let err_str = format!("{:?}", &workspace_manifest); |
| assert!( |
| err_str.contains("Missing root workspace manifest") |
| && err_str.contains("//root_pkg:Cargo.toml") |
| ); |
| } |
| |
| #[test] |
| fn splice_workspace_report_external_workspace_members() { |
| let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace(); |
| |
| // Add a new package from an existing external workspace |
| let external_workspace_root = tempfile::tempdir().unwrap(); |
| let external_manifest = external_workspace_root |
| .as_ref() |
| .join("external_workspace_member") |
| .join("Cargo.toml"); |
| fs::create_dir_all(external_manifest.parent().unwrap()).unwrap(); |
| fs::write( |
| &external_manifest, |
| textwrap::dedent( |
| r#" |
| [package] |
| name = "external_workspace_member" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| |
| [dependencies] |
| neighbor = { path = "../neighbor" } |
| "#, |
| ), |
| ) |
| .unwrap(); |
| |
| splicing_manifest.manifests.insert( |
| external_manifest.clone(), |
| Label::from_str("@remote_dep//external_workspace_member:Cargo.toml").unwrap(), |
| ); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()); |
| |
| assert!(workspace_manifest.is_err()); |
| |
| // Ensure both the external workspace member |
| let err_str = format!("{:?}", &workspace_manifest); |
| let bytes_str = format!("{:?}", external_manifest.to_string_lossy()); |
| assert!( |
| err_str |
| .contains("A package was provided that appears to be a part of another workspace.") |
| && err_str.contains(&bytes_str) |
| ); |
| } |
| |
| #[test] |
| fn splice_workspace_no_root_pkg() { |
| let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_workspace_in_root(); |
| |
| // Modify the root manifest to remove the rendered package |
| fs::write( |
| cache_dir.as_ref().join("Cargo.toml"), |
| textwrap::dedent( |
| r#" |
| [workspace] |
| members = [ |
| "sub_pkg_a", |
| "sub_pkg_b", |
| ] |
| "#, |
| ), |
| ) |
| .unwrap(); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| let metadata = generate_metadata(workspace_manifest.as_path_buf()); |
| |
| // Since no direct packages were added to the splicing manifest, the cargo_bazel |
| // deps target should __not__ have been injected into the manifest. |
| assert!(!metadata |
| .packages |
| .iter() |
| .any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME)); |
| |
| // Ensure lockfile was successfully spliced |
| cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap(); |
| } |
| |
| #[test] |
| fn splice_package() { |
| let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_package(); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| // Locate cargo |
| let cargo = cargo(); |
| |
| // Ensure metadata is valid |
| let metadata = generate_metadata(workspace_manifest.as_path_buf()); |
| assert_sort_eq!( |
| metadata.workspace_members, |
| vec![new_package_id( |
| "root_pkg", |
| workspace_root.as_ref(), |
| true, |
| &cargo |
| )] |
| ); |
| |
| // Ensure the workspace metadata annotations are not populated |
| assert_eq!( |
| metadata.workspace_metadata, |
| mock_workspace_metadata(false, None) |
| ); |
| |
| // Ensure lockfile was successfully spliced |
| cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap(); |
| } |
| |
| #[test] |
| fn splice_multi_package() { |
| let (splicing_manifest, _cache_dir) = mock_splicing_manifest_with_multi_package(); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| // Check the default resolver version |
| let cargo_manifest = cargo_toml::Manifest::from_str( |
| &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(), |
| ) |
| .unwrap(); |
| assert!(cargo_manifest.workspace.is_some()); |
| assert_eq!( |
| cargo_manifest.workspace.unwrap().resolver, |
| Some(cargo_toml::Resolver::V1) |
| ); |
| |
| // Locate cargo |
| let cargo = cargo(); |
| |
| // Ensure metadata is valid |
| let metadata = generate_metadata(workspace_manifest.as_path_buf()); |
| assert_sort_eq!( |
| metadata.workspace_members, |
| vec![ |
| new_package_id("pkg_a", workspace_root.as_ref(), false, &cargo), |
| new_package_id("pkg_b", workspace_root.as_ref(), false, &cargo), |
| new_package_id("pkg_c", workspace_root.as_ref(), false, &cargo), |
| ] |
| ); |
| |
| // Ensure the workspace metadata annotations are populated |
| assert_eq!( |
| metadata.workspace_metadata, |
| mock_workspace_metadata(false, None) |
| ); |
| |
| // Ensure lockfile was successfully spliced |
| cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap(); |
| } |
| |
| #[test] |
| fn splice_multi_package_with_resolver() { |
| let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_multi_package(); |
| |
| // Update the resolver version |
| splicing_manifest.resolver_version = cargo_toml::Resolver::V2; |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| // Check the specified resolver version |
| let cargo_manifest = cargo_toml::Manifest::from_str( |
| &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(), |
| ) |
| .unwrap(); |
| assert!(cargo_manifest.workspace.is_some()); |
| assert_eq!( |
| cargo_manifest.workspace.unwrap().resolver, |
| Some(cargo_toml::Resolver::V2) |
| ); |
| |
| // Locate cargo |
| let cargo = cargo(); |
| |
| // Ensure metadata is valid |
| let metadata = generate_metadata(workspace_manifest.as_path_buf()); |
| assert_sort_eq!( |
| metadata.workspace_members, |
| vec![ |
| new_package_id("pkg_a", workspace_root.as_ref(), false, &cargo), |
| new_package_id("pkg_b", workspace_root.as_ref(), false, &cargo), |
| new_package_id("pkg_c", workspace_root.as_ref(), false, &cargo), |
| ] |
| ); |
| |
| // Ensure the workspace metadata annotations are populated |
| assert_eq!( |
| metadata.workspace_metadata, |
| mock_workspace_metadata(false, None) |
| ); |
| |
| // Ensure lockfile was successfully spliced |
| cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap(); |
| } |
| |
| #[test] |
| fn splice_multi_package_with_direct_deps() { |
| if should_skip_network_test() { |
| return; |
| } |
| |
| let (mut splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package(); |
| |
| // Add a "direct dependency" entry |
| splicing_manifest.direct_packages.insert( |
| "syn".to_owned(), |
| cargo_toml::DependencyDetail { |
| version: Some("1.0.109".to_owned()), |
| ..syn_dependency_detail() |
| }, |
| ); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned())) |
| .unwrap(); |
| |
| // Check the default resolver version |
| let cargo_manifest = cargo_toml::Manifest::from_str( |
| &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(), |
| ) |
| .unwrap(); |
| |
| // Due to the addition of direct deps for splicing, this package should have been added to the root manfiest. |
| assert!(cargo_manifest.package.unwrap().name == DEFAULT_SPLICING_PACKAGE_NAME); |
| } |
| |
| #[test] |
| fn splice_multi_package_with_patch() { |
| if should_skip_network_test() { |
| return; |
| } |
| |
| let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package(); |
| |
| // Generate a patch entry |
| let expected = cargo_toml::PatchSet::from([( |
| "crates-io".to_owned(), |
| BTreeMap::from([( |
| "syn".to_owned(), |
| cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())), |
| )]), |
| )]); |
| |
| // Insert the patch entry to the manifests |
| let manifest_path = cache_dir.as_ref().join("pkg_a").join("Cargo.toml"); |
| let mut manifest = |
| cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap()).unwrap(); |
| manifest.patch.extend(expected.clone()); |
| fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap(); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned())) |
| .unwrap(); |
| |
| // Ensure the patches match the expected value |
| let cargo_manifest = cargo_toml::Manifest::from_str( |
| &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(), |
| ) |
| .unwrap(); |
| assert_eq!(expected, cargo_manifest.patch); |
| } |
| |
| #[test] |
| fn splice_multi_package_with_merged_patch_registries() { |
| if should_skip_network_test() { |
| return; |
| } |
| |
| let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package(); |
| |
| let expected = cargo_toml::PatchSet::from([( |
| "crates-io".to_owned(), |
| cargo_toml::DepsSet::from([ |
| ( |
| "syn".to_owned(), |
| cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())), |
| ), |
| ( |
| "lazy_static".to_owned(), |
| cargo_toml::Dependency::Detailed(Box::new(lazy_static_dependency_detail())), |
| ), |
| ]), |
| )]); |
| |
| for pkg in ["pkg_a", "pkg_b"] { |
| // Generate a patch entry |
| let mut map = BTreeMap::new(); |
| if pkg == "pkg_a" { |
| map.insert( |
| "syn".to_owned(), |
| cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())), |
| ); |
| } else { |
| map.insert( |
| "lazy_static".to_owned(), |
| cargo_toml::Dependency::Detailed(Box::new(lazy_static_dependency_detail())), |
| ); |
| } |
| let new_patch = cargo_toml::PatchSet::from([("crates-io".to_owned(), map)]); |
| |
| // Insert the patch entry to the manifests |
| let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml"); |
| let mut manifest = |
| cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap()) |
| .unwrap(); |
| manifest.patch.extend(new_patch); |
| fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap(); |
| } |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned())) |
| .unwrap(); |
| |
| // Ensure the patches match the expected value |
| let cargo_manifest = cargo_toml::Manifest::from_str( |
| &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(), |
| ) |
| .unwrap(); |
| assert_eq!(expected, cargo_manifest.patch); |
| } |
| |
| #[test] |
| fn splice_multi_package_with_merged_identical_patch_registries() { |
| if should_skip_network_test() { |
| return; |
| } |
| |
| let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package(); |
| |
| let expected = cargo_toml::PatchSet::from([( |
| "crates-io".to_owned(), |
| cargo_toml::DepsSet::from([( |
| "syn".to_owned(), |
| cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())), |
| )]), |
| )]); |
| |
| for pkg in ["pkg_a", "pkg_b"] { |
| // Generate a patch entry |
| let new_patch = cargo_toml::PatchSet::from([( |
| "crates-io".to_owned(), |
| BTreeMap::from([( |
| "syn".to_owned(), |
| cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())), |
| )]), |
| )]); |
| |
| // Insert the patch entry to the manifests |
| let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml"); |
| let mut manifest = |
| cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap()) |
| .unwrap(); |
| manifest.patch.extend(new_patch); |
| fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap(); |
| } |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let workspace_manifest = |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo().with_cargo_home(cache_dir.path().to_owned())) |
| .unwrap(); |
| |
| // Ensure the patches match the expected value |
| let cargo_manifest = cargo_toml::Manifest::from_str( |
| &fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(), |
| ) |
| .unwrap(); |
| assert_eq!(expected, cargo_manifest.patch); |
| } |
| |
| #[test] |
| fn splice_multi_package_with_conflicting_patch() { |
| let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_multi_package(); |
| |
| let mut patch = 3; |
| for pkg in ["pkg_a", "pkg_b"] { |
| // Generate a patch entry |
| let new_patch = cargo_toml::PatchSet::from([( |
| "registry".to_owned(), |
| BTreeMap::from([( |
| "foo".to_owned(), |
| cargo_toml::Dependency::Simple(format!("1.2.{patch}")), |
| )]), |
| )]); |
| |
| // Increment the patch semver to make the patch info unique. |
| patch += 1; |
| |
| // Insert the patch entry to the manifests |
| let manifest_path = cache_dir.as_ref().join(pkg).join("Cargo.toml"); |
| let mut manifest = |
| cargo_toml::Manifest::from_str(&fs::read_to_string(&manifest_path).unwrap()) |
| .unwrap(); |
| manifest.patch.extend(new_patch); |
| fs::write(manifest_path, toml::to_string(&manifest).unwrap()).unwrap(); |
| } |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| let result = Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()); |
| |
| // Confirm conflicting patches have been detected |
| assert!(result.is_err()); |
| let err_str = result.err().unwrap().to_string(); |
| assert!(err_str.starts_with("Duplicate `[patch]` entries detected in")); |
| } |
| |
| #[test] |
| fn cargo_config_setup() { |
| let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root(); |
| |
| // Write a cargo config |
| let temp_dir = tempfile::tempdir().unwrap(); |
| let external_config = temp_dir.as_ref().join("config.toml"); |
| fs::write(&external_config, "# Cargo configuration file").unwrap(); |
| splicing_manifest.cargo_config = Some(external_config); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| let cargo_config = workspace_root.as_ref().join(".cargo").join("config.toml"); |
| assert!(cargo_config.exists()); |
| assert_eq!( |
| fs::read_to_string(cargo_config).unwrap().trim(), |
| "# Cargo configuration file" |
| ); |
| } |
| |
| #[test] |
| fn unregistered_cargo_config_replaced() { |
| let (mut splicing_manifest, cache_dir) = mock_splicing_manifest_with_workspace_in_root(); |
| |
| // Generate a cargo config that is not tracked by the splicing manifest |
| fs::create_dir_all(cache_dir.as_ref().join(".cargo")).unwrap(); |
| fs::write( |
| cache_dir.as_ref().join(".cargo").join("config.toml"), |
| "# Untracked Cargo configuration file", |
| ) |
| .unwrap(); |
| |
| // Write a cargo config |
| let temp_dir = tempfile::tempdir().unwrap(); |
| let external_config = temp_dir.as_ref().join("config.toml"); |
| fs::write(&external_config, "# Cargo configuration file").unwrap(); |
| splicing_manifest.cargo_config = Some(external_config); |
| |
| // Splice the workspace |
| let workspace_root = tempfile::tempdir().unwrap(); |
| Splicer::new(workspace_root.as_ref().to_path_buf(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()) |
| .unwrap(); |
| |
| let cargo_config = workspace_root.as_ref().join(".cargo").join("config.toml"); |
| assert!(cargo_config.exists()); |
| assert_eq!( |
| fs::read_to_string(cargo_config).unwrap().trim(), |
| "# Cargo configuration file" |
| ); |
| } |
| |
| #[test] |
| fn error_on_cargo_config_in_parent() { |
| let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace_in_root(); |
| |
| // Write a cargo config |
| let temp_dir = tempfile::tempdir().unwrap(); |
| let dot_cargo_dir = temp_dir.as_ref().join(".cargo"); |
| fs::create_dir_all(&dot_cargo_dir).unwrap(); |
| let external_config = dot_cargo_dir.join("config.toml"); |
| fs::write(&external_config, "# Cargo configuration file").unwrap(); |
| splicing_manifest.cargo_config = Some(external_config.clone()); |
| |
| // Splice the workspace |
| let workspace_root = temp_dir.as_ref().join("workspace_root"); |
| let splicing_result = Splicer::new(workspace_root.clone(), splicing_manifest) |
| .unwrap() |
| .splice_workspace(&cargo()); |
| |
| // Ensure cargo config files in parent directories lead to errors |
| assert!(splicing_result.is_err()); |
| let err_str = splicing_result.err().unwrap().to_string(); |
| assert!(err_str.starts_with("A Cargo config file was found in a parent directory")); |
| assert!(err_str.contains(&format!("Workspace = {}", workspace_root.display()))); |
| assert!(err_str.contains(&format!("Cargo config = {}", external_config.display()))); |
| } |
| |
| #[test] |
| fn find_missing_manifests_correct_without_root() { |
| let temp_dir = tempfile::tempdir().unwrap(); |
| let root_manifest_dir = temp_dir.path(); |
| touch(&root_manifest_dir.join("WORKSPACE.bazel")); |
| touch(&root_manifest_dir.join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("Cargo.toml")); |
| touch(&root_manifest_dir.join("foo").join("Cargo.toml")); |
| touch(&root_manifest_dir.join("bar").join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("bar").join("Cargo.toml")); |
| |
| let known_manifest_paths = btreeset![ |
| root_manifest_dir |
| .join("foo") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| root_manifest_dir |
| .join("bar") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| ]; |
| |
| let root_manifest: cargo_toml::Manifest = toml::toml! { |
| [workspace] |
| members = [ |
| "foo", |
| "bar", |
| ] |
| [package] |
| name = "root_pkg" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| } |
| .try_into() |
| .unwrap(); |
| let missing_manifests = SplicerKind::find_missing_manifests( |
| &root_manifest, |
| root_manifest_dir, |
| &known_manifest_paths, |
| ) |
| .unwrap(); |
| assert_eq!(missing_manifests, btreeset![]); |
| } |
| |
| #[test] |
| fn find_missing_manifests_correct_with_root() { |
| let temp_dir = tempfile::tempdir().unwrap(); |
| let root_manifest_dir = temp_dir.path(); |
| touch(&root_manifest_dir.join("WORKSPACE.bazel")); |
| touch(&root_manifest_dir.join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("Cargo.toml")); |
| touch(&root_manifest_dir.join("foo").join("Cargo.toml")); |
| touch(&root_manifest_dir.join("bar").join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("bar").join("Cargo.toml")); |
| |
| let known_manifest_paths = btreeset![ |
| root_manifest_dir.join("Cargo.toml").normalize().unwrap(), |
| root_manifest_dir |
| .join("foo") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| root_manifest_dir |
| .join("bar") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| ]; |
| |
| let root_manifest: cargo_toml::Manifest = toml::toml! { |
| [workspace] |
| members = [ |
| ".", |
| "foo", |
| "bar", |
| ] |
| [package] |
| name = "root_pkg" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| } |
| .try_into() |
| .unwrap(); |
| let missing_manifests = SplicerKind::find_missing_manifests( |
| &root_manifest, |
| root_manifest_dir, |
| &known_manifest_paths, |
| ) |
| .unwrap(); |
| assert_eq!(missing_manifests, btreeset![]); |
| } |
| |
| #[test] |
| fn find_missing_manifests_missing_root() { |
| let temp_dir = tempfile::tempdir().unwrap(); |
| let root_manifest_dir = temp_dir.path(); |
| touch(&root_manifest_dir.join("WORKSPACE.bazel")); |
| touch(&root_manifest_dir.join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("Cargo.toml")); |
| touch(&root_manifest_dir.join("foo").join("Cargo.toml")); |
| touch(&root_manifest_dir.join("bar").join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("bar").join("Cargo.toml")); |
| |
| let known_manifest_paths = btreeset![ |
| root_manifest_dir |
| .join("foo") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| root_manifest_dir |
| .join("bar") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| ]; |
| |
| let root_manifest: cargo_toml::Manifest = toml::toml! { |
| [workspace] |
| members = [ |
| ".", |
| "foo", |
| "bar", |
| ] |
| [package] |
| name = "root_pkg" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| } |
| .try_into() |
| .unwrap(); |
| let missing_manifests = SplicerKind::find_missing_manifests( |
| &root_manifest, |
| root_manifest_dir, |
| &known_manifest_paths, |
| ) |
| .unwrap(); |
| assert_eq!(missing_manifests, btreeset![String::from("//:Cargo.toml")]); |
| } |
| |
| #[test] |
| fn find_missing_manifests_missing_nonroot() { |
| let temp_dir = tempfile::tempdir().unwrap(); |
| let root_manifest_dir = temp_dir.path(); |
| touch(&root_manifest_dir.join("WORKSPACE.bazel")); |
| touch(&root_manifest_dir.join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("Cargo.toml")); |
| touch(&root_manifest_dir.join("foo").join("Cargo.toml")); |
| touch(&root_manifest_dir.join("bar").join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("bar").join("Cargo.toml")); |
| touch(&root_manifest_dir.join("baz").join("BUILD.bazel")); |
| touch(&root_manifest_dir.join("baz").join("Cargo.toml")); |
| |
| let known_manifest_paths = btreeset![ |
| root_manifest_dir |
| .join("foo") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| root_manifest_dir |
| .join("bar") |
| .join("Cargo.toml") |
| .normalize() |
| .unwrap(), |
| ]; |
| |
| let root_manifest: cargo_toml::Manifest = toml::toml! { |
| [workspace] |
| members = [ |
| "foo", |
| "bar", |
| "baz", |
| ] |
| [package] |
| name = "root_pkg" |
| version = "0.0.1" |
| |
| [lib] |
| path = "lib.rs" |
| } |
| .try_into() |
| .unwrap(); |
| let missing_manifests = SplicerKind::find_missing_manifests( |
| &root_manifest, |
| root_manifest_dir, |
| &known_manifest_paths, |
| ) |
| .unwrap(); |
| assert_eq!( |
| missing_manifests, |
| btreeset![String::from("//baz:Cargo.toml")] |
| ); |
| } |
| |
| fn touch(path: &Path) { |
| std::fs::create_dir_all(path.parent().unwrap()).unwrap(); |
| std::fs::write(path, []).unwrap(); |
| } |
| |
| fn syn_dependency_detail() -> cargo_toml::DependencyDetail { |
| cargo_toml::DependencyDetail { |
| git: Some("https://github.com/dtolnay/syn.git".to_owned()), |
| tag: Some("1.0.109".to_owned()), |
| ..cargo_toml::DependencyDetail::default() |
| } |
| } |
| |
| fn lazy_static_dependency_detail() -> cargo_toml::DependencyDetail { |
| cargo_toml::DependencyDetail { |
| git: Some("https://github.com/rust-lang-nursery/lazy-static.rs.git".to_owned()), |
| tag: Some("1.5.0".to_owned()), |
| ..cargo_toml::DependencyDetail::default() |
| } |
| } |
| } |