This also provides ensurance through zig build test that all fields are documented in markdown so they do not get out of sync
297 lines
12 KiB
Zig
297 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(.{});
|
|
|
|
// External dependencies
|
|
const srf_dep = b.dependency("srf", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const vaxis_dep = b.dependency("vaxis", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const z2d_dep = b.dependency("z2d", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const zeit_dep = b.dependency("zeit", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
// karlseguin/websocket.zig: blocking-socket + std.Thread websocket
|
|
// client used by the live-price stream (src/net/LiveStream.zig).
|
|
// Internal-only, so it's wired into the unified module's imports
|
|
// below but NOT into the public `zfin` library module.
|
|
const websocket_dep = b.dependency("websocket", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
const srf_mod = srf_dep.module("srf");
|
|
|
|
const shiller_mod = b.addModule("shiller_year", .{
|
|
.root_source_file = b.path("src/models/shiller_year.zig"),
|
|
});
|
|
|
|
// Build-time info: version string (from git describe) and build timestamp.
|
|
// Exposed to application code as `@import("build_info")`.
|
|
//
|
|
// The version string is derived from `git describe --tags --always --dirty`
|
|
// so dev builds show the nearest tag plus commit hash + dirty flag. If git
|
|
// is unavailable (e.g. building from a source tarball), falls back to the
|
|
// `.version` in build.zig.zon.
|
|
const build_info = buildInfoOptions(b);
|
|
|
|
// Library module -- the public API for downstream consumers of zfin.
|
|
// Internal code (CLI, TUI) uses file-path imports instead.
|
|
_ = b.addModule("zfin", .{
|
|
.root_source_file = b.path("src/root.zig"),
|
|
.target = target,
|
|
.imports = &.{
|
|
.{ .name = "srf", .module = srf_mod },
|
|
.{ .name = "zeit", .module = zeit_dep.module("zeit") },
|
|
.{ .name = "build_info", .module = build_info },
|
|
},
|
|
});
|
|
|
|
// Shared imports for the unified module (CLI + TUI + lib in one module).
|
|
// Only external deps -- internal imports use file paths so that Zig's
|
|
// test runner can discover tests across the entire source tree.
|
|
const imports: []const std.Build.Module.Import = &.{
|
|
.{ .name = "srf", .module = srf_mod },
|
|
.{ .name = "vaxis", .module = vaxis_dep.module("vaxis") },
|
|
.{ .name = "z2d", .module = z2d_dep.module("z2d") },
|
|
.{ .name = "zeit", .module = zeit_dep.module("zeit") },
|
|
.{ .name = "websocket", .module = websocket_dep.module("websocket") },
|
|
.{ .name = "build_info", .module = build_info },
|
|
.{ .name = "shiller_year", .module = shiller_mod },
|
|
.{ .name = "config_docs", .module = configDocsModule(b) },
|
|
};
|
|
|
|
// Generate Shiller annual returns data from ie_data.csv.
|
|
// Runs build/gen_shiller.zig as a native tool; outputs a .zig file
|
|
// that shiller.zig imports as a zero-cost const array.
|
|
const gen_shiller = b.addExecutable(.{
|
|
.name = "gen_shiller",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("build/gen_shiller.zig"),
|
|
.target = b.graph.host,
|
|
}),
|
|
});
|
|
gen_shiller.root_module.addImport("shiller", shiller_mod);
|
|
|
|
const gen_shiller_run = b.addRunArtifact(gen_shiller);
|
|
gen_shiller_run.addFileArg(b.path("src/data/ie_data.csv"));
|
|
const shiller_generated = gen_shiller_run.addOutputFileArg("shiller_generated.zig");
|
|
const shiller_generated_mod = b.addModule("shiller_generated", .{
|
|
.root_source_file = shiller_generated,
|
|
});
|
|
shiller_generated_mod.addImport("shiller", shiller_mod);
|
|
|
|
// Unified executable (CLI + TUI in one binary)
|
|
const exe = b.addExecutable(.{
|
|
.name = "zfin",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = imports,
|
|
}),
|
|
});
|
|
exe.root_module.addImport("shiller_generated", shiller_generated_mod);
|
|
b.installArtifact(exe);
|
|
|
|
// Run step: `zig build run -- <args>`
|
|
const run_step = b.step("run", "Run the zfin CLI");
|
|
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);
|
|
}
|
|
|
|
// Tests: single binary, single module. refAllDeclsRecursive in
|
|
// main.zig discovers all tests via file imports.
|
|
const test_step = b.step("test", "Run all tests");
|
|
const tests = b.addTest(.{ .root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = imports,
|
|
}) });
|
|
tests.root_module.addImport("shiller_generated", shiller_generated_mod);
|
|
test_step.dependOn(&b.addRunArtifact(tests).step);
|
|
|
|
// Docs (still uses the library module for clean public API docs)
|
|
const lib = b.addLibrary(.{
|
|
.name = "zfin",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("src/root.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = &.{
|
|
.{ .name = "srf", .module = srf_mod },
|
|
.{ .name = "zeit", .module = zeit_dep.module("zeit") },
|
|
.{ .name = "build_info", .module = build_info },
|
|
},
|
|
}),
|
|
});
|
|
const docs_step = b.step("docs", "Generate documentation");
|
|
docs_step.dependOn(&b.addInstallDirectory(.{
|
|
.source_dir = lib.getEmittedDocs(),
|
|
.install_dir = .prefix,
|
|
.install_subdir = "docs",
|
|
}).step);
|
|
|
|
// Coverage: `zig build coverage` (uses kcov, Linux x86_64/aarch64 only)
|
|
{
|
|
var cov = Coverage.init(b);
|
|
const cov_mod = b.createModule(.{
|
|
.root_source_file = b.path("src/main.zig"),
|
|
.target = target,
|
|
.optimize = optimize,
|
|
.imports = imports,
|
|
});
|
|
cov_mod.addImport("shiller_generated", shiller_generated_mod);
|
|
_ = cov.addModule(cov_mod, "zfin");
|
|
}
|
|
}
|
|
|
|
/// Produce the `config_docs` module: the set of backticked identifiers
|
|
/// in each `docs/reference/config/*.md` reference page, extracted by
|
|
/// `build/gen_config_docs.zig`.
|
|
///
|
|
/// Consumed by `src/srf_lint.zig`'s doc-sync test, which asserts every
|
|
/// field of every user-authored model appears in that model's reference
|
|
/// page. `@embedFile` cannot reach outside `src/`, hence the generator.
|
|
///
|
|
/// The directory is ENUMERATED rather than listed, so adding a new
|
|
/// reference page needs no edit here. Each page is passed as a file arg
|
|
/// so the build re-runs when one changes.
|
|
fn configDocsModule(b: *std.Build) *std.Build.Module {
|
|
const rel = "docs/reference/config";
|
|
|
|
const gen = b.addExecutable(.{
|
|
.name = "gen_config_docs",
|
|
.root_module = b.createModule(.{
|
|
.root_source_file = b.path("build/gen_config_docs.zig"),
|
|
.target = b.graph.host,
|
|
}),
|
|
});
|
|
const run = b.addRunArtifact(gen);
|
|
const output = run.addOutputFileArg("config_docs.zig");
|
|
|
|
var names: std.ArrayList([]const u8) = .empty;
|
|
if (b.build_root.handle.openDir(b.graph.io, rel, .{ .iterate = true })) |dir| {
|
|
var d = dir;
|
|
defer d.close(b.graph.io);
|
|
var it = d.iterate();
|
|
while (it.next(b.graph.io) catch null) |entry| {
|
|
if (entry.kind != .file) continue;
|
|
if (!std.mem.endsWith(u8, entry.name, ".md")) continue;
|
|
names.append(b.allocator, b.dupe(entry.name)) catch @panic("OOM");
|
|
}
|
|
} else |_| {}
|
|
|
|
// Sorted so the generated file - and therefore the build cache
|
|
// hash - does not depend on directory-iteration order.
|
|
std.mem.sort([]const u8, names.items, {}, struct {
|
|
fn lessThan(_: void, a: []const u8, c: []const u8) bool {
|
|
return std.mem.lessThan(u8, a, c);
|
|
}
|
|
}.lessThan);
|
|
|
|
for (names.items) |n| {
|
|
run.addFileArg(b.path(b.fmt("{s}/{s}", .{ rel, n })));
|
|
}
|
|
|
|
return b.createModule(.{ .root_source_file = output });
|
|
}
|
|
|
|
/// Produce the `build_info` module exposing `version` (derived from `git
|
|
/// describe`) and `build_timestamp` (committer timestamp of HEAD).
|
|
/// Consumed as `@import("build_info")` from `src/version.zig`.
|
|
///
|
|
/// Both values are reproducible per-commit: `git describe` is stable
|
|
/// until a new commit/tag/dirty-flag flip, and the committer timestamp
|
|
/// comes from `git log -1 --format=%ct`. Neither varies with wall-clock
|
|
/// time, so the Options module doesn't rebuild every invocation, which
|
|
/// used to cascade into a full exe relink on every `zig build`.
|
|
///
|
|
/// Falls back to `fallbackVersion()` / `0` when git is unavailable
|
|
/// (source tarball, pre-commit environment).
|
|
fn buildInfoOptions(b: *std.Build) *std.Build.Module {
|
|
const opts = b.addOptions();
|
|
|
|
const version = gitDescribe(b) orelse fallbackVersion();
|
|
opts.addOption([]const u8, "version", version);
|
|
|
|
// Use HEAD's committer timestamp (reproducible) instead of
|
|
// `std.time.timestamp()` (changes every build). See comment above.
|
|
const timestamp = gitHeadTimestamp(b) orelse 0;
|
|
opts.addOption(i64, "build_timestamp", timestamp);
|
|
|
|
return opts.createModule();
|
|
}
|
|
|
|
/// Run `git describe --tags --always --dirty` in the repo root and return
|
|
/// the trimmed output. Returns null on any error (git missing, not a repo,
|
|
/// non-zero exit).
|
|
fn gitDescribe(b: *std.Build) ?[]const u8 {
|
|
return gitCapture(b, &.{ "git", "describe", "--tags", "--always", "--dirty" });
|
|
}
|
|
|
|
/// Run `git log -1 --format=%ct HEAD` and parse the stdout as an i64
|
|
/// (Unix seconds). Returns null on any error. Stable per-commit, which
|
|
/// keeps the Options module cache-friendly.
|
|
fn gitHeadTimestamp(b: *std.Build) ?i64 {
|
|
const text = gitCapture(b, &.{ "git", "log", "-1", "--format=%ct", "HEAD" }) orelse return null;
|
|
return std.fmt.parseInt(i64, text, 10) catch null;
|
|
}
|
|
|
|
/// Spawn a git subcommand in the repo root, capture stdout, trim and
|
|
/// dupe it through `b.allocator`. Returns null on any error (git
|
|
/// missing, non-zero exit, empty output).
|
|
fn gitCapture(b: *std.Build, argv: []const []const u8) ?[]const u8 {
|
|
const io = b.graph.io;
|
|
const result = std.process.run(b.allocator, io, .{
|
|
.argv = argv,
|
|
.cwd = .{ .path = b.build_root.path orelse "." },
|
|
}) catch return null;
|
|
defer b.allocator.free(result.stdout);
|
|
defer b.allocator.free(result.stderr);
|
|
|
|
switch (result.term) {
|
|
.exited => |code| if (code != 0) return null,
|
|
else => return null,
|
|
}
|
|
|
|
const trimmed = std.mem.trim(u8, result.stdout, " \t\r\n");
|
|
if (trimmed.len == 0) return null;
|
|
return b.dupe(trimmed);
|
|
}
|
|
|
|
/// Read `.version` from `build.zig.zon` as a fallback when git describe
|
|
/// fails (e.g. when building from a source tarball with no .git directory).
|
|
/// Returns a static string if even that fails.
|
|
fn fallbackVersion() []const u8 {
|
|
// `build.zig.zon` is embedded at compile time so the fallback never
|
|
// requires runtime filesystem access in the built binary - we only do
|
|
// this lookup at build time, on the build host.
|
|
const zon_contents = @embedFile("build.zig.zon");
|
|
if (std.mem.indexOf(u8, zon_contents, ".version = \"")) |start| {
|
|
const after = zon_contents[start + ".version = \"".len ..];
|
|
if (std.mem.indexOfScalar(u8, after, '"')) |end| {
|
|
return after[0..end];
|
|
}
|
|
}
|
|
return "unknown";
|
|
}
|