✨ feat: add self-update capability
Add `tmuxido --update` command to update binary from latest release. - New `self_update` module with version comparison - Atomic binary replacement with backup/rollback - Fetches latest release from Gitea API - Downloads correct binary for system architecture
This commit is contained in:
parent
fcb7c7f6a6
commit
ba3f923781
@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [0.4.0] - 2026-03-01
|
||||
|
||||
### Added
|
||||
- Self-update feature (`tmuxido --update`) to update binary from latest GitHub release
|
||||
- New `self_update` module with version comparison and atomic binary replacement
|
||||
- `--update` CLI flag for in-place binary updates
|
||||
- Backup and rollback mechanism if update fails
|
||||
|
||||
## [0.3.0] - 2026-03-01
|
||||
|
||||
### Added
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -542,7 +542,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tmuxido"
|
||||
version = "0.2.4"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tmuxido"
|
||||
version = "0.2.4"
|
||||
version = "0.4.0"
|
||||
edition = "2024"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@ -21,6 +21,7 @@ A Rust-based tool to quickly find and open projects in tmux using fzf. No extern
|
||||
- TOML-based configuration
|
||||
- Smart caching system for fast subsequent runs
|
||||
- Configurable cache TTL
|
||||
- Self-update capability (`tmuxido --update`)
|
||||
- Zero external dependencies (except tmux and fzf)
|
||||
|
||||
## Installation
|
||||
@ -97,6 +98,11 @@ Check cache status:
|
||||
tmuxido --cache-status
|
||||
```
|
||||
|
||||
Update tmuxido to the latest version:
|
||||
```bash
|
||||
tmuxido --update
|
||||
```
|
||||
|
||||
View help:
|
||||
```bash
|
||||
tmuxido --help
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod deps;
|
||||
pub mod self_update;
|
||||
pub mod session;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
10
src/main.rs
10
src/main.rs
@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use tmuxido::config::Config;
|
||||
use tmuxido::deps::ensure_dependencies;
|
||||
use tmuxido::self_update;
|
||||
use tmuxido::{get_projects, launch_tmux_session, show_cache_status};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@ -24,11 +25,20 @@ struct Args {
|
||||
/// Show cache status and exit
|
||||
#[arg(long)]
|
||||
cache_status: bool,
|
||||
|
||||
/// Update tmuxido to the latest version
|
||||
#[arg(long)]
|
||||
update: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
// Handle self-update before anything else
|
||||
if args.update {
|
||||
return self_update::self_update();
|
||||
}
|
||||
|
||||
// Check that fzf and tmux are installed; offer to install if missing
|
||||
ensure_dependencies()?;
|
||||
|
||||
|
||||
218
src/self_update.rs
Normal file
218
src/self_update.rs
Normal file
@ -0,0 +1,218 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
const REPO: &str = "cinco/Tmuxido";
|
||||
const BASE_URL: &str = "https://git.cincoeuzebio.com";
|
||||
|
||||
/// Check if running from cargo (development mode)
|
||||
fn is_dev_build() -> bool {
|
||||
option_env!("CARGO_PKG_NAME").is_none()
|
||||
}
|
||||
|
||||
/// Get current version from cargo
|
||||
pub fn current_version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
/// Detect system architecture
|
||||
fn detect_arch() -> Result<&'static str> {
|
||||
let arch = std::env::consts::ARCH;
|
||||
match arch {
|
||||
"x86_64" => Ok("x86_64-linux"),
|
||||
"aarch64" => Ok("aarch64-linux"),
|
||||
_ => Err(anyhow::anyhow!("Unsupported architecture: {}", arch)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch latest release tag from Gitea API
|
||||
fn fetch_latest_tag() -> Result<String> {
|
||||
let url = format!("{}/api/v1/repos/{}/releases?limit=1&page=1", BASE_URL, REPO);
|
||||
|
||||
let output = Command::new("curl")
|
||||
.args(["-fsSL", &url])
|
||||
.output()
|
||||
.context("Failed to execute curl. Make sure curl is installed.")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to fetch latest release: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let response = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Parse JSON response to extract tag_name
|
||||
let tag: serde_json::Value =
|
||||
serde_json::from_str(&response).context("Failed to parse release API response")?;
|
||||
|
||||
tag.get(0)
|
||||
.and_then(|r| r.get("tag_name"))
|
||||
.and_then(|t| t.as_str())
|
||||
.map(|t| t.to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not extract tag_name from release"))
|
||||
}
|
||||
|
||||
/// Get path to current executable
|
||||
fn get_current_exe() -> Result<PathBuf> {
|
||||
std::env::current_exe().context("Failed to get current executable path")
|
||||
}
|
||||
|
||||
/// Download binary to a temporary location
|
||||
fn download_binary(tag: &str, arch: &str, temp_path: &std::path::Path) -> Result<()> {
|
||||
let url = format!("{}/{}/releases/download/{}/{}", BASE_URL, REPO, tag, arch);
|
||||
|
||||
println!("Downloading {}...", url);
|
||||
|
||||
let output = Command::new("curl")
|
||||
.args(["-fsSL", &url, "-o", &temp_path.to_string_lossy()])
|
||||
.output()
|
||||
.context("Failed to execute curl for download")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to download binary: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
// Make executable
|
||||
let mut perms = std::fs::metadata(temp_path)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(temp_path, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform self-update
|
||||
pub fn self_update() -> Result<()> {
|
||||
if is_dev_build() {
|
||||
println!("Development build detected. Skipping self-update.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current = current_version();
|
||||
println!("Current version: {}", current);
|
||||
|
||||
let latest = fetch_latest_tag()?;
|
||||
let latest_clean = latest.trim_start_matches('v');
|
||||
println!("Latest version: {}", latest);
|
||||
|
||||
// Compare versions (simple string comparison for semver without 'v' prefix)
|
||||
if latest_clean == current {
|
||||
println!("Already up to date!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if latest is actually newer
|
||||
match version_compare(latest_clean, current) {
|
||||
std::cmp::Ordering::Less => {
|
||||
println!("Current version is newer than release. Skipping update.");
|
||||
return Ok(());
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
println!("Already up to date!");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let arch = detect_arch()?;
|
||||
let exe_path = get_current_exe()?;
|
||||
|
||||
// Create temporary file in same directory as target (for atomic rename)
|
||||
let exe_dir = exe_path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not determine executable directory"))?;
|
||||
let temp_path = exe_dir.join(".tmuxido.new");
|
||||
|
||||
println!("Downloading update...");
|
||||
download_binary(&latest, arch, &temp_path)?;
|
||||
|
||||
// Verify the downloaded binary works
|
||||
let verify = Command::new(&temp_path).arg("--version").output();
|
||||
if let Err(e) = verify {
|
||||
let _ = std::fs::remove_file(&temp_path);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Downloaded binary verification failed: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
|
||||
// Atomic replace: rename old to .old, rename new to target
|
||||
let backup_path = exe_path.with_extension("old");
|
||||
|
||||
// Remove old backup if exists
|
||||
let _ = std::fs::remove_file(&backup_path);
|
||||
|
||||
// Rename current to backup
|
||||
std::fs::rename(&exe_path, &backup_path)
|
||||
.context("Failed to backup current binary (is tmuxido running?)")?;
|
||||
|
||||
// Move new to current location
|
||||
if let Err(e) = std::fs::rename(&temp_path, &exe_path) {
|
||||
// Restore backup on failure
|
||||
let _ = std::fs::rename(&backup_path, &exe_path);
|
||||
return Err(anyhow::anyhow!("Failed to install new binary: {}", e));
|
||||
}
|
||||
|
||||
// Remove backup on success
|
||||
let _ = std::fs::remove_file(&backup_path);
|
||||
|
||||
println!("Successfully updated to {}!", latest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compare two semver versions
|
||||
fn version_compare(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
let parse = |s: &str| {
|
||||
s.split('.')
|
||||
.filter_map(|n| n.parse::<u32>().ok())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let a_parts = parse(a);
|
||||
let b_parts = parse(b);
|
||||
|
||||
for (a_part, b_part) in a_parts.iter().zip(b_parts.iter()) {
|
||||
match a_part.cmp(b_part) {
|
||||
std::cmp::Ordering::Equal => continue,
|
||||
other => return other,
|
||||
}
|
||||
}
|
||||
|
||||
a_parts.len().cmp(&b_parts.len())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_detect_current_version() {
|
||||
let version = current_version();
|
||||
// Version should be non-empty and contain dots
|
||||
assert!(!version.is_empty());
|
||||
assert!(version.contains('.'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_compare_versions_correctly() {
|
||||
assert_eq!(
|
||||
version_compare("0.3.0", "0.2.4"),
|
||||
std::cmp::Ordering::Greater
|
||||
);
|
||||
assert_eq!(version_compare("0.2.4", "0.3.0"), std::cmp::Ordering::Less);
|
||||
assert_eq!(version_compare("0.3.0", "0.3.0"), std::cmp::Ordering::Equal);
|
||||
assert_eq!(
|
||||
version_compare("1.0.0", "0.9.9"),
|
||||
std::cmp::Ordering::Greater
|
||||
);
|
||||
assert_eq!(
|
||||
version_compare("0.10.0", "0.9.0"),
|
||||
std::cmp::Ordering::Greater
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user