use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub mod brew_emulate;
pub mod error;
pub mod formula;
pub mod lockfile;
pub mod manifest;
pub mod package_index;
pub mod prebuilt;
pub mod source_build;
pub mod windows;
pub use error::{Result, ToolchainError};
pub use lockfile::{LockedTool, ToolchainLockfile, ToolchainLockfileExt, LOCKFILE_NAME};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolPlatform {
MacOS,
Windows,
}
#[derive(Debug, Clone)]
pub struct ToolchainHandle {
pub install_dir: PathBuf,
pub path_dirs: Vec<String>,
pub env: HashMap<String, String>,
}
fn arch_token() -> &'static str {
match std::env::consts::ARCH {
"aarch64" => "arm64",
other => other,
}
}
fn split_pkg(pkg: &str) -> (&str, &str) {
match pkg.split_once('@') {
Some((_, ver)) if !ver.is_empty() => (pkg, ver),
_ => (pkg, "latest"),
}
}
pub async fn ensure_toolchain(
pkg: &str,
platform: ToolPlatform,
cache_dir: &Path,
lockfile: Option<&ToolchainLockfile>,
) -> Result<ToolchainHandle> {
match platform {
ToolPlatform::Windows => {
let keg = windows::ensure_windows_keg(pkg, cache_dir, lockfile).await?;
build_handle_from_keg(keg).await
}
ToolPlatform::MacOS => {
let keg = ensure_macos_keg(pkg, cache_dir, lockfile).await?;
build_handle_from_keg(keg).await
}
}
}
pub(crate) async fn ensure_macos_keg(
pkg: &str,
cache_dir: &Path,
lockfile: Option<&ToolchainLockfile>,
) -> Result<PathBuf> {
let (formula, _version) = split_pkg(pkg);
if prebuilt::is_prebuilt_formula(formula) {
prebuilt::ensure_prebuilt(formula, cache_dir, lockfile).await
} else {
source_build::ensure_from_source(formula, cache_dir, lockfile).await
}
}
fn vendor_arch(arch: &str) -> &str {
if arch == "x86_64" {
"amd64"
} else {
arch
}
}
fn platform_token(platform: ToolPlatform) -> &'static str {
match platform {
ToolPlatform::MacOS => "macos",
ToolPlatform::Windows => "windows",
}
}
fn sanitize(tool: &str) -> String {
tool.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
pub async fn resolve_locked_tool(
tool: &str,
platform: ToolPlatform,
arch: &str,
) -> Result<LockedTool> {
let (formula, _version) = split_pkg(tool);
let (version, url, expected): (String, String, Option<String>) = match platform {
ToolPlatform::MacOS => {
if prebuilt::is_prebuilt_formula(formula) {
let r = prebuilt::resolve_prebuilt(formula, vendor_arch(arch)).await?;
(r.version, r.url, r.sha256)
} else {
let spec = source_build::resolve_source_spec(formula).await?;
let sha = (!spec.sha256.is_empty()).then_some(spec.sha256);
(spec.version, spec.tarball_url, sha)
}
}
ToolPlatform::Windows => windows::resolve_locked_windows(formula).await?,
};
let tmp = std::env::temp_dir().join(format!(
"zlayer-lock-{}-{arch}-{}",
sanitize(tool),
std::process::id()
));
let sha256 = package_index::download_verified(&url, &tmp, expected.as_deref()).await?;
let _ = tokio::fs::remove_file(&tmp).await;
Ok(LockedTool {
tool: tool.to_string(),
platform: platform_token(platform).to_string(),
arch: arch.to_string(),
version,
url,
sha256,
resolved_at: chrono::Utc::now().to_rfc3339(),
})
}
async fn build_handle_from_keg(keg: PathBuf) -> Result<ToolchainHandle> {
let manifest = manifest::KegManifest::load_or_synthesize(&keg).await?;
Ok(ToolchainHandle {
install_dir: keg,
path_dirs: manifest.path_dirs,
env: manifest.env,
})
}
pub async fn probe_ready_toolchain(
pkg: &str,
_platform: ToolPlatform,
cache_dir: &Path,
) -> Option<ToolchainHandle> {
let (formula, _version) = split_pkg(pkg);
let keg = newest_ready_keg(formula, cache_dir).await?;
build_handle_from_keg(keg).await.ok()
}
async fn newest_ready_keg(formula: &str, cache_dir: &Path) -> Option<PathBuf> {
let prefix = format!("{formula}-");
let arch_suffix = format!("-{}", arch_token());
let mut entries = tokio::fs::read_dir(cache_dir).await.ok()?;
let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
while let Ok(Some(entry)) = entries.next_entry().await {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with(&prefix) || !name.ends_with(&arch_suffix) {
continue;
}
let keg = entry.path();
if !tokio::fs::try_exists(keg.join(".ready"))
.await
.unwrap_or(false)
{
continue;
}
let mtime = entry
.metadata()
.await
.ok()
.and_then(|m| m.modified().ok())
.unwrap_or(std::time::UNIX_EPOCH);
if best.as_ref().is_none_or(|(t, _)| mtime >= *t) {
best = Some((mtime, keg));
}
}
best.map(|(_, keg)| keg)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{KegManifest, KegSource};
#[test]
fn split_pkg_plain_defaults_to_latest() {
assert_eq!(split_pkg("git"), ("git", "latest"));
}
#[test]
fn split_pkg_versioned_keeps_full_formula() {
assert_eq!(split_pkg("openssl@3"), ("openssl@3", "3"));
}
#[test]
fn split_pkg_trailing_at_is_latest() {
assert_eq!(split_pkg("weird@"), ("weird@", "latest"));
}
#[tokio::test]
async fn windows_non_portable_formula_is_not_implemented() {
let tmp = tempfile::tempdir().unwrap();
let err = ensure_toolchain("cowsay", ToolPlatform::Windows, tmp.path(), None)
.await
.unwrap_err();
assert!(matches!(err, ToolchainError::NotImplemented(_)));
}
async fn seed_legacy_git_keg(cache_dir: &Path, version: &str) -> PathBuf {
let keg = cache_dir.join(format!("git-{version}-{}", arch_token()));
tokio::fs::create_dir_all(keg.join("bin")).await.unwrap();
tokio::fs::create_dir_all(keg.join("libexec/git-core"))
.await
.unwrap();
tokio::fs::create_dir_all(keg.join("etc")).await.unwrap();
tokio::fs::write(keg.join("etc/gitconfig"), b"")
.await
.unwrap();
tokio::fs::write(keg.join(".ready"), b"").await.unwrap();
keg
}
async fn seed_keg_with_manifest(cache_dir: &Path, tool: &str, version: &str) -> PathBuf {
let keg = cache_dir.join(format!("{tool}-{version}-{}", arch_token()));
let bin = keg.join("bin");
tokio::fs::create_dir_all(&bin).await.unwrap();
let mut env = HashMap::new();
env.insert("FOO".to_string(), "bar".to_string());
let manifest = KegManifest {
tool: tool.to_string(),
version: version.to_string(),
arch: arch_token().to_string(),
platform: "macos".to_string(),
path_dirs: vec![bin.display().to_string()],
env,
source: KegSource::SourceBuild {
url: String::new(),
sha256: String::new(),
},
build_deps: vec![],
provisioned_at: "2026-06-30T00:00:00Z".to_string(),
};
manifest.write_to_keg(&keg).await.unwrap();
tokio::fs::write(keg.join(".ready"), b"").await.unwrap();
keg
}
#[tokio::test]
async fn handle_synthesized_for_legacy_git_keg_drops_dyld() {
let tmp = tempfile::tempdir().unwrap();
let keg = seed_legacy_git_keg(tmp.path(), "2.55.0").await;
let handle = build_handle_from_keg(keg.clone()).await.unwrap();
assert_eq!(handle.install_dir, keg);
assert_eq!(
handle.path_dirs,
vec![keg.join("bin").display().to_string()]
);
assert_eq!(
handle.env.get("GIT_EXEC_PATH"),
Some(&keg.join("libexec/git-core").display().to_string())
);
assert!(!handle.env.contains_key("DYLD_FALLBACK_LIBRARY_PATH"));
assert!(!handle.env.contains_key("GIT_CONFIG_SYSTEM"));
}
#[tokio::test]
async fn handle_reads_manifest_when_present() {
let tmp = tempfile::tempdir().unwrap();
let keg = seed_keg_with_manifest(tmp.path(), "jq", "1.8.2").await;
let handle = build_handle_from_keg(keg.clone()).await.unwrap();
assert_eq!(handle.install_dir, keg);
assert_eq!(
handle.path_dirs,
vec![keg.join("bin").display().to_string()]
);
assert_eq!(handle.env.get("FOO"), Some(&"bar".to_string()));
}
#[tokio::test]
async fn probe_ready_returns_handle_for_ready_keg() {
let tmp = tempfile::tempdir().unwrap();
let keg = seed_legacy_git_keg(tmp.path(), "2.55.0").await;
let handle = probe_ready_toolchain("git", ToolPlatform::MacOS, tmp.path())
.await
.expect("ready keg should be probed without install");
assert_eq!(handle.install_dir, keg);
assert_eq!(
handle.env.get("GIT_EXEC_PATH"),
Some(&keg.join("libexec/git-core").display().to_string())
);
}
#[tokio::test]
async fn probe_ready_is_generic_across_tools() {
let tmp = tempfile::tempdir().unwrap();
let keg = seed_keg_with_manifest(tmp.path(), "jq", "1.8.2").await;
let handle = probe_ready_toolchain("jq", ToolPlatform::MacOS, tmp.path())
.await
.expect("ready jq keg should be probed");
assert_eq!(handle.install_dir, keg);
assert_eq!(handle.env.get("FOO"), Some(&"bar".to_string()));
}
#[tokio::test]
async fn probe_ready_returns_none_without_ready_marker() {
let tmp = tempfile::tempdir().unwrap();
let keg = tmp.path().join(format!("git-2.55.0-{}", arch_token()));
tokio::fs::create_dir_all(keg.join("bin")).await.unwrap();
assert!(
probe_ready_toolchain("git", ToolPlatform::MacOS, tmp.path())
.await
.is_none(),
"an unstamped keg must not be injected"
);
}
#[tokio::test]
async fn probe_ready_returns_none_for_cold_or_unsupported() {
let tmp = tempfile::tempdir().unwrap();
assert!(
probe_ready_toolchain("git", ToolPlatform::MacOS, tmp.path())
.await
.is_none(),
"cold cache should probe None"
);
assert!(
probe_ready_toolchain("jq", ToolPlatform::MacOS, tmp.path())
.await
.is_none(),
"absent tool should probe None"
);
assert!(
probe_ready_toolchain("git", ToolPlatform::Windows, tmp.path())
.await
.is_none(),
"cold cache probes None on every platform"
);
}
#[tokio::test]
#[ignore = "live build test; fetches + compiles git and jq from source (macOS + CLT only)"]
async fn ensure_git_and_jq_build_from_source_and_run() {
let tmp = tempfile::tempdir().unwrap();
for (tool, version_needle) in [("git", "git version"), ("jq", "jq-")] {
let handle = ensure_toolchain(tool, ToolPlatform::MacOS, tmp.path(), None)
.await
.unwrap_or_else(|e| panic!("{tool} toolchain should build from source: {e}"));
let bin = handle.install_dir.join("bin").join(tool);
assert!(
bin.exists(),
"{tool} binary should exist at <keg>/bin/{tool}"
);
let manifest = KegManifest::read_from_keg(&handle.install_dir)
.await
.unwrap()
.unwrap_or_else(|| panic!("{tool} keg must have a manifest"));
assert_eq!(manifest.tool, tool);
let bytes = tokio::fs::read(&bin).await.expect("read binary");
assert!(
!contains_subslice_bytes(&bytes, b"@@HOMEBREW"),
"source-built {tool} must contain NO @@HOMEBREW@@ references"
);
let mut cmd = tokio::process::Command::new(&bin);
for (k, v) in &handle.env {
cmd.env(k, v);
}
let out = cmd.arg("--version").output().await.expect("run --version");
assert!(out.status.success(), "{tool} --version should succeed");
assert!(
String::from_utf8_lossy(&out.stdout).contains(version_needle)
|| String::from_utf8_lossy(&out.stderr).contains(version_needle),
"{tool} --version output should contain '{version_needle}'"
);
}
}
fn contains_subslice_bytes(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}
}