use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::error::{Error, io_context};
pub struct StagingGuard(pub PathBuf);
impl Drop for StagingGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
pub fn create_staging_overlay(
ws_root: &Path,
staging_root: &Path,
instrumented: &[(PathBuf, String)],
) -> Result<(), Error> {
let mut real_dirs: HashSet<PathBuf> = HashSet::new();
for (rel_path, _) in instrumented {
let mut dir = rel_path.parent();
while let Some(d) = dir {
if d.as_os_str().is_empty() {
break;
}
real_dirs.insert(d.to_path_buf());
dir = d.parent();
}
}
let instrumented_set: HashSet<&Path> = instrumented.iter().map(|(p, _)| p.as_path()).collect();
let _ = std::fs::remove_dir_all(staging_root);
std::fs::create_dir_all(staging_root)
.map_err(io_context("create staging directory", staging_root))?;
populate_overlay(
ws_root,
staging_root,
Path::new(""),
&real_dirs,
instrumented,
&instrumented_set,
)
}
fn populate_overlay(
ws_root: &Path,
staging_root: &Path,
rel_prefix: &Path,
real_dirs: &HashSet<PathBuf>,
instrumented: &[(PathBuf, String)],
instrumented_set: &HashSet<&Path>,
) -> Result<(), Error> {
let source_dir = ws_root.join(rel_prefix);
let staging_dir = staging_root.join(rel_prefix);
std::fs::create_dir_all(&staging_dir)
.map_err(io_context("create staging subdirectory", &staging_dir))?;
let entries =
std::fs::read_dir(&source_dir).map_err(io_context("read source directory", &source_dir))?;
for entry in entries {
let entry = entry.map_err(io_context("read directory entry", &source_dir))?;
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str == "target" || name_str == ".git" {
continue;
}
let rel_path = if rel_prefix.as_os_str().is_empty() {
PathBuf::from(&name)
} else {
rel_prefix.join(&name)
};
let staging_path = staging_dir.join(&name);
let original_abs = std::fs::canonicalize(entry.path())
.map_err(io_context("canonicalize path", &entry.path()))?;
let file_type = entry
.file_type()
.map_err(io_context("read file type", &entry.path()))?;
if file_type.is_dir() {
if real_dirs.contains(&rel_path) {
populate_overlay(
ws_root,
staging_root,
&rel_path,
real_dirs,
instrumented,
instrumented_set,
)?;
} else {
std::os::unix::fs::symlink(&original_abs, &staging_path)
.map_err(io_context("create directory symlink", &staging_path))?;
}
} else if instrumented_set.contains(rel_path.as_path()) {
let content = &instrumented.iter().find(|(p, _)| p == &rel_path).unwrap().1;
std::fs::write(&staging_path, content)
.map_err(io_context("write instrumented file", &staging_path))?;
} else {
std::os::unix::fs::symlink(&original_abs, &staging_path)
.map_err(io_context("create file symlink", &staging_path))?;
}
}
Ok(())
}
pub fn clean_stale_staging(target_piano_dir: &Path) {
let Ok(entries) = std::fs::read_dir(target_piano_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().starts_with("staging-") && entry.path().is_dir() {
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_fake_project(root: &Path) {
std::fs::create_dir_all(root.join("src/args")).unwrap();
std::fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
std::fs::write(root.join("src/args.rs"), "mod command;").unwrap();
std::fs::write(root.join("src/args/command.rs"), "pub fn run() {}").unwrap();
std::fs::write(root.join("src/utils.rs"), "pub fn help() {}").unwrap();
std::fs::write(root.join("data.txt"), "hello").unwrap();
std::fs::create_dir_all(root.join("templates")).unwrap();
std::fs::write(root.join("templates/page.html"), "<h1>hi</h1>").unwrap();
}
#[test]
fn overlay_creates_correct_structure() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
let staging = tmp.path().join("staging");
create_fake_project(&project);
let instrumented = vec![
(PathBuf::from("src/main.rs"), "// instrumented main".into()),
(PathBuf::from("src/args.rs"), "// instrumented args".into()),
(
PathBuf::from("src/args/command.rs"),
"// instrumented command".into(),
),
];
create_staging_overlay(&project, &staging, &instrumented).unwrap();
assert_eq!(
std::fs::read_to_string(staging.join("src/main.rs")).unwrap(),
"// instrumented main"
);
assert_eq!(
std::fs::read_to_string(staging.join("src/args.rs")).unwrap(),
"// instrumented args"
);
assert_eq!(
std::fs::read_to_string(staging.join("src/args/command.rs")).unwrap(),
"// instrumented command"
);
assert!(staging.join("src/utils.rs").is_symlink());
assert_eq!(
std::fs::read_to_string(staging.join("src/utils.rs")).unwrap(),
"pub fn help() {}"
);
assert!(staging.join("data.txt").is_symlink());
assert_eq!(
std::fs::read_to_string(staging.join("data.txt")).unwrap(),
"hello"
);
assert!(staging.join("templates").is_symlink());
assert_eq!(
std::fs::read_to_string(staging.join("templates/page.html")).unwrap(),
"<h1>hi</h1>"
);
assert!(staging.join("src").is_dir());
assert!(!staging.join("src").is_symlink());
assert!(staging.join("src/args").is_dir());
assert!(!staging.join("src/args").is_symlink());
}
#[test]
fn overlay_skips_target_and_git() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
create_fake_project(&project);
std::fs::create_dir_all(project.join("target/debug")).unwrap();
std::fs::write(project.join("target/debug/binary"), "bin").unwrap();
std::fs::create_dir_all(project.join(".git/objects")).unwrap();
std::fs::write(project.join(".git/HEAD"), "ref: refs/heads/main").unwrap();
let staging = tmp.path().join("staging");
let instrumented = vec![(PathBuf::from("src/main.rs"), "// inst".into())];
create_staging_overlay(&project, &staging, &instrumented).unwrap();
assert!(!staging.join("target").exists());
assert!(!staging.join(".git").exists());
}
#[test]
fn staging_guard_cleans_up_on_drop() {
let tmp = tempfile::tempdir().unwrap();
let staging = tmp.path().join("staging");
std::fs::create_dir_all(staging.join("src")).unwrap();
std::fs::write(staging.join("src/main.rs"), "fn main() {}").unwrap();
assert!(staging.exists());
{
let _guard = StagingGuard(staging.clone());
} assert!(!staging.exists());
}
#[test]
fn clean_stale_staging_removes_old_dirs() {
let tmp = tempfile::tempdir().unwrap();
let target_piano = tmp.path().join("target/piano");
std::fs::create_dir_all(target_piano.join("staging-mylib")).unwrap();
std::fs::create_dir_all(target_piano.join("staging-otherlib")).unwrap();
std::fs::write(target_piano.join("config.json"), "{}").unwrap();
clean_stale_staging(&target_piano);
assert!(!target_piano.join("staging-mylib").exists());
assert!(!target_piano.join("staging-otherlib").exists());
assert!(target_piano.join("config.json").exists());
}
}