repositories / termux-janitor
termux-janitor
Interactive cleanup assistant for Termux: transparent, safe, confirmed disk reclamation.
owned by admin
build.zig
Rawconst 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;
}