302 lines
12 KiB
Zig
302 lines
12 KiB
Zig
const std = @import("std");
|
|
const Coverage = @import("build/Coverage.zig");
|
|
|
|
pub fn build(b: *std.Build) void {
|
|
const target = b.standardTargetOptions(.{});
|
|
const optimize = b.standardOptimizeOption(.{});
|
|
|
|
// -- The engine: the calculation library every frontend imports --
|
|
const engine_mod = b.addModule("engine", .{
|
|
.root_source_file = b.path("engine/src/engine.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
// -- Vaxis dependency (TUI library) --
|
|
const vaxis_dep = b.dependency("vaxis", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
// -- Engine static library (for linking into the main binary) --
|
|
const engine_lib = b.addLibrary(.{
|
|
.linkage = .static,
|
|
.name = "tally-engine",
|
|
.root_module = engine_mod,
|
|
});
|
|
b.installArtifact(engine_lib);
|
|
|
|
// -- Engine shared library (the C ABI, for Android JNI and any other FFI caller) --
|
|
const engine_shared = b.addLibrary(.{
|
|
.linkage = .dynamic,
|
|
.name = "tally",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/c_api.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
engine_shared.installHeader(b.path("include/tally.h"), "tally.h");
|
|
b.installArtifact(engine_shared);
|
|
|
|
// -- Main binary (CLI + TUI in one) --
|
|
const exe = b.addExecutable(.{
|
|
.name = "tally",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "engine", .module = engine_mod },
|
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
|
},
|
|
}),
|
|
});
|
|
b.installArtifact(exe);
|
|
|
|
// -- Tests --
|
|
const engine_tests = b.addTest(.{
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/engine.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
|
|
const cli_tests = b.addTest(.{
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "engine", .module = engine_mod },
|
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
|
},
|
|
}),
|
|
});
|
|
|
|
const run_engine_tests = b.addRunArtifact(engine_tests);
|
|
const run_cli_tests = b.addRunArtifact(cli_tests);
|
|
|
|
// The C ABI gets its own root: `c_api.zig` imports the engine by path rather than by
|
|
// name (it is the root of the shared library, which has no import graph of its own),
|
|
// so the engine test target does not reach it.
|
|
const abi_tests = b.addTest(.{
|
|
// Only this file's tests: `c_api.zig` imports the engine by path, so an
|
|
// unfiltered run would execute the whole engine suite a second time.
|
|
.filters = &.{"c_api."},
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/c_api.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
const run_abi_tests = b.addRunArtifact(abi_tests);
|
|
|
|
// The JNI layer is Android-only code, but its tests are not: they run against a
|
|
// synthetic function table on the host, which is the only way to exercise the
|
|
// marshalling without a device.
|
|
const jni_tests = b.addTest(.{
|
|
.filters = &.{"jni."},
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/jni.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
const run_jni_tests = b.addRunArtifact(jni_tests);
|
|
|
|
// And the boundary is also tested from C, through the installed header, linked
|
|
// against the real library. That is the only way to catch a header that disagrees
|
|
// with the library: struct field order and symbol names are invisible from Zig.
|
|
const c_abi_mod = b.createModule(.{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.link_libc = true,
|
|
});
|
|
c_abi_mod.addCSourceFile(.{
|
|
.file = b.path("engine/test/c_abi_test.c"),
|
|
.flags = &.{ "-std=c99", "-Wall", "-Wextra", "-Werror" },
|
|
});
|
|
c_abi_mod.addIncludePath(b.path("include"));
|
|
c_abi_mod.linkLibrary(engine_shared);
|
|
const c_abi_test = b.addExecutable(.{ .name = "c-abi-test", .root_module = c_abi_mod });
|
|
const run_c_abi_test = b.addRunArtifact(c_abi_test);
|
|
|
|
// TUI tests need their own root: src/main.zig only reaches tui.zig from
|
|
// main(), which is never analyzed in test mode, so its tests would be
|
|
// silently skipped if we relied on the CLI test target.
|
|
const tui_tests = b.addTest(.{
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/tui.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "engine", .module = engine_mod },
|
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
|
},
|
|
}),
|
|
});
|
|
const run_tui_tests = b.addRunArtifact(tui_tests);
|
|
|
|
const test_step = b.step("test", "Run unit tests");
|
|
test_step.dependOn(&run_engine_tests.step);
|
|
test_step.dependOn(&run_cli_tests.step);
|
|
test_step.dependOn(&run_tui_tests.step);
|
|
test_step.dependOn(&run_abi_tests.step);
|
|
test_step.dependOn(&run_jni_tests.step);
|
|
test_step.dependOn(&run_c_abi_test.step);
|
|
|
|
// -- Coverage step (uses kcov, Linux x86_64/aarch64 only) --
|
|
//
|
|
// One report per source tree, with disjoint include patterns so no file is
|
|
// accounted for twice. The CLI test binary also compiles tui.zig (main.zig
|
|
// imports it) but never runs its tests, so the CLI report is narrowed to
|
|
// main.zig and src/cli; without that narrowing the TUI would appear
|
|
// near-uncovered in one report and covered in another.
|
|
{
|
|
var cov = Coverage.init(b);
|
|
|
|
const engine_cov = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/engine.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
_ = cov.addReport(engine_cov, "tally-engine", &.{"engine/src"});
|
|
|
|
const cli_cov = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "engine", .module = engine_mod },
|
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
|
},
|
|
});
|
|
_ = cov.addReport(cli_cov, "tally-cli", &.{ "src/main.zig", "src/cli" });
|
|
|
|
const tui_cov = b.createModule(.{
|
|
.root_source_file = b.path("src/tui.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "engine", .module = engine_mod },
|
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
|
},
|
|
});
|
|
// tui.zig plus its views. main.zig and src/cli are deliberately excluded
|
|
// even though they are not compiled into this binary, so the two app reports
|
|
// stay disjoint by construction rather than by accident.
|
|
_ = cov.addReport(tui_cov, "tally-tui", &.{ "src/tui.zig", "src/tui" });
|
|
|
|
// The C ABI reports separately. Its test binary contains all of `engine/src`,
|
|
// but the include path narrows the report to the one file that is not already
|
|
// accounted for by the engine report: `c_api.zig` is unreachable from
|
|
// `engine.zig`, so the engine's binary does not contain it and the two reports
|
|
// stay disjoint by file even though one include path nests inside the other.
|
|
const abi_cov = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/c_api.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
_ = cov.addReport(abi_cov, "tally-abi", &.{"engine/src/c_api.zig"});
|
|
|
|
// And the JNI layer, whose tests run against a synthetic function table. Android
|
|
// code measured on a desktop: everything except the four table indices, which
|
|
// only a JVM can confirm.
|
|
const jni_cov = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/jni.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
_ = cov.addReport(jni_cov, "tally-jni", &.{"engine/src/jni.zig"});
|
|
}
|
|
|
|
// -- Android shared libraries --
|
|
//
|
|
// One `.so` per ABI, laid out the way Gradle's `jniLibs` expects, so packaging is a
|
|
// copy rather than a build step: Gradle never invokes Zig (design 1).
|
|
//
|
|
// The root here is `jni.zig` rather than `c_api.zig`: it pulls in the C ABI and adds
|
|
// the `Java_*` entry points, which are the only Android-specific code in the tree
|
|
// (design 6.3). The host library stays C-only.
|
|
//
|
|
// Always ReleaseSmall. A debug build of this library is twenty times the size and
|
|
// would be a strange thing to ship inside an APK; `zig build` without this step
|
|
// still produces a host library at whatever `-Doptimize` says.
|
|
{
|
|
const android_step = b.step("android", "Cross-compile libtally.so for Android ABIs");
|
|
const abis = [_]struct { triple: []const u8, dir: []const u8 }{
|
|
.{ .triple = "aarch64-linux-android", .dir = "arm64-v8a" },
|
|
.{ .triple = "x86_64-linux-android", .dir = "x86_64" },
|
|
.{ .triple = "arm-linux-androideabi", .dir = "armeabi-v7a" },
|
|
};
|
|
for (abis) |abi| {
|
|
const query = std.Target.Query.parse(.{ .arch_os_abi = abi.triple }) catch
|
|
@panic("bad Android target triple");
|
|
const lib = b.addLibrary(.{
|
|
.linkage = .dynamic,
|
|
.name = "tally",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/jni.zig"),
|
|
.target = b.resolveTargetQuery(query),
|
|
.optimize = .ReleaseSmall,
|
|
}),
|
|
});
|
|
// Android 15 introduced 16KB memory pages, and Play requires shared
|
|
// libraries to be aligned for them. The default here is 4KB, which loads
|
|
// fine on a 4KB device and is rejected at submission.
|
|
lib.link_z_max_page_size = 16384;
|
|
const install = b.addInstallArtifact(lib, .{
|
|
.dest_dir = .{ .override = .{ .custom = b.fmt("android/{s}", .{abi.dir}) } },
|
|
});
|
|
android_step.dependOn(&install.step);
|
|
}
|
|
}
|
|
|
|
// -- The JNI layer against a real JVM --
|
|
//
|
|
// Its own step rather than part of `test`, because it needs a JDK and the rest of the
|
|
// suite needs nothing but Zig.
|
|
//
|
|
// This is the check that was supposed to require a device. JNI's function table is
|
|
// fixed by the specification rather than by the platform, so the indices hand-written
|
|
// in `jni.zig` resolve the same way on a desktop JVM as on Android: `System.load`
|
|
// runs `JNI_OnLoad`, which round-trips a string through two of them and fails loudly
|
|
// if they are not where it thinks.
|
|
{
|
|
const step = b.step("jvm-test", "Prove the JNI function table against a real JVM (needs a JDK)");
|
|
|
|
const jni_lib = b.addLibrary(.{
|
|
.linkage = .dynamic,
|
|
.name = "tally",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("engine/src/jni.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
}),
|
|
});
|
|
|
|
const javac = b.addSystemCommand(&.{ "javac", "-d" });
|
|
const classes = javac.addOutputDirectoryArg("classes");
|
|
javac.addFileArg(b.path("engine/test/java/dev/lerch/tally/TallyEngine.java"));
|
|
|
|
const java = b.addSystemCommand(&.{"java"});
|
|
java.addPrefixedDirectoryArg("-Djava.library.path=", jni_lib.getEmittedBinDirectory());
|
|
java.addArg("-cp");
|
|
java.addDirectoryArg(classes);
|
|
java.addArg("dev.lerch.tally.TallyEngine");
|
|
java.expectStdOutEqual("jvm jni test: all checks passed\n");
|
|
// A failing check exits non-zero, which the run step treats as a build failure.
|
|
step.dependOn(&java.step);
|
|
}
|
|
|
|
// -- Run step --
|
|
const run_step = b.step("run", "Run tally");
|
|
const run_cmd = b.addRunArtifact(exe);
|
|
run_step.dependOn(&run_cmd.step);
|
|
run_cmd.step.dependOn(b.getInstallStep());
|
|
if (b.args) |args| {
|
|
run_cmd.addArgs(args);
|
|
}
|
|
}
|