waxpkg 0.20.3

Fast Homebrew-compatible package manager
use crate::bottle::{detect_platform, BottleDownloader};
use crate::cache::Cache;
use crate::commands::install::install_extracted_bottle;
use crate::error::{Result, WaxError};
use crate::install::{InstallMode, InstallState};
use crate::signal::check_cancelled;
use crate::ui::{PROGRESS_BAR_CHARS, PROGRESS_BAR_TEMPLATE};
use console::style;
use indicatif::{ProgressBar, ProgressStyle};
use tracing::instrument;

const GHCR_BASE: &str = "https://ghcr.io/v2/homebrew/core";

async fn get_ghcr_token(client: &reqwest::Client, formula_name: &str) -> Result<String> {
    let scope = format!("repository:homebrew/core/{}:pull", formula_name);
    let token_url = format!("https://ghcr.io/token?scope={}", scope);

    #[derive(serde::Deserialize)]
    struct TokenResponse {
        token: String,
    }

    let resp = client.get(&token_url).send().await?;
    let token_resp: TokenResponse = resp.json().await?;
    Ok(token_resp.token)
}

async fn list_available_versions(
    client: &reqwest::Client,
    formula_name: &str,
    token: &str,
) -> Result<Vec<String>> {
    let url = format!("{}/{}/tags/list", GHCR_BASE, formula_name);

    let resp = client
        .get(&url)
        .header("Authorization", format!("Bearer {}", token))
        .send()
        .await?;

    if !resp.status().is_success() {
        return Err(WaxError::VersionNotFound(format!(
            "Cannot list versions for {} (HTTP {})",
            formula_name,
            resp.status()
        )));
    }

    #[derive(serde::Deserialize)]
    struct TagList {
        tags: Vec<String>,
    }

    let tag_list: TagList = resp.json().await?;
    Ok(tag_list.tags)
}

async fn resolve_bottle_for_platform(
    client: &reqwest::Client,
    formula_name: &str,
    version: &str,
    platform: &str,
    token: &str,
) -> Result<(String, String)> {
    let manifest_url = format!("{}/{}/manifests/{}", GHCR_BASE, formula_name, version);

    let resp = client
        .get(&manifest_url)
        .header("Authorization", format!("Bearer {}", token))
        .header(
            "Accept",
            "application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json",
        )
        .send()
        .await?;

    if !resp.status().is_success() {
        return Err(WaxError::VersionNotFound(format!(
            "No manifest found for {}@{} (HTTP {})",
            formula_name,
            version,
            resp.status()
        )));
    }

    let index: serde_json::Value = resp.json().await?;

    let manifests = index["manifests"].as_array().ok_or_else(|| {
        WaxError::VersionNotFound(format!(
            "Invalid image index for {}@{}",
            formula_name, version
        ))
    })?;

    let mut matched_digest: Option<String> = None;
    let mut available_platforms: Vec<String> = Vec::new();

    for manifest in manifests {
        let ref_name = manifest["annotations"]["org.opencontainers.image.ref.name"]
            .as_str()
            .unwrap_or("");

        let manifest_platform = ref_name
            .strip_prefix(&format!("{}.", version))
            .unwrap_or(ref_name);

        available_platforms.push(manifest_platform.to_string());

        if matched_digest.is_none() && manifest_platform == platform {
            matched_digest = Some(manifest["digest"].as_str().unwrap_or("").to_string());
        }
    }

    let platform_manifest_digest = matched_digest.ok_or_else(|| {
        WaxError::VersionNotFound(format!(
            "No bottle for {}@{} on {}.\nAvailable: {}",
            formula_name,
            version,
            platform,
            available_platforms.join(", ")
        ))
    })?;

    let layer_manifest_url = format!(
        "{}/{}/manifests/{}",
        GHCR_BASE, formula_name, platform_manifest_digest
    );

    let layer_resp = client
        .get(&layer_manifest_url)
        .header("Authorization", format!("Bearer {}", token))
        .header(
            "Accept",
            "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json",
        )
        .send()
        .await?;

    if !layer_resp.status().is_success() {
        return Err(WaxError::VersionNotFound(format!(
            "Cannot fetch platform manifest for {}@{} (HTTP {})",
            formula_name,
            version,
            layer_resp.status()
        )));
    }

    let layer_manifest: serde_json::Value = layer_resp.json().await?;

    let layers = layer_manifest["layers"].as_array().ok_or_else(|| {
        WaxError::VersionNotFound(format!(
            "Invalid layer manifest for {}@{}",
            formula_name, version
        ))
    })?;

    let layer = layers.first().ok_or_else(|| {
        WaxError::VersionNotFound(format!(
            "No layers in manifest for {}@{}",
            formula_name, version
        ))
    })?;

    let digest = layer["digest"].as_str().ok_or_else(|| {
        WaxError::VersionNotFound(format!(
            "No digest in layer manifest for {}@{}",
            formula_name, version
        ))
    })?;

    let sha256 = digest.strip_prefix("sha256:").unwrap_or(digest).to_string();
    let blob_url = format!("{}/{}/blobs/{}", GHCR_BASE, formula_name, digest);

    Ok((blob_url, sha256))
}

