blob: 8b4b7ea84fb9e0bd4f1fee7f75fae1c18260bb7b [file] [log] [blame]
//! 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()
}
}
}