use super::dist_checksums::ResolvedRelease;
use crate::core::types::DistConfig;
fn build_platform_blocks(dist: &DistConfig, release: &ResolvedRelease) -> Result<String, String> {
let mut os_groups: indexmap::IndexMap<&str, Vec<(&str, String, String)>> =
indexmap::IndexMap::new();
for t in &dist.targets {
let brew_os = match t.os.as_str() {
"linux" => "on_linux",
"darwin" => "on_macos",
_ => continue,
};
let brew_arch = match t.arch.as_str() {
"x86_64" => "on_intel",
"aarch64" => "on_arm",
_ => continue,
};
if t.libc.as_deref() == Some("musl") {
continue;
}
let (_asset, sha256) = release.asset_checksum(&t.asset)?;
let url = format!(
"https://github.com/{}/releases/download/v#{{version}}/{}",
dist.repo,
t.asset.replace("{version}", "#{version}")
);
os_groups
.entry(brew_os)
.or_default()
.push((brew_arch, url, sha256));
}
let mut blocks = String::new();
for (brew_os, arches) in &os_groups {
blocks.push_str(&format!("\n {brew_os} do\n"));
for (brew_arch, url, sha256) in arches {
blocks.push_str(&format!(
" {brew_arch} do\n url \"{url}\"\n sha256 \"{sha256}\"\n end\n"
));
}
blocks.push_str(" end\n");
}
Ok(blocks)
}
fn build_deps(dist: &DistConfig) -> String {
match dist.homebrew {
Some(ref hb) => hb
.dependencies
.iter()
.map(|d| format!(r#" depends_on "{}""#, d))
.collect::<Vec<_>>()
.join("\n"),
None => String::new(),
}
}
fn build_caveats(dist: &DistConfig) -> String {
let Some(caveats) = dist.homebrew.as_ref().and_then(|hb| hb.caveats.as_ref()) else {
return String::new();
};
format!(
r#"
def caveats
<<~EOS
{}
EOS
end
"#,
caveats.trim()
)
}
pub fn generate_homebrew(dist: &DistConfig, release: &ResolvedRelease) -> Result<String, String> {
let class_name = super::dist_generators_b::to_class_name(&dist.binary);
let desc = if dist.description.is_empty() {
&dist.binary
} else {
&dist.description
};
let platform_blocks = build_platform_blocks(dist, release)?;
let deps = build_deps(dist);
let caveats = build_caveats(dist);
Ok(format!(
r##"# Generated by forjar dist (do not edit)
class {class_name} < Formula
desc "{desc}"
homepage "{homepage}"
license "{license}"
version "{version}"
{platform_blocks}
{deps}
def install
bin.install "{binary}"
end
{caveats}
test do
assert_match "{binary}", shell_output("#{{bin}}/{binary} --version")
end
end
"##,
homepage = dist.homepage,
license = dist.license,
version = release.version,
binary = dist.binary,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::dist_checksums::parse_sha256sums;
use crate::core::types::DistBinaryTarget;
fn sample_release() -> ResolvedRelease {
let sums = "\
aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111 tool-2.0.0-x86_64-unknown-linux-gnu.tar.gz
bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222 tool-2.0.0-aarch64-apple-darwin.tar.gz
";
ResolvedRelease {
version: "2.0.0".into(),
checksums: parse_sha256sums(sums),
}
}
fn sample_dist() -> DistConfig {
DistConfig {
source: "github_release".into(),
repo: "acme/tool".into(),
binary: "tool".into(),
targets: vec![
DistBinaryTarget {
os: "linux".into(),
arch: "x86_64".into(),
asset: "tool-{version}-x86_64-unknown-linux-gnu.tar.gz".into(),
libc: Some("gnu".into()),
},
DistBinaryTarget {
os: "darwin".into(),
arch: "aarch64".into(),
asset: "tool-{version}-aarch64-apple-darwin.tar.gz".into(),
libc: None,
},
],
install_dir: "/usr/local/bin".into(),
install_dir_fallback: "~/.local/bin".into(),
checksums: Some("SHA256SUMS".into()),
checksum_algo: "sha256".into(),
description: "A tool".into(),
homepage: "https://example.com".into(),
license: "MIT".into(),
maintainer: "Acme".into(),
version_cmd: None,
latest_tag: true,
post_install: None,
homebrew: None,
nix: None,
}
}
#[test]
fn formula_embeds_real_version() {
let formula = generate_homebrew(&sample_dist(), &sample_release()).unwrap();
assert!(formula.contains(r#"version "2.0.0""#));
assert!(!formula.contains("\"VERSION\""));
}
#[test]
fn formula_embeds_real_checksums_per_arch() {
let formula = generate_homebrew(&sample_dist(), &sample_release()).unwrap();
assert!(
formula.contains("aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111")
);
assert!(
formula.contains("bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222")
);
assert!(!formula.contains("PLACEHOLDER"));
}
#[test]
fn formula_missing_checksum_errors_with_asset_name() {
let mut release = sample_release();
release.checksums.clear();
let err = generate_homebrew(&sample_dist(), &release).unwrap_err();
assert!(err.contains("tool-2.0.0-x86_64-unknown-linux-gnu.tar.gz"));
assert!(err.contains("--checksums-file"));
}
#[test]
fn build_deps_empty_without_homebrew_config() {
assert_eq!(build_deps(&sample_dist()), "");
}
#[test]
fn build_caveats_empty_without_homebrew_config() {
assert_eq!(build_caveats(&sample_dist()), "");
}
}