use anyhow::Context as _;
use std::path::Path;
const BUILD_ZIG_RELATIVE: &str = "packages/zig/build.zig";
fn replace_in_place(path: &Path, content: &str) -> anyhow::Result<()> {
let parent = path
.parent()
.with_context(|| format!("{} has no parent directory", path.display()))?;
let mut temporary = tempfile::NamedTempFile::new_in(parent)
.with_context(|| format!("failed to create temporary file in {}", parent.display()))?;
std::io::Write::write_all(&mut temporary, content.as_bytes())
.with_context(|| format!("failed to write temporary file for {}", path.display()))?;
temporary
.persist(path)
.map_err(|error| error.error)
.with_context(|| format!("failed to replace {}", path.display()))?;
Ok(())
}
fn ffi_include_default_span(content: &str) -> Option<std::ops::Range<usize>> {
const OPTION_NAME: &str = "\"ffi_include_path\",";
const ORELSE_LITERAL: &str = "orelse \"";
let anchor = content.find(OPTION_NAME)? + OPTION_NAME.len();
let start = content[anchor..].find(ORELSE_LITERAL)? + anchor + ORELSE_LITERAL.len();
let end = start + content[start..].find('"')?;
Some(start..end)
}
fn is_crate_name_derived_include_default(value: &str) -> bool {
value
.strip_prefix("../../crates/")
.and_then(|directory| directory.strip_suffix("-ffi/include"))
.is_some_and(|crate_directory| !crate_directory.is_empty() && !crate_directory.contains('/'))
}
pub(crate) fn migrate_zig_build_ffi_include_default(base_dir: &Path, generated: &str) -> anyhow::Result<bool> {
let path = crate::cli::pipeline::generate::write::contained_output_path(base_dir, BUILD_ZIG_RELATIVE.as_ref())?;
let Ok(content) = std::fs::read_to_string(&path) else {
return Ok(false);
};
let (Some(expected), Some(span)) = (
ffi_include_default_span(generated).map(|span| &generated[span]),
ffi_include_default_span(&content),
) else {
return Ok(false);
};
let existing = &content[span.clone()];
if existing == expected || !is_crate_name_derived_include_default(existing) {
return Ok(false);
}
let repaired = format!("{}{expected}{}", &content[..span.start], &content[span.end..]);
replace_in_place(&path, &repaired)?;
tracing::info!(
path = %path.display(),
previous = existing,
corrected = expected,
"repaired pre-existing build.zig: -Dffi_include_path default now points at the FFI crate \
directory [crates.output] ffi configures instead of the one guessed from the crate name"
);
Ok(true)
}
pub(crate) fn migrate_zig_build_c_translation(base_dir: &Path, generated: &str) -> anyhow::Result<bool> {
let path = crate::cli::pipeline::generate::write::contained_output_path(base_dir, BUILD_ZIG_RELATIVE.as_ref())?;
let Ok(content) = std::fs::read_to_string(&path) else {
return Ok(false);
};
let Some(repaired) = repair_zig_build_c_translation(&content, generated) else {
return Ok(false);
};
replace_in_place(&path, &repaired)?;
tracing::info!(
path = %path.display(),
"repaired pre-existing build.zig: C header translation and Zig 0.17-compatible paths are wired"
);
Ok(true)
}
fn repair_zig_build_c_translation(content: &str, generated: &str) -> Option<String> {
const MODULE_START: &str = " const module = b.addModule(";
const TRANSLATION_START: &str = " const ffi_header = b.pathJoin(";
const TRANSLATION_END: &str = " translate_c.addIncludePath(ffi_include);\n";
if content.contains(".addImport(\"c\",") || content.contains(".addTranslateC(") {
return None;
}
let translation_start = generated.find(TRANSLATION_START)?;
let translation_end =
generated[translation_start..].find(TRANSLATION_END)? + translation_start + TRANSLATION_END.len();
let translation = &generated[translation_start..translation_end];
let mut repaired = if content.contains(" const ffi_path = b.option(")
&& content.contains(" const ffi_include = b.option(")
{
upgrade_direct_option(content, "ffi_path").and_then(|value| upgrade_direct_option(&value, "ffi_include"))?
} else if content.contains(" const ffi_path = b.pathResolve(&.{ build_root, ffi_path_option });")
&& content.contains(" const ffi_include = b.pathResolve(&.{ build_root, ffi_include_option });")
{
content
.replacen(" const build_root = b.build_root.path orelse \".\";\n\n", "", 1)
.replacen(
" const ffi_path = b.pathResolve(&.{ build_root, ffi_path_option });",
&lazy_path_declaration("ffi_path"),
1,
)
.replacen(
" const ffi_include = b.pathResolve(&.{ build_root, ffi_include_option });",
&lazy_path_declaration("ffi_include"),
1,
)
} else {
return None;
};
if !repaired.contains(" module.addLibraryPath(.{ .cwd_relative = ffi_path });")
|| !repaired.contains(" module.addIncludePath(.{ .cwd_relative = ffi_include });")
{
return None;
}
repaired = repaired.replace(
".addLibraryPath(.{ .cwd_relative = ffi_path });",
".addLibraryPath(ffi_path);",
);
repaired = repaired.replace(
".addIncludePath(.{ .cwd_relative = ffi_include });",
".addIncludePath(ffi_include);",
);
let module_start = repaired.find(MODULE_START)?;
repaired.insert_str(module_start, &format!("{translation}\n\n"));
let module_start = module_start + translation.len() + 2;
if !repaired[module_start..].starts_with(MODULE_START) {
return None;
}
let module_end = repaired[module_start..].find(" });\n")? + module_start + " });\n".len();
repaired.insert_str(module_end, " module.addImport(\"c\", translate_c.createModule());\n");
Some(repaired)
}
fn upgrade_direct_option(content: &str, binding: &str) -> Option<String> {
let declaration = format!(" const {binding} = b.option(");
let start = content.find(&declaration)?;
let statement_end = content[start..].find(';')? + start + 1;
let line_end = content[statement_end..]
.find('\n')
.map(|offset| statement_end + offset + 1)
.unwrap_or(content.len());
let mut repaired = content.to_owned();
repaired.replace_range(
start..start + declaration.len(),
&format!(" const {binding}_option = b.option("),
);
let shift = "_option".len();
repaired.insert_str(line_end + shift, &format!("{}\n", lazy_path_declaration(binding)));
Some(repaired)
}
fn lazy_path_declaration(binding: &str) -> String {
format!(
r#" const {binding}: std.Build.LazyPath = if (std.fs.path.isAbsolute({binding}_option))
.{{ .cwd_relative = {binding}_option }}
else
b.path({binding}_option);"#
)
}
pub(crate) fn migrate_build_zig_test_target(base_dir: &Path) -> anyhow::Result<bool> {
let path = crate::cli::pipeline::generate::write::contained_output_path(base_dir, BUILD_ZIG_RELATIVE.as_ref())?;
let Ok(content) = std::fs::read_to_string(&path) else {
return Ok(false);
};
let Some(migrated) = repair_build_zig_test_target(&content) else {
return Ok(false);
};
if migrated == content {
return Ok(false);
}
replace_in_place(&path, &migrated)?;
tracing::info!(
path = %path.display(),
"repaired pre-existing build.zig: test_module now points at test/<module>_test.zig \
instead of the generated src/<module>.zig (zero test blocks)"
);
Ok(true)
}
fn repair_build_zig_test_target(content: &str) -> Option<String> {
const BLOCK_ANCHOR: &str = "const test_module = b.createModule(.{";
const BAD_PREFIX: &str = ".root_source_file = b.path(\"src/";
const BAD_SUFFIX: &str = ".zig\"),";
let lines: Vec<&str> = content.lines().collect();
let block_start = lines.iter().position(|line| line.trim() == BLOCK_ANCHOR)?;
let block_end = lines[block_start..]
.iter()
.position(|line| line.trim() == "});")
.map(|offset| block_start + offset)?;
let bad_line_index = (block_start..block_end).find(|&index| {
let trimmed = lines[index].trim();
trimmed.starts_with(BAD_PREFIX) && trimmed.ends_with(BAD_SUFFIX)
})?;
let trimmed = lines[bad_line_index].trim();
let module_name = trimmed.strip_prefix(BAD_PREFIX)?.strip_suffix(BAD_SUFFIX)?;
let field_indent = &lines[bad_line_index][..lines[bad_line_index].len() - lines[bad_line_index].trim_start().len()];
let stmt_indent = &lines[block_end][..lines[block_end].len() - lines[block_end].trim_start().len()];
let mut repaired: Vec<String> = lines.iter().map(|line| line.to_string()).collect();
repaired[bad_line_index] = format!("{field_indent}.root_source_file = b.path(\"test/{module_name}_test.zig\"),");
let import_call = format!("test_module.addImport(\"{module_name}\", module);");
if !repaired.iter().any(|line| line.contains(import_call.as_str())) {
repaired.insert(block_end + 1, format!("{stmt_indent}{import_call}"));
}
let mut joined = repaired.join("\n");
if content.ends_with('\n') {
joined.push('\n');
}
Some(joined)
}
const STALE_EXAMPLE_ZIG: &str = "const std = @import(\"std\");\n\npub fn main() !void {\n var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n defer _ = gpa.deinit();\n const allocator = gpa.allocator();\n\n const stdout = std.io.getStdOut().writer();\n try stdout.print(\"Example: module loaded successfully\\n\", .{});\n}\n";
pub(crate) fn migrate_zig_example(base_dir: &Path, relative_path: &Path, replacement: &str) -> anyhow::Result<bool> {
let path = crate::cli::pipeline::generate::write::contained_output_path(base_dir, relative_path)?;
let Ok(existing) = std::fs::read_to_string(&path) else {
return Ok(false);
};
if existing != STALE_EXAMPLE_ZIG {
return Ok(false);
}
if existing == replacement {
return Ok(false);
}
replace_in_place(&path, replacement)?;
tracing::info!(
path = %path.display(),
"repaired pre-existing packages/zig/examples/example.zig: replaced the pre-Zig-0.16 \
example (std.heap.GeneralPurposeAllocator/std.io.getStdOut) with the \
std.Io.Threaded-based one"
);
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::{NewAlefConfig, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use std::path::PathBuf;
fn freshly_generated_build_zig() -> String {
let config: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["zig"]
[[crates]]
name = "my-lib"
sources = []
"#,
)
.expect("valid config");
let resolved: ResolvedCrateConfig = config.resolve().expect("resolve").remove(0);
super::super::zig::scaffold_zig(&ApiSurface::default(), &resolved)
.expect("scaffold")
.into_iter()
.find(|file| file.path == std::path::Path::new("packages/zig/build.zig"))
.expect("build.zig must be scaffolded")
.content
}
fn known_bad_build_zig() -> String {
r#"const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const ffi_path = b.option(
[]const u8,
"ffi_path",
"Path to directory containing libmy_lib_ffi.{dylib,so,dll,a}"
) orelse "../../target/release";
const ffi_include = b.option(
[]const u8,
"ffi_include_path",
"Path to directory containing the FFI C header"
) orelse "../../crates/my-lib-ffi/include"; // hand-fixed after a crate rename
const module = b.addModule("my_lib", .{
.root_source_file = b.path("src/my_lib.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
module.addLibraryPath(.{ .cwd_relative = ffi_path });
module.addIncludePath(.{ .cwd_relative = ffi_include });
module.linkSystemLibrary("my_lib_ffi", .{});
const test_module = b.createModule(.{
.root_source_file = b.path("src/my_lib.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
test_module.addLibraryPath(.{ .cwd_relative = ffi_path });
test_module.addIncludePath(.{ .cwd_relative = ffi_include });
test_module.linkSystemLibrary("my_lib_ffi", .{});
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);
}
"#
.to_string()
}
#[test]
fn c_translation_migration_upgrades_the_direct_option_scaffold() {
let original = known_bad_build_zig();
assert!(original.contains("const ffi_path = b.option("));
assert!(original.contains("module.addIncludePath(.{ .cwd_relative = ffi_include });"));
let repaired = repair_zig_build_c_translation(&original, &freshly_generated_build_zig())
.expect("historical Alef scaffold must be recognized");
assert!(
repaired.contains("const ffi_path_option = b.option("),
"got:\n{repaired}"
);
assert!(
repaired.contains("const ffi_path: std.Build.LazyPath = if (std.fs.path.isAbsolute(ffi_path_option))"),
"got:\n{repaired}"
);
assert!(
repaired.contains("const ffi_header = b.pathJoin(&.{ ffi_include_option, \"my_lib.h\" });"),
"got:\n{repaired}"
);
assert!(
repaired.contains("const translate_c = b.addTranslateC(.{"),
"got:\n{repaired}"
);
assert_eq!(
repaired
.matches("module.addImport(\"c\", translate_c.createModule());")
.count(),
1,
"translated C module must be wired exactly once:\n{repaired}"
);
assert!(
repaired.contains("module.addLibraryPath(ffi_path);"),
"got:\n{repaired}"
);
assert!(
repaired.contains("module.addIncludePath(ffi_include);"),
"got:\n{repaired}"
);
assert!(
!repaired.contains("module.addLibraryPath(.{ .cwd_relative"),
"got:\n{repaired}"
);
assert!(
repaired.contains("orelse \"../../crates/my-lib-ffi/include\"; // hand-fixed after a crate rename"),
"unrelated hand edit must survive:\n{repaired}"
);
assert!(
repair_zig_build_c_translation(&repaired, &freshly_generated_build_zig()).is_none(),
"second pass must be a no-op"
);
}
#[test]
fn c_translation_migration_upgrades_the_interim_build_root_scaffold() {
let generated = freshly_generated_build_zig();
let translation_start = generated
.find(" const ffi_header = b.pathJoin(")
.expect("translation start");
let module_start = generated[translation_start..]
.find(" const module = b.addModule(")
.unwrap()
+ translation_start;
let mut interim = generated.clone();
interim.replace_range(translation_start..module_start, "");
interim = interim
.replacen(" module.addImport(\"c\", translate_c.createModule());\n", "", 1)
.replacen(
&lazy_path_declaration("ffi_path"),
" const ffi_path = b.pathResolve(&.{ build_root, ffi_path_option });",
1,
)
.replacen(
&lazy_path_declaration("ffi_include"),
" const ffi_include = b.pathResolve(&.{ build_root, ffi_include_option });",
1,
)
.replacen(
" const ffi_path_option = b.option(",
" const build_root = b.build_root.path orelse \".\";\n\n const ffi_path_option = b.option(",
1,
)
.replace(
".addLibraryPath(ffi_path);",
".addLibraryPath(.{ .cwd_relative = ffi_path });",
)
.replace(
".addIncludePath(ffi_include);",
".addIncludePath(.{ .cwd_relative = ffi_include });",
);
assert!(interim.contains("const build_root = b.build_root.path orelse \".\";"));
assert!(interim.contains("const ffi_path = b.pathResolve(&.{ build_root, ffi_path_option });"));
let repaired =
repair_zig_build_c_translation(&interim, &generated).expect("interim Alef scaffold must be recognized");
assert!(!repaired.contains("b.build_root"), "got:\n{repaired}");
assert!(!repaired.contains("b.pathResolve"), "got:\n{repaired}");
assert!(
repaired.contains("const translate_c = b.addTranslateC(.{"),
"got:\n{repaired}"
);
assert!(
repaired.contains("module.addImport(\"c\", translate_c.createModule());"),
"got:\n{repaired}"
);
}
#[test]
fn repairs_only_the_test_module_target() {
let original = known_bad_build_zig();
let repaired = repair_build_zig_test_target(&original).expect("known-bad shape must match");
assert!(
repaired.contains(".root_source_file = b.path(\"test/my_lib_test.zig\"),"),
"got:\n{repaired}"
);
assert!(
repaired.contains("test_module.addImport(\"my_lib\", module);"),
"got:\n{repaired}"
);
assert!(
repaired.contains(
"const module = b.addModule(\"my_lib\", .{\n .root_source_file = b.path(\"src/my_lib.zig\"),"
),
"library module's root_source_file must not be touched, got:\n{repaired}"
);
assert!(
repaired.contains("orelse \"../../crates/my-lib-ffi/include\"; // hand-fixed after a crate rename"),
"hand edit must survive untouched, got:\n{repaired}"
);
let original_lines: std::collections::HashSet<&str> = original.lines().collect();
let unexpected_new_lines: Vec<&str> = repaired.lines().filter(|line| !original_lines.contains(line)).collect();
assert_eq!(
unexpected_new_lines,
vec![
" .root_source_file = b.path(\"test/my_lib_test.zig\"),",
" test_module.addImport(\"my_lib\", module);",
],
"no line beyond the two expected repairs should differ, got:\n{repaired}"
);
}
#[test]
fn is_a_no_op_once_already_repaired() {
let repaired_once = repair_build_zig_test_target(&known_bad_build_zig()).expect("first repair applies");
assert!(
repair_build_zig_test_target(&repaired_once).is_none(),
"a second pass over already-repaired content must be a no-op"
);
}
#[test]
fn ignores_a_build_zig_without_a_test_module_block() {
let custom = "const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n _ = b;\n}\n";
assert!(repair_build_zig_test_target(custom).is_none());
}
#[test]
fn ignores_a_build_zig_already_pointing_at_the_test_dir() {
let already_fixed = known_bad_build_zig().replacen(
".root_source_file = b.path(\"src/my_lib.zig\"),\n .target = target,\n .optimize = optimize,\n .link_libc = true,\n });\n test_module.addLibraryPath",
".root_source_file = b.path(\"test/my_lib_test.zig\"),\n .target = target,\n .optimize = optimize,\n .link_libc = true,\n });\n test_module.addImport(\"my_lib\", module);\n test_module.addLibraryPath",
1,
);
assert!(repair_build_zig_test_target(&already_fixed).is_none());
}
#[test]
fn migrates_on_disk_and_preserves_a_hand_edit() {
let dir = tempfile::tempdir().expect("tempdir");
let zig_dir = dir.path().join("packages/zig");
std::fs::create_dir_all(&zig_dir).expect("create packages/zig");
std::fs::write(zig_dir.join("build.zig"), known_bad_build_zig()).expect("write build.zig");
let changed = migrate_build_zig_test_target(dir.path()).expect("migration must not error");
assert!(changed, "known-bad file must be reported as changed");
let on_disk = std::fs::read_to_string(zig_dir.join("build.zig")).expect("read migrated build.zig");
assert!(on_disk.contains(".root_source_file = b.path(\"test/my_lib_test.zig\"),"));
assert!(
on_disk.contains("orelse \"../../crates/my-lib-ffi/include\"; // hand-fixed after a crate rename"),
"hand-fixed ffi_include_path default must survive the on-disk migration, got:\n{on_disk}"
);
let changed_again = migrate_build_zig_test_target(dir.path()).expect("second pass must not error");
assert!(
!changed_again,
"second pass over an already-repaired file must be a no-op"
);
}
#[test]
fn is_a_no_op_when_build_zig_does_not_exist() {
let dir = tempfile::tempdir().expect("tempdir");
let changed = migrate_build_zig_test_target(dir.path()).expect("must not error on a missing file");
assert!(!changed);
assert!(!dir.path().join("packages/zig/build.zig").exists());
}
const FIXED_EXAMPLE_ZIG: &str = "const std = @import(\"std\");\n\npub fn main() !void {\n var threaded: std.Io.Threaded = .init(std.heap.smp_allocator, .{});\n defer threaded.deinit();\n\n var stdout_buffer: [64]u8 = undefined;\n var stdout_writer = std.Io.File.stdout().writer(threaded.io(), &stdout_buffer);\n const stdout = &stdout_writer.interface;\n\n try stdout.print(\"Example: module loaded successfully\\n\", .{});\n try stdout.flush();\n}\n";
#[test]
fn should_replace_stale_example_zig_predating_the_zig_0_16_rewrite() {
let dir = tempfile::tempdir().expect("tempdir");
let examples_dir = dir.path().join("packages/zig/examples");
std::fs::create_dir_all(&examples_dir).expect("create packages/zig/examples");
std::fs::write(examples_dir.join("example.zig"), STALE_EXAMPLE_ZIG).expect("write stale example.zig");
let relative_path = Path::new("packages/zig/examples/example.zig");
let changed =
migrate_zig_example(dir.path(), relative_path, FIXED_EXAMPLE_ZIG).expect("migration must not error");
assert!(changed, "the known-stale example.zig must be reported as changed");
let on_disk = std::fs::read_to_string(examples_dir.join("example.zig")).expect("read migrated file");
assert_eq!(on_disk, FIXED_EXAMPLE_ZIG);
assert!(
!on_disk.contains("GeneralPurposeAllocator") && !on_disk.contains("getStdOut"),
"removed Zig 0.16 APIs must not remain"
);
let changed_again =
migrate_zig_example(dir.path(), relative_path, FIXED_EXAMPLE_ZIG).expect("second pass must not error");
assert!(
!changed_again,
"second pass over an already-migrated file must be a no-op"
);
}
#[test]
fn should_not_touch_a_hand_edited_example_zig() {
let dir = tempfile::tempdir().expect("tempdir");
let examples_dir = dir.path().join("packages/zig/examples");
std::fs::create_dir_all(&examples_dir).expect("create packages/zig/examples");
let hand_written =
"const std = @import(\"std\");\n\npub fn main() !void {\n std.debug.print(\"hello\\n\", .{});\n}\n";
std::fs::write(examples_dir.join("example.zig"), hand_written).expect("write hand-edited example.zig");
let relative_path = Path::new("packages/zig/examples/example.zig");
let changed =
migrate_zig_example(dir.path(), relative_path, FIXED_EXAMPLE_ZIG).expect("migration must not error");
assert!(!changed, "a hand-edited example.zig must never be touched");
let on_disk = std::fs::read_to_string(examples_dir.join("example.zig")).expect("read file");
assert_eq!(
on_disk, hand_written,
"hand-edited example.zig must survive byte-for-byte"
);
}
#[test]
fn migrate_zig_example_is_a_no_op_when_file_does_not_exist() {
let dir = tempfile::tempdir().expect("tempdir");
let relative_path = Path::new("packages/zig/examples/example.zig");
let changed = migrate_zig_example(dir.path(), relative_path, FIXED_EXAMPLE_ZIG).expect("must not error");
assert!(!changed);
assert!(!dir.path().join(relative_path).exists());
}
fn crate_name_derived_build_zig(include_default: &str) -> String {
format!(
"const std = @import(\"std\");\n\
\n\
pub fn build(b: *std.Build) void {{\n\
\x20 const ffi_include = b.option(\n\
\x20 []const u8,\n\
\x20 \"ffi_include_path\",\n\
\x20 \"Path to directory containing the FFI C header\"\n\
\x20 ) orelse \"{include_default}\";\n\
\x20 const module = b.addModule(\"my_lib\", .{{\n\
\x20 .root_source_file = b.path(\"src/my_lib.zig\"),\n\
\x20 }});\n\
\x20 module.addIncludePath(.{{ .cwd_relative = ffi_include }});\n\
\x20 _ = b.step(\"docs\", \"a hand-added step alef never emitted\");\n\
}}\n"
)
}
fn write_build_zig(base_dir: &Path, content: &str) -> PathBuf {
let path = base_dir.join(BUILD_ZIG_RELATIVE);
std::fs::create_dir_all(path.parent().expect("build.zig has a parent")).expect("create packages/zig");
std::fs::write(&path, content).expect("write build.zig");
path
}
#[test]
fn c_translation_migration_repairs_on_disk_and_is_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let original = known_bad_build_zig();
let path = write_build_zig(dir.path(), &original);
let generated = freshly_generated_build_zig();
let first = migrate_zig_build_c_translation(dir.path(), &generated).expect("first migration");
let once = std::fs::read_to_string(&path).expect("read first migration");
let second = migrate_zig_build_c_translation(dir.path(), &generated).expect("second migration");
let twice = std::fs::read_to_string(&path).expect("read second migration");
assert!(first, "known historical scaffold must migrate");
assert!(!second, "second pass must report no change");
assert_eq!(once, twice, "second pass must preserve bytes");
assert_ne!(once, original, "first pass must actually modify the file");
assert!(once.contains("module.addImport(\"c\", translate_c.createModule());"));
assert!(once.contains("// hand-fixed after a crate rename"));
}
#[test]
fn c_translation_migration_preserves_custom_existing_translation_wiring() {
let dir = tempfile::tempdir().expect("tempdir");
let existing = known_bad_build_zig()
.replacen(
" const module = b.addModule(",
concat!(
" const c_bindings = b.addTranslateC(.{\n",
" .root_source_file = b.path(\"custom.h\"),\n",
" .target = target,\n",
" .optimize = optimize,\n",
" });\n\n",
" const module = b.addModule(",
),
1,
)
.replacen(
" });\n module.addLibraryPath",
" });\n module.addImport(\"c\", c_bindings.createModule());\n module.addLibraryPath",
1,
);
let path = write_build_zig(dir.path(), &existing);
let changed = migrate_zig_build_c_translation(dir.path(), &freshly_generated_build_zig())
.expect("custom translation migration check");
let after = std::fs::read_to_string(path).expect("read preserved build.zig");
assert!(!changed, "custom C translation wiring must not be migrated");
assert_eq!(
after, existing,
"custom C translation wiring must remain byte-identical"
);
}
#[test]
fn migration_repairs_an_include_default_guessed_from_the_crate_name() {
let dir = tempfile::tempdir().expect("tempdir");
let stale = crate_name_derived_build_zig("../../crates/my-lib-rs-ffi/include");
let path = write_build_zig(dir.path(), &stale);
let generated = crate_name_derived_build_zig("../../crates/my-lib-ffi/include");
let changed = migrate_zig_build_ffi_include_default(dir.path(), &generated).expect("must not error");
assert!(changed);
assert_eq!(
std::fs::read_to_string(&path).expect("read build.zig"),
generated,
"only the include default may change; every other line stays byte-identical"
);
}
#[test]
fn migration_is_idempotent_once_the_include_default_is_correct() {
let dir = tempfile::tempdir().expect("tempdir");
let generated = crate_name_derived_build_zig("../../crates/my-lib-ffi/include");
write_build_zig(
dir.path(),
&crate_name_derived_build_zig("../../crates/my-lib-rs-ffi/include"),
);
let first = migrate_zig_build_ffi_include_default(dir.path(), &generated).expect("must not error");
let second = migrate_zig_build_ffi_include_default(dir.path(), &generated).expect("must not error");
assert!(first);
assert!(!second, "a second pass must report no change");
}
#[test]
fn migration_keeps_an_include_default_the_consumer_repointed() {
let dir = tempfile::tempdir().expect("tempdir");
let hand_edited = crate_name_derived_build_zig("../../vendor/headers");
let path = write_build_zig(dir.path(), &hand_edited);
let generated = crate_name_derived_build_zig("../../crates/my-lib-ffi/include");
let changed = migrate_zig_build_ffi_include_default(dir.path(), &generated).expect("must not error");
assert!(!changed);
assert_eq!(std::fs::read_to_string(&path).expect("read build.zig"), hand_edited);
}
#[test]
fn migration_is_a_no_op_when_build_zig_does_not_exist() {
let dir = tempfile::tempdir().expect("tempdir");
let generated = crate_name_derived_build_zig("../../crates/my-lib-ffi/include");
let changed = migrate_zig_build_ffi_include_default(dir.path(), &generated).expect("must not error");
assert!(!changed);
assert!(!dir.path().join(BUILD_ZIG_RELATIVE).exists());
}
#[test]
fn migration_reads_the_corrected_default_out_of_freshly_generated_content() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_build_zig(
dir.path(),
&crate_name_derived_build_zig("../../crates/my-lib-rs-ffi/include"),
);
let generated = freshly_generated_build_zig();
let changed = migrate_zig_build_ffi_include_default(dir.path(), &generated).expect("must not error");
assert!(changed);
assert!(
std::fs::read_to_string(&path)
.expect("read build.zig")
.contains(") orelse \"../../crates/my-lib-ffi/include\";"),
"the default must come from the generated content, not be re-derived here"
);
}
}