const std = @import("std"); // The build graph reads the canonical registries directly, so adding a test or a // binding never requires editing this file. const test_registry = @import("spec/model/tests.zon"); const binding_registry = @import("tests/implementations.zon"); const profile = @import("tools/janitor/profile.zig"); fn suiteOf(comptime test_spec_id: []const u8) profile.Suite { inline for (test_registry) |entry| { if (comptime std.mem.eql(u8, entry.id, test_spec_id)) return entry.suite; } @compileError("binding references unknown test specification: " ++ test_spec_id); } fn suiteHasBinding(comptime suite: profile.Suite) bool { inline for (binding_registry) |binding| { if (comptime suiteOf(binding.test_spec.id) == suite) return true; } return false; } fn specFixtureFor(comptime test_spec_id: []const u8) struct { []const u8, u32, []const u8 } { inline for (test_registry) |entry| { if (comptime std.mem.eql(u8, entry.id, test_spec_id)) { return .{ entry.fixture.id, entry.fixture.revision, entry.fixture.sha256 }; } } @compileError("binding references unknown test specification: " ++ test_spec_id); } pub fn build(b: *std.Build) void { const host = b.graph.host; const optimize: std.builtin.OptimizeMode = .Debug; // The specification engine is generic. The repository profile supplies every // fact that is specific to this repository. const profile_module = b.createModule(.{ .root_source_file = b.path("tools/janitor/profile.zig"), .target = host, .optimize = optimize, }); const engine_module = b.createModule(.{ .root_source_file = b.path("tools/spec-engine/src/engine.zig"), .target = host, .optimize = optimize, .imports = &.{.{ .name = "profile", .module = profile_module }}, }); const tool = b.addExecutable(.{ .name = "spec-tool", .root_module = b.createModule(.{ .root_source_file = b.path("tools/janitor/main.zig"), .target = host, .optimize = optimize, .imports = &.{ .{ .name = "engine", .module = engine_module }, .{ .name = "profile", .module = profile_module }, }, }), }); b.installArtifact(tool); const specification_run = b.addRunArtifact(tool); specification_run.setCwd(b.path(".")); specification_run.addArg("check"); specification_run.has_side_effects = true; const specification_step = b.step("spec", "Validate specification structure and projections"); specification_step.dependOn(&specification_run.step); const tool_run = b.addRunArtifact(tool); tool_run.setCwd(b.path(".")); tool_run.has_side_effects = true; if (b.args) |args| tool_run.addArgs(args); const tool_step = b.step("spec-tool", "Query or generate the specification model"); tool_step.dependOn(&tool_run.step); const generate_run = b.addRunArtifact(tool); generate_run.setCwd(b.path(".")); generate_run.addArgs(&.{ "generate", "all" }); generate_run.has_side_effects = true; const generate_step = b.step("update-spec", "Update generated specification projections"); generate_step.dependOn(&generate_run.step); const generated_run = b.addRunArtifact(tool); generated_run.setCwd(b.path(".")); generated_run.addArg("emit-zig"); const generated_source = generated_run.addOutputFileArg("spec_data.zig"); const generation_inputs = [_][]const u8{ // The package manifest owns the canonical version projected into the // executable, so generation reruns when it changes. "build.zig.zon", "spec/model/cli.zon", "spec/model/concepts.zon", "spec/model/documents.zon", "spec/model/limits.zon", "spec/model/android_packages.zon", "spec/model/classification.zon", "spec/model/thresholds.zon", "spec/model/requirements.zon", "spec/model/tests.zon", "spec/model/verification.zon", "spec/model/implementation.zon", "tests/implementations.zon", "spec/ADAPTERS.md", "spec/ARTIFACT.md", "spec/CLASSIFICATION.md", "spec/CODING_STYLE.md", "spec/CONFIGURATION.md", "spec/EXPERIMENTAL_EVIDENCE.md", "spec/FILESYSTEM_CAPABILITIES.md", "spec/GLOSSARY.md", "spec/GOALS.md", "spec/GOVERNANCE.md", "spec/LIMITS.md", "spec/MAN_PAGE.md", "spec/PRODUCT.md", "spec/REQUIREMENTS.md", "spec/TERMUX_PACKAGES.md", "spec/TESTING.md", "spec/TODO.md", "spec/UI_GUIDELINES.md", "spec/ZIG_DOCUMENTATION.md", }; for (generation_inputs) |input| generated_run.addFileInput(b.path(input)); inline for (test_registry) |entry| generated_run.addFileInput(b.path(entry.fixture.path)); const generated_format = b.addSystemCommand(&.{ b.graph.zig_exe, "fmt", "--check" }); generated_format.addFileArg(generated_source); const application_spec_data = b.createModule(.{ .root_source_file = generated_source, .target = host, .optimize = optimize, }); const application = b.addExecutable(.{ .name = "termux-janitor", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = host, .optimize = optimize, .imports = &.{.{ .name = "spec_data", .module = application_spec_data }}, }), }); b.installArtifact(application); b.getInstallStep().dependOn(&specification_run.step); const application_run = b.addRunArtifact(application); if (b.args) |args| application_run.addArgs(args); const run_step = b.step("run", "Run termux-janitor"); run_step.dependOn(&application_run.step); const generated_module = b.createModule(.{ .root_source_file = generated_source, .target = host, .optimize = optimize, }); const fixture_spec_data = b.createModule(.{ .root_source_file = generated_source, .target = host, .optimize = optimize, }); const engine_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("tools/spec-engine/src/engine.zig"), .target = host, .optimize = optimize, .imports = &.{.{ .name = "profile", .module = profile_module }}, }), }); const synthetic_profile_module = b.createModule(.{ .root_source_file = b.path("tools/spec-engine/test-profile.zig"), .target = host, .optimize = optimize, }); const synthetic_engine_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("tools/spec-engine/src/engine.zig"), .target = host, .optimize = optimize, .imports = &.{.{ .name = "profile", .module = synthetic_profile_module }}, }), }); const tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("tools/janitor/main.zig"), .target = host, .optimize = optimize, .imports = &.{ .{ .name = "engine", .module = engine_module }, .{ .name = "profile", .module = profile_module }, }, }), }); const generated_tests = b.addTest(.{ .root_module = generated_module, }); const selected_suite = b.option( []const u8, "suite", "Restrict generated fixture tests to one declared suite", ); const gate = b.addExecutable(.{ .name = "janitor-gate", .root_module = b.createModule(.{ .root_source_file = b.path("tools/janitor/gate.zig"), .target = host, .optimize = optimize, .imports = &.{.{ .name = "spec_data", .module = application_spec_data }}, }), }); const gate_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("tools/janitor/gate.zig"), .target = host, .optimize = optimize, .imports = &.{.{ .name = "spec_data", .module = application_spec_data }}, }), }); const engine_tests_run = b.addRunArtifact(engine_tests); const synthetic_engine_tests_run = b.addRunArtifact(synthetic_engine_tests); const tests_run = b.addRunArtifact(tests); const generated_tests_run = b.addRunArtifact(generated_tests); const gate_tests_run = b.addRunArtifact(gate_tests); const test_step = b.step("test", "Run specification, generated-data, and fixture tests"); const ci_only_step = b.step("test-ci-suites", "Run fixture suites reserved for CI"); test_step.dependOn(&engine_tests_run.step); test_step.dependOn(&synthetic_engine_tests_run.step); test_step.dependOn(&tests_run.step); test_step.dependOn(&generated_tests_run.step); test_step.dependOn(&gate_tests_run.step); test_step.dependOn(&generated_format.step); test_step.dependOn(&specification_run.step); inline for (comptime std.meta.tags(profile.Suite)) |suite_tag| { if (comptime profile.suiteIsConcrete(suite_tag)) { if (comptime suiteHasBinding(suite_tag)) { const suite = @tagName(suite_tag); const suite_step = addSuiteTests(b, .{ .suite = suite_tag, .tool = tool, .host = host, .optimize = optimize, .generated_source = generated_source, .fixture_spec_data = fixture_spec_data, .generation_inputs = &generation_inputs, .suite_filter = selected_suite, }); const selected = selected_suite == null or std.mem.eql(u8, selected_suite.?, suite); const local = comptime profile.suiteRunsLocally(suite_tag); if (selected and local) test_step.dependOn(suite_step); if (selected and !local) ci_only_step.dependOn(suite_step); } } } const format = b.addFmt(.{ .paths = &.{ "build.zig", "src", "tests", "tools", "spec/model" }, .check = true, }); const lint_step = b.step("lint", "Check formatting and specification structure"); lint_step.dependOn(&format.step); lint_step.dependOn(&generated_format.step); lint_step.dependOn(&specification_run.step); const check_inner_step = b.step("check-inner", "Run local gates inside the immutability gate"); check_inner_step.dependOn(test_step); check_inner_step.dependOn(lint_step); const ci_inner_step = b.step("ci-inner", "Run CI gates inside the immutability gate"); ci_inner_step.dependOn(check_inner_step); ci_inner_step.dependOn(ci_only_step); const check_gate_run = b.addRunArtifact(gate); check_gate_run.setCwd(b.path(".")); check_gate_run.addArg(b.graph.zig_exe); check_gate_run.addArg("build"); check_gate_run.addArg("check-inner"); if (selected_suite) |suite| check_gate_run.addArg(b.fmt("-Dsuite={s}", .{suite})); check_gate_run.has_side_effects = true; const ci_gate_run = b.addRunArtifact(gate); ci_gate_run.setCwd(b.path(".")); ci_gate_run.addArg(b.graph.zig_exe); ci_gate_run.addArg("build"); ci_gate_run.addArg("ci-inner"); if (selected_suite) |suite| ci_gate_run.addArg(b.fmt("-Dsuite={s}", .{suite})); ci_gate_run.has_side_effects = true; const check_step = b.step("check", "Run local gates and reject tracked-file changes"); check_step.dependOn(&check_gate_run.step); const ci_step = b.step("ci", "Run CI gates and reject tracked-file changes"); ci_step.dependOn(&ci_gate_run.step); } const SuiteTests = struct { suite: profile.Suite, tool: *std.Build.Step.Compile, host: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, generated_source: std.Build.LazyPath, fixture_spec_data: *std.Build.Module, generation_inputs: []const []const u8, suite_filter: ?[]const u8, }; /// Generate, format-check, compile, and run every bound fixture of one suite. fn addSuiteTests(b: *std.Build, options: SuiteTests) *std.Build.Step { const suite = @tagName(options.suite); const emit = b.addRunArtifact(options.tool); emit.setCwd(b.path(".")); emit.addArg("emit-tests"); const root = emit.addOutputFileArg(b.fmt("fixture_tests_{s}.zig", .{suite})); emit.addArg(suite); for (options.generation_inputs) |input| emit.addFileInput(b.path(input)); inline for (test_registry) |entry| emit.addFileInput(b.path(entry.fixture.path)); const root_module = b.createModule(.{ .root_source_file = root, .target = options.host, .optimize = options.optimize, }); inline for (binding_registry) |binding| { if (suiteOf(binding.test_spec.id) == options.suite) { emit.addFileInput(b.path(binding.source)); const fixture_module = b.createModule(.{ .root_source_file = b.path(binding.source), .target = options.host, .optimize = options.optimize, .imports = &.{.{ .name = "spec_data", .module = options.fixture_spec_data }}, }); root_module.addImport(binding.module, fixture_module); } } const format = b.addSystemCommand(&.{ b.graph.zig_exe, "fmt", "--check" }); format.addFileArg(root); const suite_tests = b.addTest(.{ .root_module = root_module }); const run = b.addRunArtifact(suite_tests); run.step.dependOn(b.getInstallStep()); run.step.dependOn(&format.step); // The build graph records one bounded, transient suite-evidence file only // when this suite's tests finished successfully. The record freezes every // graph, binding, fixture, artifact, toolchain, and option identity from // build time, so evidence can never be attributed to changed manifests. // The suite step then owns the record; a failing or interrupted run writes // no success record, and a missing record is never a failure verdict. const record = b.addRunArtifact(options.tool); record.setCwd(b.path(".")); record.addArg("record-suite"); record.addArgs(&.{ "--suite", suite }); record.addArg("--artifact"); record.addFileArg(suite_tests.getEmittedBin()); record.addArgs(&.{ "--zig", b.fmt("{d}.{d}.{d}", .{ @import("builtin").zig_version.major, @import("builtin").zig_version.minor, @import("builtin").zig_version.patch, }), "--target", options.host.result.zigTriple(b.allocator) catch @panic("out of memory"), "--optimize", @tagName(options.optimize), }); if (options.suite_filter) |filter| { record.addArgs(&.{ "--suite-filter", filter }); } inline for (binding_registry) |binding| { if (suiteOf(binding.test_spec.id) == options.suite) { const fixture_tuple = comptime specFixtureFor(binding.test_spec.id); record.addArgs(&.{"--spec"}); record.addArg(b.fmt("{s}@{d}", .{ binding.test_spec.id, binding.test_spec.revision, })); record.addArgs(&.{"--fixture"}); record.addArg(b.fmt("{s}@{d}:{s}", .{ fixture_tuple[0], fixture_tuple[1], fixture_tuple[2], })); record.addArgs(&.{"--binding"}); record.addArg(b.fmt("{s}:{s}", .{ binding.source, binding.source_sha256, })); } } record.has_side_effects = true; record.step.dependOn(&run.step); const step = b.step( b.fmt("test-suite-{s}", .{suite}), b.fmt("Run generated fixture tests for suite {s}", .{suite}), ); step.dependOn(&record.step); return step; }