fix(cli): replace shell-based update with native Rust implementation (#7148)
Signed-off-by: JF <john.franklin@gmail.com> Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Jack Amadeo <jackamadeo@squareup.com>
This commit is contained in:
Generated
+34
-1
@@ -1524,6 +1524,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bzip2"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||||
|
dependencies = [
|
||||||
|
"bzip2-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bzip2-sys"
|
name = "bzip2-sys"
|
||||||
version = "0.1.13+1.0.8"
|
version = "0.1.13+1.0.8"
|
||||||
@@ -3518,6 +3527,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4309,6 +4319,7 @@ dependencies = [
|
|||||||
"axum 0.8.8",
|
"axum 0.8.8",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bat",
|
"bat",
|
||||||
|
"bzip2 0.5.2",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
@@ -4327,6 +4338,7 @@ dependencies = [
|
|||||||
"open",
|
"open",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
|
"reqwest 0.12.28",
|
||||||
"rmcp 0.15.0",
|
"rmcp 0.15.0",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4347,6 +4359,7 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"webbrowser",
|
"webbrowser",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
"zip 8.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -12460,7 +12473,7 @@ checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bzip2",
|
"bzip2 0.4.4",
|
||||||
"constant_time_eq 0.1.5",
|
"constant_time_eq 0.1.5",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
@@ -12501,6 +12514,26 @@ dependencies = [
|
|||||||
"typed-path",
|
"typed-path",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "8.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"flate2",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"memchr",
|
||||||
|
"typed-path",
|
||||||
|
"zopfli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a33bbf307b25a1774cee0687694ec72fa7814b3ab5c1c12a9d2fc6a36fc439c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ async-trait = { workspace = true }
|
|||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
|
reqwest = { workspace = true, features = ["blocking", "rustls-tls-native-roots"] }
|
||||||
|
zip = { version = "^8.0", default-features = false, features = ["deflate"] }
|
||||||
|
bzip2 = "0.5"
|
||||||
# Web server dependencies
|
# Web server dependencies
|
||||||
axum = { workspace = true, features = ["ws", "macros"] }
|
axum = { workspace = true, features = ["ws", "macros"] }
|
||||||
tower-http = { workspace = true, features = ["cors", "fs", "auth"] }
|
tower-http = { workspace = true, features = ["cors", "fs", "auth"] }
|
||||||
|
|||||||
@@ -1518,7 +1518,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
canary,
|
canary,
|
||||||
reconfigure,
|
reconfigure,
|
||||||
}) => {
|
}) => {
|
||||||
crate::commands::update::update(canary, reconfigure)?;
|
crate::commands::update::update(canary, reconfigure).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
|
Some(Command::Recipe { command }) => handle_recipe_subcommand(command),
|
||||||
|
|||||||
@@ -1,39 +1,460 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{bail, Context, Result};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
const DOWNLOAD_SCRIPT_URL: &str =
|
/// Asset name for this platform (compile-time).
|
||||||
"https://github.com/block/goose/releases/download/stable/download_cli.sh";
|
fn asset_name() -> &'static str {
|
||||||
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||||
|
{
|
||||||
|
"goose-aarch64-apple-darwin.tar.bz2"
|
||||||
|
}
|
||||||
|
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
||||||
|
{
|
||||||
|
"goose-x86_64-apple-darwin.tar.bz2"
|
||||||
|
}
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
|
{
|
||||||
|
"goose-x86_64-unknown-linux-gnu.tar.bz2"
|
||||||
|
}
|
||||||
|
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
|
||||||
|
{
|
||||||
|
"goose-aarch64-unknown-linux-gnu.tar.bz2"
|
||||||
|
}
|
||||||
|
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||||
|
{
|
||||||
|
"goose-x86_64-pc-windows-gnu.zip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(canary: bool, reconfigure: bool) -> Result<()> {
|
/// Binary name for this platform.
|
||||||
if cfg!(feature = "disable-update") {
|
fn binary_name() -> &'static str {
|
||||||
anyhow::bail!("This command is disabled");
|
#[cfg(target_os = "windows")]
|
||||||
};
|
{
|
||||||
|
"goose.exe"
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
"goose"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the download script from github
|
/// Update the goose binary to the latest release.
|
||||||
let curl_output = Command::new("curl")
|
///
|
||||||
.arg("-fsSL")
|
/// Downloads the platform-appropriate archive from GitHub releases,
|
||||||
.arg(DOWNLOAD_SCRIPT_URL)
|
/// extracts it, and replaces the current binary in-place.
|
||||||
.output()?;
|
pub async fn update(canary: bool, reconfigure: bool) -> Result<()> {
|
||||||
|
#[cfg(feature = "disable-update")]
|
||||||
if !curl_output.status.success() {
|
{
|
||||||
anyhow::bail!(
|
bail!("Update is disabled in this build.");
|
||||||
"Failed to download update script: {}",
|
|
||||||
std::str::from_utf8(&curl_output.stderr)?
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let shell_str = std::str::from_utf8(&curl_output.stdout)?;
|
#[cfg(not(feature = "disable-update"))]
|
||||||
|
{
|
||||||
|
let tag = if canary { "canary" } else { "stable" };
|
||||||
|
let asset = asset_name();
|
||||||
|
let url = format!("https://github.com/block/goose/releases/download/{tag}/{asset}");
|
||||||
|
|
||||||
let update = Command::new("bash")
|
println!("Downloading {asset} from {tag} release...");
|
||||||
.arg("-c")
|
|
||||||
.arg(shell_str)
|
|
||||||
.env("CANARY", canary.to_string())
|
|
||||||
.env("CONFIGURE", reconfigure.to_string())
|
|
||||||
.env("GOOSE_TERMINAL", "1")
|
|
||||||
.env("AGENT", "goose")
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
update.wait_with_output()?;
|
// --- Download -----------------------------------------------------------
|
||||||
|
let response = reqwest::get(&url)
|
||||||
|
.await
|
||||||
|
.context("Failed to download release archive")?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
bail!(
|
||||||
|
"Download failed with HTTP status {}. URL: {}",
|
||||||
|
response.status(),
|
||||||
|
url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.context("Failed to read response body")?;
|
||||||
|
|
||||||
|
println!("Downloaded {} bytes.", bytes.len());
|
||||||
|
|
||||||
|
// --- Extract to temp dir ------------------------------------------------
|
||||||
|
let tmp_dir = tempfile::tempdir().context("Failed to create temp directory")?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
extract_zip(&bytes, tmp_dir.path())?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
extract_tar_bz2(&bytes, tmp_dir.path())?;
|
||||||
|
|
||||||
|
// --- Locate the binary in the extracted archive -------------------------
|
||||||
|
let binary = binary_name();
|
||||||
|
let extracted_binary = find_binary(tmp_dir.path(), binary)
|
||||||
|
.with_context(|| format!("Could not find {binary} in extracted archive"))?;
|
||||||
|
|
||||||
|
// --- Replace the current binary -----------------------------------------
|
||||||
|
let current_exe =
|
||||||
|
env::current_exe().context("Failed to determine current executable path")?;
|
||||||
|
|
||||||
|
replace_binary(&extracted_binary, ¤t_exe)
|
||||||
|
.context("Failed to replace current binary")?;
|
||||||
|
|
||||||
|
// --- Copy DLLs on Windows -----------------------------------------------
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
copy_dlls(&extracted_binary, ¤t_exe)?;
|
||||||
|
|
||||||
|
println!("goose updated successfully!");
|
||||||
|
|
||||||
|
// --- Reconfigure if requested -------------------------------------------
|
||||||
|
if reconfigure {
|
||||||
|
println!("Running goose configure...");
|
||||||
|
let status = Command::new(current_exe)
|
||||||
|
.arg("configure")
|
||||||
|
.status()
|
||||||
|
.context("Failed to run goose configure")?;
|
||||||
|
if !status.success() {
|
||||||
|
eprintln!("Warning: goose configure exited with {status}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Archive extraction
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Extract a .zip archive (Windows).
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn extract_zip(data: &[u8], dest: &Path) -> Result<()> {
|
||||||
|
use std::io::Cursor;
|
||||||
|
let cursor = Cursor::new(data);
|
||||||
|
let mut archive = zip::ZipArchive::new(cursor).context("Failed to open zip archive")?;
|
||||||
|
archive
|
||||||
|
.extract(dest)
|
||||||
|
.context("Failed to extract zip archive")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a .tar.bz2 archive (macOS / Linux).
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn extract_tar_bz2(data: &[u8], dest: &Path) -> Result<()> {
|
||||||
|
use bzip2::read::BzDecoder;
|
||||||
|
let decoder = BzDecoder::new(data);
|
||||||
|
let mut archive = tar::Archive::new(decoder);
|
||||||
|
archive
|
||||||
|
.unpack(dest)
|
||||||
|
.context("Failed to extract tar.bz2 archive")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Binary location
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Find the binary inside the extracted archive.
|
||||||
|
///
|
||||||
|
/// The archive may place it in:
|
||||||
|
/// 1. A `goose-package/` subdirectory (Windows releases)
|
||||||
|
/// 2. Directly at the top level
|
||||||
|
/// 3. In some other single subdirectory
|
||||||
|
fn find_binary(extract_dir: &Path, binary_name: &str) -> Option<PathBuf> {
|
||||||
|
// 1. Check goose-package subdir (matches download_cli.sh / download_cli.ps1)
|
||||||
|
let package_dir = extract_dir.join("goose-package");
|
||||||
|
if package_dir.is_dir() {
|
||||||
|
let p = package_dir.join(binary_name);
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check top level
|
||||||
|
let p = extract_dir.join(binary_name);
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Search one level of subdirectories
|
||||||
|
if let Ok(entries) = fs::read_dir(extract_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if entry.path().is_dir() {
|
||||||
|
let candidate = entry.path().join(binary_name);
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Binary replacement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Replace the current binary with the newly downloaded one.
|
||||||
|
///
|
||||||
|
/// On Windows we must rename the running exe (Windows allows rename but not
|
||||||
|
/// delete/overwrite of a locked file) then copy the new file in.
|
||||||
|
///
|
||||||
|
/// On Unix we can simply copy over the existing binary.
|
||||||
|
fn replace_binary(new_binary: &Path, current_exe: &Path) -> Result<()> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let old_exe = current_exe.with_extension("exe.old");
|
||||||
|
|
||||||
|
// Clean up leftover from a previous update
|
||||||
|
if old_exe.exists() {
|
||||||
|
fs::remove_file(&old_exe).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to remove old backup {}. Is another goose process running?",
|
||||||
|
old_exe.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the running binary out of the way
|
||||||
|
fs::rename(current_exe, &old_exe).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to rename running binary to {}. Try closing Goose Desktop if it's open.",
|
||||||
|
old_exe.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Copy the new binary into place
|
||||||
|
fs::copy(new_binary, current_exe).with_context(|| {
|
||||||
|
// Try to restore the old binary
|
||||||
|
let _ = fs::rename(&old_exe, current_exe);
|
||||||
|
format!("Failed to copy new binary to {}", current_exe.display())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
// On Unix, copy the new binary over the existing one
|
||||||
|
fs::copy(new_binary, current_exe)
|
||||||
|
.with_context(|| format!("Failed to copy new binary to {}", current_exe.display()))?;
|
||||||
|
|
||||||
|
// Ensure the binary is executable
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = fs::metadata(current_exe)?.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(current_exe, perms)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DLL handling (Windows only)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Copy any .dll files from the extracted archive alongside the installed binary.
|
||||||
|
/// Windows GNU builds ship with libgcc, libstdc++, libwinpthread DLLs.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn copy_dlls(extracted_binary: &Path, current_exe: &Path) -> Result<()> {
|
||||||
|
let source_dir = extracted_binary
|
||||||
|
.parent()
|
||||||
|
.context("Extracted binary has no parent directory")?;
|
||||||
|
let dest_dir = current_exe
|
||||||
|
.parent()
|
||||||
|
.context("Current executable has no parent directory")?;
|
||||||
|
|
||||||
|
if let Ok(entries) = fs::read_dir(source_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(ext) = path.extension() {
|
||||||
|
if ext.eq_ignore_ascii_case("dll") {
|
||||||
|
let file_name = path.file_name().unwrap();
|
||||||
|
let dest = dest_dir.join(file_name);
|
||||||
|
// Remove existing DLL first (it may be locked by another process)
|
||||||
|
if dest.exists() {
|
||||||
|
let _ = fs::remove_file(&dest);
|
||||||
|
}
|
||||||
|
fs::copy(&path, &dest).with_context(|| {
|
||||||
|
format!("Failed to copy {} to {}", path.display(), dest.display())
|
||||||
|
})?;
|
||||||
|
println!(" Copied {}", file_name.to_string_lossy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_asset_name_valid() {
|
||||||
|
let name = asset_name();
|
||||||
|
assert!(!name.is_empty());
|
||||||
|
assert!(name.starts_with("goose-"));
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
assert!(name.ends_with(".zip"));
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
assert!(name.ends_with(".tar.bz2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_binary_name() {
|
||||||
|
let name = binary_name();
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
assert_eq!(name, "goose.exe");
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
assert_eq!(name, "goose");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_binary_in_package_subdir() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let pkg = tmp.path().join("goose-package");
|
||||||
|
fs::create_dir_all(&pkg).unwrap();
|
||||||
|
fs::write(pkg.join(binary_name()), b"fake").unwrap();
|
||||||
|
|
||||||
|
let found = find_binary(tmp.path(), binary_name());
|
||||||
|
assert!(found.is_some());
|
||||||
|
assert!(found.unwrap().ends_with(binary_name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_binary_top_level() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
fs::write(tmp.path().join(binary_name()), b"fake").unwrap();
|
||||||
|
|
||||||
|
let found = find_binary(tmp.path(), binary_name());
|
||||||
|
assert!(found.is_some());
|
||||||
|
assert_eq!(found.unwrap(), tmp.path().join(binary_name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_binary_nested_subdir() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let nested = tmp.path().join("some-dir");
|
||||||
|
fs::create_dir_all(&nested).unwrap();
|
||||||
|
fs::write(nested.join(binary_name()), b"fake").unwrap();
|
||||||
|
|
||||||
|
let found = find_binary(tmp.path(), binary_name());
|
||||||
|
assert!(found.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_binary_not_found() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let found = find_binary(tmp.path(), binary_name());
|
||||||
|
assert!(found.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_replace_binary_basic() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let new_bin = tmp.path().join("new_goose");
|
||||||
|
let current = tmp.path().join("current_goose");
|
||||||
|
|
||||||
|
fs::write(&new_bin, b"new version").unwrap();
|
||||||
|
fs::write(¤t, b"old version").unwrap();
|
||||||
|
|
||||||
|
replace_binary(&new_bin, ¤t).unwrap();
|
||||||
|
|
||||||
|
let content = fs::read_to_string(¤t).unwrap();
|
||||||
|
assert_eq!(content, "new version");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn test_replace_binary_windows_rename_away() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let current = tmp.path().join("goose.exe");
|
||||||
|
let new_bin = tmp.path().join("new_goose.exe");
|
||||||
|
|
||||||
|
fs::write(¤t, b"old version").unwrap();
|
||||||
|
fs::write(&new_bin, b"new version").unwrap();
|
||||||
|
|
||||||
|
replace_binary(&new_bin, ¤t).unwrap();
|
||||||
|
|
||||||
|
// Current should now have new content
|
||||||
|
let content = fs::read_to_string(¤t).unwrap();
|
||||||
|
assert_eq!(content, "new version");
|
||||||
|
|
||||||
|
// Old backup should exist
|
||||||
|
let old = current.with_extension("exe.old");
|
||||||
|
assert!(old.exists());
|
||||||
|
let old_content = fs::read_to_string(&old).unwrap();
|
||||||
|
assert_eq!(old_content, "old version");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn test_replace_binary_windows_cleanup_old() {
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
let current = tmp.path().join("goose.exe");
|
||||||
|
let old = current.with_extension("exe.old");
|
||||||
|
let new_bin = tmp.path().join("new_goose.exe");
|
||||||
|
|
||||||
|
// Simulate a previous update left .old behind
|
||||||
|
fs::write(¤t, b"version 2").unwrap();
|
||||||
|
fs::write(&old, b"version 1").unwrap();
|
||||||
|
fs::write(&new_bin, b"version 3").unwrap();
|
||||||
|
|
||||||
|
replace_binary(&new_bin, ¤t).unwrap();
|
||||||
|
|
||||||
|
let content = fs::read_to_string(¤t).unwrap();
|
||||||
|
assert_eq!(content, "version 3");
|
||||||
|
|
||||||
|
// Old should now contain version 2 (not version 1)
|
||||||
|
let old_content = fs::read_to_string(&old).unwrap();
|
||||||
|
assert_eq!(old_content, "version 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn test_extract_zip_with_package_dir() {
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let tmp = tempdir().unwrap();
|
||||||
|
|
||||||
|
// Create a zip in memory with goose-package/ structure
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
{
|
||||||
|
let cursor = Cursor::new(&mut buf);
|
||||||
|
let mut writer = zip::ZipWriter::new(cursor);
|
||||||
|
let options = zip::write::SimpleFileOptions::default()
|
||||||
|
.compression_method(zip::CompressionMethod::Stored);
|
||||||
|
|
||||||
|
writer.add_directory("goose-package/", options).unwrap();
|
||||||
|
writer
|
||||||
|
.start_file("goose-package/goose.exe", options)
|
||||||
|
.unwrap();
|
||||||
|
writer.write_all(b"fake goose binary").unwrap();
|
||||||
|
writer
|
||||||
|
.start_file("goose-package/libtest.dll", options)
|
||||||
|
.unwrap();
|
||||||
|
writer.write_all(b"fake dll").unwrap();
|
||||||
|
writer.finish().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_zip(&buf, tmp.path()).unwrap();
|
||||||
|
|
||||||
|
let binary = find_binary(tmp.path(), "goose.exe");
|
||||||
|
assert!(binary.is_some());
|
||||||
|
|
||||||
|
let content = fs::read_to_string(binary.unwrap()).unwrap();
|
||||||
|
assert_eq!(content, "fake goose binary");
|
||||||
|
|
||||||
|
// DLL should be in goose-package too
|
||||||
|
assert!(tmp.path().join("goose-package/libtest.dll").exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ with multiple lines.
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
fn test_symlink_raii_cleanup_on_panic() {
|
fn test_symlink_raii_cleanup_on_panic() {
|
||||||
use std::os::unix::fs;
|
use std::os::unix::fs;
|
||||||
use std::panic;
|
use std::panic;
|
||||||
@@ -351,6 +352,7 @@ with multiple lines.
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
fn test_symlink_creation_and_cleanup() {
|
fn test_symlink_creation_and_cleanup() {
|
||||||
use std::os::unix::fs;
|
use std::os::unix::fs;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user