Artifact 5bb4db1341c980e4b68851302c2de657dc553f7e60f91a35538666c7df8c298b:
- File
build.zig
— part of check-in
[dc0aa2f858]
at
2026-07-31 02:56:07
on branch sub-manifest
— git-import: translate a Git fast-export stream into a zit history
`zit import-git SOURCE DEST` reads a Git repository through `git fast- export` and builds a zit repository from it. The destination appears only after the export, artifact creation and publication have all completed, so a failure anywhere leaves no half-published repository behind; this is the first caller of the logstore artifact batch added earlier on this branch, which is what makes a check-in's blobs arrive as a unit.
Mode 160000 now parses as a gitlink instead of error.UnsupportedMode. That completes the split begun in the previous check-in: reporting what the stream said is the parser's job, and deciding that zit has no submodule concept is the importer's. The refusal still happens, but it now explains itself instead of failing as a parse error on input git legitimately emits.
Signed commits are refused by default rather than imported with the signature silently dropped. Fossil has no signature card, so importing one is a loss the operator should choose; --drop-signatures accepts it explicitly.
The sub-manifest parameter is analyzed, not guessed. Preflight measures the source's own name set and pins its measured optimum, and --submanifest-k overrides that only to match another repository's existing pin, which is what lets two repositories share sub-manifests by content address. On a 100,000 file corpus the analyzed value cuts the objective by roughly 3.4x against the fixed default it replaces.
Correctness is checked differentially against real Git rather than against fixtures we wrote: every materialized tree is compared to `git ls-tree` for the corresponding commit, refs and annotated tags are compared to the source, and both comparators now assert that a gitlink is absent rather than merely tolerating it, so the omission is a contract instead of an accident.
The rest is wiring. extras learns the import provenance names, root.zig exports the importer modules across the executable and library boundary, tidy and the CI step guard learn the new test roots and build steps, and the e2e help check learns that a verb spelled import_git in Zig is import-git on the command line. (user: dmestas, size: 20693) [annotate] [blame] [check-ins using]
const std = @import("std"); /// Config threaded into each per-artifact builder: target/optimize choice plus /// the single `zit` library module every root imports. storage, logstore, /// checkout, and update are folded into that one module (re-exported from /// root.zig), so there are no separate store peers to wire. const Env = struct { b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, mod: *std.Build.Module, }; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // The zit library: a Zig implementation of the Fossil formats and protocol // as specified by the four RFC drafts in docs/rfcs/. root.zig re-exports the // store layers (storage/logstore/checkout/update) and the Repository facade; // one module is the whole embeddable surface. const mod = b.addModule("zit", .{ .root_source_file = b.path("src/root.zig"), .target = target, }); const env = Env{ .b = b, .target = target, .optimize = optimize, .mod = mod, }; // `zig build test` — runs library + CLI unit tests; each helper below // wires its own artifact's inline tests into this step as it goes. The // store layers' and their sibling tests aggregate into `mod_tests` via // root.zig's test block. const test_step = b.step("test", "Run unit tests"); const mod_tests = b.addTest(.{ .root_module = build_unit_test_module(b, target, optimize) }); test_step.dependOn(&b.addRunArtifact(mod_tests).step); const git_import_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/git_import_test.zig"), .target = target, .optimize = optimize, }) }); test_step.dependOn(&b.addRunArtifact(git_import_tests).step); // Task 9 refs/provenance coverage intentionally remains opt-in until the // finalization API exists, so translation tests still run independently. const git_import_refs_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/git_import_refs_test.zig"), .target = target, .optimize = optimize, }) }); const git_import_refs_run = b.addRunArtifact(git_import_refs_tests); const git_import_refs_step = b.step("git-import-refs", "Run git import refs tests"); git_import_refs_step.dependOn(&git_import_refs_run.step); test_step.dependOn(&git_import_refs_run.step); const git_import_pipe_helper = b.addExecutable(.{ .name = "git-import-cli-pipe-helper", .root_module = b.createModule(.{ .root_source_file = b.path("src/git_import_cli_pipe_helper.zig"), .target = target, .optimize = optimize, }), }); const git_import_cli_options = b.addOptions(); git_import_cli_options.addOptionPath("git_import_pipe_helper", git_import_pipe_helper.getEmittedBin()); // Task 10 transaction coverage guards the executable adapter as part of // the ordinary unit-test gate, and remains separately runnable for focus. const git_import_cli_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/git_import_cli_test.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "zit", .module = mod }, .{ .name = "git_import_cli_options", .module = git_import_cli_options.createModule() }, }, }) }); const git_import_cli_run = b.addRunArtifact(git_import_cli_tests); git_import_cli_run.step.dependOn(&git_import_pipe_helper.step); const git_import_cli_step = b.step("git-import-cli", "Run git import CLI transaction tests"); git_import_cli_step.dependOn(&git_import_cli_run.step); test_step.dependOn(&git_import_cli_run.step); const cli = build_cli(env, test_step); build_e2e(env, cli.exe, test_step); build_git_import_e2e(env, cli.exe, test_step); build_fuzz(env); const dst_tests = build_dst(env, test_step); build_bench(env, cli.exe, test_step); build_git_import_bench(env, test_step); build_smoke(env, cli.exe, test_step); build_cov(env, &.{ mod_tests, cli.tests, dst_tests }); } /// The unit-test compile of root.zig. A *separate* module from the exported /// `zit` one, purely so the repository files that src/packaging_test.zig pins /// (LICENSE, the package manifest, the versioned ignore-glob) can be reached /// as anonymous imports without attaching them to the public module. /// /// Attaching them to `zit` itself made every consumer's build depend on files /// outside `build.zig.zon`'s `.paths` — a fetched package then failed with /// `failed to check cache: '.fossil-settings/ignore-glob' file_hash /// FileNotFound` before compiling a line. The need is test-only, so the scope /// is test-only. fn build_unit_test_module( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) *std.Build.Module { const test_mod = b.createModule(.{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); test_mod.addAnonymousImport("license_text", .{ .root_source_file = b.path("LICENSE") }); test_mod.addAnonymousImport("build_manifest", .{ .root_source_file = b.path("build.zig.zon") }); test_mod.addAnonymousImport("ignore_glob", .{ .root_source_file = b.path(".fossil-settings/ignore-glob") }); test_mod.addAnonymousImport("build_source", .{ .root_source_file = b.path("build.zig") }); test_mod.addAnonymousImport("ci_workflow", .{ .root_source_file = b.path(".github/workflows/ci.yml") }); // The repository root, handed to the tests as a constant. src/packaging_test.zig // asks fossil which files are version-controlled, and a test binary's working // directory is not the checkout; without this the query has nowhere to run. // Embedding a *file* cannot answer the question — whether a path is tracked is // a property of the repository, not of the working tree. // // A null `path` gets a panic rather than a "." fallback. "." is the test // binary's working directory, not the checkout, so the fallback would not // degrade the pin to a wrong answer — it would degrade it to a *silent // skip*, since `fossil ls` outside a checkout fails and the pin treats that // as "no fossil here". A configure-time panic naming the cause is the // legible failure; a pin that quietly stops checking is not. const repo = b.addOptions(); const repo_root = b.build_root.path orelse @panic("build.zig: build root has no path; src/packaging_test.zig's version-control " ++ "pin needs the checkout directory and would otherwise skip itself into silence"); repo.addOption([]const u8, "root", repo_root); test_mod.addOptions("repo_options", repo); return test_mod; } /// build_cli's two artifacts: the installed `zit` exe (the e2e harness spawns /// it) and its unit-test compile (the out-of-band cov step traces it). const Cli = struct { exe: *std.Build.Step.Compile, tests: *std.Build.Step.Compile, }; /// The `zit` command-line tool: `zig build` installs it, `zig build run -- /// <args>` runs it, and its own unit tests join `test_step`. fn build_cli(env: Env, test_step: *std.Build.Step) Cli { const exe = env.b.addExecutable(.{ .name = "zit", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/main.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, }, }), }); env.b.installArtifact(exe); // `zig build run -- <args>` const run_step = env.b.step("run", "Run the zit CLI"); const run_cmd = env.b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); run_cmd.step.dependOn(env.b.getInstallStep()); if (env.b.args) |args| run_cmd.addArgs(args); const exe_tests = env.b.addTest(.{ .root_module = exe.root_module }); test_step.dependOn(&env.b.addRunArtifact(exe_tests).step); return .{ .exe = exe, .tests = exe_tests }; } /// `zig build e2e` — end-to-end tests that spawn the REAL `zit` binary in an /// isolated, pinned environment and read the resulting repository back through /// the library. The exe's path is threaded in as a build option (a `LazyPath`, /// so the binary is always rebuilt before the tests run); the `zit` module is /// imported so the tests can decode `.zit`/`.zit-checkout` state. /// /// The subprocess-driven run stays out of `zig build test` (it spawns the real /// binary ~150 times), but the test binary is *compiled* by `test` so this /// 4.6k-line file cannot silently rot — a type error here fails `test`, at no /// execution cost. fn build_e2e(env: Env, exe: *std.Build.Step.Compile, test_step: *std.Build.Step) void { const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", exe.getEmittedBin()); const filter: ?[]const u8 = blk: { const args = env.b.args orelse break :blk null; if (args.len == 2 and std.mem.eql(u8, args[0], "--test-filter")) break :blk args[1]; if (args.len == 1 and std.mem.startsWith(u8, args[0], "--test-filter=")) break :blk args[0]["--test-filter=".len..]; break :blk null; }; const e2e_mod = env.b.createModule(.{ .root_source_file = env.b.path("src/e2e.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }); const e2e_tests = env.b.addTest(.{ .root_module = e2e_mod, .filters = if (filter) |value| &.{value} else &.{} }); // Compile-only gate: `test` builds the e2e binary but never runs it. test_step.dependOn(&e2e_tests.step); const e2e_run = env.b.addRunArtifact(e2e_tests); const e2e_step = env.b.step("e2e", "End-to-end tests: spawn the real zit binary"); e2e_step.dependOn(&e2e_run.step); } /// `zig build git-import-e2e -Dgit-import-e2e-filter=<fixture>` — real-Git /// differential tests for `import-git`. Filtering is compiled into the Zig /// test artifact, rather than forwarded to its runtime argument parser. /// Like the general e2e binary this is compile-gated by `test`, while process /// execution remains opt-in; the emitted CLI path is a LazyPath, so the test /// cannot accidentally use a stale binary. fn build_git_import_e2e(env: Env, exe: *std.Build.Step.Compile, test_step: *std.Build.Step) void { const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", exe.getEmittedBin()); const filter = env.b.option([]const u8, "git-import-e2e-filter", "Compile only matching real-Git import E2E tests"); const test_mod = env.b.createModule(.{ .root_source_file = env.b.path("src/git_import_e2e.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }); const tests = env.b.addTest(.{ .root_module = test_mod, .filters = if (filter) |value| &.{value} else &.{}, }); test_step.dependOn(&tests.step); const run = env.b.addRunArtifact(tests); const step = env.b.step("git-import-e2e", "Run real-Git import differential tests"); step.dependOn(&run.step); } /// `zig build fuzz -- <target> [--seed N] [--rounds N]` — seeded deterministic /// fuzz targets, driven in a loop by an out-of-band runner; not part of `test`. fn build_fuzz(env: Env) void { const fuzz_exe = env.b.addExecutable(.{ .name = "fuzz", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/fuzz_tests.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, }, }), }); const fuzz_step = env.b.step("fuzz", "Run a seeded fuzz target (zig build fuzz -- <target> --seed N)"); const fuzz_run = env.b.addRunArtifact(fuzz_exe); if (env.b.args) |args| fuzz_run.addArgs(args); fuzz_step.dependOn(&fuzz_run.step); } /// `zig build dst [-- --seed N | --seeds K] [--steps M]` — deterministic /// simulation of the repostate derivation. No args runs the fixed smoke /// sweep (seeds 1..25 x 10_000 steps); deep sweeps pass `--seeds`. /// /// dst.zig's module reaches sim/*.zig (kept out of the public `zit` module); /// running it as a test binary is how those sibling harness files' inline unit /// tests join `zig build test`. fn build_dst(env: Env, test_step: *std.Build.Step) *std.Build.Step.Compile { const dst_exe = env.b.addExecutable(.{ .name = "dst", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/dst.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, }, }), }); const dst_step = env.b.step("dst", "Deterministic simulation (default smoke sweep; zig build dst -- --seed N)"); const dst_run = env.b.addRunArtifact(dst_exe); if (env.b.args) |args| dst_run.addArgs(args); dst_step.dependOn(&dst_run.step); const dst_tests = env.b.addTest(.{ .root_module = dst_exe.root_module }); test_step.dependOn(&env.b.addRunArtifact(dst_tests).step); return dst_tests; } /// `zig build bench -- <put|read|open|sync|submanifest> [flags]` — LogStore /// throughput, sync timing, and reproducible clone-shape measurement. Always /// ReleaseFast (a benchmark measures optimized code, not the top-level choice). /// The executable reaches the store layers through the `zit` module; its pure /// corpus tests join `zig build test` in Debug (no timing loop ever runs under /// `test`). /// /// The `serve` subcommand spawns the REAL `zit` binary (the #13 listen-before- /// prime probe), so the exe's path is threaded in as a build option — a /// `LazyPath`, so the binary is always rebuilt before the bench runs — exactly /// as e2e/smoke do. The option module is shared by the bench exe and its test /// compile so `serve_benchmark.zig` resolves `build_options.zit_exe` in both. fn build_bench(env: Env, _: *std.Build.Step.Compile, test_step: *std.Build.Step) void { const bench_zit = env.b.addExecutable(.{ .name = "zit-bench", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/main.zig"), .target = env.target, .optimize = .ReleaseFast, .imports = &.{.{ .name = "zit", .module = env.mod }}, }), }); const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", bench_zit.getEmittedBin()); const bench_imports = [_]std.Build.Module.Import{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }; const bench_exe = env.b.addExecutable(.{ .name = "bench", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/logstore_benchmark.zig"), .target = env.target, .optimize = .ReleaseFast, .imports = &bench_imports, }), }); const bench_step = env.b.step("bench", "LogStore + clone benchmarks (zig build bench -- <put|read|open|sync|submanifest> [flags])"); const bench_run = env.b.addRunArtifact(bench_exe); if (env.b.args) |args| bench_run.addArgs(args); bench_step.dependOn(&bench_run.step); const bench_tests = env.b.addTest(.{ .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/logstore_benchmark.zig"), .target = env.target, .optimize = env.optimize, .imports = &bench_imports, }) }); test_step.dependOn(&env.b.addRunArtifact(bench_tests).step); } /// `zig build import-git-bench -- [flags]` — bounded native fast-export /// translation controls. Deliberately opt-in: its full-corpus mode is a /// qualification workload, not an ordinary unit-test dependency. fn build_git_import_bench(env: Env, test_step: *std.Build.Step) void { const opts = env.b.addOptions(); opts.addOption([]const u8, "revision", env.b.option([]const u8, "revision", "Source revision recorded by import-git-bench") orelse "unknown"); const exe = env.b.addExecutable(.{ .name = "import-git-bench", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/git_import_benchmark.zig"), .target = env.target, .optimize = .ReleaseFast, .imports = &.{ .{ .name = "build_options", .module = opts.createModule() }, }, }), }); const step = env.b.step("import-git-bench", "Bounded native Git import scaling benchmark"); const run = env.b.addRunArtifact(exe); if (env.b.args) |args| run.addArgs(args); step.dependOn(&run.step); const tests = env.b.addTest(.{ .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/git_import_benchmark.zig"), .target = env.target, .optimize = env.optimize, }) }); test_step.dependOn(&env.b.addRunArtifact(tests).step); } /// `zig build smoke` — the one sanctioned use of the real `fossil` binary: /// confirms zit-produced artifacts/deltas against fossil's own /// parser/hasher/store. Confined to this step; never part of `test` or /// pre-commit. `zig build test` still compiles smoke.zig (catching type /// errors) without running its fossil-driving scenarios. fn build_smoke(env: Env, exe: *std.Build.Step.Compile, test_step: *std.Build.Step) void { // Thread the real `zit` binary's path in as a build option (a `LazyPath`, so // it is always rebuilt first), same as e2e: S5 spawns `zit serve`/`pack` as // subprocesses and clones from them with the real `fossil` client. const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", exe.getEmittedBin()); const smoke_exe = env.b.addExecutable(.{ .name = "smoke", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/smoke.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }), }); test_step.dependOn(&smoke_exe.step); const smoke_step = env.b.step("smoke", "Fossil interop smoke check (milestone-manual)"); const smoke_run = env.b.addRunArtifact(smoke_exe); if (env.b.args) |args| smoke_run.addArgs(args); smoke_step.dependOn(&smoke_run.step); } /// Reads kcov's merged `coverage.json` and prints a `src/`-only per-file line /// summary, ascending by coverage so the least-tested regions surface first. const cov_summary_py = \\import json,sys \\d=json.load(open(sys.argv[1])) \\for f in sorted(d["files"],key=lambda f:float(f["percent_covered"])): \\ print(f"{float(f['percent_covered']):6.2f}% {f['covered_lines']:>5}/{f['total_lines']:<5} {f['file'].split('/zit/')[-1]}") \\print(f"total {d['percent_covered']}% html: zig-out/cov/kcov-merged/index.html") ; /// `zig build cov` — merged line coverage over every unit-test binary via /// kcov (`brew install kcov`). Out-of-band: never part of `test`/pre-commit, /// so untested regions surface mechanically without gating a commit. fn build_cov(env: Env, test_bins: []const *std.Build.Step.Compile) void { const b = env.b; const merge = b.addSystemCommand(&.{ "kcov", "--merge" }); const merged = merge.addOutputDirectoryArg("cov"); for (test_bins) |t| { const run = b.addSystemCommand(&.{"kcov"}); run.addArg(b.fmt("--include-pattern={s}/", .{b.pathFromRoot("src")})); merge.addDirectoryArg(run.addOutputDirectoryArg("kcov")); run.addArtifactArg(t); } const install = b.addInstallDirectory(.{ .source_dir = merged, .install_dir = .prefix, .install_subdir = "cov", }); const summary = b.addSystemCommand(&.{ "python3", "-c", cov_summary_py }); summary.addFileArg(merged.path(b, "kcov-merged/coverage.json")); summary.step.dependOn(&install.step); b.step("cov", "Merged kcov line coverage (out-of-band)").dependOn(&summary.step); }