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.lang.Optimize,
mod: *std.Build.Module,
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
// `--release` means ReleaseFast: zit SHIPS ReleaseFast, so that is the mode
// the default has to name. This reverses an earlier ReleaseSafe pin, and the
// reversal is deliberate rather than incidental — the cost that decided it
// is real, but it is SMALLER than this comment used to claim, and the
// correction matters more than the decision it supports.
//
// Measured on wapp (6.4 MB store), warm page cache, modes interleaved and
// repeated so neither pays a cold-start penalty:
//
// zit pack fast ~8.5 s safe ~9.5 s small ~10.7 s
// ls tip fast 0.31 s safe 0.33 s small 0.36 s (tcl, 300 MB)
// leaves 0.01 s in all three
//
// So fast buys ~12% on the write path and nothing measurable on the read
// path. The figure previously recorded here — "599 ms fast against 15.2 s
// safe, ~25x" — DOES NOT REPRODUCE on current code, in either the ratio or
// the absolute numbers, and should be treated as stale rather than as a
// second opinion: the delta encoder's search was reworked since it was
// taken. A ~25x claim would justify almost any correctness cost, which is
// exactly why leaving it uncorrected would have been the expensive part.
//
// WHAT THIS COSTS, stated plainly because it is not free. src/ carries over
// a thousand `std.debug.assert` calls, and ReleaseFast compiles every one of
// them to nothing. TigerStyle's rule is that assertions "downgrade
// catastrophic correctness bugs into liveness bugs"; that trade only exists
// while the assertion does, so in this mode a guarded corruption becomes an
// unguarded one — in an append-only store, where a bad frame is permanent
// and a crash is not.
//
// `release_assert_test.zig` is what keeps that from being dangerous, and its
// job is now load-bearing rather than belt-and-braces: it forbids a NEW
// `std.debug.assert` from standing in for validation of untrusted input in a
// `pub fn` that returns an error union. An assert is not a rejection, and in
// this mode it is not even a crash — it is nothing at all, and the bad data
// flows through. Validation of anything reaching zit from a peer or a
// repository file must be a returned error. The guard is the enforcement.
//
// STATING A PREFERENCE MAKES IT THE ONLY REACHABLE RELEASE MODE, and that is
// stronger than it reads. It replaces `-Doptimize=<mode>` with
// `-Drelease=<bool>` in the option surface, so `-Doptimize=ReleaseSafe` no
// longer parses — and the top-level `--release=safe` does not reach it
// either: every `--release[=mode]` resolves to the preference. Verified by
// building each and checking for the safety-check panic strings; all three
// of `--release`, `--release=safe` and `--release=fast` produce one
// byte-identical binary.
//
// The prior ReleaseSafe pin had the same property in the other direction,
// which is worth recording because the comment here used to claim otherwise:
// `--release=fast` was documented as "the explicit escape hatch" and was not
// one. It produced a ReleaseSafe binary, carrying the safety strings to prove
// it, for as long as the preference said ReleaseSafe. Anything measured
// through that flag before this commit measured the checked build.
//
// So changing the shipped mode means editing this line. That is the intended
// cost: the mode is a project decision, not a packaging accident.
//
// `zig build bench` builds its executables in this same mode. A benchmark
// must measure optimized code, and it must measure the code that SHIPS.
const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .fast });
// 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` — library + CLI unit tests, and nothing else. The store
// layers and their sibling tests aggregate into `mod_tests` via root.zig's
// test block; build_cli wires the CLI's inline tests in below. Every other
// lane hangs off `gate`, not here.
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/git_import_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "zit", .module = mod }},
}) });
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/git_import_refs_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "zit", .module = mod }},
}) });
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/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/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_export_cli_tests = b.addTest(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/git/git_export_cli_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "zit", .module = mod }},
}) });
test_step.dependOn(&b.addRunArtifact(git_export_cli_tests).step);
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);
// `zig build gate` — everything `test` runs, plus the compile-only proofs
// that the e2e and smoke harnesses still build against the current library.
//
// Those two compiles used to hang off `test`, and they are not cheap: both
// rebuild the whole `zit` module in the pinned release mode. Measured at the
// time of the split, `zig build test` was 9m13s wall while the tests it
// actually RAN accounted for well under a minute of it -- the rest was
// compilation, a large share of it these two artifacts that `test` builds
// and then never executes. A suite that slow between edits stops being run
// between edits, which is the opposite of what a unit lane is for.
//
// What did NOT move: the dst, bench and smoke *unit* tests. Those are unit
// tests despite their siblings' names, and `zig build dst`/`smoke` run the
// simulation and the interop check rather than them -- so moving them here
// would have dropped them from CI entirely, since CI invokes the lanes
// individually and never invokes `gate`. Cheap to run, and the only thing
// covering that code.
const gate_step = b.step("gate", "Full pre-merge gate: unit tests plus the e2e and smoke compile proofs");
gate_step.dependOn(test_step);
// Formatting is a merge gate because the git mirror's CI checks it: an
// unformatted commit passes every local lane and then fails in Actions,
// which is exactly the discovery order a gate exists to prevent.
const fmt_check = b.addFmt(.{ .paths = b.pathList(&.{ "src", "build.zig" }), .check = true });
gate_step.dependOn(&fmt_check.step);
// Same reasoning as the fmt check, for a class it cannot see: this project
// links no libc, and macOS links libc ALWAYS. So a call that demands libc
// -- `std.c.getenv`, `std.c.environ` -- compiles on a developer's mac,
// passes every local lane, and then refuses on both Linux runners. That is
// the discovery order a gate exists to prevent, and it happened.
//
// Compiling for the two targets CI runs is enough to catch it: the error is
// raised by the compiler at semantic analysis, so nothing needs to be run,
// and a cross-compile of the CLI costs seconds against the minutes the
// round trip through Actions costs.
const cross_step = b.step("cross", "Compile the CLI for the targets CI runs (catches libc-linkage regressions)");
for ([_][]const u8{ "x86_64-linux", "aarch64-linux" }) |triple| {
const resolved = b.resolveTargetQuery(std.Target.Query.parse(.{ .arch_os_abi = triple }) catch @panic("bad target triple"));
const cross_mod = b.createModule(.{
.root_source_file = b.path("src/root.zig"),
.target = resolved,
});
const cross_exe = b.addExecutable(.{
.name = b.fmt("zit-{s}", .{triple}),
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = resolved,
.optimize = optimize,
.imports = &.{.{ .name = "zit", .module = cross_mod }},
}),
});
// The binary is never installed or run -- only proven to compile.
cross_step.dependOn(&cross_exe.step);
}
gate_step.dependOn(cross_step);
const cli = build_cli(env, test_step);
build_e2e(env, cli.exe, gate_step);
build_git_import_e2e(env, cli.exe, gate_step);
build_git_export_e2e(env, cli.exe, gate_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, gate_step);
build_muscle(env, cli.exe, test_step, gate_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.lang.Optimize,
) *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") });
test_mod.addAnonymousImport("release_workflow", .{ .root_source_file = b.path(".github/workflows/release.yml") });
test_mod.addAnonymousImport("soak_workflow", .{ .root_source_file = b.path(".github/workflows/soak.yml") });
// The embedded command reference, pinned by src/reference_test.zig so a
// verb cannot ship undocumented. Test-only, like the workflow above: the
// exported `zit` module must not depend on a repo file outside `.paths`.
test_mod.addAnonymousImport("reference_doc", .{ .root_source_file = b.path("www/reference.html") });
// The verb sources, pinned by src/readpath_test.zig so a read verb cannot
// silently fall onto the `.full` derive. Test-only for the same
// reason as the workflow above.
test_mod.addAnonymousImport("cmd_history_source", .{ .root_source_file = b.path("src/cmd/history.zig") });
test_mod.addAnonymousImport("cmd_tracking_source", .{ .root_source_file = b.path("src/cmd/tracking.zig") });
// 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.pathResolve(&.{ b.root.root_dir.path orelse ".", b.root.sub_path });
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());
run_cmd.addPassthruArgs();
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, gate_step: *std.Build.Step) void {
const opts = env.b.addOptions();
opts.addOptionPath("zit_exe", exe.getEmittedBin());
// `zig build e2e -De2e-filter=NAME`: the build script can no longer see
// pass-through arguments, so the filter is a build option.
const filter: ?[]const u8 = env.b.option([]const u8, "e2e-filter", "Compile only matching e2e tests");
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() },
},
});
// The embedded command reference, so the e2e lane can hold every verb's
// real `--help` against what the reference documents. It lives here rather
// than beside src/reference_test.zig's structural pins because the check
// needs the help TEXT, which only the spawned binary can produce -- the
// help constants live in main.zig, outside the library test module.
e2e_mod.addAnonymousImport("reference_doc", .{ .root_source_file = env.b.path("www/reference.html") });
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.
gate_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 its compile is proved by `gate` rather than 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_export_e2e(env: Env, exe: *std.Build.Step.Compile, gate_step: *std.Build.Step) void {
const opts = env.b.addOptions();
opts.addOptionPath("zit_exe", exe.getEmittedBin());
const test_mod = env.b.createModule(.{
.root_source_file = env.b.path("src/git/git_export_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 });
gate_step.dependOn(&tests.step);
const run = env.b.addRunArtifact(tests);
const step = env.b.step("git-export-e2e", "Run real-Git export differential tests");
step.dependOn(&run.step);
}
fn build_git_import_e2e(env: Env, exe: *std.Build.Step.Compile, gate_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/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 &.{},
});
gate_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);
fuzz_run.addPassthruArgs();
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);
dst_run.addPassthruArgs();
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. Pinned to
/// ReleaseFast: the mode that ships, so a measurement describes the real binary.
/// A ReleaseSafe number would describe one nobody runs. The margin is modest --
/// ~12% on `pack`, nothing measurable on the read path -- but a benchmark's job
/// is to describe the shipped binary, not to be worth the difference.
///
/// HOW TO MEASURE THIS PROJECT WITHOUT BEING LIED TO. Every rule here was paid
/// for by a wrong answer that looked convincing, and they live beside the build
/// because each is only true of THIS build script -- a wiki page would rot the
/// day one of these lines changes, silently, while nothing failed.
///
/// * BARE `zig build` IS DEBUG, and Debug is ~150x slower than an optimized
/// build on history-walking work (`zit log tip` over tcl: 58.5 s Debug
/// against 0.33 s optimized, same store, byte-identical output). An issue
/// was once filed against this repository proposing an architectural change
/// on the strength of a 58 s figure that was simply a Debug number. Build
/// with `--release` before believing any timing.
/// * TRUST THE BINARY, NOT THE FLAG. `preferred_optimize_mode` above makes
/// the preference the ONLY reachable release mode: `--release`,
/// `--release=safe` and `--release=fast` all resolve to it and produce one
/// byte-identical binary. The `~25x pack` figure this file used to carry
/// came from a `--release=fast` that was quietly building ReleaseSafe.
/// Check which mode you actually got by looking for safety-check panic
/// strings in the binary ("integer overflow", "index out of bounds"):
/// present means ReleaseSafe, absent means Fast or Small.
/// * REPORT THE MINIMUM OF N, NEVER THE MEAN. Timing noise is one-sided -- a
/// run can be delayed by another process but never finish faster than the
/// machine allows -- so the minimum estimates the true cost and the mean
/// mostly measures how busy the box was. A single run once showed a 20%
/// REGRESSION for a change that was a 3.7x improvement.
/// * INTERLEAVE THE THINGS BEING COMPARED inside each cell, and warm the
/// store first. Run in blocks and whichever went first pays the cold read;
/// that alone reversed a fast-vs-safe verdict here.
/// * DIFF THE OUTPUT EVERY TIME. An optimization that "won" by 50x turned out
/// to be erroring out on the first artifact and printing nothing. Compare
/// against the previous binary's output before believing a speedup.
/// 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 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 = .fast,
.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 = .fast,
.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);
bench_run.addPassthruArgs();
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/git_import_benchmark.zig"),
.target = env.target,
.optimize = .fast,
.imports = &.{
.{ .name = "build_options", .module = opts.createModule() },
.{ .name = "zit", .module = env.mod },
},
}),
});
const step = env.b.step("import-git-bench", "Bounded native Git import scaling benchmark");
const run = env.b.addRunArtifact(exe);
run.addPassthruArgs();
step.dependOn(&run.step);
const tests = env.b.addTest(.{ .root_module = env.b.createModule(.{
.root_source_file = env.b.path("src/git/git_import_benchmark.zig"),
.target = env.target,
.optimize = env.optimize,
.imports = &.{.{ .name = "zit", .module = env.mod }},
}) });
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, gate_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() },
},
}),
});
gate_step.dependOn(&smoke_exe.step);
// The scenarios need a real `fossil` and stay out of `test`, but smoke.zig's
// pure REPORT ORACLES (the helpers that decide pass/fail from fossil's stdout)
// are ordinary string code and are unit-tested here: an oracle that cannot
// fail is worse than no oracle, and only a test says which it is. Runs no
// subprocess — only the `test` blocks in this file execute.
const smoke_tests = env.b.addTest(.{ .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(&env.b.addRunArtifact(smoke_tests).step);
const smoke_step = env.b.step("smoke", "Fossil interop smoke check (milestone-manual)");
const smoke_run = env.b.addRunArtifact(smoke_exe);
smoke_run.addPassthruArgs();
smoke_step.dependOn(&smoke_run.step);
}
/// `zig build muscle` — the fossil-muscle-memory differential transcript
///: one scripted session through the real `fossil` and the real
/// `zit`, compared step for step. Milestone-manual like smoke; its pure
/// judge is unit-tested under `test`.
fn build_muscle(env: Env, exe: *std.Build.Step.Compile, test_step: *std.Build.Step, gate_step: *std.Build.Step) void {
const opts = env.b.addOptions();
opts.addOptionPath("zit_exe", exe.getEmittedBin());
const muscle_exe = env.b.addExecutable(.{
.name = "muscle",
.root_module = env.b.createModule(.{
.root_source_file = env.b.path("src/muscle.zig"),
.target = env.target,
.optimize = env.optimize,
.imports = &.{
.{ .name = "build_options", .module = opts.createModule() },
},
}),
});
gate_step.dependOn(&muscle_exe.step);
const muscle_tests = env.b.addTest(.{ .root_module = env.b.createModule(.{
.root_source_file = env.b.path("src/muscle.zig"),
.target = env.target,
.optimize = env.optimize,
.imports = &.{
.{ .name = "build_options", .module = opts.createModule() },
},
}) });
test_step.dependOn(&env.b.addRunArtifact(muscle_tests).step);
const muscle_step = env.b.step("muscle", "fossil-muscle-memory differential transcript (milestone-manual)");
const muscle_run = env.b.addRunArtifact(muscle_exe);
muscle_run.addPassthruArgs();
muscle_step.dependOn(&muscle_run.step);
// `zig build ux` — the same session, with every step's exact bytes kept
// under zig-out/ux for the fossil-vs-zit reading page.
const ux_step = env.b.step("ux", "muscle session with full transcripts under zig-out/ux");
const ux_run = env.b.addRunArtifact(muscle_exe);
ux_run.setEnvironmentVariable("MUSCLE_TRANSCRIPT", env.b.pathResolve(&.{ env.b.root.root_dir.path orelse ".", env.b.root.sub_path, "zig-out", "ux" }));
ux_step.dependOn(&ux_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.pathResolve(&.{ b.root.root_dir.path orelse ".", b.root.sub_path, "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);
}