forjar 1.6.2

Rust-native Infrastructure as Code — bare-metal first, BLAKE3 state, provenance tracing
Documentation
//! FJ-3602 + PMAT-080: Homebrew formula generation with real checksums.
//!
//! The formula embeds the real release version and per-target SHA256
//! digests resolved from the pinned `--version` tag (spec F-3608/F-3610)
//! instead of `PLACEHOLDER_CHECKSUM` / `VERSION` literals.

use super::dist_checksums::ResolvedRelease;
use crate::core::types::DistConfig;

/// Build nested `on_linux`/`on_macos` + arch blocks with real checksums
/// from the pinned release.
fn build_platform_blocks(dist: &DistConfig, release: &ResolvedRelease) -> Result<String, String> {
    // Group targets by OS, then nest arch blocks inside.
    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,
        };
        // Skip musl targets for Homebrew (Homebrew uses glibc)
        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)
}

/// Build the `depends_on` lines from homebrew config.
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(),
    }
}

/// Build the optional `def caveats` block from homebrew config.
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()
    )
}

/// Generate a Homebrew formula with real version + checksums resolved
/// from the pinned release (PMAT-080, spec F-3608/F-3610).
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()), "");
    }
}