use std::{
io::Write,
path::{Path, PathBuf},
};
use anyhow::{Context, Result, anyhow};
use repo::{Repository, ThreadManager, ThreadMode};
use sha2::{Digest, Sha256};
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 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");
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-target`. Redirects cargo's\n\
# `target/` directory to a workspace-wide shared path so\n\
# 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.\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()));
}
Ok(true)
}
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; consider `heddle start --shared-target {name}` to share cargo's \
target/ across threads (saves multiple GB).",
);
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[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(),)));
}
#[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",
);
}
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());
}
}