use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use toml_edit::{DocumentMut, InlineTable, Item, Table, Value};
const DEP_SECTIONS: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
pub struct VendorResult {
pub vendor_dir: PathBuf,
pub workspace_manifest: Option<PathBuf>,
}
pub fn vendor_core_only(
workspace_root: &Path,
core_crate_dir: &Path,
dest_dir: &Path,
generate_workspace_manifest: bool,
) -> Result<VendorResult> {
let core_crate_name = core_crate_dir
.file_name()
.context("core crate dir has no name")?
.to_string_lossy();
let workspace_manifest_path = workspace_root.join("Cargo.toml");
let workspace_toml = fs::read_to_string(&workspace_manifest_path)
.with_context(|| format!("reading {}", workspace_manifest_path.display()))?;
let workspace_doc: DocumentMut = workspace_toml
.parse()
.with_context(|| format!("parsing {}", workspace_manifest_path.display()))?;
let workspace_table = workspace_doc
.get("workspace")
.and_then(|w| w.as_table())
.context("no [workspace] in root Cargo.toml")?;
let workspace_pkg = workspace_table.get("package").and_then(|p| p.as_table());
let workspace_deps = workspace_table.get("dependencies").and_then(|d| d.as_table());
let vendor_crate_dir = dest_dir.join(&*core_crate_name);
if vendor_crate_dir.exists() {
fs::remove_dir_all(&vendor_crate_dir).with_context(|| format!("removing {}", vendor_crate_dir.display()))?;
}
copy_dir_filtered(core_crate_dir, &vendor_crate_dir)?;
let crate_manifest_path = vendor_crate_dir.join("Cargo.toml");
let crate_toml = fs::read_to_string(&crate_manifest_path)
.with_context(|| format!("reading {}", crate_manifest_path.display()))?;
let mut crate_doc: DocumentMut = crate_toml
.parse()
.with_context(|| format!("parsing {}", crate_manifest_path.display()))?;
if let Some(ws_pkg) = workspace_pkg {
inline_workspace_package_fields(&mut crate_doc, ws_pkg)?;
}
if let Some(ws_deps) = workspace_deps {
inline_workspace_deps(&mut crate_doc, "dependencies", ws_deps)?;
inline_workspace_deps(&mut crate_doc, "dev-dependencies", ws_deps)?;
inline_workspace_deps(&mut crate_doc, "build-dependencies", ws_deps)?;
}
remove_workspace_lints(&mut crate_doc);
fs::write(&crate_manifest_path, crate_doc.to_string())
.with_context(|| format!("writing {}", crate_manifest_path.display()))?;
let ws_manifest_path = if generate_workspace_manifest {
let path = dest_dir.join("Cargo.toml");
let content = generate_vendor_workspace_manifest(&core_crate_name, workspace_pkg, workspace_deps);
fs::write(&path, content).with_context(|| format!("writing {}", path.display()))?;
Some(path)
} else {
None
};
tracing::info!(
crate_name = %core_crate_name,
dest = %vendor_crate_dir.display(),
"vendored core crate"
);
Ok(VendorResult {
vendor_dir: vendor_crate_dir,
workspace_manifest: ws_manifest_path,
})
}
pub fn vendor_full(workspace_root: &Path, core_crate_dir: &Path, dest_dir: &Path) -> Result<VendorResult> {
let result = vendor_core_only(workspace_root, core_crate_dir, dest_dir, true)?;
let vendor_deps_dir = dest_dir.join("vendor");
let status = std::process::Command::new("cargo")
.arg("vendor")
.arg(&vendor_deps_dir)
.current_dir(dest_dir)
.status()
.context("running cargo vendor")?;
if !status.success() {
bail!("cargo vendor failed with exit code {}", status.code().unwrap_or(-1));
}
let cargo_config_dir = dest_dir.join(".cargo");
fs::create_dir_all(&cargo_config_dir)?;
let config_content = format!(
"[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n\
[source.vendored-sources]\ndirectory = \"{}\"\n",
vendor_deps_dir.display()
);
fs::write(cargo_config_dir.join("config.toml"), config_content)?;
clean_vendored_deps(&vendor_deps_dir)?;
tracing::info!(dest = %dest_dir.display(), "full vendor complete");
Ok(result)
}
fn copy_dir_filtered(src: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest)?;
for entry in fs::read_dir(src).with_context(|| format!("reading {}", src.display()))? {
let entry = entry?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if matches!(name_str.as_ref(), "target" | ".git" | ".gitignore" | ".fastembed_cache") {
continue;
}
if name_str.ends_with(".swp")
|| name_str.ends_with(".bak")
|| name_str.ends_with(".tmp")
|| name_str.ends_with('~')
{
continue;
}
let src_path = entry.path();
let dest_path = dest.join(&name);
if entry.file_type()?.is_dir() {
copy_dir_filtered(&src_path, &dest_path)?;
} else {
fs::copy(&src_path, &dest_path)
.with_context(|| format!("copying {} to {}", src_path.display(), dest_path.display()))?;
}
}
Ok(())
}
fn inline_workspace_package_fields(doc: &mut DocumentMut, ws_pkg: &Table) -> Result<()> {
let pkg = match doc.get_mut("package").and_then(|p| p.as_table_mut()) {
Some(t) => t,
None => return Ok(()),
};
let inheritable_fields = [
"version",
"edition",
"rust-version",
"authors",
"license",
"repository",
"homepage",
"documentation",
"description",
"readme",
"keywords",
"categories",
];
for field in &inheritable_fields {
if is_workspace_inherited(pkg, field) {
if let Some(ws_value) = ws_pkg.get(field) {
pkg.insert(field, ws_value.clone());
} else {
pkg.remove(field);
}
}
}
Ok(())
}
fn is_workspace_inherited(table: &Table, key: &str) -> bool {
if let Some(item) = table.get(key) {
if let Some(tbl) = item.as_table_like() {
if let Some(ws) = tbl.get("workspace") {
return ws.as_value().and_then(|v| v.as_bool()) == Some(true);
}
}
}
false
}
fn inline_workspace_deps(doc: &mut DocumentMut, section: &str, ws_deps: &Table) -> Result<()> {
let deps = match doc.get_mut(section).and_then(|d| d.as_table_mut()) {
Some(t) => t,
None => return Ok(()),
};
let keys_to_inline: Vec<String> = deps
.iter()
.filter_map(|(key, item)| {
if let Some(tbl) = item.as_table_like() {
if tbl
.get("workspace")
.and_then(|w| w.as_value())
.and_then(|v| v.as_bool())
== Some(true)
{
return Some(key.to_string());
}
}
None
})
.collect();
for key in keys_to_inline {
let crate_extras = extract_non_workspace_fields(deps.get(&key));
let ws_spec = ws_deps.get(&key);
let inlined = build_inlined_dep(ws_spec, &crate_extras);
deps.insert(&key, inlined);
}
Ok(())
}
fn extract_non_workspace_fields(item: Option<&Item>) -> BTreeMap<String, Item> {
let mut extras = BTreeMap::new();
if let Some(tbl) = item.and_then(|i| i.as_table_like()) {
for (k, v) in tbl.iter() {
if k != "workspace" {
extras.insert(k.to_string(), v.clone());
}
}
}
extras
}
fn build_inlined_dep(ws_spec: Option<&Item>, extras: &BTreeMap<String, Item>) -> Item {
let ws_spec = match ws_spec {
Some(spec) => spec,
None => {
if extras.is_empty() {
return Item::None;
}
let mut tbl = toml_edit::InlineTable::new();
for (k, v) in extras {
if let Some(val) = v.as_value() {
tbl.insert(k, val.clone());
}
}
return Item::Value(Value::InlineTable(tbl));
}
};
if let Some(version_str) = ws_spec.as_str() {
if extras.is_empty() {
return Item::Value(Value::String(toml_edit::Formatted::new(version_str.to_string())));
}
let mut tbl = toml_edit::InlineTable::new();
tbl.insert(
"version",
Value::String(toml_edit::Formatted::new(version_str.to_string())),
);
for (k, v) in extras {
if let Some(val) = v.as_value() {
tbl.insert(k, val.clone());
}
}
return Item::Value(Value::InlineTable(tbl));
}
if let Some(ws_tbl) = ws_spec.as_table_like() {
let tbl = inline_table_without_path(ws_tbl.iter(), extras.iter().map(|(k, v)| (k.as_str(), v)));
return Item::Value(Value::InlineTable(tbl));
}
ws_spec.clone()
}
fn inline_table_without_path<'a, B, O>(base: B, overlay: O) -> InlineTable
where
B: Iterator<Item = (&'a str, &'a Item)>,
O: Iterator<Item = (&'a str, &'a Item)>,
{
let mut tbl = InlineTable::new();
for (k, v) in base.chain(overlay) {
if k == "path" || k == "workspace" {
continue;
}
if let Some(val) = v.as_value() {
tbl.insert(k, val.clone());
}
}
tbl
}
pub fn rewrite_path_deps_to_registry(
manifest_path: &Path,
members: &crate::publish::workspace::WorkspaceMembers,
version: &str,
) -> Result<()> {
let content = fs::read_to_string(manifest_path).with_context(|| format!("reading {}", manifest_path.display()))?;
let mut doc: DocumentMut = content
.parse()
.with_context(|| format!("parsing {}", manifest_path.display()))?;
for section in DEP_SECTIONS {
if let Some(table) = doc.get_mut(section).and_then(|d| d.as_table_mut()) {
rewrite_dep_table(table, members, version);
}
}
if let Some(target_tbl) = doc.get_mut("target").and_then(|t| t.as_table_mut()) {
for (_cfg, cfg_item) in target_tbl.iter_mut() {
let Some(cfg_tbl) = cfg_item.as_table_mut() else {
continue;
};
for section in DEP_SECTIONS {
if let Some(table) = cfg_tbl.get_mut(section).and_then(|d| d.as_table_mut()) {
rewrite_dep_table(table, members, version);
}
}
}
}
fs::write(manifest_path, doc.to_string()).with_context(|| format!("writing {}", manifest_path.display()))?;
Ok(())
}
fn rewrite_dep_table(table: &mut Table, members: &crate::publish::workspace::WorkspaceMembers, version: &str) {
let keys: Vec<String> = table
.iter()
.filter_map(|(key, item)| {
if !members.names.contains(key) {
return None;
}
let has_path = item.as_table_like().is_some_and(|t| t.contains_key("path"));
if has_path { Some(key.to_string()) } else { None }
})
.collect();
for key in keys {
let Some(existing) = table.get(&key) else {
continue;
};
let rewritten = strip_path_set_version(existing, version);
table.insert(&key, rewritten);
}
}
fn strip_path_set_version(existing: &Item, version: &str) -> Item {
let version_item = Item::Value(Value::String(toml_edit::Formatted::new(version.to_string())));
let base = std::iter::once(("version", &version_item));
match existing.as_table_like() {
Some(tbl) => {
let extras: Vec<(&str, &Item)> = tbl.iter().filter(|(k, _)| *k != "version").collect();
let merged = inline_table_without_path(base, extras.into_iter());
Item::Value(Value::InlineTable(merged))
}
None => {
let merged = inline_table_without_path(base, std::iter::empty());
Item::Value(Value::InlineTable(merged))
}
}
}
pub(crate) fn scrub_or_regenerate_lock(manifest_dir: &Path, regenerate: bool, strict: bool) -> Result<()> {
let lock_path = manifest_dir.join("Cargo.lock");
if regenerate {
let manifest = manifest_dir.join("Cargo.toml");
let status = std::process::Command::new("cargo")
.arg("generate-lockfile")
.arg("--manifest-path")
.arg(&manifest)
.status();
match status {
Ok(s) if s.success() => return Ok(()),
Ok(s) => {
if strict {
bail!(
"cargo generate-lockfile failed (exit code {}) for {} — a referenced \
workspace-member version is likely not yet published to the registry. \
Publish the core crate(s) before the language packages, then retry.",
s.code().unwrap_or(-1),
manifest.display()
);
}
tracing::warn!(
code = s.code().unwrap_or(-1),
"cargo generate-lockfile failed; deleting Cargo.lock so it regenerates at build time"
);
}
Err(error) => {
if strict {
return Err(error).with_context(|| {
format!(
"could not run cargo generate-lockfile for {} — a referenced \
workspace-member version is likely not yet published to the registry. \
Publish the core crate(s) before the language packages, then retry.",
manifest.display()
)
});
}
tracing::warn!(%error, "could not run cargo generate-lockfile; deleting Cargo.lock");
}
}
}
if lock_path.exists() {
fs::remove_file(&lock_path).with_context(|| format!("removing {}", lock_path.display()))?;
}
Ok(())
}
fn remove_workspace_lints(doc: &mut DocumentMut) {
if let Some(lints) = doc.get("lints").and_then(|l| l.as_table_like()) {
if lints
.get("workspace")
.and_then(|w| w.as_value())
.and_then(|v| v.as_bool())
== Some(true)
{
doc.remove("lints");
}
}
}
fn generate_vendor_workspace_manifest(crate_name: &str, ws_pkg: Option<&Table>, ws_deps: Option<&Table>) -> String {
let mut lines = Vec::new();
lines.push("[workspace]".to_string());
lines.push(format!("members = [\"{crate_name}\"]"));
lines.push("resolver = \"2\"".to_string());
lines.push(String::new());
if let Some(pkg) = ws_pkg {
lines.push("[workspace.package]".to_string());
for (key, value) in pkg.iter() {
lines.push(format!("{key} = {value}"));
}
lines.push(String::new());
}
if let Some(deps) = ws_deps {
lines.push("[workspace.dependencies]".to_string());
for (key, value) in deps.iter() {
let has_path = value.as_table_like().is_some_and(|t| t.contains_key("path"));
if !has_path {
lines.push(format!("{key} = {value}"));
}
}
lines.push(String::new());
}
lines.join("\n")
}
fn clean_vendored_deps(vendor_dir: &Path) -> Result<()> {
if !vendor_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(vendor_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let crate_dir = entry.path();
for dir_name in &["tests", "benches", "examples", "test", "bench"] {
let dir = crate_dir.join(dir_name);
if dir.exists() {
fs::remove_dir_all(&dir).ok();
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup_workspace(root: &Path) {
fs::write(
root.join("Cargo.toml"),
r#"
[workspace]
resolver = "2"
members = ["crates/my-lib", "crates/my-lib-py"]
[workspace.package]
version = "1.2.3"
edition = "2024"
rust-version = "1.85"
license = "MIT"
authors = ["Test Author"]
repository = "https://github.com/test/my-lib"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
anyhow = "1"
my-lib = { version = "1.2.3", path = "crates/my-lib" }
tokio = { version = "1", features = ["full"] }
"#,
)
.unwrap();
let core_dir = root.join("crates/my-lib/src");
fs::create_dir_all(&core_dir).unwrap();
fs::write(core_dir.join("lib.rs"), "pub fn hello() {}").unwrap();
fs::write(
root.join("crates/my-lib/Cargo.toml"),
r#"
[package]
name = "my-lib"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
serde = { workspace = true }
anyhow = { workspace = true, optional = true }
[dev-dependencies]
tokio = { workspace = true }
[lints]
workspace = true
"#,
)
.unwrap();
}
#[test]
fn vendor_core_only_copies_and_rewrites() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
setup_workspace(root);
let dest = root.join("vendor");
fs::create_dir_all(&dest).unwrap();
let result = vendor_core_only(root, &root.join("crates/my-lib"), &dest, true).unwrap();
assert!(result.vendor_dir.join("src/lib.rs").exists());
let vendored = fs::read_to_string(result.vendor_dir.join("Cargo.toml")).unwrap();
let doc: DocumentMut = vendored.parse().unwrap();
let pkg = doc["package"].as_table().unwrap();
assert_eq!(pkg["version"].as_str(), Some("1.2.3"));
assert_eq!(pkg["edition"].as_str(), Some("2024"));
assert_eq!(pkg["license"].as_str(), Some("MIT"));
let deps = doc["dependencies"].as_table().unwrap();
let serde = deps["serde"].as_inline_table().unwrap();
assert_eq!(serde.get("version").and_then(|v| v.as_str()), Some("1"));
assert!(serde.get("features").is_some());
assert!(serde.get("workspace").is_none());
let anyhow = deps["anyhow"].as_inline_table().unwrap();
assert_eq!(anyhow.get("version").and_then(|v| v.as_str()), Some("1"));
assert_eq!(anyhow.get("optional").and_then(|v| v.as_bool()), Some(true));
assert!(doc.get("lints").is_none());
let ws_manifest = result.workspace_manifest.unwrap();
assert!(ws_manifest.exists());
let ws_content = fs::read_to_string(&ws_manifest).unwrap();
assert!(ws_content.contains("[workspace]"));
assert!(ws_content.contains("\"my-lib\""));
assert!(!ws_content.contains("my-lib = { version"));
assert!(ws_content.contains("serde"));
assert!(ws_content.contains("anyhow"));
}
#[test]
fn vendor_core_only_cleans_target_dir() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
setup_workspace(root);
let target = root.join("crates/my-lib/target");
fs::create_dir_all(&target).unwrap();
fs::write(target.join("debug.txt"), "fake").unwrap();
let dest = root.join("vendor");
fs::create_dir_all(&dest).unwrap();
let result = vendor_core_only(root, &root.join("crates/my-lib"), &dest, false).unwrap();
assert!(!result.vendor_dir.join("target").exists());
}
#[test]
fn vendor_core_only_without_workspace_manifest() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
setup_workspace(root);
let dest = root.join("vendor");
fs::create_dir_all(&dest).unwrap();
let result = vendor_core_only(root, &root.join("crates/my-lib"), &dest, false).unwrap();
assert!(result.workspace_manifest.is_none());
assert!(!dest.join("Cargo.toml").exists());
}
#[test]
fn vendor_idempotent() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
setup_workspace(root);
let dest = root.join("vendor");
fs::create_dir_all(&dest).unwrap();
vendor_core_only(root, &root.join("crates/my-lib"), &dest, true).unwrap();
let result = vendor_core_only(root, &root.join("crates/my-lib"), &dest, true).unwrap();
assert!(result.vendor_dir.join("src/lib.rs").exists());
}
use crate::publish::workspace::WorkspaceMembers;
use std::collections::{BTreeMap, BTreeSet};
fn members_with(names: &[&str]) -> WorkspaceMembers {
WorkspaceMembers {
names: names.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>(),
versions: BTreeMap::new(),
}
}
fn write_binding_manifest(path: &Path) {
fs::write(
path,
r#"
[package]
name = "my-lib-py"
version = "0.1.0"
edition = "2024"
[dependencies]
my-lib = { path = "../my-lib", features = ["serde"] }
my-lib-core = { path = "../my-lib-core" }
anyhow = "1"
[dev-dependencies]
my-lib-testkit = { path = "../my-lib-testkit", default-features = false }
[target.'cfg(unix)'.dependencies]
my-lib-unix = { path = "../my-lib-unix", optional = true }
"#,
)
.unwrap();
}
#[test]
fn rewrite_path_deps_replaces_members_keeps_others() {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("Cargo.toml");
write_binding_manifest(&manifest);
let members = members_with(&["my-lib", "my-lib-core", "my-lib-testkit", "my-lib-unix"]);
rewrite_path_deps_to_registry(&manifest, &members, "1.2.3").unwrap();
let doc: DocumentMut = fs::read_to_string(&manifest).unwrap().parse().unwrap();
let deps = doc["dependencies"].as_table().unwrap();
let my_lib = deps["my-lib"].as_inline_table().unwrap();
assert_eq!(my_lib.get("version").and_then(|v| v.as_str()), Some("1.2.3"));
assert!(my_lib.get("path").is_none(), "path must be stripped");
assert!(my_lib.get("features").is_some(), "features preserved");
let core = deps["my-lib-core"].as_inline_table().unwrap();
assert_eq!(core.get("version").and_then(|v| v.as_str()), Some("1.2.3"));
assert!(core.get("path").is_none());
assert_eq!(deps["anyhow"].as_str(), Some("1"));
let dev = doc["dev-dependencies"].as_table().unwrap();
let testkit = dev["my-lib-testkit"].as_inline_table().unwrap();
assert_eq!(testkit.get("version").and_then(|v| v.as_str()), Some("1.2.3"));
assert!(testkit.get("path").is_none());
assert_eq!(testkit.get("default-features").and_then(|v| v.as_bool()), Some(false));
let target = doc["target"].as_table().unwrap();
let cfg = target["cfg(unix)"].as_table().unwrap();
let unix_deps = cfg["dependencies"].as_table().unwrap();
let unix = unix_deps["my-lib-unix"].as_inline_table().unwrap();
assert_eq!(unix.get("version").and_then(|v| v.as_str()), Some("1.2.3"));
assert!(unix.get("path").is_none());
assert_eq!(unix.get("optional").and_then(|v| v.as_bool()), Some(true));
}
#[test]
fn rewrite_path_deps_is_idempotent() {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("Cargo.toml");
write_binding_manifest(&manifest);
let members = members_with(&["my-lib", "my-lib-core", "my-lib-testkit", "my-lib-unix"]);
rewrite_path_deps_to_registry(&manifest, &members, "1.2.3").unwrap();
let once = fs::read_to_string(&manifest).unwrap();
rewrite_path_deps_to_registry(&manifest, &members, "1.2.3").unwrap();
let twice = fs::read_to_string(&manifest).unwrap();
assert_eq!(once, twice, "second rewrite must be a no-op");
}
#[test]
fn rewrite_path_deps_dual_form_strips_path_and_sets_version() {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("Cargo.toml");
fs::write(
&manifest,
r#"
[package]
name = "b"
version = "0.1.0"
[dependencies]
my-lib = { version = "0.1", path = "../my-lib" }
"#,
)
.unwrap();
let members = members_with(&["my-lib"]);
rewrite_path_deps_to_registry(&manifest, &members, "9.9.9").unwrap();
let doc: DocumentMut = fs::read_to_string(&manifest).unwrap().parse().unwrap();
let my_lib = doc["dependencies"]["my-lib"].as_inline_table().unwrap();
assert_eq!(my_lib.get("version").and_then(|v| v.as_str()), Some("9.9.9"));
assert!(my_lib.get("path").is_none());
}
#[test]
fn rewrite_path_deps_leaves_non_members_and_plain_strings() {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("Cargo.toml");
fs::write(
&manifest,
r#"
[package]
name = "b"
version = "0.1.0"
[dependencies]
# A path dep that is NOT a workspace member — left untouched.
external = { path = "../external" }
# A plain version string member — left untouched (no path key).
my-lib = "1"
"#,
)
.unwrap();
let members = members_with(&["my-lib"]);
rewrite_path_deps_to_registry(&manifest, &members, "2.0.0").unwrap();
let doc: DocumentMut = fs::read_to_string(&manifest).unwrap().parse().unwrap();
let deps = doc["dependencies"].as_table().unwrap();
assert_eq!(
deps["external"]
.as_inline_table()
.unwrap()
.get("path")
.and_then(|v| v.as_str()),
Some("../external")
);
assert_eq!(deps["my-lib"].as_str(), Some("1"));
}
#[test]
fn scrub_lock_deletes_existing_when_not_regenerating() {
let tmp = TempDir::new().unwrap();
let lock = tmp.path().join("Cargo.lock");
fs::write(&lock, "# lock").unwrap();
scrub_or_regenerate_lock(tmp.path(), false, false).unwrap();
assert!(!lock.exists(), "Cargo.lock must be deleted on the offline path");
}
#[test]
fn scrub_lock_no_lock_is_noop() {
let tmp = TempDir::new().unwrap();
scrub_or_regenerate_lock(tmp.path(), false, false).unwrap();
}
fn write_clean_room_crate(dir: &Path, deps_body: &str) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(dir.join("src/lib.rs"), "pub fn smoke() {}\n").unwrap();
fs::write(
dir.join("Cargo.toml"),
format!(
"[package]\nname = \"clean-room\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[dependencies]\n{deps_body}\n"
),
)
.unwrap();
}
#[test]
fn clean_room_registry_dep_resolves_without_workspace() {
if std::env::var("ALEF_SMOKE_REGISTRY").is_err() {
eprintln!("skipping clean_room_registry_dep_resolves_without_workspace (set ALEF_SMOKE_REGISTRY=1)");
return;
}
let tmp = TempDir::new().unwrap();
let crate_dir = tmp.path().join("clean-room");
write_clean_room_crate(&crate_dir, "anyhow = \"1\"");
let status = std::process::Command::new("cargo")
.arg("generate-lockfile")
.arg("--manifest-path")
.arg(crate_dir.join("Cargo.toml"))
.status()
.expect("running cargo generate-lockfile");
assert!(
status.success(),
"registry version-dep must resolve without a workspace"
);
assert!(crate_dir.join("Cargo.lock").exists(), "lockfile should be produced");
}
#[test]
fn clean_room_bad_path_dep_fails_offline() {
let tmp = TempDir::new().unwrap();
let crate_dir = tmp.path().join("clean-room");
write_clean_room_crate(&crate_dir, "ghost = { path = \"../does-not-exist\" }");
let output = std::process::Command::new("cargo")
.arg("metadata")
.arg("--format-version")
.arg("1")
.arg("--manifest-path")
.arg(crate_dir.join("Cargo.toml"))
.env("CARGO_NET_OFFLINE", "true")
.output()
.expect("running cargo metadata");
assert!(
!output.status.success(),
"a path dep to a nonexistent crate must fail resolution"
);
}
fn write_unresolvable_crate(dir: &Path) {
write_clean_room_crate(dir, "ghost = { path = \"../does-not-exist\" }");
}
fn scrub_regenerate(crate_dir: &Path, strict: bool) -> Result<()> {
scrub_or_regenerate_lock(crate_dir, true, strict)
}
#[test]
fn scrub_lock_strict_errors_when_lockfile_cannot_resolve() {
let tmp = TempDir::new().unwrap();
let crate_dir = tmp.path().join("clean-room");
write_unresolvable_crate(&crate_dir);
let err = scrub_regenerate(&crate_dir, true)
.expect_err("strict mode must return Err when the lockfile cannot resolve");
let msg = err.to_string();
assert!(
msg.contains("not yet published to the registry"),
"error must be actionable about unpublished member versions; got: {msg}"
);
assert!(
msg.contains(&crate_dir.join("Cargo.toml").display().to_string()),
"error must name the manifest; got: {msg}"
);
}
#[test]
fn scrub_lock_lenient_falls_back_to_delete_when_lockfile_cannot_resolve() {
let tmp = TempDir::new().unwrap();
let crate_dir = tmp.path().join("clean-room");
write_unresolvable_crate(&crate_dir);
let lock = crate_dir.join("Cargo.lock");
fs::write(&lock, "# stale lock").unwrap();
scrub_regenerate(&crate_dir, false).expect("lenient mode must return Ok and fall back to deleting the lock");
assert!(!lock.exists(), "lenient fallback must delete the unresolved Cargo.lock");
}
}