mod path;
mod verify_runs;
pub use path::{check_path, check_shadowing, on_path, resolve_on_path};
pub use verify_runs::{highest_glibc_need, runs};
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use crate::platform::{self, BIN_NAME};
use crate::release::Version;
use crate::ui::Ui;
pub fn binary_in(dir: &Path) -> PathBuf {
dir.join(BIN_NAME)
}
pub fn installed_version(dir: &Path) -> Option<Version> {
let output = Command::new(binary_in(dir))
.arg("--version")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let text = String::from_utf8_lossy(&output.stdout);
let token = text.split_whitespace().nth(1)?;
Version::parse(token).ok()
}
pub fn place(
staged: &Path,
dir: &Path,
check: impl FnOnce(&Path) -> Result<()>,
) -> Result<PathBuf> {
std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
let target = binary_in(dir);
let pending = dir.join(format!(".{BIN_NAME}.new"));
std::fs::copy(staged, &pending).with_context(|| format!("writing {}", pending.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&pending, std::fs::Permissions::from_mode(0o755))
.with_context(|| format!("making {} executable", pending.display()))?;
}
if let Err(err) = check(&pending) {
let _ = std::fs::remove_file(&pending);
return Err(anyhow!(
"{err}\nNothing was installed; any earlier version at {} is unchanged.",
target.display()
));
}
#[cfg(windows)]
let backup = dir.join(format!(".{BIN_NAME}.old"));
#[cfg(windows)]
if target.exists() {
let _ = std::fs::remove_file(&backup);
std::fs::rename(&target, &backup)
.context("could not replace the running binary — close Accent CMS and retry")?;
}
std::fs::rename(&pending, &target).with_context(|| {
let _ = std::fs::remove_file(&pending);
format!("installing {}", target.display())
})?;
#[cfg(windows)]
let _ = std::fs::remove_file(&backup);
Ok(target)
}
pub fn remove(dir: &Path) -> Result<bool> {
let binary = binary_in(dir);
if !binary.exists() {
return Ok(false);
}
std::fs::remove_file(&binary).with_context(|| format!("removing {}", binary.display()))?;
Ok(true)
}
pub fn purge_data(ui: &Ui) -> Result<()> {
for dir in platform::data_dirs() {
if dir.exists() {
std::fs::remove_dir_all(&dir).with_context(|| format!("removing {}", dir.display()))?;
ui.say(format!("Removed {}", dir.display()));
}
}
Ok(())
}
#[cfg(all(test, unix))]
pub(crate) fn stub(dir: &Path, name: &str, body: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let path = dir.join(name);
std::fs::write(&path, body).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn places_a_binary_and_replaces_it_in_place() {
let staging = tempfile::tempdir().unwrap();
let target = tempfile::tempdir().unwrap();
let dir = target.path().join("bin");
let first = staging.path().join("accent");
std::fs::write(&first, b"version one").unwrap();
let installed = place(&first, &dir, |_| Ok(())).unwrap();
assert_eq!(std::fs::read(&installed).unwrap(), b"version one");
std::fs::write(&first, b"version two").unwrap();
place(&first, &dir, |_| Ok(())).unwrap();
assert_eq!(std::fs::read(&installed).unwrap(), b"version two");
let leftovers: Vec<_> = std::fs::read_dir(&dir)
.unwrap()
.map(|e| e.unwrap().file_name())
.filter(|name| name != BIN_NAME)
.collect();
assert!(leftovers.is_empty(), "{leftovers:?}");
}
#[cfg(unix)]
#[test]
fn placed_binaries_are_executable() {
use std::os::unix::fs::PermissionsExt;
let staging = tempfile::tempdir().unwrap();
let target = tempfile::tempdir().unwrap();
let staged = staging.path().join("accent");
std::fs::write(&staged, b"#!/bin/sh\n").unwrap();
std::fs::set_permissions(&staged, std::fs::Permissions::from_mode(0o600)).unwrap();
let installed = place(&staged, target.path(), |_| Ok(())).unwrap();
let mode = std::fs::metadata(&installed).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o755);
}
#[test]
fn a_failed_check_installs_nothing_and_keeps_the_old_binary() {
let staging = tempfile::tempdir().unwrap();
let target = tempfile::tempdir().unwrap();
let installed = binary_in(target.path());
std::fs::write(&installed, b"old and working").unwrap();
let staged = staging.path().join("accent");
std::fs::write(&staged, b"new and broken").unwrap();
let mut checked = None;
let err = place(&staged, target.path(), |pending| {
checked = Some(pending.to_path_buf());
assert_eq!(std::fs::read(pending).unwrap(), b"new and broken");
Err(anyhow!("cannot run: GLIBC_2.39 not found"))
})
.unwrap_err();
assert_eq!(checked.unwrap().parent().unwrap(), target.path());
let leftovers: Vec<_> = std::fs::read_dir(target.path())
.unwrap()
.map(|e| e.unwrap().file_name())
.filter(|name| name != BIN_NAME)
.collect();
assert!(leftovers.is_empty(), "{leftovers:?}");
assert_eq!(std::fs::read(&installed).unwrap(), b"old and working");
let text = err.to_string();
assert!(
text.starts_with("cannot run: GLIBC_2.39 not found"),
"{text}"
);
assert!(
text.contains(&format!(
"Nothing was installed; any earlier version at {} is unchanged.",
installed.display()
)),
"{text}"
);
}
#[test]
fn remove_reports_whether_anything_was_there() {
let dir = tempfile::tempdir().unwrap();
assert!(!remove(dir.path()).unwrap());
std::fs::write(binary_in(dir.path()), b"binary").unwrap();
assert!(remove(dir.path()).unwrap());
assert!(!binary_in(dir.path()).exists());
}
#[cfg(unix)]
#[test]
fn reads_the_version_an_installed_binary_reports() {
let dir = tempfile::tempdir().unwrap();
stub(
dir.path(),
BIN_NAME,
"#!/bin/sh\necho 'accent 0.25.0 (abc1234 2026-08-21)'\n",
);
assert_eq!(
installed_version(dir.path()).unwrap(),
Version::parse("0.25.0").unwrap()
);
}
#[cfg(unix)]
#[test]
fn an_unparseable_version_is_unknown_not_a_match() {
let dir = tempfile::tempdir().unwrap();
stub(dir.path(), BIN_NAME, "#!/bin/sh\necho garbage\n");
assert_eq!(installed_version(dir.path()), None);
let empty = tempfile::tempdir().unwrap();
assert_eq!(installed_version(empty.path()), None);
}
}