Files
fx/build.zig
T
Pranit b1d515851a Upgrade to Zig 0.17
Build fx with Zig 0.17.0. build.zig.zon requires it, every workflow
installs it, and PGSO qualification uses LLVM 22.1.8 to match.

Zig 0.17 removes `**`, `errdefer |err|`, `@cImport`, and the old type
info field lists. The source moves to `@splat`, a comptime
`text_utils.repeat` helper, split cleanup functions, and the new
field_names and field_types arrays. The Node-API addon translates
node_api.h with a build step. Test fakes checked with `@hasDecl` become
pub, because `@hasDecl` no longer sees private declarations.

`zig build test` gives the test binary its fx and source paths through
a generated test_paths module, since the build no longer exposes install
paths for environment variables.

Zig 0.17's SafeAllocator grows a small allocation in place only when it
sits at the end of a bucket, and buckets are never reused, so an
allocation-failure sweep could count a different number of allocations
on each run. Sweeps and allocation-count tests now use
testing_allocator.no_resize, which never resizes in place.

allocRemaining now treats its limit as inclusive. readFileToEnd keeps
rejecting a file of exactly max_bytes by reading through
appendRemaining. Direct allocRemaining callers for remote responses now
accept a body exactly at their limit.

Zig 0.17's memset stores a full vector per iteration, so the Linux
memset replacement is removed. macOS release binaries now require macOS
15, Zig 0.17's default minimum.
2026-10-05 19:44:53 -04:00

477 lines
18 KiB
Zig

