diff --git a/src/commands/self_update.rs b/src/commands/self_update.rs index dca6279..80eb1e7 100644 --- a/src/commands/self_update.rs +++ b/src/commands/self_update.rs @@ -4,7 +4,7 @@ use super::*; use crate::download; use anyhow::{anyhow, bail, Context, Result}; use std::cmp::Ordering; -use std::path::Path; +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 { @@ -51,19 +51,26 @@ pub fn run(app: &App, check: bool, to: Option<&Path>) -> Result { 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.new"); + 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(&tmp, &new_path) + 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(&tmp, &dest) + std::fs::copy(&staged, &dest) .with_context(|| format!("cannot write {}", dest.display()))?; #[cfg(unix)] { @@ -79,3 +86,66 @@ pub fn run(app: &App, check: bool, to: Option<&Path>) -> Result { 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"); + } +}