use crate::error::{Error, Result};
use super::{info, success};
use self_update::backends::github::ReleaseList;
use self_update::self_replace;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
struct TempFileGuard {
path: PathBuf,
}
impl TempFileGuard {
fn new(path: PathBuf) -> Self {
TempFileGuard { path }
}
}
impl Drop for TempFileGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
pub fn run(force: bool) -> Result<()> {
let current_version = env!("CARGO_PKG_VERSION");
info(&format!("Current version: {}", current_version));
info("Checking for updates...");
let releases = ReleaseList::configure()
.repo_owner("yusukeshib")
.repo_name("nixy")
.build()
.map_err(|e| Error::SelfUpdate(e.to_string()))?
.fetch()
.map_err(|e| Error::SelfUpdate(e.to_string()))?;
let latest = releases
.first()
.ok_or_else(|| Error::SelfUpdate("No releases found".to_string()))?;
let latest_version = latest.version.trim_start_matches('v');
info(&format!("Latest version: {}", latest_version));
if !force && current_version == latest_version {
success("Already at latest version");
return Ok(());
}
let asset_name = get_asset_name()?;
info(&format!("Looking for asset: {}", asset_name));
let asset_exists = latest.assets.iter().any(|a| a.name == asset_name);
if !asset_exists {
return Err(Error::SelfUpdate(format!(
"Asset '{}' not found for this platform. Available assets: {}",
asset_name,
latest
.assets
.iter()
.map(|a| a.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)));
}
let download_url = format!(
"https://github.com/yusukeshib/nixy/releases/download/v{}/{}",
latest_version, asset_name
);
info("Downloading new version...");
let tmp_path = download_binary(&download_url)?;
let _tmp_guard = TempFileGuard::new(tmp_path.clone());
info("Installing update...");
self_replace::self_replace(&tmp_path).map_err(|e| {
let err_str = e.to_string();
if err_str.contains("Permission denied") || err_str.contains("permission denied") {
Error::SelfUpdate(
"Permission denied. If nixy is installed in a system directory, try running with elevated privileges (e.g., sudo nixy self-upgrade)".to_string()
)
} else {
Error::SelfUpdate(err_str)
}
})?;
success(&format!("Upgraded to {}", latest_version));
Ok(())
}
fn get_asset_name() -> Result<String> {
let arch = std::env::consts::ARCH; let os = std::env::consts::OS; let os_name = match os {
"macos" => "darwin",
"linux" => "linux",
other => {
return Err(Error::SelfUpdate(format!(
"Self-upgrade is only supported on macOS and Linux (detected platform: '{}')",
other
)));
}
};
Ok(format!("nixy-{}-{}", arch, os_name))
}
fn download_binary(url: &str) -> Result<PathBuf> {
let tmp_dir = std::env::temp_dir();
let pid = std::process::id();
let tmp_path = tmp_dir.join(format!("nixy-update-{}", pid));
let mut tmp_file = File::create(&tmp_path)?;
self_update::Download::from_url(url)
.download_to(&mut tmp_file)
.map_err(|e| Error::SelfUpdate(e.to_string()))?;
tmp_file.flush()?;
drop(tmp_file);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&tmp_path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&tmp_path, perms)?;
}
Ok(tmp_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_asset_name_format() {
let result = get_asset_name();
assert!(result.is_ok());
let name = result.unwrap();
assert!(name.starts_with("nixy-"));
assert!(name.contains("-"));
}
#[test]
fn test_temp_file_guard_cleanup() {
let tmp_dir = std::env::temp_dir();
let tmp_path = tmp_dir.join("nixy-test-guard");
std::fs::write(&tmp_path, "test").unwrap();
assert!(tmp_path.exists());
{
let _guard = TempFileGuard::new(tmp_path.clone());
}
assert!(!tmp_path.exists());
}
}