moonup 0.5.0

Manage multiple MoonBit installations
Documentation
use clap::Parser;
use miette::IntoDiagnostic;
use std::env;
use std::path::PathBuf;
use tokio::io::AsyncReadExt;

use crate::archive;
use crate::utils::{self, build_http_client_with_retry};

/// Update Moonup to the latest version
#[derive(Parser, Debug)]
pub struct Args {}

pub async fn execute(_: Args) -> miette::Result<()> {
    let current_version = env!("CARGO_PKG_VERSION");

    // self_update does not work well with async, see:
    //   https://github.com/jaemk/self_update/issues/44
    let latest_release = tokio::task::spawn_blocking(move || {
        let updater = self_update::backends::github::Update::configure()
            .repo_owner("chawyehsu")
            .repo_name("moonup")
            .bin_name(env!("CARGO_PKG_NAME"))
            .current_version(current_version)
            .build()?;
        updater.get_latest_release()
    })
    .await
    .expect("spawn blocking task")
    .into_diagnostic()?;

    let is_greater =
        self_update::version::bump_is_greater(current_version, &latest_release.version)
            .ok()
            .unwrap_or(false);

    // Environment variable to force self-update for testing purposes
    let env_test_trigger = std::env::var("MOONUP_TEST_FORCE_SELFUPDATE").is_ok();

    if !is_greater && !env_test_trigger {
        println!("moonup is already up-to-date");
        return Ok(());
    }

    println!(
        "Updating moonup: {} -> {}",
        current_version, latest_release.version
    );

    let assets = latest_release
        .assets
        .iter()
        .filter(|a| a.name.contains(self_update::get_target()))
        .collect::<Vec<_>>();

    tracing::trace!("moonup assets: {:?}", assets);
    assert_eq!(assets.len(), 2, "expected two assets");

    let client = build_http_client_with_retry();
    let temp_dir = self_update::TempDir::with_prefix("moonup").into_diagnostic()?;

    let mut sha256_actual = String::new();
    let mut sha256_expected = String::new();

    let extract_to = temp_dir.path();

    for asset in assets {
        let url = format!(
            "https://github.com/chawyehsu/moonup/releases/download/v{}/{}",
            latest_release.version, asset.name
        );
        let url = url::Url::parse(url.as_str()).into_diagnostic()?;
        tracing::debug!("downloading {} from {}", asset.name, url);

        let mut reader = utils::url_to_reader(url, &client, None).await?;

        if asset.name.ends_with(".sha256") {
            let mut content = String::new();
            reader
                .read_to_string(&mut content)
                .await
                .into_diagnostic()?;
            sha256_expected = content.trim().to_ascii_lowercase();
        } else {
            tracing::debug!("extracting to {}", extract_to.display());

            let sha256 = match asset.name.ends_with(".zip") {
                false => archive::extract_tar_gz(reader, extract_to).await?,
                true => archive::extract_zip(reader, extract_to).await?,
            };

            sha256_actual = format!("{:x}", sha256);
        }
    }

    assert_eq!(sha256_actual, sha256_expected, "SHA256 checksum mismatch");

    let args = env::args_os().collect::<Vec<_>>();
    for bin in ["moonup", "moonup-shim"] {
        let ext = if cfg!(windows) { ".exe" } else { "" };
        let name = format!("{}{}", bin, ext);

        let src = extract_to.join(&name);
        let dst = env::current_exe()
            .unwrap_or_else(|_| PathBuf::from(&args[0]))
            .with_file_name(name);

        utils::replace_exe(&src, &dst)?;
    }
    Ok(())
}