use std::env;
use std::process::Command;
use anyhow::{Context, bail};
use semver::Version;
use crate::config::{FRONTEND_CRATE, TOOL_VERSION};
pub fn run_update(_args: &[String]) -> anyhow::Result<i32> {
let index = fetch_sparse_index(FRONTEND_CRATE)?;
let versions = parse_versions(&index);
let Some(latest) = select_update(&versions, TOOL_VERSION)? else {
println!("cargo-cgp is already up to date (v{TOOL_VERSION})");
return Ok(0);
};
println!("cargo-cgp: updating v{TOOL_VERSION} → v{latest}…");
reinstall_frontend()?;
run_new_setup()
}
fn fetch_sparse_index(crate_name: &str) -> anyhow::Result<String> {
let url = format!("https://index.crates.io/{}", sparse_index_path(crate_name));
let user_agent = format!("cargo-cgp/{TOOL_VERSION}");
ureq::get(url.as_str())
.header("User-Agent", &user_agent)
.call()
.with_context(|| format!("failed to query the crates.io index at {url}"))?
.body_mut()
.read_to_string()
.context("failed to read the crates.io index response")
}
pub fn sparse_index_path(crate_name: &str) -> String {
let name = crate_name.to_ascii_lowercase();
let dir = match name.len() {
0 => String::new(),
1 => "1".to_owned(),
2 => "2".to_owned(),
3 => format!("3/{}", &name[0..1]),
_ => format!("{}/{}", &name[0..2], &name[2..4]),
};
format!("{dir}/{name}")
}
pub fn parse_versions(index_body: &str) -> Vec<String> {
index_body
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
return None;
}
let value: serde_json::Value = serde_json::from_str(line).ok()?;
if value.get("yanked").and_then(serde_json::Value::as_bool) == Some(true) {
return None;
}
value
.get("vers")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
})
.collect()
}
pub fn select_update(versions: &[String], current: &str) -> anyhow::Result<Option<String>> {
let current = Version::parse(current)
.with_context(|| format!("`{current}` is not a valid semver version"))?;
let want_prerelease = !current.pre.is_empty();
let best = versions
.iter()
.filter_map(|version| Version::parse(version).ok())
.filter(|version| !version.pre.is_empty() == want_prerelease)
.filter(|version| *version > current)
.max();
Ok(best.map(|version| version.to_string()))
}
fn reinstall_frontend() -> anyhow::Result<()> {
let status = Command::new("cargo")
.args(["install", FRONTEND_CRATE])
.status()
.context("failed to run `cargo install` (is cargo on PATH?)")?;
if !status.success() {
bail!(
"`cargo install {FRONTEND_CRATE}` failed with status {status}.\n\
On Windows the running executable is locked and cannot be replaced in place; \
from a shell where cargo-cgp is not running, update by hand with:\n\
\x20 cargo install {FRONTEND_CRATE}\n\
\x20 cargo cgp setup"
);
}
Ok(())
}
fn run_new_setup() -> anyhow::Result<i32> {
let exe = env::current_exe().context("failed to locate the cargo-cgp executable")?;
let status = Command::new(exe)
.arg("setup")
.status()
.context("failed to run the updated `cargo cgp setup`")?;
Ok(status.code().unwrap_or(1))
}