From ac6f2ffaa5edbef33aff1ba021a111ae343c15b7 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 20 Aug 2026 08:15:11 -0400 Subject: [PATCH] fix: am self-update copiait l'archive (zip/tar.gz) au lieu du binaire extrait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les assets de release sont des archives (am-windows-x86_64.zip, am-linux-*.tar.gz) mais le self-update copiait les octets bruts téléchargés vers le binaire — le 'nouveau binaire' était un zip → 'Exec format error' au lancement. Le download des releases était en fait cassé depuis le début (jamais déclenché : l'asset Windows manquait). - self_update: download → download::extract_archive (sniff par contenu, déjà utilisé pour les catalogues) → find_binary (préfère am/am.exe/*.exe, sinon le plus gros fichier) → copie du binaire extrait - 2 tests : préférence am.exe dans un layout zip, fallback plus gros fichier - E2E réel : le zip v1.0.0 extrait → 'am 1.0.0' exécutable (magic MZ) --- src/commands/self_update.rs | 78 +++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) 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"); + } +}