zlayer-toolchain 0.14.1

Runtime toolchain provisioning (macOS Homebrew bottle resolver/installer) for ZLayer
Documentation
//! The single Homebrew formula parser shared by every macOS provisioning path.
//!
//! The formula shape now lives in `zlayer-types`
//! ([`zlayer_types::package_index::FormulaData`]) so it is shared verbatim with
//! `zlayer-builder` (a later commit). This module re-exports it as [`Formula`]
//! and resolves a formula THROUGH the [`crate::package_index::PackageIndexClient`]:
//! the ZLayer package index (`packages.zlayer.dev`) first, with the direct
//! `formulae.brew.sh` upstream as a graceful fallback. The resolved formula
//! carries `versions.stable`, `urls.stable.{url,checksum}` (the source-tarball
//! sha256), the per-platform `bottle.stable.files`, the dependency graph, and
//! `ruby_source_path` — everything the source-build + dependency-resolution
//! pipeline needs.

use crate::error::Result;

/// A parsed Homebrew formula — re-exported from `zlayer-types` so the toolchain
/// and the builder consume the exact same wire shape.
pub use zlayer_types::package_index::{FormulaData as Formula, UsesFromMacos};

/// Fetch and parse a formula through the package index.
///
/// Resolves against `packages.zlayer.dev` (or `ZLAYER_PACKAGE_INDEX_URL`) with a
/// `formulae.brew.sh` fallback. The version is **not** hardcoded — a formula bump
/// is picked up automatically — and the resolved formula now carries
/// `urls.stable.checksum` (the source-tarball sha256).
///
/// # Errors
///
/// Returns [`crate::error::ToolchainError::RegistryError`] if the formula cannot
/// be fetched or parsed from either the index or the upstream.
pub async fn fetch_formula(formula: &str) -> Result<Formula> {
    tracing::info!("Resolving Homebrew formula metadata for: {formula}");
    crate::package_index::PackageIndexClient::from_env()
        .get_formula(formula)
        .await
}

#[cfg(test)]
mod tests {
    use super::*;

    const GIT_JSON: &str = r#"{
        "versions": {"stable": "2.55.0"},
        "urls": {"stable": {"url": "https://example/git-2.55.0.tar.xz", "checksum": "sha256:abc123"}},
        "dependencies": ["pcre2", "gettext"],
        "build_dependencies": ["gettext", "pkgconf"],
        "uses_from_macos": ["curl", "expat", {"llvm": ["build"]}],
        "ruby_source_path": "Formula/g/git.rb"
    }"#;

    #[test]
    fn parses_all_unified_fields_including_checksum() {
        let f: Formula = serde_json::from_str(GIT_JSON).unwrap();
        assert_eq!(f.stable_version(), Some("2.55.0"));
        assert_eq!(f.stable_url(), Some("https://example/git-2.55.0.tar.xz"));
        // The `sha256:` prefix is stripped by the accessor.
        assert_eq!(f.stable_checksum().as_deref(), Some("abc123"));
        assert_eq!(f.dependencies, vec!["pcre2", "gettext"]);
        assert_eq!(f.build_dependencies, vec!["gettext", "pkgconf"]);
        assert_eq!(f.ruby_source_path.as_deref(), Some("Formula/g/git.rb"));
        // uses_from_macos mixes bare names and a conditional object.
        assert_eq!(f.macos_provided(), vec!["curl", "expat", "llvm"]);
    }

    #[test]
    fn missing_fields_default_cleanly() {
        let f: Formula = serde_json::from_str("{}").unwrap();
        assert_eq!(f.stable_version(), None);
        assert_eq!(f.stable_url(), None);
        assert_eq!(f.stable_checksum(), None);
        assert!(f.dependencies.is_empty());
        assert!(f.build_dependencies.is_empty());
        assert!(f.macos_provided().is_empty());
        assert!(f.ruby_source_path.is_none());
    }

    #[test]
    fn empty_version_and_url_are_treated_as_absent() {
        let f: Formula =
            serde_json::from_str(r#"{"versions":{"stable":""},"urls":{"stable":{"url":""}}}"#)
                .unwrap();
        assert_eq!(f.stable_version(), None);
        assert_eq!(f.stable_url(), None);
    }

    #[test]
    fn uses_from_macos_name_accessor() {
        let bare = UsesFromMacos::Name("curl".to_string());
        assert_eq!(bare.name(), Some("curl"));
    }
}