use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, FunctionDef, TypeRef};
use crate::core::template_versions::toolchain;
use crate::scaffold::{readme_language_configured, scaffold_meta};
use std::path::PathBuf;
pub(crate) fn scaffold_zig(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let meta = scaffold_meta(config);
let version = &api.version;
let ffi_lib_name = config.ffi_lib_name();
let module_name = config.zig_module_name();
let ffi_crate_path = config.ffi_crate_path();
let (module_capsule_imports, test_capsule_imports): (String, String) = config
.zig
.as_ref()
.map(|c| {
let import_names = crate::core::config::languages::zig_capsule_import_names(&c.capsule_types);
let mut module_block = String::new();
let mut test_block = String::new();
for name in &import_names {
module_block.push_str(&format!(
"\n const {name}_dep = b.dependency(\"{name}\", .{{\n \
.target = target,\n .optimize = optimize,\n }});\n \
module.addImport(\"{name}\", {name}_dep.module(\"{name}\"));\n"
));
test_block.push_str(&format!(
" test_module.addImport(\"{name}\", {name}_dep.module(\"{name}\"));\n"
));
}
(module_block, test_block)
})
.unwrap_or_default();
let test_seed = scaffold_zig_test(api, config, &module_name);
let test_target_block = if test_seed.is_some() {
format!(
r#"
// Scaffold also seeds `test/{module_name}_test.zig` (create-only — never overwrites
// a real test suite once one exists) so `zig build test` has a real target to compile
// from day one instead of silently re-running `src/{module_name}.zig` with zero `test`
// blocks. ~keep
const test_module = b.createModule(.{{
.root_source_file = b.path("test/{module_name}_test.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}});
test_module.addImport("{module_name}", module);
test_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});
test_module.addIncludePath(.{{ .cwd_relative = ffi_include }});
test_module.linkSystemLibrary("{ffi_lib}", .{{}});
{test_capsule_imports}
const tests = b.addTest(.{{
.root_module = test_module,
}});
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
"#,
module_name = module_name,
ffi_lib = ffi_lib_name,
test_capsule_imports = test_capsule_imports,
)
} else {
String::new()
};
let build_zig = format!(
r#"const std = @import("std");
pub fn build(b: *std.Build) void {{
const target = b.standardTargetOptions(.{{}});
const optimize = b.standardOptimizeOption(.{{}});
// Default library/include search paths follow the conventional Cargo workspace
// layout. `alef publish package --lang zig` rewrites this file for the
// distributed tarball so consumers link the bundled lib/ and include/ dirs.
// Override with -Dffi_path=... and -Dffi_include_path=... if your layout differs.
// Both are rebased onto this package's own build root before use: `.cwd_relative`
// below resolves against the invoking process's working directory, so without this
// the defaults only find anything when zig is run from inside this directory, and
// never when the package is built as a `.path`/`.url` dependency of another
// project -- which is exactly how alef's own snippet validator consumes it. ~keep
const build_root = b.build_root.path orelse ".";
const ffi_path_option = b.option(
[]const u8,
"ffi_path",
"Path to directory containing lib{ffi_lib}.{{dylib,so,dll,a}}"
) orelse "../../target/release";
const ffi_path = b.pathResolve(&.{{ build_root, ffi_path_option }});
const ffi_include_option = b.option(
[]const u8,
"ffi_include_path",
"Path to directory containing the FFI C header"
) orelse "{ffi_crate_path}/include";
const ffi_include = b.pathResolve(&.{{ build_root, ffi_include_option }});
const module = b.addModule("{module_name}", .{{
.root_source_file = b.path("src/{module_name}.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}});
module.addLibraryPath(.{{ .cwd_relative = ffi_path }});
module.addIncludePath(.{{ .cwd_relative = ffi_include }});
module.linkSystemLibrary("{ffi_lib}", .{{}});
{module_capsule_imports}{test_target_block}
const example_module = b.createModule(.{{
.root_source_file = b.path("examples/example.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}});
example_module.addImport("{module_name}", module);
example_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});
example_module.addIncludePath(.{{ .cwd_relative = ffi_include }});
example_module.linkSystemLibrary("{ffi_lib}", .{{}});
const example_exe = b.addExecutable(.{{
.name = "example",
.root_module = example_module,
}});
const run_example = b.addRunArtifact(example_exe);
const example_step = b.step("example", "Run the example");
example_step.dependOn(&run_example.step);
}}
"#,
module_name = module_name,
ffi_lib = ffi_lib_name,
ffi_crate_path = ffi_crate_path,
module_capsule_imports = module_capsule_imports,
test_target_block = test_target_block,
);
let fingerprint = zig_fingerprint(&module_name);
let zig_capsule_deps: String = config
.zig
.as_ref()
.map(|c| {
let mut entries: Vec<String> = c
.capsule_types
.values()
.filter(|cap| !cap.package.is_empty())
.filter_map(|cap| {
let import_name = crate::core::config::languages::zig_capsule_import_name(&cap.host_type)?;
let hash_field = if cap.package_version.is_empty() {
String::new()
} else {
format!("\n .hash = \"{}\",", cap.package_version)
};
Some(format!(
" .{import_name} = .{{\n .url = \"{}\",{}\n }},",
cap.package, hash_field
))
})
.collect();
entries.sort();
entries.dedup();
entries.join("\n")
})
.unwrap_or_default();
let dependencies_block = if zig_capsule_deps.is_empty() {
".{}".to_string()
} else {
format!(".{{\n{zig_capsule_deps}\n }}")
};
let build_zig_zon = format!(
r#".{{
.name = .{module_name},
.version = "{version}",
.fingerprint = 0x{fingerprint:016x},
.minimum_zig_version = "{min_zig}",
.dependencies = {dependencies_block},
.paths = .{{
"build.zig",
"build.zig.zon",
"src",
}},
}}
"#,
module_name = module_name,
version = version,
fingerprint = fingerprint,
min_zig = toolchain::MIN_ZIG_VERSION,
dependencies_block = dependencies_block,
);
let gitignore = "zig-cache/\nzig-out/\n.zig-cache/\n";
let editorconfig = "[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\n\n[*.zig]\nindent_style = space\nindent_size = 4\n";
let license_section = meta
.license
.as_deref()
.map(|license| format!("\n## License\n\n{license}\n"))
.unwrap_or_default();
let readme = format!(
r#"# {module_name}
{description}
## Installation
Install Zig from [ziglang.org](https://ziglang.org/download/).
## Building
```sh
zig build
zig build test
```
## Usage
Add to your `build.zig.zon`:
```text
.dependencies = .{{
.{module_name} = .{{
.path = "path/to/{module_name}",
}},
}},
```
"#,
module_name = module_name,
description = meta.description,
) + &license_section;
let example_zig = r#"const std = @import("std");
pub fn main() !void {
var threaded: std.Io.Threaded = .init(std.heap.smp_allocator, .{});
defer threaded.deinit();
var stdout_buffer: [64]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(threaded.io(), &stdout_buffer);
const stdout = &stdout_writer.interface;
try stdout.print("Example: module loaded successfully\n", .{});
try stdout.flush();
}
"#;
let main_zig = format!(
"// Generated by alef. Imports the full {module_name} API.\npub const api = @import(\"{module_name}.zig\");\n",
module_name = module_name,
);
let mut files = vec![
GeneratedFile {
path: PathBuf::from("packages/zig/build.zig"),
content: build_zig,
generated_header: false,
},
GeneratedFile {
path: PathBuf::from("packages/zig/build.zig.zon"),
content: build_zig_zon,
generated_header: false,
},
GeneratedFile {
path: PathBuf::from("packages/zig/.gitignore"),
content: gitignore.to_string(),
generated_header: false,
},
GeneratedFile {
path: PathBuf::from("packages/zig/.editorconfig"),
content: editorconfig.to_string(),
generated_header: false,
},
];
if let Some(test_seed) = test_seed {
files.push(GeneratedFile {
path: PathBuf::from(format!("packages/zig/test/{module_name}_test.zig")),
content: test_seed,
generated_header: false,
});
}
files.push(GeneratedFile {
path: PathBuf::from("packages/zig/examples/example.zig"),
content: example_zig.to_string(),
generated_header: false,
});
files.push(GeneratedFile {
path: PathBuf::from("packages/zig/src/main.zig"),
content: main_zig.to_string(),
generated_header: false,
});
if !readme_language_configured(config, "zig") {
files.insert(
4,
GeneratedFile {
path: PathBuf::from("packages/zig/README.md"),
content: readme,
generated_header: false,
},
);
}
Ok(files)
}
fn scaffold_zig_test(api: &ApiSurface, config: &ResolvedCrateConfig, module_name: &str) -> Option<String> {
let (exclude_functions, exclude_types) = zig_binding_exclusions(api, config);
let function_is_visible = |f: &FunctionDef| !f.binding_excluded && !exclude_functions.contains(&f.name);
let import_line = format!("const {module_name} = @import(\"{module_name}\");\n\n");
let trivial_call_fn = api
.functions
.iter()
.find(|f| function_is_visible(f) && f.params.is_empty() && matches!(f.return_type, TypeRef::Primitive(_)));
if let Some(f) = trivial_call_fn {
return Some(import_line + &trivial_call_test(module_name, f));
}
if let Some(f) = api.functions.iter().find(|f| function_is_visible(f)) {
return Some(import_line + &symbol_reference_test(module_name, &f.name));
}
if let Some(t) = api
.types
.iter()
.find(|t| !t.binding_excluded && !t.is_trait && !exclude_types.contains(&t.name))
{
return Some(import_line + &hasdecl_test(module_name, &t.name, "type"));
}
if let Some(e) = api
.enums
.iter()
.find(|e| !e.binding_excluded && !exclude_types.contains(&e.name))
{
return Some(import_line + &hasdecl_test(module_name, &e.name, "enum"));
}
None
}
fn zig_binding_exclusions(
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> (std::collections::HashSet<String>, std::collections::HashSet<String>) {
let mut exclude_functions: std::collections::HashSet<String> = config
.zig
.as_ref()
.map(|c| c.exclude_functions.iter().cloned().collect())
.unwrap_or_default();
let mut exclude_types: std::collections::HashSet<String> = config
.zig
.as_ref()
.map(|c| c.exclude_types.iter().cloned().collect())
.unwrap_or_default();
if let Some(ffi) = &config.ffi {
exclude_functions.extend(ffi.exclude_functions.iter().cloned());
exclude_types.extend(ffi.exclude_types.iter().cloned());
}
exclude_types.extend(api.types.iter().filter(|t| t.binding_excluded).map(|t| t.name.clone()));
(exclude_functions, exclude_types)
}
fn trivial_call_test(module_name: &str, f: &FunctionDef) -> String {
let call = if f.error_type.is_some() {
format!("try {module_name}.{}()", f.name)
} else {
format!("{module_name}.{}()", f.name)
};
format!(
"// Calls the generated `{fn_name}` binding end-to-end (real FFI link, real call), so a\n\
// broken build or a removed/renamed export fails `zig build test` immediately instead of\n\
// shipping green with a suite that links nothing. Create-only scaffold seed. ~keep\n\
test \"{module_name}.{fn_name} runs\" {{\n const result = {call};\n _ = result;\n}}\n",
fn_name = f.name,
)
}
fn symbol_reference_test(module_name: &str, name: &str) -> String {
format!(
"// `{name}` isn't a zero-arg, primitive-returning function this seed can safely call\n\
// generically — its per-parameter allocator/ownership/JSON conversion contract is not\n\
// knowable here — so this *references* it rather than calling it. That is not a weaker\n\
// `@hasDecl`: taking the address forces Zig to semantically analyse the wrapper's body\n\
// and to resolve the extern C symbol that body calls, neither of which a comptime\n\
// `@hasDecl` does. Measured on Zig 0.16.0 with a deleted extern symbol: `@hasDecl` exits\n\
// 0 (\"All 1 tests passed\"); this line exits 1 with `undefined symbol: ... referenced\n\
// by ...`, matching a real call (the positive control). Same split for a type error in\n\
// the wrapper body with no extern involved.\n\
//\n\
// LIMIT — read this before trusting a green run. This proves the symbol EXISTS and the\n\
// wrapper typechecks. It does NOT prove the symbol is CORRECT. A C-level ABI change that\n\
// preserves the symbol name is invisible to it: the linker resolves by name and C\n\
// symbols carry no type information, so if the C signature changes and the generated Zig\n\
// `extern` declaration is regenerated to match it, both move together and nothing ever\n\
// disagrees. This closes \"the symbol does not exist\". It leaves \"the symbol lies\" wide\n\
// open. Create-only scaffold seed. ~keep\n\
test \"{module_name}.{name} symbol resolves\" {{\n _ = &{module_name}.{name};\n}}\n",
)
}
fn hasdecl_test(module_name: &str, name: &str, kind: &str) -> String {
format!(
"// `{name}` isn't a zero-arg, primitive-returning function this seed can safely call\n\
// generically, so this checks the generated {kind} exists at comptime instead. Create-only\n\
// scaffold seed. ~keep\n\
test \"{module_name} exposes `{name}`\" {{\n \
comptime {{\n \
if (!@hasDecl({module_name}, \"{name}\")) {{\n \
@compileError(\"{module_name} is missing expected declaration `{name}`\");\n \
}}\n \
}}\n}}\n",
)
}
fn zig_fingerprint(name: &str) -> u64 {
let name_crc = crc32_ieee(name.as_bytes());
let mut id: u32 = 0x811c_9dc5;
for byte in name.as_bytes() {
id ^= *byte as u32;
id = id.wrapping_mul(0x0100_0193);
}
if id == 0 || id == 0xffff_ffff {
id = 0x1;
}
((name_crc as u64) << 32) | (id as u64)
}
fn crc32_ieee(bytes: &[u8]) -> u32 {
let mut crc: u32 = 0xffff_ffff;
for byte in bytes {
crc ^= *byte as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xedb8_8320 & mask);
}
}
!crc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NewAlefConfig;
use crate::core::ir::{EnumDef, PrimitiveType, TypeDef};
fn resolve_config(toml_text: &str) -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(toml_text).expect("valid config");
cfg.resolve().expect("resolve").remove(0)
}
fn minimal_config() -> ResolvedCrateConfig {
resolve_config(
r#"
[workspace]
languages = ["zig"]
[[crates]]
name = "my-lib"
sources = []
"#,
)
}
fn trivial_function(name: &str) -> FunctionDef {
FunctionDef {
name: name.to_string(),
return_type: TypeRef::Primitive(PrimitiveType::Bool),
..Default::default()
}
}
#[test]
fn calls_a_visible_trivial_function_end_to_end() {
let api = ApiSurface {
functions: vec![trivial_function("ping")],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("const my_lib = @import(\"my_lib\");"), "got:\n{out}");
assert!(out.contains("test \"my_lib.ping runs\""), "got:\n{out}");
assert!(out.contains("const result = my_lib.ping();"), "got:\n{out}");
assert!(
!out.contains("try my_lib.ping()"),
"non-fallible call must not use try, got:\n{out}"
);
}
#[test]
fn calls_a_fallible_trivial_function_with_try() {
let api = ApiSurface {
functions: vec![FunctionDef {
error_type: Some("MyError".to_string()),
..trivial_function("ping")
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("const result = try my_lib.ping();"), "got:\n{out}");
}
#[test]
fn references_without_calling_a_function_that_fails_the_trivial_call_tier() {
let api = ApiSurface {
functions: vec![FunctionDef {
name: "greet".to_string(),
return_type: TypeRef::String,
..Default::default()
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("test \"my_lib.greet symbol resolves\" {"), "got:\n{out}");
assert!(out.contains("\n _ = &my_lib.greet;\n"), "got:\n{out}");
assert!(
!out.contains("my_lib.greet("),
"the reference tier must never synthesize a call, got:\n{out}"
);
assert!(
!out.contains("if (!@hasDecl("),
"a visible function must not fall through to the weaker comptime tier, got:\n{out}"
);
}
#[test]
fn reference_tier_documents_that_it_cannot_catch_an_abi_change() {
let api = ApiSurface {
functions: vec![FunctionDef {
name: "greet".to_string(),
return_type: TypeRef::String,
..Default::default()
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("It does NOT prove the symbol is CORRECT."), "got:\n{out}");
assert!(
out.contains("A C-level ABI change that"),
"the limit must name the uncaught change class, got:\n{out}"
);
assert!(out.contains("~keep"), "got:\n{out}");
}
#[test]
fn an_optional_return_falls_through_the_trivial_call_tier() {
for return_type in [
TypeRef::Optional(Box::new(TypeRef::String)),
TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Bool))),
] {
let api = ApiSurface {
functions: vec![FunctionDef {
name: "maybe_name".to_string(),
return_type: return_type.clone(),
..Default::default()
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(
out.contains("\n _ = &my_lib.maybe_name;\n"),
"{return_type:?} must fall through to the reference tier, got:\n{out}"
);
assert!(
!out.contains("my_lib.maybe_name()"),
"{return_type:?} is not a bare primitive and must never be called, got:\n{out}"
);
}
}
#[test]
fn never_seeds_against_the_synthetic_binding_helpers() {
let api = ApiSurface {
functions: vec![FunctionDef {
name: "greet".to_string(),
return_type: TypeRef::String,
..Default::default()
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
for helper in ["_last_error", "_free_string", "_error_with_message"] {
assert!(
!out.contains(helper),
"synthetic helper `{helper}` must never be the seed subject, got:\n{out}"
);
}
}
#[test]
fn hasdecl_tier_neither_calls_nor_references_its_subject() {
let api = ApiSurface {
types: vec![TypeDef {
name: "Widget".to_string(),
is_opaque: true,
..Default::default()
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("if (!@hasDecl(my_lib, \"Widget\"))"), "got:\n{out}");
assert!(out.contains("comptime {"), "got:\n{out}");
assert!(
!out.contains("my_lib.Widget"),
"the comptime tier must not emit a field access, got:\n{out}"
);
assert!(
!out.contains("_ = &"),
"the comptime tier must not emit a symbol reference, got:\n{out}"
);
assert!(
!out.contains("Widget()"),
"the comptime tier must not emit a call, got:\n{out}"
);
}
#[test]
fn skips_binding_excluded_functions() {
let api = ApiSurface {
functions: vec![
FunctionDef {
binding_excluded: true,
..trivial_function("hidden")
},
trivial_function("visible"),
],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("my_lib.visible"), "got:\n{out}");
assert!(!out.contains("hidden"), "got:\n{out}");
}
#[test]
fn skips_functions_excluded_via_zig_config() {
let config = resolve_config(
r#"
[workspace]
languages = ["zig"]
[[crates]]
name = "my-lib"
sources = []
[crates.zig]
exclude_functions = ["ping"]
"#,
);
let api = ApiSurface {
functions: vec![trivial_function("ping")],
..Default::default()
};
assert!(
scaffold_zig_test(&api, &config, "my_lib").is_none(),
"the only function is excluded, leaving nothing visible to assert against — seeding \
anything here would assert against a declaration the real generator never emits"
);
}
#[test]
fn falls_back_to_hasdecl_for_a_type_when_no_functions_exist() {
let api = ApiSurface {
types: vec![TypeDef {
name: "Widget".to_string(),
is_opaque: true,
..Default::default()
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("if (!@hasDecl(my_lib, \"Widget\"))"), "got:\n{out}");
}
#[test]
fn falls_back_to_hasdecl_for_an_enum_when_no_functions_or_types_exist() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "Color".to_string(),
..Default::default()
}],
..Default::default()
};
let out = scaffold_zig_test(&api, &minimal_config(), "my_lib").expect("a visible item must produce a seed");
assert!(out.contains("if (!@hasDecl(my_lib, \"Color\"))"), "got:\n{out}");
}
#[test]
fn seeds_nothing_for_an_empty_api_surface() {
assert!(scaffold_zig_test(&ApiSurface::default(), &minimal_config(), "my_lib").is_none());
}
#[test]
fn seeds_when_any_single_kind_is_visible() {
let with_function = ApiSurface {
functions: vec![trivial_function("ping")],
..Default::default()
};
let with_type = ApiSurface {
types: vec![TypeDef {
name: "Widget".to_string(),
is_opaque: true,
..Default::default()
}],
..Default::default()
};
let with_enum = ApiSurface {
enums: vec![EnumDef {
name: "Color".to_string(),
..Default::default()
}],
..Default::default()
};
for api in [with_function, with_type, with_enum] {
assert!(scaffold_zig_test(&api, &minimal_config(), "my_lib").is_some());
}
}
#[test]
fn build_zig_has_no_test_step_when_api_surface_is_empty() {
let files = scaffold_zig(&ApiSurface::default(), &minimal_config()).expect("scaffold");
let build_zig = &files
.iter()
.find(|f| f.path == *"packages/zig/build.zig")
.expect("build.zig must be scaffolded")
.content;
assert!(!build_zig.contains("test_module"), "got:\n{build_zig}");
assert!(!build_zig.contains("b.addTest"), "got:\n{build_zig}");
assert!(!build_zig.contains("b.step(\"test\""), "got:\n{build_zig}");
assert!(
!files.iter().any(|f| f.path == *"packages/zig/test/my_lib_test.zig"),
"no test file should be seeded when there is nothing to assert against, got: {:?}",
files.iter().map(|f| &f.path).collect::<Vec<_>>()
);
}
#[test]
fn build_zig_has_a_test_step_pointing_at_the_seed_when_api_surface_is_non_empty() {
let api = ApiSurface {
functions: vec![trivial_function("ping")],
..Default::default()
};
let files = scaffold_zig(&api, &minimal_config()).expect("scaffold");
let build_zig = &files
.iter()
.find(|f| f.path == *"packages/zig/build.zig")
.expect("build.zig must be scaffolded")
.content;
assert!(
build_zig.contains(".root_source_file = b.path(\"test/my_lib_test.zig\"),"),
"got:\n{build_zig}"
);
assert!(
build_zig.contains("b.step(\"test\", \"Run unit tests\");"),
"got:\n{build_zig}"
);
assert!(
!build_zig.contains(".root_source_file = b.path(\"src/my_lib.zig\"),\n .target = target,\n .optimize = optimize,\n .link_libc = true,\n });\n test_module"),
"test target must never point at the generated bindings module, got:\n{build_zig}"
);
let test_file = files
.iter()
.find(|f| f.path == *"packages/zig/test/my_lib_test.zig")
.expect("test/my_lib_test.zig must be seeded when the api surface is non-empty");
assert!(
test_file.content.contains("test \"my_lib.ping runs\""),
"got:\n{}",
test_file.content
);
}
fn build_zig_of(config: &ResolvedCrateConfig) -> String {
scaffold_zig(&ApiSurface::default(), config)
.expect("scaffold")
.into_iter()
.find(|f| f.path == *"packages/zig/build.zig")
.expect("build.zig must be scaffolded")
.content
}
#[test]
fn ffi_include_default_follows_configured_output_path_not_the_crate_name() {
let config = resolve_config(
r#"
[workspace]
languages = ["zig"]
[[crates]]
name = "html-to-markdown-rs"
sources = []
[crates.output]
ffi = "crates/html-to-markdown-ffi/src/"
"#,
);
let build_zig = build_zig_of(&config);
assert!(
build_zig.contains(") orelse \"../../crates/html-to-markdown-ffi/include\";"),
"got:\n{build_zig}"
);
assert!(
!build_zig.contains("html-to-markdown-rs-ffi"),
"the crate-name template must not leak back in, got:\n{build_zig}"
);
}
#[test]
fn ffi_include_default_falls_back_to_the_crate_name_convention_when_unconfigured() {
let build_zig = build_zig_of(&minimal_config());
assert!(
build_zig.contains(") orelse \"../../crates/my-lib-ffi/include\";"),
"got:\n{build_zig}"
);
}
#[test]
fn ffi_search_paths_resolve_against_the_packages_own_build_root() {
let build_zig = build_zig_of(&minimal_config());
assert!(
build_zig.contains("const build_root = b.build_root.path orelse \".\";"),
"got:\n{build_zig}"
);
assert!(
build_zig.contains("const ffi_path = b.pathResolve(&.{ build_root, ffi_path_option });"),
"got:\n{build_zig}"
);
assert!(
build_zig.contains("const ffi_include = b.pathResolve(&.{ build_root, ffi_include_option });"),
"got:\n{build_zig}"
);
}
#[test]
fn rebasing_keeps_both_option_defaults_readable_as_orelse_literals() {
let build_zig = build_zig_of(&minimal_config());
assert!(
build_zig.contains("\"ffi_path\",\n \"Path to directory containing libmy_lib_ffi.{dylib,so,dll,a}\"\n ) orelse \"../../target/release\";"),
"got:\n{build_zig}"
);
assert!(
build_zig.contains("\"ffi_include_path\",\n \"Path to directory containing the FFI C header\"\n ) orelse \"../../crates/my-lib-ffi/include\";"),
"got:\n{build_zig}"
);
}
fn declared_module_name(build_zig: &str) -> &str {
let marker = "addModule(\"";
let start = build_zig.find(marker).expect("build.zig declares a module") + marker.len();
let end = start + build_zig[start..].find('"').expect("module name is terminated");
&build_zig[start..end]
}
#[test]
fn scaffolded_build_zig_declares_the_module_the_snippet_imports() {
use crate::e2e::codegen::E2eCodegen as _;
let config = resolve_config(
r#"
[workspace]
languages = ["zig"]
[[crates]]
name = "my-lib"
sources = []
[crates.zig]
module_name = "my_lib_rs"
"#,
);
let api = ApiSurface {
functions: vec![trivial_function("ping")],
..Default::default()
};
let files = scaffold_zig(&api, &config).expect("scaffold");
let build_zig = &files
.iter()
.find(|file| file.path == *"packages/zig/build.zig")
.expect("build.zig must be scaffolded")
.content;
let module = declared_module_name(build_zig);
let mut e2e = crate::e2e::config::E2eConfig::default();
e2e.call.function = "ping".into();
let fixture = crate::e2e::fixture::Fixture {
id: "ping".into(),
description: "Ping".into(),
..Default::default()
};
let snippet = crate::e2e::codegen::zig::ZigE2eCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("snippet renders");
assert!(
snippet.contains(&format!("@import(\"{module}\")")),
"snippet must import the module build.zig declares (`{module}`):\n{snippet}"
);
}
}