Luigit
repositories / termux-janitor

termux-janitor

Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.

owned by admin

build.zig

Raw
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;
}