#[instrument(skip(cache))]
pub async fn version_install(
    cache: &Cache,
    formula_name: &str,
    version: &str,
    user: bool,
    global: bool,
) -> Result<()> {
    let start = std::time::Instant::now();

    cache.ensure_fresh().await?;

    let formulae = cache.load_all_formulae().await?;
    formulae
        .iter()
        .find(|f| f.name == formula_name || f.full_name == formula_name)
        .ok_or_else(|| WaxError::FormulaNotFound(formula_name.to_string()))?;

    let install_mode = match InstallMode::from_flags(user, global)? {
        Some(mode) => mode,
        None => InstallMode::detect(),
    };
    install_mode.validate()?;

    let state = InstallState::new()?;
    let platform = detect_platform();

    println!(
        "{} {}@{}",
        style("version-install").bold(),
        style(formula_name).magenta(),
        style(version).cyan()
    );

    let client = crate::http_client::default_client();

    let spinner = ProgressBar::new_spinner();
    spinner.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.cyan} {msg}")
            .unwrap(),
    );
    spinner.enable_steady_tick(std::time::Duration::from_millis(100));

    spinner.set_message("Authenticating with ghcr.io...");
    let token = get_ghcr_token(client, formula_name).await?;

    spinner.set_message("Listing available versions...");
    let tags = list_available_versions(client, formula_name, &token).await?;

    if !tags.contains(&version.to_string()) {
        spinner.finish_and_clear();

        let mut available = tags.clone();
        available.sort();
        available.reverse();
        let show: Vec<&String> = available.iter().take(15).collect();

        return Err(WaxError::VersionNotFound(format!(
            "Version {} not found for {}.\nAvailable versions: {}{}",
            version,
            formula_name,
            show.iter()
                .map(|s| s.as_str())
                .collect::<Vec<_>>()
                .join(", "),
            if available.len() > 15 {
                format!(" (+{} more)", available.len() - 15)
            } else {
                String::new()
            }
        )));
    }

    spinner.set_message(format!(
        "Resolving bottle for {}@{} ({})...",
        formula_name, version, platform
    ));

    let (blob_url, sha256) =
        resolve_bottle_for_platform(client, formula_name, version, &platform, &token).await?;

    spinner.finish_and_clear();
    check_cancelled()?;

    let temp_dir = tempfile::TempDir::new()?;
    let tarball_path = temp_dir
        .path()
        .join(format!("{}-{}.tar.gz", formula_name, version));

    let downloader = BottleDownloader::new();
    let pb = ProgressBar::new(0);
    pb.set_style(
        ProgressStyle::default_bar()
            .template(PROGRESS_BAR_TEMPLATE)
            .unwrap()
            .progress_chars(PROGRESS_BAR_CHARS),
    );
    pb.set_message(format!("{}@{}", formula_name, version));

    downloader
        .download(
            &blob_url,
            &tarball_path,
            Some(&pb),
            BottleDownloader::GLOBAL_CONNECTION_POOL,
            None,
        )
        .await?;
    pb.finish_and_clear();

    crate::digest::verify_sha256_file(&tarball_path, &sha256)?;

    let extract_dir = temp_dir.path().join(formula_name);
    BottleDownloader::extract(&tarball_path, &extract_dir)?;

    let cellar = install_mode.cellar_path()?;
    install_extracted_bottle(
        formula_name,
        version,
        &extract_dir,
        sha256,
        0,
        &cellar,
        install_mode,
        &platform,
        &state,
        false,
        true,
        None,
        None,
    )
    .await?;

    let elapsed = start.elapsed();
    println!(
        "\n+ {}@{}{}",
        style(formula_name).magenta(),
        style(version).cyan(),
        crate::ui::elapsed_suffix(elapsed)
    );

    Ok(())
}