use std::fs;
use std::path::Path;
use anyhow::{anyhow, Context as _};
use crate::error::{PmpxError, EXIT_OK};
use crate::style;
mod archive;
mod checksum;
mod ledger;
mod release;
mod swap;
mod version;
use self::archive::extract;
use self::checksum::{parse_sha256sums, sha256_hex};
use self::ledger::{install_kind, InstallKind};
use self::release::{archive_ext, asset_stem, download, latest_version, repository_parts};
use self::swap::{append_to_name, remove_dir_if_exists, replace, Scratch, UpdateLock};
use self::version::Version;
pub use self::swap::cleanup_stale_old;
pub struct Request {
pub check: bool,
pub version: Option<String>,
pub force: bool,
}
pub fn run(request: &Request) -> crate::error::Result<u8> {
let requested = match &request.version {
Some(text) => Some(Version::parse(text).ok_or_else(|| {
PmpxError::Usage(format!("`--version {text}` is not a version like 0.2.0"))
})?),
None => None,
};
run_validated(request, requested).map_err(|error| PmpxError::Other(anyhow!("{error:#}")))
}
fn run_validated(request: &Request, requested: Option<Version>) -> anyhow::Result<u8> {
let exe = std::env::current_exe().context("cannot tell which binary is running")?;
let kind = install_kind(&exe);
let current = Version::parse(env!("CARGO_PKG_VERSION"))
.ok_or_else(|| anyhow!("the compiled-in version is not a version"))?;
if request.check {
let latest = latest_version().context("cannot find out what the latest release is")?;
report_check(¤t, &latest, kind);
return Ok(EXIT_OK);
}
if kind == InstallKind::Cargo {
return Err(anyhow!(
"this pmpx was installed by cargo, which keeps its own record of what it put \
there -- replacing the file behind its back would make `cargo install --list` \
disagree with reality.\n\
Use `cargo install pmpx --force` instead."
));
}
let wanted = match requested {
Some(version) => version,
None => latest_version().context("cannot find out what the latest release is")?,
};
if wanted == current && !request.force {
anstream::println!(
"pmpx {current} is already installed. Use {} to reinstall it.",
style::paint(style::PM, "--force")
);
return Ok(EXIT_OK);
}
update(&exe, &wanted)?;
anstream::println!("{} {current} -> {wanted}", style::paint(style::PM, "pmpx"));
anstream::println!(
"{}",
style::paint(
style::DIM,
"The new binary takes effect next time you run pmpx."
)
);
Ok(EXIT_OK)
}
fn report_check(current: &Version, latest: &Version, kind: InstallKind) {
match latest.cmp(current) {
std::cmp::Ordering::Greater => anstream::println!(
"pmpx {latest} is available {}",
style::paint(style::DIM, format!("(you are on {current})"))
),
std::cmp::Ordering::Equal => {
anstream::println!("pmpx {current} is the latest release.")
}
std::cmp::Ordering::Less => anstream::println!(
"pmpx {current} is newer than the newest release {}",
style::paint(style::DIM, format!("(v{latest})"))
),
}
if kind == InstallKind::Cargo {
anstream::println!(
"{}",
style::paint(
style::DIM,
"This installation was made by cargo, so `pmpx self update` will not \
replace it; use `cargo install pmpx --force`."
)
);
}
}
fn update(exe: &Path, version: &Version) -> anyhow::Result<()> {
let (owner, repo) = repository_parts()?;
let archive_name = format!("{}.{}", asset_stem(), archive_ext(std::env::consts::OS));
let base = format!("https://github.com/{owner}/{repo}/releases/download/v{version}");
let sums_url = format!("{base}/SHA256SUMS");
let sums = download(&sums_url)
.with_context(|| format!("cannot download {sums_url}"))?
.ok_or_else(|| {
anyhow!(
"release v{version} does not exist, or it has no SHA256SUMS to verify the \
download against -- refusing to install it"
)
})?;
let sums = String::from_utf8(sums).context("SHA256SUMS is not valid UTF-8")?;
let wanted = parse_sha256sums(&sums, &archive_name).ok_or_else(|| {
anyhow!("SHA256SUMS does not list {archive_name}, so this release has nothing for this platform")
})?;
let archive_url = format!("{base}/{archive_name}");
let archive = download(&archive_url)
.with_context(|| format!("cannot download {archive_url}"))?
.ok_or_else(|| {
anyhow!(
"release v{version} does not exist, or it has no {archive_name} for this platform"
)
})?;
let got = sha256_hex(&archive);
if got != wanted {
return Err(anyhow!(
"the downloaded {archive_name} does not match SHA256SUMS\n expected {wanted}\n got {got}\n\
Nothing was changed."
));
}
let directory = exe.parent().ok_or_else(|| {
anyhow!(
"cannot tell which directory the binary is in: {}",
exe.display()
)
})?;
let _lock = UpdateLock::acquire(exe)?;
let scratch = directory.join(format!(".pmpx-update-{}", std::process::id()));
remove_dir_if_exists(&scratch);
fs::create_dir_all(&scratch).with_context(|| {
format!(
"cannot create {} (is the directory writable?)",
scratch.display()
)
})?;
let scratch = Scratch::new(scratch);
let extracted = extract(archive_name.ends_with(".zip"), &archive, &scratch.path)?;
let staged = append_to_name(exe, ".new");
fs::rename(&extracted, &staged)
.with_context(|| format!("cannot stage the new binary at {}", staged.display()))?;
replace(exe, &staged)?;
anstream::println!(
"{}",
style::paint(style::DIM, format!("replaced {}", exe.display()))
);
Ok(())
}