use std::io::Write;
use std::path::{Path, PathBuf};
use crate::ComposeError;
pub fn platform_asset() -> Option<&'static str> {
asset_for(std::env::consts::OS, std::env::consts::ARCH)
}
fn asset_for(os: &str, arch: &str) -> Option<&'static str> {
match (os, arch) {
("linux", "x86_64") => Some("podup-linux-x86_64"),
("linux", "aarch64") => Some("podup-linux-arm64"),
("macos", "aarch64") => Some("podup-darwin-arm64"),
("macos", "x86_64") => Some("podup-darwin-x86_64"),
("windows", "x86_64") => Some("podup-windows-x86_64.exe"),
("windows", "aarch64") => Some("podup-windows-arm64.exe"),
_ => None,
}
}
pub fn require_platform_asset() -> crate::Result<&'static str> {
platform_asset().ok_or_else(|| {
ComposeError::Update(format!(
"self-update is not supported on {}/{}; reinstall manually from \
https://github.com/Glyndor/podup/releases",
std::env::consts::OS,
std::env::consts::ARCH
))
})
}
#[cfg(target_os = "linux")]
pub fn managing_package_manager() -> Option<&'static str> {
let exe = std::env::current_exe().ok()?;
let path = std::fs::canonicalize(&exe).unwrap_or(exe);
let output = std::process::Command::new("dpkg-query")
.arg("-S")
.arg(&path)
.output()
.ok()?;
output.status.success().then_some("apt")
}
#[cfg(not(target_os = "linux"))]
pub fn managing_package_manager() -> Option<&'static str> {
None
}
pub fn package_managed_error(pm: &str) -> ComposeError {
ComposeError::Update(format!(
"this podup was installed by {pm}; update it with your package manager \
(e.g. `apt upgrade podup`) rather than `podup update`, which would break \
the package's record of the file"
))
}
pub fn install_binary(new_bytes: &[u8]) -> crate::Result<PathBuf> {
let exe = std::env::current_exe()
.map_err(|e| ComposeError::Update(format!("cannot locate current executable: {e}")))?;
let target = std::fs::canonicalize(&exe).unwrap_or(exe);
install_at(&target, new_bytes)?;
Ok(target)
}
pub fn install_at(target: &Path, new_bytes: &[u8]) -> crate::Result<()> {
let dir = target.parent().ok_or_else(|| {
ComposeError::Update(format!(
"target {} has no parent directory",
target.display()
))
})?;
let file_name = target
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "podup".to_string());
let tmp = dir.join(format!(".{file_name}.update-{}", std::process::id()));
write_temp(&tmp, new_bytes, target).inspect_err(|_| {
let _ = std::fs::remove_file(&tmp);
})?;
if let Err(e) = swap_into_place(&tmp, target) {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
Ok(())
}
fn write_temp(tmp: &Path, new_bytes: &[u8], target: &Path) -> crate::Result<()> {
#[cfg(unix)]
let mut f = {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(tmp)
.map_err(|e| {
ComposeError::Update(format!("cannot write update to {}: {e}", tmp.display()))
})?
};
#[cfg(not(unix))]
let mut f = std::fs::File::create(tmp).map_err(|e| {
ComposeError::Update(format!("cannot write update to {}: {e}", tmp.display()))
})?;
f.write_all(new_bytes).map_err(ComposeError::Io)?;
f.flush().map_err(ComposeError::Io)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(target)
.map(|m| m.permissions().mode())
.unwrap_or(0o755);
std::fs::set_permissions(tmp, std::fs::Permissions::from_mode(mode))
.map_err(ComposeError::Io)?;
}
#[cfg(not(unix))]
{
let _ = target; }
f.sync_all().map_err(ComposeError::Io)?;
Ok(())
}
#[cfg(not(windows))]
fn swap_into_place(tmp: &Path, target: &Path) -> crate::Result<()> {
std::fs::rename(tmp, target).map_err(|e| rename_error(e, target))
}
#[cfg(windows)]
fn swap_into_place(tmp: &Path, target: &Path) -> crate::Result<()> {
let backup = target.with_extension("old");
let _ = std::fs::remove_file(&backup);
if target.exists() {
std::fs::rename(target, &backup).map_err(|e| rename_error(e, target))?;
}
if let Err(e) = std::fs::rename(tmp, target) {
let _ = std::fs::rename(&backup, target);
return Err(rename_error(e, target));
}
let _ = std::fs::remove_file(&backup);
Ok(())
}
fn rename_error(e: std::io::Error, target: &Path) -> ComposeError {
if e.kind() == std::io::ErrorKind::PermissionDenied {
ComposeError::Update(format!(
"permission denied writing {}; re-run with elevated privileges \
(e.g. sudo) or set a writable install location",
target.display()
))
} else {
ComposeError::Update(format!(
"failed to install update to {}: {e}",
target.display()
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_asset_matches_known_targets() {
if let Some(asset) = platform_asset() {
assert!(asset.starts_with("podup-"));
}
}
#[test]
fn platform_asset_covers_every_release_target() {
let expected = [
(("linux", "x86_64"), "podup-linux-x86_64"),
(("linux", "aarch64"), "podup-linux-arm64"),
(("macos", "aarch64"), "podup-darwin-arm64"),
(("macos", "x86_64"), "podup-darwin-x86_64"),
(("windows", "x86_64"), "podup-windows-x86_64.exe"),
(("windows", "aarch64"), "podup-windows-arm64.exe"),
];
for ((os, arch), asset) in expected {
assert_eq!(
asset_for(os, arch),
Some(asset),
"self-update mapping drifted for {os}/{arch}"
);
}
}
#[test]
fn install_at_replaces_contents() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("podup");
std::fs::write(&target, b"old version").unwrap();
install_at(&target, b"new version").unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"new version");
}
#[test]
fn install_at_creates_when_absent() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("podup");
install_at(&target, b"fresh").unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"fresh");
}
#[cfg(unix)]
#[test]
fn install_at_preserves_executable_mode() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("podup");
std::fs::write(&target, b"old").unwrap();
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o755)).unwrap();
install_at(&target, b"new").unwrap();
let mode = std::fs::metadata(&target).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o755);
}
#[test]
fn install_at_leaves_no_temp_files() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("podup");
install_at(&target, b"data").unwrap();
let leftovers: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains("update-"))
.collect();
assert!(leftovers.is_empty(), "temp file left behind");
}
#[test]
fn install_at_fails_when_target_dir_is_missing() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("no-such-subdir");
let target = missing.join("podup");
assert!(install_at(&target, b"data").is_err());
assert!(!missing.exists(), "must not create the missing parent dir");
}
#[test]
fn require_platform_asset_is_consistent() {
match (platform_asset(), require_platform_asset()) {
(Some(a), Ok(b)) => assert_eq!(a, b),
(None, Err(_)) => {}
_ => panic!("platform_asset and require_platform_asset disagree"),
}
}
#[test]
fn package_managed_error_names_the_manager() {
let e = package_managed_error("apt");
match e {
ComposeError::Update(msg) => {
assert!(msg.contains("apt"));
assert!(msg.contains("podup update"));
}
_ => panic!("expected an Update error"),
}
}
#[test]
fn test_binary_is_not_package_managed() {
assert_eq!(managing_package_manager(), None);
}
}