mirror of
https://github.com/vercel-labs/fx.git
synced 2026-10-28 10:15:17 +08:00
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.
477 lines
18 KiB
Zig
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");
|
|
}
|