use std::collections::{BTreeMap, HashSet};
use std::fs;
use std::path::Path;
use anyhow::{bail, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_toml::Manifest;
use tracing::debug;
use crate::config::CrateId;
use crate::splicing::{SplicedManifest, SplicingManifest};
use crate::utils::symlink::{remove_symlink, symlink};
use super::{read_manifest, DirectPackageManifest, WorkspaceMetadata};
pub(crate) enum SplicerKind<'a> {
Workspace {
path: &'a Utf8PathBuf,
manifest: &'a Manifest,
splicing_manifest: &'a SplicingManifest,
},
Package {
path: &'a Utf8PathBuf,
manifest: &'a Manifest,
splicing_manifest: &'a SplicingManifest,
},
MultiPackage {
manifests: &'a BTreeMap<Utf8PathBuf, Manifest>,
splicing_manifest: &'a SplicingManifest,
},
}
const IGNORE_LIST: &[&str] = &[".git", "bazel-*", ".svn"];
fn parent_workspace(cargo_toml_path: &Utf8PathBuf) -> Option<Utf8PathBuf> {
for dir in cargo_toml_path.ancestors() {
let maybe_cargo_toml_path = dir.join("Cargo.toml");
let maybe_manifest = Manifest::from_path(&maybe_cargo_toml_path).ok();
if let Some(manifest) = maybe_manifest {
if manifest.workspace.is_some() {
return Some(maybe_cargo_toml_path);
}
}
}
None
}
impl<'a> SplicerKind<'a> {
pub(crate) fn new(
manifests: &'a BTreeMap<Utf8PathBuf, Manifest>,
splicing_manifest: &'a SplicingManifest,
) -> Result<Self> {
let workspace_roots: HashSet<Utf8PathBuf> =
manifests.keys().filter_map(parent_workspace).collect();
if workspace_roots.len() > 1 {
bail!("When splicing manifests, manifests are not allowed to from from different workspaces. Saw manifests which belong to the following workspaces: {}", workspace_roots.iter().map(|wr| wr.to_string()).collect::<Vec<_>>().join(", "));
}
if let Some((path, manifest)) = workspace_roots
.iter()
.next()
.and_then(|path| manifests.get_key_value(path))
{
if manifests.len() > 1 {
let workspace_name = manifest
.package
.as_ref()
.map(|p| p.name.as_str())
.unwrap_or_else(|| {
path.parent()
.and_then(|p| p.file_name())
.unwrap_or_else(|| path.file_name().unwrap_or("unknown"))
});
eprintln!("INFO: Only the workspace's Cargo.toml is required in the `manifests` attribute of workspace `{workspace_name}`; the rest can be removed");
}
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,
})
}
}
#[tracing::instrument(skip_all)]
pub(crate) fn splice(
&self,
workspace_dir: &Utf8Path,
nonhermetic_root_bazel_workspace_dir: &Utf8Path,
) -> Result<SplicedManifest> {
match self {
SplicerKind::Workspace {
path,
manifest,
splicing_manifest,
} => Self::splice_workspace(
workspace_dir,
path,
manifest,
splicing_manifest,
nonhermetic_root_bazel_workspace_dir,
),
SplicerKind::Package {
path,
manifest,
splicing_manifest,
} => Self::splice_package(
workspace_dir,
path,
manifest,
splicing_manifest,
nonhermetic_root_bazel_workspace_dir,
),
SplicerKind::MultiPackage {
manifests,
splicing_manifest,
} => Self::splice_multi_package(
workspace_dir,
manifests,
splicing_manifest,
nonhermetic_root_bazel_workspace_dir,
),
}
}
#[tracing::instrument(skip_all)]
fn splice_workspace(
workspace_dir: &Utf8Path,
path: &&Utf8PathBuf,
manifest: &&Manifest,
splicing_manifest: &&SplicingManifest,
nonhermetic_root_bazel_workspace_dir: &Utf8Path,
) -> Result<SplicedManifest> {
let mut manifest = (*manifest).clone();
let manifest_dir = path
.parent()
.expect("Every manifest should have a parent directory");
symlink_roots(
manifest_dir.as_std_path(),
workspace_dir.as_std_path(),
Some(IGNORE_LIST),
)?;
Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir.as_std_path())?;
if !splicing_manifest.direct_packages.is_empty() {
Self::inject_direct_packages(
&mut manifest,
&splicing_manifest.direct_packages,
nonhermetic_root_bazel_workspace_dir,
)?;
}
let root_manifest_path = workspace_dir.join("Cargo.toml");
let member_manifests = BTreeMap::from([(*path, String::new())]);
let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, member_manifests)?;
workspace_metadata.inject_into(&mut manifest)?;
write_root_manifest(root_manifest_path.as_std_path(), manifest)?;
Ok(SplicedManifest::Workspace(root_manifest_path))
}
#[tracing::instrument(skip_all)]
fn splice_package(
workspace_dir: &Utf8Path,
path: &&Utf8PathBuf,
manifest: &&Manifest,
splicing_manifest: &&SplicingManifest,
nonhermetic_root_bazel_workspace_dir: &Utf8Path,
) -> Result<SplicedManifest> {
let manifest_dir = path
.parent()
.expect("Every manifest should have a parent directory");
symlink_roots(
manifest_dir.as_std_path(),
workspace_dir.as_std_path(),
Some(IGNORE_LIST),
)?;
Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir.as_std_path())?;
let mut manifest = (*manifest).clone();
if manifest.workspace.is_none() {
manifest.workspace =
default_cargo_workspace_manifest(&splicing_manifest.resolver_version).workspace
}
if !splicing_manifest.direct_packages.is_empty() {
Self::inject_direct_packages(
&mut manifest,
&splicing_manifest.direct_packages,
nonhermetic_root_bazel_workspace_dir,
)?;
}
let root_manifest_path = workspace_dir.join("Cargo.toml");
let member_manifests = BTreeMap::from([(*path, String::new())]);
let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, member_manifests)?;
workspace_metadata.inject_into(&mut manifest)?;
write_root_manifest(root_manifest_path.as_std_path(), manifest)?;
Ok(SplicedManifest::Package(root_manifest_path))
}
#[tracing::instrument(skip_all)]
fn splice_multi_package(
workspace_dir: &Utf8Path,
manifests: &&BTreeMap<Utf8PathBuf, Manifest>,
splicing_manifest: &&SplicingManifest,
nonhermetic_root_bazel_workspace_dir: &Utf8Path,
) -> Result<SplicedManifest> {
let mut manifest = default_cargo_workspace_manifest(&splicing_manifest.resolver_version);
Self::setup_cargo_config(&splicing_manifest.cargo_config, workspace_dir.as_std_path())?;
let installations =
Self::inject_workspace_members(&mut manifest, manifests, workspace_dir.as_std_path())?;
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.to_string())
.collect::<Vec<String>>()
)
})?;
}
let workspace_metadata = WorkspaceMetadata::new(splicing_manifest, installations)?;
workspace_metadata.inject_into(&mut manifest)?;
if !splicing_manifest.direct_packages.is_empty() {
Self::inject_direct_packages(
&mut manifest,
&splicing_manifest.direct_packages,
nonhermetic_root_bazel_workspace_dir,
)?;
}
let root_manifest_path = workspace_dir.join("Cargo.toml");
write_root_manifest(root_manifest_path.as_std_path(), manifest)?;
Ok(SplicedManifest::MultiPackage(root_manifest_path))
}
fn setup_cargo_config(
cargo_config_path: &Option<Utf8PathBuf>,
workspace_dir: &Path,
) -> Result<()> {
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()
)
})?;
}
}
}
}
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()
)
})?;
}
}
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()
}
if let Some(cargo_config_path) = cargo_config_path {
if !dot_cargo_dir.exists() {
fs::create_dir_all(&dot_cargo_dir)?;
}
debug!("Using Cargo config: {}", cargo_config_path);
fs::copy(cargo_config_path, dot_cargo_dir.join("config.toml"))?;
}
Ok(())
}
fn inject_workspace_members<'b>(
root_manifest: &mut Manifest,
manifests: &'b BTreeMap<Utf8PathBuf, Manifest>,
workspace_dir: &Path,
) -> Result<BTreeMap<&'b Utf8PathBuf, 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 have a parent directory");
let dest_package_dir = workspace_dir.join(package_name);
match symlink_roots(
manifest_dir.as_std_path(),
&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,
nonhermetic_root_bazel_workspace_dir: &Utf8Path,
) -> Result<()> {
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;
}
}
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
)
}
for (name, details) in direct_packages_manifest.iter() {
let mut details = details.clone();
details.path = details
.path
.map(|path| nonhermetic_root_bazel_workspace_dir.join(path).to_string());
manifest.dependencies.insert(
name.clone(),
cargo_toml::Dependency::Detailed(Box::new(details)),
);
}
Ok(())
}
fn inject_patches(manifest: &mut Manifest, patches: &cargo_toml::PatchSet) -> Result<()> {
for (registry, new_patches) in patches.iter() {
if let Some(existing_patches) = manifest.patch.get_mut(registry) {
existing_patches.extend(
new_patches
.iter()
.map(|(pkg, info)| {
if let Some(existing_info) = existing_patches.get(pkg) {
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: Utf8PathBuf,
manifests: BTreeMap<Utf8PathBuf, Manifest>,
splicing_manifest: SplicingManifest,
}
impl Splicer {
pub(crate) fn new(
workspace_dir: Utf8PathBuf,
splicing_manifest: SplicingManifest,
) -> Result<Self> {
let manifests = splicing_manifest
.manifests
.keys()
.map(|path| {
let m = read_manifest(path)
.with_context(|| format!("Failed to read manifest at {}", path))?;
Ok((path.clone(), m))
})
.collect::<Result<BTreeMap<Utf8PathBuf, Manifest>>>()?;
Ok(Self {
workspace_dir,
manifests,
splicing_manifest,
})
}
pub(crate) fn splice_workspace(
&self,
nonhermetic_root_bazel_workspace_dir: &Utf8Path,
) -> Result<SplicedManifest> {
SplicerKind::new(&self.manifests, &self.splicing_manifest)?
.splice(&self.workspace_dir, nonhermetic_root_bazel_workspace_dir)
}
pub(crate) fn prepare(&self) -> Result<SplicerKind<'_>> {
SplicerKind::new(&self.manifests, &self.splicing_manifest)
}
}
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 {
cargo_toml::Manifest::from_str(
&toml::toml! {
[package]
name = DEFAULT_SPLICING_PACKAGE_NAME
version = DEFAULT_SPLICING_PACKAGE_VERSION
edition = "2018"
[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 {
let mut manifest = cargo_toml::Manifest::from_str(&textwrap::dedent(&format!(
r#"
[workspace]
resolver = "{resolver_version}"
"#,
)))
.unwrap();
manifest.workspace.as_mut().unwrap().members.pop();
manifest
}
pub(crate) fn write_root_manifest(path: &Path, manifest: cargo_toml::Manifest) -> Result<()> {
if path.exists() {
fs::remove_file(path)?;
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
write_manifest(path, &manifest)?;
Ok(())
}
pub(crate) fn write_manifest(path: &Path, manifest: &cargo_toml::Manifest) -> Result<()> {
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()))
}
pub(crate) fn symlink_roots(
source: &Path,
dest: &Path,
ignore_list: Option<&[&str]>,
) -> Result<()> {
if !source.is_dir() {
bail!("Source path is not a directory: {}", source.display());
}
if dest.exists() && !dest.is_dir() {
bail!("Dest path is not a directory: {}", dest.display());
}
fs::create_dir_all(dest)?;
for entry in (source.read_dir()?).flatten() {
let basename = entry.file_name();
if let Some(base_str) = basename.to_str() {
if let Some(list) = ignore_list {
for item in list.iter() {
if item.ends_with('*') && base_str.starts_with(item.trim_end_matches('*')) {
continue;
}
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 crate::splicing::Cargo;
use crate::utils::starlark::Label;
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 {
use std::net::ToSocketAddrs;
if "github.com:443".to_socket_addrs().is_err() {
eprintln!("This test case requires network access.");
true
} else {
false
}
}
#[cfg(not(feature = "cargo"))]
fn get_cargo_and_rustc_paths() -> (std::path::PathBuf, std::path::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)
}
#[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<P: AsRef<Path>>(manifest_path: P) -> cargo_metadata::Metadata {
cargo()
.metadata_command_with_options(manifest_path.as_ref(), vec!["--offline".to_owned()])
.unwrap()
.exec()
.unwrap()
}
fn mock_cargo_toml<P: AsRef<Path>>(path: P, name: &str) -> cargo_toml::Manifest {
mock_cargo_toml_with_dependencies(path, name, &[])
}
fn mock_cargo_toml_with_dependencies<P: AsRef<Path>>(
path: P,
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();
let path = path.as_ref();
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();
for pkg in &["sub_pkg_a", "sub_pkg_b"] {
let manifest_path = Utf8PathBuf::try_from(
cache_dir
.as_ref()
.join("root_pkg")
.join(pkg)
.join("Cargo.toml"),
)
.unwrap();
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(),
);
}
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 = Utf8PathBuf::try_from(root_pkg.join("Cargo.toml")).unwrap();
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();
for pkg in &["sub_pkg_a", "sub_pkg_b"] {
let manifest_path =
Utf8PathBuf::try_from(cache_dir.as_ref().join(pkg).join("Cargo.toml")).unwrap();
mock_cargo_toml(&manifest_path, pkg);
splicing_manifest.manifests.insert(
manifest_path,
Label::from_str(&format!("//{pkg}:Cargo.toml")).unwrap(),
);
}
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 = Utf8PathBuf::try_from(workspace_root.join("Cargo.toml")).unwrap();
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();
let manifest_path =
Utf8PathBuf::try_from(cache_dir.as_ref().join("root_pkg").join("Cargo.toml")).unwrap();
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();
for pkg in &["pkg_a", "pkg_b", "pkg_c"] {
let manifest_path =
Utf8PathBuf::try_from(cache_dir.as_ref().join(pkg).join("Cargo.toml")).unwrap();
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();
if cfg!(target_os = "windows") {
workspace_root = format!("/{}", workspace_root.replace('\\', "/"))
};
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();
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
let cargo = cargo();
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),
]
);
assert_eq!(
metadata.workspace_metadata,
mock_workspace_metadata(false, None)
);
assert!(!metadata
.packages
.iter()
.any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME));
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();
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
let cargo = cargo();
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),
]
);
assert_eq!(
metadata.workspace_metadata,
mock_workspace_metadata(false, None)
);
assert!(!metadata
.packages
.iter()
.any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME));
cargo_lock::Lockfile::load(workspace_root.as_ref().join("Cargo.lock")).unwrap();
}
#[test]
fn splice_workspace_report_external_workspace_members() {
let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_workspace();
let external_workspace_root = tempfile::tempdir().unwrap();
let external_manifest = Utf8PathBuf::try_from(
external_workspace_root
.as_ref()
.join("external_workspace_member")
.join("Cargo.toml"),
)
.unwrap();
fs::create_dir_all(external_manifest.parent().unwrap()).unwrap();
fs::write(
external_workspace_root.as_ref().join("Cargo.toml"),
textwrap::dedent(
r#"
[workspace]
[package]
name = "external_workspace_root"
version = "0.0.1"
[lib]
path = "lib.rs"
"#,
),
)
.unwrap();
fs::write(
&external_manifest,
textwrap::dedent(
r#"
[package]
name = "external_workspace_member"
version = "0.0.1"
[lib]
path = "lib.rs"
"#,
),
)
.unwrap();
splicing_manifest.manifests.insert(
external_manifest.clone(),
Label::from_str("@remote_dep//external_workspace_member:Cargo.toml").unwrap(),
);
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"));
assert!(workspace_manifest.is_err());
let err_str = format!("{:?}", &workspace_manifest);
assert!(
err_str
.contains("When splicing manifests, manifests are not allowed to from from different workspaces. Saw manifests which belong to the following workspaces:")
&& err_str.contains(external_workspace_root.path().to_string_lossy().as_ref())
);
}
#[test]
fn splice_workspace_no_root_pkg() {
let (splicing_manifest, cache_dir) = mock_splicing_manifest_with_workspace_in_root();
fs::write(
cache_dir.as_ref().join("Cargo.toml"),
textwrap::dedent(
r#"
[workspace]
members = [
"sub_pkg_a",
"sub_pkg_b",
]
"#,
),
)
.unwrap();
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
let metadata = generate_metadata(workspace_manifest.as_path_buf());
assert!(!metadata
.packages
.iter()
.any(|pkg| pkg.name == DEFAULT_SPLICING_PACKAGE_NAME));
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();
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
let cargo = cargo();
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
)]
);
assert_eq!(
metadata.workspace_metadata,
mock_workspace_metadata(false, None)
);
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();
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
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)
);
let cargo = cargo();
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),
]
);
assert_eq!(
metadata.workspace_metadata,
mock_workspace_metadata(false, None)
);
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();
splicing_manifest.resolver_version = cargo_toml::Resolver::V2;
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
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)
);
let cargo = cargo();
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),
]
);
assert_eq!(
metadata.workspace_metadata,
mock_workspace_metadata(false, None)
);
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();
splicing_manifest.direct_packages.insert(
"syn".to_owned(),
cargo_toml::DependencyDetail {
version: Some("1.0.109".to_owned()),
..syn_dependency_detail()
},
);
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
let cargo_manifest = cargo_toml::Manifest::from_str(
&fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
)
.unwrap();
assert!(cargo_manifest.package.unwrap().name == DEFAULT_SPLICING_PACKAGE_NAME);
}
#[test]
fn splice_multi_package_with_direct_path_deps() {
if should_skip_network_test() {
return;
}
let (mut splicing_manifest, _cache_dir) = mock_splicing_manifest_with_multi_package();
splicing_manifest.direct_packages.insert(
"syn".to_owned(),
cargo_toml::DependencyDetail {
path: Some("local/vendor/copy/of/syn".to_string()),
..cargo_toml::DependencyDetail::default()
},
);
splicing_manifest.direct_packages.insert(
"quote".to_owned(),
cargo_toml::DependencyDetail {
path: Some("/absolute/vendor/copy/of/quote".to_string()),
..cargo_toml::DependencyDetail::default()
},
);
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/repo/root"))
.unwrap();
let cargo_manifest = cargo_toml::Manifest::from_str(
&fs::read_to_string(workspace_manifest.as_path_buf()).unwrap(),
)
.unwrap();
assert!(
cargo_manifest
.dependencies
.get("syn")
.unwrap()
.detail()
.unwrap()
.path
.as_deref()
== Some("/doesnotexist/repo/root/local/vendor/copy/of/syn")
);
assert!(
cargo_manifest
.dependencies
.get("quote")
.unwrap()
.detail()
.unwrap()
.path
.as_deref()
== Some("/absolute/vendor/copy/of/quote")
);
}
#[test]
fn splice_multi_package_with_patch() {
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(),
BTreeMap::from([(
"syn".to_owned(),
cargo_toml::Dependency::Detailed(Box::new(syn_dependency_detail())),
)]),
)]);
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();
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
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"] {
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)]);
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();
}
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
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"] {
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())),
)]),
)]);
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();
}
let workspace_root = tempfile::tempdir().unwrap();
let workspace_manifest =
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.unwrap();
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();
for (pkg, patch) in [("pkg_a", 3), ("pkg_b", 4)] {
let new_patch = cargo_toml::PatchSet::from([(
"registry".to_owned(),
BTreeMap::from([(
"foo".to_owned(),
cargo_toml::Dependency::Simple(format!("1.2.{patch}")),
)]),
)]);
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();
}
let workspace_root = tempfile::tempdir().unwrap();
let result = Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"));
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();
let temp_dir = tempfile::tempdir().unwrap();
let external_config = tempdir_utf8pathbuf(&temp_dir).join("config.toml");
fs::write(&external_config, "# Cargo configuration file").unwrap();
splicing_manifest.cargo_config = Some(external_config);
let workspace_root = tempfile::tempdir().unwrap();
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.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();
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();
let temp_dir = tempfile::tempdir().unwrap();
let external_config = tempdir_utf8pathbuf(&temp_dir).join("config.toml");
fs::write(&external_config, "# Cargo configuration file").unwrap();
splicing_manifest.cargo_config = Some(external_config);
let workspace_root = tempfile::tempdir().unwrap();
Splicer::new(tempdir_utf8pathbuf(&workspace_root), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"))
.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();
let temp_dir = tempfile::tempdir().unwrap();
let dot_cargo_dir = tempdir_utf8pathbuf(&temp_dir).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());
let workspace_root = tempdir_utf8pathbuf(&temp_dir).join("workspace_root");
let splicing_result = Splicer::new(workspace_root.clone(), splicing_manifest)
.unwrap()
.splice_workspace(Utf8Path::new("/doesnotexist/unused/repo/root"));
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)));
assert!(err_str.contains(&format!("Cargo config = {}", external_config)));
}
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()
}
}
fn tempdir_utf8pathbuf(tempdir: &tempfile::TempDir) -> Utf8PathBuf {
Utf8PathBuf::try_from(tempdir.as_ref().to_path_buf()).unwrap()
}
}