use anyhow::{Context, Result, bail};
use std::path::{Path, PathBuf};
pub const BUNDLED_BINARIES: &[&str] = &["spool", "spool-mcp", "spool-daemon"];
#[derive(Debug, Clone)]
pub struct ReleaseReport {
pub copied: Vec<PathBuf>,
pub skipped: Vec<PathBuf>,
}
pub fn release_binaries(
source_dir: &Path,
target_bin_dir: &Path,
force: bool,
) -> Result<ReleaseReport> {
if !source_dir.is_dir() {
bail!("source directory does not exist: {}", source_dir.display());
}
std::fs::create_dir_all(target_bin_dir)
.with_context(|| format!("creating {}", target_bin_dir.display()))?;
let mut copied = Vec::new();
let mut skipped = Vec::new();
for name in BUNDLED_BINARIES {
let exe_name = if cfg!(windows) {
format!("{name}.exe")
} else {
(*name).to_string()
};
let source = source_dir.join(&exe_name);
let target = target_bin_dir.join(&exe_name);
if !source.exists() {
continue;
}
if !force && target.exists() && files_appear_identical(&source, &target)? {
skipped.push(target);
continue;
}
copy_binary(&source, &target)?;
copied.push(target);
}
Ok(ReleaseReport { copied, skipped })
}
fn copy_binary(source: &Path, target: &Path) -> Result<()> {
std::fs::copy(source, target)
.with_context(|| format!("copying {} → {}", source.display(), target.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(target)
.with_context(|| format!("reading permissions for {}", target.display()))?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(target, perms)
.with_context(|| format!("setting permissions on {}", target.display()))?;
}
Ok(())
}
fn files_appear_identical(a: &Path, b: &Path) -> Result<bool> {
let meta_a =
std::fs::metadata(a).with_context(|| format!("reading metadata for {}", a.display()))?;
let meta_b =
std::fs::metadata(b).with_context(|| format!("reading metadata for {}", b.display()))?;
Ok(meta_a.len() == meta_b.len())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn write_fake_binary(dir: &Path, name: &str, content: &str) -> PathBuf {
let exe_name = if cfg!(windows) {
format!("{name}.exe")
} else {
name.to_string()
};
let path = dir.join(&exe_name);
fs::write(&path, content).unwrap();
path
}
#[test]
fn release_should_copy_existing_bundled_binaries() {
let temp = tempdir().unwrap();
let source = temp.path().join("source");
let target = temp.path().join("target");
fs::create_dir_all(&source).unwrap();
write_fake_binary(&source, "spool", "fake spool");
write_fake_binary(&source, "spool-mcp", "fake spool-mcp");
let report = release_binaries(&source, &target, false).unwrap();
assert_eq!(report.copied.len(), 2);
assert!(report.skipped.is_empty());
let target_spool = if cfg!(windows) { "spool.exe" } else { "spool" };
assert!(target.join(target_spool).exists());
}
#[test]
fn release_should_skip_when_not_forced_and_target_exists() {
let temp = tempdir().unwrap();
let source = temp.path().join("source");
let target = temp.path().join("target");
fs::create_dir_all(&source).unwrap();
fs::create_dir_all(&target).unwrap();
write_fake_binary(&source, "spool", "v1");
write_fake_binary(&target, "spool", "v1");
let report = release_binaries(&source, &target, false).unwrap();
assert_eq!(report.copied.len(), 0);
assert_eq!(report.skipped.len(), 1);
}
#[test]
fn release_should_overwrite_when_forced() {
let temp = tempdir().unwrap();
let source = temp.path().join("source");
let target = temp.path().join("target");
fs::create_dir_all(&source).unwrap();
fs::create_dir_all(&target).unwrap();
write_fake_binary(&source, "spool", "new version");
write_fake_binary(&target, "spool", "old version");
let report = release_binaries(&source, &target, true).unwrap();
assert_eq!(report.copied.len(), 1);
let target_spool = if cfg!(windows) { "spool.exe" } else { "spool" };
let copied = fs::read_to_string(target.join(target_spool)).unwrap();
assert_eq!(copied, "new version");
}
#[test]
fn release_should_fail_when_source_dir_missing() {
let temp = tempdir().unwrap();
let source = temp.path().join("nonexistent");
let target = temp.path().join("target");
let result = release_binaries(&source, &target, false);
assert!(result.is_err());
}
#[cfg(unix)]
#[test]
fn release_should_set_executable_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp = tempdir().unwrap();
let source = temp.path().join("source");
let target = temp.path().join("target");
fs::create_dir_all(&source).unwrap();
write_fake_binary(&source, "spool", "binary");
release_binaries(&source, &target, false).unwrap();
let mode = fs::metadata(target.join("spool"))
.unwrap()
.permissions()
.mode();
assert_eq!(mode & 0o755, 0o755);
}
}