use crate::core::types::DistConfig;
fn build_checksum_snippet(dist: &DistConfig) -> String {
if dist.checksums.is_some() {
let sums_file = dist.checksums.as_deref().unwrap_or("SHA256SUMS");
format!(
r#"
verify_checksum() {{
SUMS_URL="https://github.com/${{REPO}}/releases/download/${{TAG}}/{sums_file}"
info "downloading checksums..."
CHECKSUMS=$(download "$SUMS_URL") || die "failed to download checksums"
EXPECTED=$(echo "$CHECKSUMS" | grep "$ASSET" | awk '{{print $1}}')
if [ -z "$EXPECTED" ]; then
warn "no checksum found for $ASSET — skipping verification"
return
fi
ACTUAL=$(compute_checksum "$ARCHIVE")
if [ "$ACTUAL" != "$EXPECTED" ]; then
die "checksum mismatch: expected $EXPECTED, got $ACTUAL"
fi
info "checksum verified"
}}"#
)
} else {
r#"
verify_checksum() {
info "no checksums configured — skipping verification"
}"#
.to_string()
}
}
fn build_asset_cases(dist: &DistConfig) -> String {
let mut grouped: indexmap::IndexMap<
(String, String),
Vec<&crate::core::types::DistBinaryTarget>,
> = indexmap::IndexMap::new();
for t in &dist.targets {
grouped
.entry((t.os.clone(), t.arch.clone()))
.or_default()
.push(t);
}
let mut asset_cases = String::new();
for ((os, arch), targets) in &grouped {
let body = build_case_body(targets);
asset_cases.push_str(&format!(
r#"
{os}/{arch}){body}
;;"#
));
}
asset_cases
}
fn build_case_body(targets: &[&crate::core::types::DistBinaryTarget]) -> String {
let mut body = String::new();
let has_libc_variants = targets.iter().any(|t| t.libc.is_some());
if has_libc_variants {
for t in targets {
if let Some(ref libc) = t.libc {
body.push_str(&format!(
r#"
if [ "$LIBC" = "{libc}" ]; then
ASSET="{asset}"
fi"#,
libc = libc,
asset = t.asset
));
}
}
if let Some(first) = targets.first() {
body.push_str(&format!(
r#"
[ -z "$ASSET" ] && ASSET="{}""#,
first.asset
));
}
} else if let Some(t) = targets.first() {
body.push_str(&format!(
r#"
ASSET="{}""#,
t.asset
));
}
body
}
fn build_post_install_snippet(dist: &DistConfig) -> String {
if let Some(ref script) = dist.post_install {
format!(
r#"
post_install() {{
{}
}}"#,
script.trim()
)
} else {
r#"
post_install() {
:
}"#
.to_string()
}
}
fn build_version_verify_snippet(dist: &DistConfig) -> String {
if let Some(ref cmd) = dist.version_cmd {
format!(
r#"
info "verifying install..."
if {cmd} >/dev/null 2>&1; then
info "$({cmd})"
else
warn "version check failed — binary may not be on PATH"
fi"#
)
} else {
String::new()
}
}
pub fn generate_installer(dist: &DistConfig) -> String {
let binary = &dist.binary;
let repo = &dist.repo;
let install_dir = &dist.install_dir;
let fallback_dir = &dist.install_dir_fallback;
let description = if dist.description.is_empty() {
binary
} else {
&dist.description
};
let checksum_verify = build_checksum_snippet(dist);
let asset_cases = build_asset_cases(dist);
let post_install = build_post_install_snippet(dist);
let version_verify = build_version_verify_snippet(dist);
format!(
r#"#!/bin/sh
# install.sh — generated by forjar dist (do not edit)
# {description}
# Usage: curl -sSf https://example.com/install.sh | sh
# Pinned: curl -sSf https://example.com/install.sh | sh -s -- --version v1.0.0
set -eu
BINARY="{binary}"
REPO="{repo}"
INSTALL_DIR="{install_dir}"
FALLBACK_DIR="{fallback_dir}"
TAG=""
FORCE=0
YES=0
PREFIX=""
# ── Argument parsing ──
while [ $# -gt 0 ]; do
case "$1" in
--version) TAG="$2"; shift 2 ;;
--prefix) PREFIX="$2"; shift 2 ;;
--force) FORCE=1; shift ;;
--yes|-y) YES=1; shift ;;
--help|-h) usage; exit 0 ;;
*) die "unknown option: $1" ;;
esac
done
# ── Output helpers ──
RED='' GREEN='' YELLOW='' BOLD='' RESET=''
if [ -t 1 ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'
BOLD='\033[1m'; RESET='\033[0m'
fi
info() {{ printf "${{GREEN}}info:${{RESET}} %s\n" "$1"; }}
warn() {{ printf "${{YELLOW}}warn:${{RESET}} %s\n" "$1" >&2; }}
die() {{ printf "${{RED}}error:${{RESET}} %s\n" "$1" >&2; exit 1; }}
usage() {{
cat <<USAGE
Install {binary}
USAGE:
curl -sSf <url> | sh
curl -sSf <url> | sh -s -- [OPTIONS]
OPTIONS:
--version <TAG> Install a specific version (e.g., v1.0.0)
--prefix <DIR> Install to a custom directory
--force Overwrite existing binary
--yes, -y Non-interactive mode
--help, -h Show this help
USAGE
}}
# ── Platform detection ──
detect_os() {{
case "$(uname -s)" in
Linux*) echo "linux" ;;
Darwin*) echo "darwin" ;;
*) die "unsupported OS: $(uname -s)" ;;
esac
}}
detect_arch() {{
case "$(uname -m)" in
x86_64|amd64) echo "x86_64" ;;
aarch64|arm64) echo "aarch64" ;;
*) die "unsupported architecture: $(uname -m)" ;;
esac
}}
detect_libc() {{
if [ "$(detect_os)" != "linux" ]; then
echo "none"
return
fi
if ldd --version 2>&1 | grep -qi musl; then
echo "musl"
elif command -v ldd >/dev/null 2>&1; then
echo "gnu"
else
echo "musl"
fi
}}
# ── Download helpers ──
download() {{
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1"
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$1"
else
die "curl or wget required"
fi
}}
download_file() {{
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "$2" "$1"
elif command -v wget >/dev/null 2>&1; then
wget -q -O "$2" "$1"
else
die "curl or wget required"
fi
}}
compute_checksum() {{
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{{print $1}}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{{print $1}}'
else
warn "no sha256sum or shasum found — skipping checksum"
echo ""
fi
}}
{checksum_verify}
# ── Version resolution ──
resolve_version() {{
if [ -n "$TAG" ]; then
return
fi
info "resolving latest version..."
TAG=$(download "https://api.github.com/repos/${{REPO}}/releases/latest" \
| grep '"tag_name"' | head -1 | cut -d'"' -f4) \
|| die "failed to resolve latest version"
if [ -z "$TAG" ]; then
die "could not determine latest version"
fi
info "latest version: $TAG"
}}
# ── Asset resolution ──
resolve_asset() {{
OS=$(detect_os)
ARCH=$(detect_arch)
LIBC=$(detect_libc)
ASSET=""
case "$OS/$ARCH" in{asset_cases}
*) die "no pre-built binary for $OS/$ARCH" ;;
esac
if [ -z "$ASSET" ]; then
die "no matching asset for $OS/$ARCH (libc=$LIBC)"
fi
# Expand {{version}} placeholder
VERSION_NUM="${{TAG#v}}"
ASSET=$(echo "$ASSET" | sed "s/{{version}}/$VERSION_NUM/g")
}}
{post_install}
# ── Main ──
main() {{
resolve_version
resolve_asset
ASSET_URL="https://github.com/${{REPO}}/releases/download/${{TAG}}/${{ASSET}}"
TMPDIR=$(mktemp -d)
ARCHIVE="$TMPDIR/$ASSET"
trap 'rm -rf "$TMPDIR"' EXIT
info "downloading $BINARY $TAG..."
download_file "$ASSET_URL" "$ARCHIVE" || die "download failed: $ASSET_URL"
verify_checksum
info "extracting..."
tar xzf "$ARCHIVE" -C "$TMPDIR" || die "extraction failed"
# Determine install location
DEST="${{PREFIX:-$INSTALL_DIR}}"
if [ ! -w "$DEST" ] 2>/dev/null; then
if [ -w "$FALLBACK_DIR" ] || mkdir -p "$FALLBACK_DIR" 2>/dev/null; then
DEST="$FALLBACK_DIR"
warn "$INSTALL_DIR not writable, installing to $DEST"
else
# Try with sudo
info "$INSTALL_DIR not writable, using sudo..."
sudo mkdir -p "$DEST" 2>/dev/null || die "cannot create $DEST"
sudo cp "$TMPDIR/$BINARY" "$DEST/$BINARY" || die "install failed"
sudo chmod +x "$DEST/$BINARY"
info "installed $BINARY to $DEST/$BINARY"
post_install{version_verify}
return
fi
fi
# Check existing binary
if [ -f "$DEST/$BINARY" ] && [ "$FORCE" = "0" ]; then
warn "$DEST/$BINARY already exists — use --force to overwrite"
return 1
fi
mkdir -p "$DEST" 2>/dev/null || true
cp "$TMPDIR/$BINARY" "$DEST/$BINARY" || die "install failed"
chmod +x "$DEST/$BINARY"
info "installed $BINARY to $DEST/$BINARY"
post_install{version_verify}
# PATH hint
case ":$PATH:" in
*":$DEST:"*) ;;
*) warn "add $DEST to your PATH: export PATH=\"$DEST:\$PATH\"" ;;
esac
}}
main
"#
)
}
pub fn generate_homebrew(dist: &DistConfig) -> 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 mut os_groups: indexmap::IndexMap<&str, Vec<(&str, 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 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));
}
let mut platform_blocks = String::new();
for (brew_os, arches) in &os_groups {
platform_blocks.push_str(&format!("\n {brew_os} do\n"));
for (brew_arch, url) in arches {
platform_blocks.push_str(&format!(
" {brew_arch} do\n url \"{url}\"\n sha256 \"PLACEHOLDER_CHECKSUM\"\n end\n"
));
}
platform_blocks.push_str(" end\n");
}
let deps = if let Some(ref hb) = dist.homebrew {
hb.dependencies
.iter()
.map(|d| format!(r#" depends_on "{}""#, d))
.collect::<Vec<_>>()
.join("\n")
} else {
String::new()
};
let caveats = if let Some(ref hb) = dist.homebrew {
if let Some(ref c) = hb.caveats {
format!(
r#"
def caveats
<<~EOS
{}
EOS
end
"#,
c.trim()
)
} else {
String::new()
}
} else {
String::new()
};
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,
binary = dist.binary,
)
}
pub fn generate_binstall(dist: &DistConfig) -> String {
let repo_url = format!("https://github.com/{}", dist.repo);
let mut overrides = String::new();
for t in &dist.targets {
let rust_target = super::dist_generators_b::to_rust_triple(t);
let asset_tpl = t.asset.replace("{version}", "{ version }");
overrides.push_str(&format!(
r#"
[package.metadata.binstall.overrides.{rust_target}]
pkg-url = "{repo_url}/releases/download/v{{{{ version }}}}/{asset_tpl}"
"#
));
}
format!(
r#"# Generated by forjar dist — paste into Cargo.toml
[package.metadata.binstall]
pkg-url = "{repo_url}/releases/download/v{{{{ version }}}}/{{{{ name }}}}-{{{{ version }}}}-{{{{ target }}}}{{{{ archive-suffix }}}}"
bin-dir = "{{{{ bin }}}}{{{{ binary-ext }}}}"
pkg-fmt = "tgz"
{overrides}"#
)
}