const std = @import("std");
const UpdateChannel = enum { stable, dev };
const WasmSurface = enum {
none,
core,
term,
};
const PgsoArtifact = enum {
fx,
file_index,
ui_activity,
approval_review,
};
const NapiSurface = enum {
none,
core,
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const pgso_artifact = b.option(
PgsoArtifact,
"pgso-artifact",
"Emit ReleaseSafe LLVM bitcode for one PGO/PGSO artifact",
);
const wasm_surface = b.option(
WasmSurface,
"wasm-surface",
"Build a WASI WebAssembly surface for JavaScript hosts (core or term)",
) orelse .none;
const napi_surface = b.option(
NapiSurface,
"napi-surface",
"Build a Node-API addon surface (core)",
) orelse .none;
const git_commit = readGitCommit(b);
const app_version = readAppVersion(b);
const update_channel = b.option(UpdateChannel, "update-channel", "Build update channel (stable or dev)") orelse .stable;
const build_options = b.addOptions();
build_options.addOption([]const u8, "git_commit", git_commit);
build_options.addOption([]const u8, "app_version", app_version);
build_options.addOption([]const u8, "update_channel", @tagName(update_channel));
build_options.addOption(WasmSurface, "wasm_surface", .none);
const exe = b.addExecutable(.{
.name = "fx",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.stack_check = false,
.stack_protector = false,
.omit_frame_pointer = true,
.unwind_tables = .none,
.error_tracing = false,
.strip = optimize != .debug,
}),
});
exe.root_module.addImport("build_options", build_options.createModule());
const session_manager = addSessionManager(b, exe.root_module, target, optimize, null);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_cmd.addPassthruArgs();
const run_step = b.step("run", "Run fx");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest(.{
.root_module = exe.root_module,
});
const run_exe_tests = b.addRunArtifact(exe_tests);
run_exe_tests.step.dependOn(b.getInstallStep());
// Tests launch the installed fx binary, and the session boundary test
// walks the source tree.
const test_paths = b.addOptions();
test_paths.addOptionPathUntracked("product_exe", b.graph.path(.install_bin, "fx"));
test_paths.addOptionPathUntracked("source_root", b.path("src"));
exe.root_module.addImport("test_paths", test_paths.createModule());
// The session manager's own tests, as fx compiles it (no hooks).
const session_manager_tests = b.addTest(.{ .root_module = session_manager });
const run_session_manager_tests = b.addRunArtifact(session_manager_tests);
const session_manager_test_step = b.step("test-session-manager", "Run the session manager's tests");
session_manager_test_step.dependOn(&run_session_manager_tests.step);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_exe_tests.step);
test_step.dependOn(&run_session_manager_tests.step);
if (wasm_surface != .none) {
addWasmArtifact(b, wasm_surface, git_commit, app_version, update_channel);
}
if (napi_surface != .none) {
addNapiArtifact(b, napi_surface, target, git_commit, app_version, update_channel);
}
const mcp_test_exports = b.createModule(.{
.root_source_file = b.path("src/mcp_test_exports.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
mcp_test_exports.addImport("build_options", build_options.createModule());
const mcp_dispatcher_e2e = b.addExecutable(.{
.name = "mcp-stdio-dispatcher-driver",
.root_module = b.createModule(.{
.root_source_file = b.path(
"tests/e2e/fixtures/mcp-stdio-dispatcher-driver.zig",
),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
mcp_dispatcher_e2e.root_module.addImport(
"mcp_test_exports",
mcp_test_exports,
);
const run_mcp_dispatcher_e2e = b.addRunArtifact(mcp_dispatcher_e2e);
run_mcp_dispatcher_e2e.addPassthruArgs();
const mcp_dispatcher_e2e_step = b.step(
"run-mcp-stdio-dispatcher-e2e",
"Run the MCP stdio dispatcher E2E driver",
);
mcp_dispatcher_e2e_step.dependOn(&run_mcp_dispatcher_e2e.step);
// --- file_index search benchmark ---
const benchmark_exports_mod = b.createModule(.{
.root_source_file = b.path("src/benchmark_exports.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
const file_index_bench = b.addExecutable(.{
.name = "file-index-bench",
.root_module = b.createModule(.{
.root_source_file = b.path("benchmarks/file_index_bench.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
file_index_bench.root_module.addImport("file_index", benchmark_exports_mod);
const install_bench = b.addInstallArtifact(file_index_bench, .{});
const bench_step = b.step("bench-file-index", "Build file_index search benchmark");
bench_step.dependOn(&install_bench.step);
const run_bench = b.addRunArtifact(file_index_bench);
run_bench.step.dependOn(&install_bench.step);
run_bench.addPassthruArgs();
const run_bench_step = b.step("run-bench-file-index", "Build and run file_index search benchmark");
run_bench_step.dependOn(&run_bench.step);
// --- UI activity progress benchmark ---
const ui_activity_bench = b.addExecutable(.{
.name = "ui-activity-progress-bench",
.root_module = b.createModule(.{
.root_source_file = b.path("benchmarks/activity_progress.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
ui_activity_bench.root_module.addImport(
"benchmark_exports",
benchmark_exports_mod,
);
const install_ui_activity_bench = b.addInstallArtifact(ui_activity_bench, .{});
const ui_activity_bench_step = b.step(
"bench-ui-activity",
"Build the UI activity progress benchmark",
);
ui_activity_bench_step.dependOn(&install_ui_activity_bench.step);
const run_ui_activity_bench = b.addRunArtifact(ui_activity_bench);
run_ui_activity_bench.step.dependOn(&install_ui_activity_bench.step);
const run_ui_activity_bench_step = b.step(
"run-bench-ui-activity",
"Build and run the UI activity progress benchmark",
);
run_ui_activity_bench_step.dependOn(&run_ui_activity_bench.step);
const ui_activity_bench_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("benchmarks/activity_progress.zig"),
.target = target,
.optimize = optimize,
}),
});
ui_activity_bench_tests.root_module.addImport(
"benchmark_exports",
benchmark_exports_mod,
);
const run_ui_activity_bench_tests = b.addRunArtifact(ui_activity_bench_tests);
test_step.dependOn(&run_ui_activity_bench_tests.step);
const test_ui_activity_bench_step = b.step(
"test-ui-activity-benchmark",
"Run UI activity benchmark policy tests",
);
test_ui_activity_bench_step.dependOn(&run_ui_activity_bench_tests.step);
// --- file-diff approval review benchmark ---
const approval_review_bench = b.addExecutable(.{
.name = "approval-review-bench",
.root_module = b.createModule(.{
.root_source_file = b.path("benchmarks/approval_review.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
approval_review_bench.root_module.addImport(
"benchmark_exports",
benchmark_exports_mod,
);
const install_approval_review_bench = b.addInstallArtifact(
approval_review_bench,
.{},
);
const approval_review_bench_step = b.step(
"bench-approval-review",
"Build the file-diff approval review benchmark",
);
approval_review_bench_step.dependOn(&install_approval_review_bench.step);
const run_approval_review_bench = b.addRunArtifact(approval_review_bench);
run_approval_review_bench.step.dependOn(&install_approval_review_bench.step);
run_approval_review_bench.addPassthruArgs();
const run_approval_review_bench_step = b.step(
"run-bench-approval-review",
"Build and run the file-diff approval review benchmark",
);
run_approval_review_bench_step.dependOn(&run_approval_review_bench.step);
const approval_review_bench_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("benchmarks/approval_review.zig"),
.target = target,
.optimize = optimize,
}),
});
approval_review_bench_tests.root_module.addImport(
"benchmark_exports",
benchmark_exports_mod,
);
const run_approval_review_bench_tests = b.addRunArtifact(
approval_review_bench_tests,
);
const test_approval_review_bench_step = b.step(
"test-approval-review-benchmark",
"Run file-diff approval review benchmark qualification tests",
);
test_approval_review_bench_step.dependOn(
&run_approval_review_bench_tests.step,
);
const pgso_ir_step = b.step(
"pgso-ir",
"Emit selected ReleaseSafe LLVM bitcode for PGO/PGSO qualification",
);
if (pgso_artifact) |artifact| {
const selected: *std.Build.Step.Compile = switch (artifact) {
.fx => exe,
.file_index => file_index_bench,
.ui_activity => ui_activity_bench,
.approval_review => approval_review_bench,
};
const output_name = switch (artifact) {
.fx => "pgso/fx.bc",
.file_index => "pgso/file-index.bc",
.ui_activity => "pgso/ui-activity.bc",
.approval_review => "pgso/approval-review.bc",
};
const install_ir = b.addInstallFile(
selected.getEmittedLlvmBc(),
output_name,
);
pgso_ir_step.dependOn(&install_ir.step);
} else {
const missing_artifact = b.addFail(
"pgso-ir requires -Dpgso-artifact",
);
pgso_ir_step.dependOn(&missing_artifact.step);
}
}
fn addWasmArtifact(
b: *std.Build,
surface: WasmSurface,
git_commit: []const u8,
app_version: []const u8,
update_channel: UpdateChannel,
) void {
const wasm_target = b.resolveTargetQuery(.{
.cpu_arch = .wasm32,
.os_tag = .wasi,
});
const name = switch (surface) {
.core => "fx-core",
.term => "fx-term",
.none => unreachable,
};
const description = switch (surface) {
.core => "Build the headless fx WebAssembly artifact",
.term => "Build the terminal fx WebAssembly artifact",
.none => unreachable,
};
const wasm_options = b.addOptions();
wasm_options.addOption([]const u8, "git_commit", git_commit);
wasm_options.addOption([]const u8, "app_version", app_version);
wasm_options.addOption([]const u8, "update_channel", @tagName(update_channel));
wasm_options.addOption(WasmSurface, "wasm_surface", surface);
const wasm_root = switch (surface) {
.core => "src/wasm_core_main.zig",
.term => "src/wasm_term_main.zig",
.none => unreachable,
};
const wasm_exe = b.addExecutable(.{
.name = name,
.root_module = b.createModule(.{
.root_source_file = b.path(wasm_root),
.target = wasm_target,
.optimize = .small,
.single_threaded = true,
.link_libc = true,
.stack_check = false,
.stack_protector = false,
.omit_frame_pointer = true,
.unwind_tables = .none,
.error_tracing = false,
.strip = true,
}),
});
if (surface == .core) wasm_exe.stack_size = 1024 * 1024;
wasm_exe.root_module.addImport("build_options", wasm_options.createModule());
_ = addSessionManager(b, wasm_exe.root_module, wasm_target, .small, true);
const install_wasm = b.addInstallArtifact(wasm_exe, .{});
const wasm_step = b.step(name ++ "-wasm", description);
wasm_step.dependOn(&install_wasm.step);
b.getInstallStep().dependOn(&install_wasm.step);
}
fn addNapiArtifact(
b: *std.Build,
surface: NapiSurface,
target: std.Build.ResolvedTarget,
git_commit: []const u8,
app_version: []const u8,
update_channel: UpdateChannel,
) void {
const napi_options = b.addOptions();
napi_options.addOption([]const u8, "git_commit", git_commit);
napi_options.addOption([]const u8, "app_version", app_version);
napi_options.addOption([]const u8, "update_channel", @tagName(update_channel));
napi_options.addOption(NapiSurface, "napi_surface", surface);
const root = switch (surface) {
.core => "src/napi_core_main.zig",
.none => unreachable,
};
const lib = b.addLibrary(.{
.name = "libfx",
.linkage = .dynamic,
.root_module = b.createModule(.{
.root_source_file = b.path(root),
.target = target,
.optimize = .safe,
.link_libc = true,
.strip = true,
}),
});
lib.root_module.addImport("build_options", napi_options.createModule());
_ = addSessionManager(b, lib.root_module, target, .safe, null);
const node_include = b.option(
[]const u8,
"node-include-dir",
"Directory containing node_api.h for the N-API addon",
) orelse discoverNodeIncludeDir(b);
const node_api = b.addTranslateC(.{
.root_source_file = .{ .cwd_relative = b.pathJoin(&.{ node_include, "node_api.h" }) },
.target = target,
.optimize = .safe,
});
node_api.addSystemIncludePath(.{ .cwd_relative = node_include });
lib.root_module.addImport("node_api", node_api.createModule());
lib.linker_allow_shlib_undefined = true;
const install = b.addInstallArtifact(lib, .{ .dest_sub_path = "libfx.node" });
const step = b.step("libfx-napi", "Build the libfx Node-API core addon");
step.dependOn(&install.step);
b.getInstallStep().dependOn(&install.step);
}
fn discoverNodeIncludeDir(b: *std.Build) []const u8 {
const out = switch (b.runFallible(
&.{ "node", "-p", "require('node:path').join(require('node:path').dirname(process.execPath), '..', 'include', 'node')" },
.{ .stderr_behavior = .ignore },
)) {
.success => |stdout| stdout,
.spawn_failed, .crashed => std.process.fatal("Node.js is required to locate node_api.h; pass -Dnode-include-dir=<path>", .{}),
.bad_exit_code => std.process.fatal("could not locate node_api.h; pass -Dnode-include-dir=<path>", .{}),
};
const trimmed = std.mem.trim(u8, out, " \t\r\n");
return b.allocator.dupe(u8, trimmed) catch std.process.fatal("could not allocate Node include path", .{});
}
fn readGitCommit(b: *std.Build) []const u8 {
// The configure cache cannot see a new commit, so rerun configure.
b.graph.poisonCache();
const out = switch (b.runFallible(
&.{ "git", "rev-parse", "--short=12", "HEAD" },
.{ .stderr_behavior = .ignore },
)) {
.success => |stdout| stdout,
.spawn_failed, .bad_exit_code, .crashed => return "unknown",
};
const trimmed = std.mem.trim(u8, out, " \t\r\n");
return b.allocator.dupe(u8, trimmed) catch "unknown";
}
/// The session manager as its own module, rooted at its API. It imports
/// nothing but its own options, and a file cannot belong to two modules, so
/// fx reaches it only through `@import("session_manager")`.
fn addSessionManager(
b: *std.Build,
importer: *std.Build.Module,
target: std.Build.ResolvedTarget,
optimize: std.lang.Optimize,
single_threaded: ?bool,
) *std.Build.Module {
const options = b.addOptions();
options.addOption(bool, "hooks", false);
options.addOptionPathUntracked("trace_dir", b.graph.path(.install_prefix, "session-manager-traces"));
options.addOptionPathUntracked("src_dir", b.path("src/core/session_manager"));
const module = b.createModule(.{
.root_source_file = b.path("src/core/session_manager/api.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.single_threaded = single_threaded,
});
module.addImport("build_options", options.createModule());
importer.addImport("session_manager", module);
return module;
}
fn readAppVersion(b: *std.Build) []const u8 {
b.dependOnFileContents(b.path("src/main.zig"));
const main_path = b.root.joinString(b.allocator, "src/main.zig") catch @panic("OOM");
const bytes = std.Io.Dir.cwd().readFileAlloc(b.graph.io, main_path, b.allocator, .limited(1024 * 1024)) catch
@panic("could not read src/main.zig to resolve app version");
defer b.allocator.free(bytes);
const prefix = "pub const version = \"";
const start = (std.mem.find(u8, bytes, prefix) orelse
@panic("could not find pub const version in src/main.zig")) + prefix.len;
const end_rel = std.mem.findScalar(u8, bytes[start..], '"') orelse
@panic("could not parse pub const version in src/main.zig");
return b.allocator.dupe(u8, bytes[start .. start + end_rel]) catch
@panic("could not allocate app version");
}