//! self-update: download a newer agent-manager binary from GitHub Releases. use super::*; use crate::download; use anyhow::{anyhow, bail, Context, Result}; use std::cmp::Ordering; use std::path::{Path, PathBuf}; pub fn run(app: &App, check: bool, to: Option<&Path>) -> Result { let Some(repo) = app.config.settings.self_update_repo.clone() else { bail!( "self-update is not configured: set 'settings.self_update_repo: \"owner/repo\"' in your config" ); }; let current = env!("CARGO_PKG_VERSION"); let release = download::latest_release_with_base( &repo, app.config.settings.self_update_base_url.as_deref(), )?; let tag = release.tag_name.trim_start_matches('v').to_string(); if check { match crate::version::compare(&tag, current) { Some(Ordering::Greater) => { println!("update available: {current} -> {tag}"); Ok(0) } _ => { println!("agent-manager {current} is up to date"); Ok(0) } } } else { if crate::version::compare(&tag, current) != Some(Ordering::Greater) { app.log.success(&format!( "agent-manager {current} is already the latest version" )); return Ok(0); } if app.dry_run() { app.log .dry(format!("would download agent-manager {tag}")); return Ok(0); } let picked = download::pick_asset(&release, &[], &[], None) .ok_or_else(|| anyhow!("no release asset of '{repo}' matches this platform"))?; app.log.info(&format!( "downloading agent-manager {tag} ({})", picked.name )); let current_exe = std::env::current_exe()?; let dest = to.map(|p| p.to_path_buf()).unwrap_or_else(|| current_exe.clone()); let tmpdir = tempfile::tempdir()?; let tmp = tmpdir.path().join("am.dl"); download::download(&picked.url, &tmp, None, true)?; // Release assets are archives (zip on Windows, tar.gz elsewhere): // unpack them and locate the binary inside. let staged = if download::extract_archive(&tmp, tmpdir.path())? { find_binary(tmpdir.path())? } else { tmp }; if dest == current_exe && cfg!(windows) { // The running executable is locked on Windows. let new_path = current_exe.with_extension("exe.new"); std::fs::copy(&staged, &new_path) .with_context(|| format!("cannot write {}", new_path.display()))?; app.log.success(&format!( "downloaded {tag} to {} — replace the running binary manually", new_path.display() )); } else { std::fs::copy(&staged, &dest) .with_context(|| format!("cannot write {}", dest.display()))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let _ = std::fs::set_permissions( &dest, std::fs::Permissions::from_mode(0o755), ); } app.log .success(&format!("updated agent-manager to {tag} ({})", dest.display())); } Ok(0) } } /// Locate the binary inside an unpacked release archive. fn find_binary(dir: &Path) -> Result { let mut entries: Vec = Vec::new(); collect_files(dir, &mut entries); if entries.is_empty() { bail!("no binary found in the release archive"); } // Prefer a file named exactly `am` / `am.exe`, otherwise the largest // file (the archive only contains the binary). entries.sort_by_key(|p| { let name = p .file_name() .map(|n| n.to_string_lossy().to_lowercase()) .unwrap_or_default(); let prefer = name == "am" || name == "am.exe" || name.ends_with(".exe"); ( !prefer, std::cmp::Reverse(std::fs::metadata(p).map(|m| m.len()).unwrap_or(0)), ) }); Ok(entries.remove(0)) } fn collect_files(dir: &Path, out: &mut Vec) { if let Ok(rd) = std::fs::read_dir(dir) { for e in rd.flatten() { let p = e.path(); if p.is_dir() { collect_files(&p, out); } else { out.push(p); } } } } #[cfg(test)] mod tests { use super::*; #[test] fn find_binary_prefers_am_exe_in_zip_layout() { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("README.md"), "docs").unwrap(); std::fs::write(dir.path().join("am-windows-x86_64.exe"), b"exe").unwrap(); std::fs::write(dir.path().join("am-linux-x86_64"), b"elf").unwrap(); let found = find_binary(dir.path()).unwrap(); assert_eq!( found.file_name().unwrap().to_str().unwrap(), "am-windows-x86_64.exe" ); } #[test] fn find_binary_falls_back_to_largest_file() { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("am"), b"small").unwrap(); std::fs::write(dir.path().join("data.bin"), vec![0u8; 4096]).unwrap(); let found = find_binary(dir.path()).unwrap(); assert_eq!(found.file_name().unwrap().to_str().unwrap(), "am"); } }