use std::{
io::Write,
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow};
use repo::{Repository, ThreadManager, ThreadMode};
use sha2::{Digest, Sha256};
#[cfg(test)]
thread_local! {
static TEST_FAIL_BEFORE_EXCLUDE_WRITE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[derive(Clone, Debug)]
pub(crate) struct SharedTargetFilesSnapshot {
config: Option<Vec<u8>>,
exclude: Option<Vec<u8>>,
}
impl SharedTargetFilesSnapshot {
pub(crate) fn capture(checkout: &Path) -> Result<Self> {
Ok(Self {
config: read_optional_file(&checkout.join(".cargo/config.toml"))?,
exclude: read_optional_file(&super::hydrate::hydrate_exclude_path(checkout))?,
})
}
pub(crate) fn restore(&self, checkout: &Path) -> Result<()> {
restore_optional_file(&checkout.join(".cargo/config.toml"), self.config.as_deref())?;
restore_optional_file(
&super::hydrate::hydrate_exclude_path(checkout),
self.exclude.as_deref(),
)
}
}
const FINGERPRINT_HEX_WIDTH: usize = 16;
pub(crate) const ADVISORY_ACTIVE_THREAD_THRESHOLD: usize = 1;
pub(crate) fn workspace_root_is_rust(repo: &Repository) -> bool {
repo.root().join("Cargo.toml").is_file()
}
pub(crate) fn shared_target_requested(
shared_target: bool,
no_shared_target: bool,
is_rust: bool,
) -> bool {
if no_shared_target {
false
} else if shared_target {
true
} else {
is_rust
}
}
pub(crate) fn workspace_fingerprint(repo: &Repository) -> Result<String> {
let lock = repo.root().join("Cargo.lock");
let toml = repo.root().join("Cargo.toml");
let bytes = if lock.is_file() {
std::fs::read(&lock).with_context(|| format!("read {}", lock.display()))?
} else if toml.is_file() {
std::fs::read(&toml).with_context(|| format!("read {}", toml.display()))?
} else {
return Err(anyhow!(
"no Cargo.toml at workspace root '{}'; --shared-target only applies to Rust workspaces",
repo.root().display()
));
};
let mut hasher = Sha256::new();
hasher.update(&bytes);
let digest = hasher.finalize();
let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
Ok(hex[..FINGERPRINT_HEX_WIDTH].to_string())
}
pub(crate) fn shared_target_dir(repo: &Repository) -> Result<PathBuf> {
let fingerprint = workspace_fingerprint(repo)?;
let dir = repo.heddle_dir().join("targets").join(fingerprint);
std::fs::create_dir_all(&dir)
.with_context(|| format!("create shared target dir '{}'", dir.display()))?;
Ok(dir)
}
pub(crate) fn write_cargo_config(checkout: &Path, target_dir: &Path) -> Result<bool> {
let cargo_dir = checkout.join(".cargo");
let config_path = cargo_dir.join("config.toml");
if cargo_dir.join("config").exists() || config_path.exists() {
return Ok(false);
}
let prior = SharedTargetFilesSnapshot::capture(checkout)?;
std::fs::create_dir_all(&cargo_dir)
.with_context(|| format!("create '{}'", cargo_dir.display()))?;
let escaped = target_dir
.display()
.to_string()
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\t', "\\t");
let body = format!(
"# Written by `heddle start` (shared cargo target). Redirects\n\
# cargo's `target/` directory to a workspace-wide shared path\n\
# so multiple parallel materialized threads don't each carry\n\
# their own multi-gigabyte build tree.\n\
#\n\
# Safe to delete: cargo will fall back to a per-checkout\n\
# `target/` next build. Opt out of writing this file with\n\
# `heddle start --no-shared-target`.\n\
[build]\n\
target-dir = \"{escaped}\"\n",
);
let file = match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&config_path)
{
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
return Ok(false);
}
Err(err) => {
return Err(err).with_context(|| format!("create '{}'", config_path.display()));
}
};
let file = write_body_or_cleanup(file, body.as_bytes(), &config_path)?;
if let Err(err) = file.sync_all() {
drop(file);
let _ = std::fs::remove_file(&config_path);
return Err(err).with_context(|| format!("sync '{}'", config_path.display()));
}
drop(file);
if cargo_dir.join("config").exists() {
std::fs::remove_file(&config_path)
.with_context(|| format!("remove generated '{}'", config_path.display()))?;
objects::fs_atomic::sync_directory(&cargo_dir)?;
return Ok(false);
}
if let Err(error) = preserve_shared_cargo_ignores(checkout) {
if let Err(rollback) = prior.restore(checkout) {
return Err(error).context(format!(
"install shared-target files; rollback also failed: {rollback:#}"
));
}
return Err(error);
}
Ok(true)
}
fn preserve_shared_cargo_ignores(checkout: &Path) -> Result<()> {
let exclude_path = super::hydrate::hydrate_exclude_path(checkout);
let existing = match std::fs::read_to_string(&exclude_path) {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(err) => {
return Err(err).with_context(|| format!("read '{}'", exclude_path.display()));
}
};
let rule = "/.cargo/config.toml";
let already = existing.lines().any(|l| {
let t = l.trim();
t == rule
});
if already {
return Ok(());
}
if let Some(parent) = exclude_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create '{}'", parent.display()))?;
}
let mut out = existing;
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
if out.is_empty() {
out.push_str(
"# Written by `heddle start` (shared cargo target).\n\
# Keeps the redirect config out of capture/restack dirt.\n",
);
}
out.push_str(rule);
out.push('\n');
maybe_fail_before_exclude_write().map_err(anyhow::Error::from)?;
objects::fs_atomic::write_file_atomic(&exclude_path, out.as_bytes())
.with_context(|| format!("atomically write '{}'", exclude_path.display()))?;
Ok(())
}
fn maybe_fail_before_exclude_write() -> std::io::Result<()> {
#[cfg(test)]
if TEST_FAIL_BEFORE_EXCLUDE_WRITE.get() {
return Err(std::io::Error::other(
"test failure before shared-target exclude write",
));
}
objects::fault_inject::maybe_fail_at("shared_target_before_exclude_write")
}
fn read_optional_file(path: &Path) -> Result<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(bytes) => Ok(Some(bytes)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error).with_context(|| format!("read '{}'", path.display())),
}
}
fn restore_optional_file(path: &Path, bytes: Option<&[u8]>) -> Result<()> {
match bytes {
Some(bytes) => {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create '{}'", parent.display()))?;
}
objects::fs_atomic::write_file_atomic(path, bytes)
.with_context(|| format!("restore '{}'", path.display()))
}
None => match std::fs::remove_file(path) {
Ok(()) => {
if let Some(parent) = path.parent() {
objects::fs_atomic::sync_directory(parent)?;
}
Ok(())
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error).with_context(|| format!("remove '{}'", path.display())),
},
}
}
fn write_body_or_cleanup<W: Write>(mut writer: W, body: &[u8], cleanup_path: &Path) -> Result<W> {
match writer.write_all(body) {
Ok(()) => Ok(writer),
Err(err) => {
drop(writer);
let _ = std::fs::remove_file(cleanup_path);
Err(err).with_context(|| format!("write '{}'", cleanup_path.display()))
}
}
}
fn count_active_materialized_threads(repo: &Repository) -> usize {
let manager = ThreadManager::new(repo.heddle_dir());
let Ok(threads) = manager.list() else {
return 0;
};
threads
.into_iter()
.filter(|thread| {
matches!(thread.mode, ThreadMode::Solid | ThreadMode::Materialized)
&& thread.state == repo::ThreadState::Active
})
.count()
}
pub(crate) fn should_advise_shared_target(repo: &Repository) -> bool {
workspace_root_is_rust(repo)
&& count_active_materialized_threads(repo) >= ADVISORY_ACTIVE_THREAD_THRESHOLD
}
pub(crate) fn print_advisory(name: &str) {
eprintln!(
"note: starting materialized thread '{name}' alongside an existing materialized thread \
in a Rust workspace without a shared cargo target; omit `--no-shared-target` (or pass \
`--shared-target`) so threads share cargo's target/ (saves multiple GB).",
);
}
pub(crate) fn print_blocked_warning(checkout: &Path) {
let cargo_dir = checkout.join(".cargo");
let config = if cargo_dir.join("config").exists() {
cargo_dir.join("config")
} else {
cargo_dir.join("config.toml")
};
eprintln!(
"warning: shared cargo target redirect not applied: '{}' already exists; \
leaving the existing config in place (Cargo target behavior follows that file). \
Remove or rename that file to allow the redirect, or pass `--no-shared-target` \
to opt out explicitly.",
config.display(),
);
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn shared_target_requested_precedence() {
assert!(shared_target_requested(false, false, true));
assert!(!shared_target_requested(false, false, false));
assert!(shared_target_requested(true, false, false));
assert!(shared_target_requested(true, false, true));
assert!(!shared_target_requested(false, true, true));
assert!(!shared_target_requested(true, true, true));
}
#[test]
fn fingerprint_is_stable_across_calls() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("Cargo.toml"), b"[package]\nname=\"x\"\n").unwrap();
let bytes = std::fs::read(temp.path().join("Cargo.toml")).unwrap();
let mut a = Sha256::new();
a.update(&bytes);
let mut b = Sha256::new();
b.update(&bytes);
assert_eq!(a.finalize(), b.finalize());
}
#[test]
fn write_cargo_config_creates_file_with_target_dir() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("targets").join("abc123");
std::fs::create_dir_all(&target).unwrap();
let wrote = write_cargo_config(temp.path(), &target).unwrap();
assert!(wrote, "writer must report a write when no prior config");
let written =
std::fs::read_to_string(temp.path().join(".cargo").join("config.toml")).unwrap();
assert!(written.contains("[build]"));
assert!(written.contains(&format!("target-dir = \"{}\"", target.display(),)));
let exclude = std::fs::read_to_string(
crate::cli::commands::worktree_cmd::hydrate::hydrate_exclude_path(temp.path()),
)
.expect("local exclude written for shared-target config");
assert!(
exclude.lines().any(|l| l.trim() == "/.cargo/config.toml"),
"expected narrow config ignore rule, got:\n{exclude}"
);
assert!(!exclude.lines().any(|l| l.trim() == ".cargo/"));
}
#[test]
fn write_cargo_config_preserves_existing_user_config() {
let temp = TempDir::new().unwrap();
let cargo_dir = temp.path().join(".cargo");
std::fs::create_dir_all(&cargo_dir).unwrap();
let user = "[net]\noffline = true\n";
std::fs::write(cargo_dir.join("config.toml"), user).unwrap();
let target = temp.path().join("shared");
std::fs::create_dir_all(&target).unwrap();
let wrote = write_cargo_config(temp.path(), &target).unwrap();
assert!(
!wrote,
"writer must report no-op when user config is preserved"
);
let after = std::fs::read_to_string(cargo_dir.join("config.toml")).unwrap();
assert_eq!(
after, user,
"shared-target writer must not overwrite user-managed config",
);
}
#[test]
fn write_cargo_config_restores_config_and_exclude_after_second_write_failure() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("shared");
std::fs::create_dir_all(&target).unwrap();
let exclude = super::super::hydrate::hydrate_exclude_path(temp.path());
std::fs::create_dir_all(exclude.parent().unwrap()).unwrap();
let original_exclude = b"user-owned\n";
std::fs::write(&exclude, original_exclude).unwrap();
TEST_FAIL_BEFORE_EXCLUDE_WRITE.set(true);
let result = write_cargo_config(temp.path(), &target);
TEST_FAIL_BEFORE_EXCLUDE_WRITE.set(false);
assert!(result.is_err());
assert!(!temp.path().join(".cargo/config.toml").exists());
assert_eq!(std::fs::read(&exclude).unwrap(), original_exclude);
}
#[test]
fn write_cargo_config_preserves_legacy_cargo_config() {
let temp = TempDir::new().unwrap();
let cargo_dir = temp.path().join(".cargo");
std::fs::create_dir_all(&cargo_dir).unwrap();
let user = "[net]\noffline = true\n";
std::fs::write(cargo_dir.join("config"), user).unwrap();
let target = temp.path().join("shared");
std::fs::create_dir_all(&target).unwrap();
assert!(!write_cargo_config(temp.path(), &target).unwrap());
assert_eq!(
std::fs::read_to_string(cargo_dir.join("config")).unwrap(),
user
);
assert!(!cargo_dir.join("config.toml").exists());
}
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("simulated write failure"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn write_body_or_cleanup_removes_orphan_on_write_failure() {
let temp = TempDir::new().unwrap();
let orphan = temp.path().join(".cargo").join("config.toml");
std::fs::create_dir_all(orphan.parent().unwrap()).unwrap();
std::fs::write(&orphan, b"").unwrap();
assert!(orphan.exists(), "test precondition: orphan staged");
let writer = FailingWriter;
let result = write_body_or_cleanup(writer, b"would-be body", &orphan);
assert!(
result.is_err(),
"writer failure must surface to caller, not be swallowed"
);
assert!(
!orphan.exists(),
"orphan file must be removed so a retry can re-create it cleanly"
);
}
struct DropTrackingFailingWriter<'a> {
dropped: &'a std::cell::Cell<bool>,
}
impl Write for DropTrackingFailingWriter<'_> {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("simulated write failure"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Drop for DropTrackingFailingWriter<'_> {
fn drop(&mut self) {
self.dropped.set(true);
}
}
#[test]
fn write_body_or_cleanup_drops_writer_before_returning_on_failure() {
let temp = TempDir::new().unwrap();
let orphan = temp.path().join(".cargo").join("config.toml");
std::fs::create_dir_all(orphan.parent().unwrap()).unwrap();
std::fs::write(&orphan, b"").unwrap();
let dropped = std::cell::Cell::new(false);
let writer = DropTrackingFailingWriter { dropped: &dropped };
let result = write_body_or_cleanup(writer, b"would-be body", &orphan);
assert!(result.is_err());
assert!(
dropped.get(),
"writer must be dropped before the helper returns on failure — \
on Windows, the file handle must be closed before remove_file"
);
assert!(!orphan.exists());
}
#[test]
fn write_cargo_config_escapes_quotes_in_path() {
let temp = TempDir::new().unwrap();
let weird = temp.path().join("dir with \"quotes\"");
std::fs::create_dir_all(&weird).unwrap();
let wrote = write_cargo_config(temp.path(), &weird).unwrap();
assert!(wrote);
let written =
std::fs::read_to_string(temp.path().join(".cargo").join("config.toml")).unwrap();
let parsed: toml::Value = toml::from_str(&written).unwrap();
let target_dir = parsed
.get("build")
.and_then(|t| t.get("target-dir"))
.and_then(|v| v.as_str())
.expect("[build].target-dir present");
assert_eq!(target_dir, weird.display().to_string());
}
}