Artifact 12186e74e5671bc951a87c638269ca4abb39535c5f608c84bf554c2759f91ab1:
- File
build.zig
— part of check-in
[269e24b3c3]
at
2026-08-18 16:04:59
on branch trunk
— www: embed the docs, pinned so the reference cannot drift
The command reference and the caching write-up move into the tree as embedded documentation, which quarry serves on fossil's own /doc/VERSION/PATH route, already implemented in its api/register.go.
Fossil's www/embeddeddoc.wiki gives the reason to prefer this over the wiki, and it is the reason the pin below can exist:
The documentation files are versioned together with the source code files so it is always clear what version of the documentation goes with a particular release.
A wiki page cannot be pinned by a test; nothing ties it to a checkout. A tracked file can. reference_test.zig checks both directions -- a dispatched verb with no section, and a section naming no verb -- with a reviewed-exception list for sections that document an absence on purpose, which is what the export-fossil section is. The reference had drifted for weeks: grep, purge, config and trust all shipped undocumented, and nothing noticed because noticing needed a human to compare two lists.
The caching page's ladder is corrected while embedding it. It reported 641.9s / 58.3s / 0.31s; those were Debug numbers. Measured in the mode that ships: 29.63s full derive, 0.34s record-body replay, 0.04s image. Having a cache is worth 87x and the image a further 8.5x -- both real, neither 188x. Its footnote about a separate environment reporting 0.38s 'unreconciled' is resolved: same machine, optimized build. info also moved to the image since it was written, leaving branch and tag on the record-body rung. (user: dmestas, size: 31241) [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(.{}); // `--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 = .ReleaseFast }); // 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 = &.{ "src", "build.zig" }, .check = true }); gate_step.dependOn(&fmt_check.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_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 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 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, gate_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. 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); 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. 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 #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/git_import_benchmark.zig"), .target = env.target, .optimize = .ReleaseFast, .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); 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/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); 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); }