use super::PackageArtifact;
use super::util::copy_dir_recursive;
use crate::core::config::ResolvedCrateConfig;
use crate::publish::platform::RustTarget;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
pub fn package_zig(
config: &ResolvedCrateConfig,
target: &RustTarget,
workspace_root: &Path,
output_dir: &Path,
version: &str,
) -> Result<PackageArtifact> {
let lib_name = config.ffi_lib_name();
let header_name = config.ffi_header_name();
let module_name = config.zig_module_name();
let crate_name = &config.name;
let pkg_dir = config.package_dir(crate::core::config::extras::Language::Zig);
let platform = target.platform_for(crate::core::config::extras::Language::Go);
let pkg_name = format!("{crate_name}-zig-v{version}-{platform}");
let staging = output_dir.join(&pkg_name);
if staging.exists() {
fs::remove_dir_all(&staging)?;
}
fs::create_dir_all(&staging)?;
let pkg_src = workspace_root.join(&pkg_dir);
if !pkg_src.exists() {
anyhow::bail!("Zig package directory not found: {}", pkg_dir);
}
copy_dir_recursive(&pkg_src, &staging).context("copying Zig package")?;
let lib_dir = staging.join("lib");
let include_dir = staging.join("include");
fs::create_dir_all(&lib_dir)?;
fs::create_dir_all(&include_dir)?;
let shared_lib = target.shared_lib_name(&lib_name);
let shared_src = super::find_built_artifact(workspace_root, target, &shared_lib)
.with_context(|| format!("locating built FFI artifact `{shared_lib}` for Zig package"))?;
let shared_dst = lib_dir.join(&shared_lib);
fs::copy(&shared_src, &shared_dst).context("copying FFI .so into Zig package")?;
super::util::fix_macos_dylib_id(target, &shared_dst, &shared_lib)?;
let ffi_crate_dir = crate::publish::ffi_stage::find_ffi_crate_dir_pub(config, workspace_root);
let header_src = ffi_crate_dir.join("include").join(&header_name);
if !header_src.exists() {
anyhow::bail!(
"FFI C header not found at {} — run `alef build --lang=ffi` first",
header_src.display()
);
}
fs::copy(&header_src, include_dir.join(&header_name)).context("copying FFI header into Zig package")?;
add_bundled_paths_to_manifest(&staging.join("build.zig.zon"))?;
fs::write(
staging.join("build.zig"),
render_distributable_build_zig(&module_name, &lib_name),
)
.context("writing distributable build.zig into Zig package")?;
let archive_name = format!("{pkg_name}.tar.gz");
let archive_path = output_dir.join(&archive_name);
super::create_tar_gz(&staging, &archive_path)?;
fs::remove_dir_all(&staging).ok();
Ok(PackageArtifact {
path: archive_path,
name: archive_name,
checksum: None,
})
}
fn render_distributable_build_zig(module_name: &str, ffi_lib_name: &str) -> String {
format!(
r#"const std = @import("std");
// alef-generated for distribution. The prebuilt FFI library (lib/) and C header
// (include/) ship inside this package; link them package-relative so consumers
// resolve the native library from the fetched package itself.
pub fn build(b: *std.Build) void {{
const target = b.standardTargetOptions(.{{}});
const optimize = b.standardOptimizeOption(.{{}});
const module = b.addModule("{module_name}", .{{
.root_source_file = b.path("src/{module_name}.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}});
module.addLibraryPath(b.path("lib"));
module.addIncludePath(b.path("include"));
module.linkSystemLibrary("{ffi_lib_name}", .{{}});
}}
"#
)
}
fn add_bundled_paths_to_manifest(manifest: &Path) -> Result<()> {
let zon = fs::read_to_string(manifest).context("reading staged build.zig.zon")?;
const MARKER: &str = ".paths = .{";
let Some(pos) = zon.find(MARKER) else {
anyhow::bail!("build.zig.zon is missing a `.paths` block: {}", manifest.display());
};
if zon.contains("\"lib\"") && zon.contains("\"include\"") {
return Ok(());
}
let insert_at = pos + MARKER.len();
let mut patched = String::with_capacity(zon.len() + 32);
patched.push_str(&zon[..insert_at]);
patched.push_str("\n \"lib\",\n \"include\",");
patched.push_str(&zon[insert_at..]);
fs::write(manifest, patched).context("writing patched build.zig.zon")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn distributable_build_zig_links_bundled_lib() {
let s = render_distributable_build_zig("sample_router", "sample_router_ffi");
assert!(
s.contains("b.addModule(\"sample_router\""),
"must export the module:\n{s}"
);
assert!(
s.contains("module.addLibraryPath(b.path(\"lib\"))"),
"must link bundled lib/:\n{s}"
);
assert!(
s.contains("module.addIncludePath(b.path(\"include\"))"),
"must add bundled include/:\n{s}"
);
assert!(
s.contains("module.linkSystemLibrary(\"sample_router_ffi\""),
"must link the FFI lib:\n{s}"
);
assert!(
s.contains(".link_libc = true"),
"must link libc for FFI header symbols:\n{s}"
);
assert!(!s.contains("cwd_relative"), "must not use cwd_relative paths:\n{s}");
assert!(
!s.contains("../../target/release"),
"must not reference the workspace target dir:\n{s}"
);
}
#[test]
fn bundled_paths_added_to_manifest_idempotently() {
let dir = tempfile::tempdir().expect("tempdir");
let manifest = dir.path().join("build.zig.zon");
fs::write(
&manifest,
".{\n .name = .sample_router,\n .paths = .{\n \"build.zig\",\n \"src\",\n },\n}\n",
)
.expect("write manifest");
add_bundled_paths_to_manifest(&manifest).expect("first patch");
let once = fs::read_to_string(&manifest).expect("read");
assert!(once.contains("\"lib\""), "lib added:\n{once}");
assert!(once.contains("\"include\""), "include added:\n{once}");
add_bundled_paths_to_manifest(&manifest).expect("second patch");
let twice = fs::read_to_string(&manifest).expect("read");
assert_eq!(
once.matches("\"lib\"").count(),
twice.matches("\"lib\"").count(),
"second call must be a no-op"
);
}
#[test]
fn packaged_tarball_includes_rewritten_build_zig_and_ffis() {
let s = render_distributable_build_zig("sample_lib", "sample_lib_ffi");
assert!(
!s.contains("../../target/release"),
"rewritten build.zig must not reference workspace target dir:\n{s}"
);
assert!(
!s.contains("../../crates/sample-lib-ffi"),
"rewritten build.zig must not reference workspace crate dir:\n{s}"
);
assert!(
!s.contains("cwd_relative"),
"rewritten build.zig must use package-relative paths only:\n{s}"
);
assert!(s.contains("b.path(\"lib\")"), "must link bundled lib/ directory:\n{s}");
assert!(
s.contains("b.path(\"include\")"),
"must link bundled include/ directory:\n{s}"
);
assert!(s.contains(".link_libc = true"), "must enable libc linking:\n{s}");
}
}