const std = @import("std");
/// Config threaded into each per-artifact builder: target/optimize choice
/// plus the public and private modules each root needs.
const Env = struct {
b: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
mod: *std.Build.Module,
storage: *std.Build.Module,
logstore: *std.Build.Module,
update: *std.Build.Module,
checkout: *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/.
const mod = b.addModule("zit", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
});
const checkout = b.createModule(.{
.root_source_file = b.path("src/checkout.zig"),
.target = target,
.optimize = optimize,
});
const storage = b.createModule(.{
.root_source_file = b.path("src/storage.zig"),
.target = target,
.optimize = optimize,
});
const logstore = b.createModule(.{
.root_source_file = b.path("src/logstore.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zit", .module = mod },
.{ .name = "zit_storage", .module = storage },
},
});
const update = b.createModule(.{
.root_source_file = b.path("src/update.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zit", .module = mod },
.{ .name = "zit_logstore", .module = logstore },
.{ .name = "zit_storage", .module = storage },
.{ .name = "zit_checkout", .module = checkout },
},
});
const env = Env{
.b = b,
.target = target,
.optimize = optimize,
.mod = mod,
.checkout = checkout,
.storage = storage,
.logstore = logstore,
.update = update,
};
// `zig build test` — runs library + CLI unit tests; each helper below
// wires its own artifact's inline tests into this step as it goes.
const test_step = b.step("test", "Run unit tests");
const mod_tests = b.addTest(.{ .root_module = mod });
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
const update_tests = b.addTest(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/update_test.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zit", .module = mod },
.{ .name = "zit_checkout", .module = checkout },
.{ .name = "zit_storage", .module = storage },
.{ .name = "zit_logstore", .module = logstore },
},
}) });
const update_step = b.step("test-update", "Run update component tests");
update_step.dependOn(&b.addRunArtifact(update_tests).step);
const exe = build_cli(env, test_step);
build_e2e(env, exe, test_step);
build_fuzz(env);
build_dst(env, test_step);
build_bench(env, test_step);
build_smoke(env, test_step);
}
/// 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) *std.Build.Step.Compile {
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 },
.{ .name = "zit_checkout", .module = env.checkout },
.{ .name = "zit_storage", .module = env.storage },
.{ .name = "zit_logstore", .module = env.logstore },
.{ .name = "zit_update", .module = env.update },
},
}),
});
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;
}
/// `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 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 = "zit_checkout", .module = env.checkout },
.{ .name = "zit_storage", .module = env.storage },
.{ .name = "zit_logstore", .module = env.logstore },
.{ .name = "build_options", .module = opts.createModule() },
},
});
const e2e_tests = env.b.addTest(.{ .root_module = e2e_mod });
// 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 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 also reaches sim/*.zig and storage.zig (both kept
/// out of the public `zit` module); running it as a test binary is how
/// their inline unit tests join `zig build test`.
fn build_dst(env: Env, test_step: *std.Build.Step) void {
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 },
.{ .name = "zit_storage", .module = env.storage },
.{ .name = "zit_logstore", .module = env.logstore },
.{ .name = "zit_checkout", .module = env.checkout },
},
}),
});
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);
}
/// `zig build bench -- <put|read|open> [flags]` — LogStore throughput and
/// latency measurement. Always ReleaseFast (a benchmark measures optimized
/// code, not the top-level optimize choice). The executable reaches the
/// non-public logstore.zig/storage.zig by relative import, the same way dst
/// does; its one pure corpus test joins `zig build test` in Debug (no timing
/// loop ever runs under `test`).
fn build_bench(env: Env, test_step: *std.Build.Step) void {
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 = &.{.{ .name = "zit", .module = env.mod }},
}),
});
const bench_step = env.b.step("bench", "LogStore benchmarks (zig build bench -- <put|read|open> [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 = &.{.{ .name = "zit", .module = env.mod }},
}) });
test_step.dependOn(&env.b.addRunArtifact(bench_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, test_step: *std.Build.Step) void {
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 }},
}),
});
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);
}