fix: am self-update copiait l'archive (zip/tar.gz) au lieu du binaire extrait
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)
This commit is contained in:
@@ -4,7 +4,7 @@ use super::*;
|
|||||||
use crate::download;
|
use crate::download;
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub fn run(app: &App, check: bool, to: Option<&Path>) -> Result<i32> {
|
pub fn run(app: &App, check: bool, to: Option<&Path>) -> Result<i32> {
|
||||||
let Some(repo) = app.config.settings.self_update_repo.clone() else {
|
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<i32> {
|
|||||||
let current_exe = std::env::current_exe()?;
|
let current_exe = std::env::current_exe()?;
|
||||||
let dest = to.map(|p| p.to_path_buf()).unwrap_or_else(|| current_exe.clone());
|
let dest = to.map(|p| p.to_path_buf()).unwrap_or_else(|| current_exe.clone());
|
||||||
let tmpdir = tempfile::tempdir()?;
|
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)?;
|
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) {
|
if dest == current_exe && cfg!(windows) {
|
||||||
// The running executable is locked on Windows.
|
// The running executable is locked on Windows.
|
||||||
let new_path = current_exe.with_extension("exe.new");
|
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()))?;
|
.with_context(|| format!("cannot write {}", new_path.display()))?;
|
||||||
app.log.success(&format!(
|
app.log.success(&format!(
|
||||||
"downloaded {tag} to {} — replace the running binary manually",
|
"downloaded {tag} to {} — replace the running binary manually",
|
||||||
new_path.display()
|
new_path.display()
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
std::fs::copy(&tmp, &dest)
|
std::fs::copy(&staged, &dest)
|
||||||
.with_context(|| format!("cannot write {}", dest.display()))?;
|
.with_context(|| format!("cannot write {}", dest.display()))?;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
@@ -79,3 +86,66 @@ pub fn run(app: &App, check: bool, to: Option<&Path>) -> Result<i32> {
|
|||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Locate the binary inside an unpacked release archive.
|
||||||
|
fn find_binary(dir: &Path) -> Result<PathBuf> {
|
||||||
|
let mut entries: Vec<PathBuf> = 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<PathBuf>) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user