use std::path::PathBuf;
use crate::error::{Error, Result};
use crate::release;
pub struct SelfManager {
pub repo: String,
pub binary_name: String,
pub tag_prefix: String,
pub current_version: String,
pub target: String,
}
#[derive(Debug)]
pub struct CheckResult {
pub installed: String,
pub latest: String,
pub update_available: bool,
}
#[derive(Debug)]
pub struct UpdateResult {
pub updated: bool,
pub old_version: String,
pub new_version: String,
pub exe: PathBuf,
}
#[derive(Debug)]
pub struct SelfInfo {
pub version: String,
pub target: String,
pub exe: PathBuf,
}
impl SelfManager {
pub fn new(
repo: &str,
binary_name: &str,
tag_prefix: &str,
current_version: &str,
target: &str,
) -> Self {
Self {
repo: repo.to_string(),
binary_name: binary_name.to_string(),
tag_prefix: tag_prefix.to_string(),
current_version: current_version.to_string(),
target: target.to_string(),
}
}
pub fn info(&self) -> SelfInfo {
SelfInfo {
version: self.current_version.clone(),
target: self.target.clone(),
exe: std::env::current_exe().unwrap_or_else(|_| PathBuf::from("unknown")),
}
}
pub fn print_info(&self) {
let info = self.info();
println!("{} {}", self.binary_name, info.version);
println!("target: {}", info.target);
println!("exe: {}", info.exe.display());
}
pub fn check(&self) -> Result<CheckResult> {
let rel = release::fetch_latest_release_by_tag_prefix(&self.repo, &self.tag_prefix)?;
let latest = release::parse_version_from_tag(&rel.tag_name).to_string();
let update_available = is_newer_version(&self.current_version, &latest);
Ok(CheckResult {
installed: self.current_version.clone(),
latest,
update_available,
})
}
pub fn print_check(&self) -> Result<()> {
let result = self.check()?;
println!("installed: {}", result.installed);
println!("latest: {}", result.latest);
if result.update_available {
println!(
"\nUpdate available: {} -> {}",
result.installed, result.latest
);
println!("Run `{} self update` to install it.", self.binary_name);
} else {
println!("\nAlready up to date.");
}
Ok(())
}
pub fn update(&self, version: Option<&str>) -> Result<UpdateResult> {
let rel = match version {
Some(v) => {
let ver = v.strip_prefix('v').unwrap_or(v);
let tag = format!("{}{}", self.tag_prefix, ver);
release::fetch_github_release(&self.repo, Some(&tag))?
}
None => release::fetch_latest_release_by_tag_prefix(&self.repo, &self.tag_prefix)?,
};
let latest = release::parse_version_from_tag(&rel.tag_name).to_string();
if version.is_none() && !is_newer_version(&self.current_version, &latest) {
return Ok(UpdateResult {
updated: false,
old_version: self.current_version.clone(),
new_version: latest,
exe: std::env::current_exe().unwrap_or_else(|_| PathBuf::from("unknown")),
});
}
let platform = release::Platform::detect();
let asset_names: Vec<String> = rel.assets.iter().map(|a| a.name.clone()).collect();
let asset_name = platform
.match_asset(&self.binary_name, &asset_names)
.ok_or_else(|| {
Error::Other(format!(
"No prebuilt binary found for {}. Available assets: {}",
platform.target_triple(),
asset_names.join(", ")
))
})?;
let asset = rel
.assets
.iter()
.find(|a| a.name == asset_name)
.expect("matched asset must exist in release");
let tmp_dir = tempfile::tempdir()?;
let archive_path = tmp_dir.path().join(&asset_name);
release::download_file(&asset.browser_download_url, &archive_path)?;
let extract_dir = tmp_dir.path().join("extracted");
release::extract_tar_gz(&archive_path, &extract_dir)?;
let new_binary = release::find_binary_in_dir(&extract_dir, &self.binary_name)?;
let installed_path = replace_exe(&new_binary)?;
Ok(UpdateResult {
updated: true,
old_version: self.current_version.clone(),
new_version: latest,
exe: installed_path,
})
}
pub fn run_update(&self, version: Option<&str>) -> Result<()> {
let result = self.update(version)?;
if !result.updated {
println!("Already up to date ({}).", result.old_version);
return Ok(());
}
println!(
"Updated {} {} -> {}",
self.binary_name, result.old_version, result.new_version
);
println!("exe: {}", result.exe.display());
Ok(())
}
}
pub fn replace_exe(new_binary: &std::path::Path) -> Result<PathBuf> {
let current_exe = std::env::current_exe()?.canonicalize()?;
let backup = current_exe.with_extension("old");
if let Err(e) = std::fs::rename(¤t_exe, &backup) {
if e.kind() == std::io::ErrorKind::PermissionDenied {
return Err(Error::Other(format!(
"Permission denied. Try: sudo {} self update",
current_exe.display()
)));
}
return Err(Error::Other(format!(
"Failed to back up current executable: {}",
e
)));
}
if let Err(e) = std::fs::copy(new_binary, ¤t_exe) {
let _ = std::fs::rename(&backup, ¤t_exe);
return Err(Error::Other(format!("Failed to install new binary: {}", e)));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(¤t_exe, std::fs::Permissions::from_mode(0o755))?;
}
let _ = std::fs::remove_file(&backup);
Ok(current_exe)
}
fn parse_version_tuple(v: &str) -> Option<(u64, u64, u64)> {
let mut parts = v.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next()?.parse().ok()?;
Some((major, minor, patch))
}
pub fn is_newer_version(current: &str, latest: &str) -> bool {
match (parse_version_tuple(current), parse_version_tuple(latest)) {
(Some(c), Some(l)) => l > c,
_ => current != latest,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_newer_version() {
assert!(is_newer_version("0.1.0", "0.1.1"));
assert!(is_newer_version("0.1.0", "0.2.0"));
assert!(is_newer_version("0.1.0", "1.0.0"));
assert!(!is_newer_version("0.1.0", "0.1.0"));
assert!(!is_newer_version("0.2.0", "0.1.0"));
}
#[test]
fn test_self_manager_info() {
let manager =
SelfManager::new("owner/repo", "mytool", "v", "1.0.0", "aarch64-apple-darwin");
let info = manager.info();
assert_eq!(info.version, "1.0.0");
assert_eq!(info.target, "aarch64-apple-darwin");
}
}