waxpkg 0.15.9

Fast Homebrew-compatible package manager
use crate::bottle::{detect_platform, BottleDownloader};
use crate::cache::Cache;
use crate::error::{Result, WaxError};
use crate::install::{create_symlinks, InstallMode, InstallState, InstalledPackage};
use crate::signal::check_cancelled;
use crate::ui::{copy_dir_all, 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 = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(60))
        .redirect(reqwest::redirect::Policy::limited(10))
        .build()
        .map_err(WaxError::HttpError)?;

    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();

    BottleDownloader::verify_checksum(&tarball_path, &sha256)?;

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

    let cellar = install_mode.cellar_path()?;
    let formula_cellar = cellar.join(formula_name).join(version);

    if formula_cellar.exists() {
        tokio::fs::remove_dir_all(&formula_cellar)
            .await
            .or_else(|_| crate::sudo::sudo_remove(&formula_cellar).map(|_| ()))?;
    }
    tokio::fs::create_dir_all(&formula_cellar)
        .await
        .or_else(|_| crate::sudo::sudo_mkdir(&formula_cellar))?;

    let actual_content_dir = extract_dir.join(formula_name).join(version);
    if actual_content_dir.exists() {
        copy_dir_all(&actual_content_dir, &formula_cellar)?;
    } else {
        let name_dir = extract_dir.join(formula_name);
        if name_dir.exists() {
            let mut found_version_dir = None;
            if let Ok(mut entries) = std::fs::read_dir(&name_dir) {
                while let Some(Ok(entry)) = entries.next() {
                    let entry_name = entry.file_name().to_string_lossy().to_string();
                    if entry_name.starts_with(version) && entry.path().is_dir() {
                        found_version_dir = Some(entry.path());
                        break;
                    }
                }
            }
            if let Some(version_dir) = found_version_dir {
                copy_dir_all(&version_dir, &formula_cellar)?;
            } else {
                copy_dir_all(&extract_dir, &formula_cellar)?;
            }
        } else {
            copy_dir_all(&extract_dir, &formula_cellar)?;
        }
    }

    {
        let prefix = install_mode.prefix()?;
        let default_prefix = if cfg!(target_os = "macos") {
            "/opt/homebrew"
        } else {
            "/home/linuxbrew/.linuxbrew"
        };
        BottleDownloader::relocate_bottle(
            &formula_cellar,
            prefix.to_str().unwrap_or(default_prefix),
        )?;
    }

    create_symlinks(formula_name, version, &cellar, false, install_mode).await?;

    let package = InstalledPackage {
        name: formula_name.to_string(),
        version: version.to_string(),
        platform: platform.clone(),
        install_date: std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64,
        install_mode,
        from_source: false,
        bottle_rebuild: 0,
        bottle_sha256: Some(sha256),
        pinned: false,
    };
    state.add(package).await?;

    let elapsed = start.elapsed();
    println!(
        "\n+ {}@{} [{}ms]",
        style(formula_name).magenta(),
        style(version).cyan(),
        elapsed.as_millis()
    );

    Ok(())
}