use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallMethod {
Cargo,
Homebrew,
Npm,
Standalone,
}
impl InstallMethod {
pub fn label(self) -> &'static str {
match self {
InstallMethod::Cargo => "cargo",
InstallMethod::Homebrew => "Homebrew",
InstallMethod::Npm => "npm",
InstallMethod::Standalone => "standalone",
}
}
pub fn upgrade_display(self) -> &'static str {
match self {
InstallMethod::Cargo => "cargo install vallum --force",
InstallMethod::Homebrew => "brew upgrade vallum",
InstallMethod::Npm => "npm install -g vallum",
InstallMethod::Standalone => {
"curl -LsSf https://github.com/kahramanemir/Vallum/releases/latest/download/vallum-installer.sh | sh"
}
}
}
pub fn upgrade_argv(self) -> Option<&'static [&'static str]> {
match self {
InstallMethod::Cargo => Some(&["cargo", "install", "vallum", "--force"]),
InstallMethod::Homebrew => Some(&["brew", "upgrade", "vallum"]),
InstallMethod::Npm => Some(&["npm", "install", "-g", "vallum"]),
InstallMethod::Standalone => None,
}
}
}
pub fn detect_install_method(exe: &Path) -> InstallMethod {
let p = exe.to_string_lossy();
if p.contains("/.cargo/") {
InstallMethod::Cargo
} else if p.contains("/Cellar/") || p.contains("/Caskroom/") || p.contains("/homebrew/") {
InstallMethod::Homebrew
} else if p.contains("/node_modules/") {
InstallMethod::Npm
} else {
InstallMethod::Standalone
}
}
pub fn parse_latest_from_index(body: &str) -> Option<String> {
body.lines()
.rev()
.filter(|l| !l.trim().is_empty())
.find_map(|l| {
let v: serde_json::Value = serde_json::from_str(l).ok()?;
if v.get("yanked").and_then(|y| y.as_bool()).unwrap_or(false) {
return None;
}
v.get("vers")?.as_str().map(str::to_string)
})
}
fn semver_core(v: &str) -> (u64, u64, u64) {
let core = v.split(['+', '-']).next().unwrap_or(v);
let mut it = core.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
(
it.next().unwrap_or(0),
it.next().unwrap_or(0),
it.next().unwrap_or(0),
)
}
pub fn is_outdated(current: &str, latest: &str) -> bool {
semver_core(latest) > semver_core(current)
}
fn fetch_latest() -> Result<String, String> {
let out = std::process::Command::new("curl")
.args([
"-sSf",
"--max-time",
"10",
"https://index.crates.io/va/ll/vallum",
])
.output()
.map_err(|e| format!("could not run curl ({e})"))?;
if !out.status.success() {
return Err("network lookup failed".to_string());
}
let body = String::from_utf8(out.stdout).map_err(|_| "non-UTF-8 response".to_string())?;
parse_latest_from_index(&body).ok_or_else(|| "could not parse the index".to_string())
}
pub fn run(check: bool, run_upgrade: bool) -> i32 {
let current = env!("CARGO_PKG_VERSION");
let method = std::env::current_exe()
.ok()
.map(|p| detect_install_method(&p))
.unwrap_or(InstallMethod::Standalone);
let latest = match fetch_latest() {
Ok(v) => v,
Err(e) => {
eprintln!("vallum update: couldn't check for the latest version ({e}).");
println!("vallum {current} (installed via {})", method.label());
println!("to upgrade, run:\n {}", method.upgrade_display());
return 0;
}
};
if !is_outdated(current, &latest) {
println!("vallum {current} is up to date (latest {latest}).");
return 0;
}
println!(
"vallum {current} — {latest} is available (installed via {}).",
method.label()
);
if check {
println!("to upgrade, run:\n {}", method.upgrade_display());
return 10;
}
if run_upgrade {
return match method.upgrade_argv() {
Some(argv) => {
println!("running: {}", method.upgrade_display());
match std::process::Command::new(argv[0])
.args(&argv[1..])
.status()
{
Ok(s) if s.success() => {
println!("upgraded.");
0
}
Ok(s) => {
eprintln!("upgrade command exited with {s}.");
1
}
Err(e) => {
eprintln!("could not run the upgrade command ({e}).");
1
}
}
}
None => {
println!(
"this install can't be auto-upgraded safely; run:\n {}",
method.upgrade_display()
);
0
}
};
}
println!("to upgrade, run:\n {}", method.upgrade_display());
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_install_method_from_path() {
assert_eq!(
detect_install_method(Path::new("/Users/x/.cargo/bin/vallum")),
InstallMethod::Cargo
);
assert_eq!(
detect_install_method(Path::new("/opt/homebrew/Cellar/vallum/0.8.4/bin/vallum")),
InstallMethod::Homebrew
);
assert_eq!(
detect_install_method(Path::new("/usr/local/lib/node_modules/vallum/vallum")),
InstallMethod::Npm
);
assert_eq!(
detect_install_method(Path::new("/Users/x/.local/bin/vallum")),
InstallMethod::Standalone
);
}
#[test]
fn upgrade_command_per_method() {
assert!(InstallMethod::Cargo
.upgrade_display()
.contains("cargo install vallum"));
assert!(InstallMethod::Homebrew
.upgrade_display()
.contains("brew upgrade vallum"));
assert!(InstallMethod::Npm
.upgrade_display()
.contains("npm install -g vallum"));
assert!(InstallMethod::Standalone.upgrade_argv().is_none());
assert!(InstallMethod::Cargo.upgrade_argv().is_some());
}
#[test]
fn parses_latest_from_sparse_index() {
let body = concat!(
r#"{"name":"vallum","vers":"0.8.3","yanked":false}"#,
"\n",
r#"{"name":"vallum","vers":"0.8.4","yanked":false}"#,
"\n",
r#"{"name":"vallum","vers":"0.9.0","yanked":true}"#,
"\n"
);
assert_eq!(parse_latest_from_index(body).as_deref(), Some("0.8.4"));
}
#[test]
fn version_comparison() {
assert!(is_outdated("0.8.4", "0.8.5"));
assert!(is_outdated("0.8.4", "0.9.0"));
assert!(is_outdated("0.8.4", "1.0.0"));
assert!(!is_outdated("0.8.4", "0.8.4"));
assert!(!is_outdated("0.8.5", "0.8.4"));
assert!(!is_outdated("0.8.4", "0.8.4+abc123")); }
}