Skip to main content

zlayer_toolchain/
formula.rs

1//! The single Homebrew formula parser shared by every macOS provisioning path.
2//!
3//! The formula shape now lives in `zlayer-types`
4//! ([`zlayer_types::package_index::FormulaData`]) so it is shared verbatim with
5//! `zlayer-builder` (a later commit). This module re-exports it as [`Formula`]
6//! and resolves a formula THROUGH the [`crate::package_index::PackageIndexClient`]:
7//! the ZLayer package index (`packages.zlayer.dev`) first, with the direct
8//! `formulae.brew.sh` upstream as a graceful fallback. The resolved formula
9//! carries `versions.stable`, `urls.stable.{url,checksum}` (the source-tarball
10//! sha256), the per-platform `bottle.stable.files`, the dependency graph, and
11//! `ruby_source_path` — everything the source-build + dependency-resolution
12//! pipeline needs.
13
14use crate::error::Result;
15
16/// A parsed Homebrew formula — re-exported from `zlayer-types` so the toolchain
17/// and the builder consume the exact same wire shape.
18pub use zlayer_types::package_index::{FormulaData as Formula, UsesFromMacos};
19
20/// Fetch and parse a formula through the package index.
21///
22/// Resolves against `packages.zlayer.dev` (or `ZLAYER_PACKAGE_INDEX_URL`) with a
23/// `formulae.brew.sh` fallback. The version is **not** hardcoded — a formula bump
24/// is picked up automatically — and the resolved formula now carries
25/// `urls.stable.checksum` (the source-tarball sha256).
26///
27/// # Errors
28///
29/// Returns [`crate::error::ToolchainError::RegistryError`] if the formula cannot
30/// be fetched or parsed from either the index or the upstream.
31pub async fn fetch_formula(formula: &str) -> Result<Formula> {
32    tracing::info!("Resolving Homebrew formula metadata for: {formula}");
33    crate::package_index::PackageIndexClient::from_env()
34        .get_formula(formula)
35        .await
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    const GIT_JSON: &str = r#"{
43        "versions": {"stable": "2.55.0"},
44        "urls": {"stable": {"url": "https://example/git-2.55.0.tar.xz", "checksum": "sha256:abc123"}},
45        "dependencies": ["pcre2", "gettext"],
46        "build_dependencies": ["gettext", "pkgconf"],
47        "uses_from_macos": ["curl", "expat", {"llvm": ["build"]}],
48        "ruby_source_path": "Formula/g/git.rb"
49    }"#;
50
51    #[test]
52    fn parses_all_unified_fields_including_checksum() {
53        let f: Formula = serde_json::from_str(GIT_JSON).unwrap();
54        assert_eq!(f.stable_version(), Some("2.55.0"));
55        assert_eq!(f.stable_url(), Some("https://example/git-2.55.0.tar.xz"));
56        // The `sha256:` prefix is stripped by the accessor.
57        assert_eq!(f.stable_checksum().as_deref(), Some("abc123"));
58        assert_eq!(f.dependencies, vec!["pcre2", "gettext"]);
59        assert_eq!(f.build_dependencies, vec!["gettext", "pkgconf"]);
60        assert_eq!(f.ruby_source_path.as_deref(), Some("Formula/g/git.rb"));
61        // uses_from_macos mixes bare names and a conditional object.
62        assert_eq!(f.macos_provided(), vec!["curl", "expat", "llvm"]);
63    }
64
65    #[test]
66    fn missing_fields_default_cleanly() {
67        let f: Formula = serde_json::from_str("{}").unwrap();
68        assert_eq!(f.stable_version(), None);
69        assert_eq!(f.stable_url(), None);
70        assert_eq!(f.stable_checksum(), None);
71        assert!(f.dependencies.is_empty());
72        assert!(f.build_dependencies.is_empty());
73        assert!(f.macos_provided().is_empty());
74        assert!(f.ruby_source_path.is_none());
75    }
76
77    #[test]
78    fn empty_version_and_url_are_treated_as_absent() {
79        let f: Formula =
80            serde_json::from_str(r#"{"versions":{"stable":""},"urls":{"stable":{"url":""}}}"#)
81                .unwrap();
82        assert_eq!(f.stable_version(), None);
83        assert_eq!(f.stable_url(), None);
84    }
85
86    #[test]
87    fn uses_from_macos_name_accessor() {
88        let bare = UsesFromMacos::Name("curl".to_string());
89        assert_eq!(bare.name(), Some("curl"));
90    }
91}