commit 0ff175b69ef806f421820d33dade7a8163fe3f16 (tree)
parent c84f0f49d692e08c235ff939d4322fe723fe2823
Author: Andrew Kelley <andrew@ziglang.org>
Date: Tue, 26 May 2026 18:00:16 +0200
Merge pull request 'zig build: separate the maker process from the configurer process' (#35428) from build-runner-process into master
Reviewed-on: https://codeberg.org/ziglang/zig/pulls/35428
Diffstat:
104 files changed, 19998 insertions(+), 13649 deletions(-)
diff --git a/build.zig b/build.zig
@@ -16,6 +16,8 @@ const IoMode = enum { threaded, evented };
const ValueInterpretMode = enum { direct, by_name };
pub fn build(b: *std.Build) !void {
+ const arena = b.graph.arena;
+
const only_c = b.option(bool, "only-c", "Translate the Zig compiler to C code, with only the C backend enabled") orelse false;
const target = b.standardTargetOptions(.{
.default_target = .{
@@ -35,7 +37,7 @@ pub fn build(b: *std.Build) !void {
const no_bin = b.option(bool, "no-bin", "skip emitting compiler binary") orelse false;
const enable_superhtml = b.option(bool, "enable-superhtml", "Check langref output HTML validity") orelse false;
- const langref_file = generateLangRef(b);
+ const langref_file = try generateLangRef(b);
const install_langref = b.addInstallFileWithDir(langref_file, .prefix, "doc/langref.html");
const check_langref = superHtmlCheck(b, langref_file);
if (enable_superhtml) install_langref.step.dependOn(check_langref);
@@ -208,7 +210,8 @@ pub fn build(b: *std.Build) !void {
.single_threaded = single_threaded,
});
exe.pie = pie;
- exe.entitlements = entitlements;
+ // https://codeberg.org/ziglang/zig/issues/32173
+ exe.entitlements = if (entitlements) |p| .{ .cwd_relative = p } else null;
exe.use_new_linker = b.option(bool, "new-linker", "Use the new linker");
const use_llvm = b.option(bool, "use-llvm", "Use the llvm backend");
@@ -261,7 +264,7 @@ pub fn build(b: *std.Build) !void {
var code: u8 = undefined;
const git_describe_untrimmed = b.runAllowFail(&[_][]const u8{
"git",
- "-C", b.build_root.path orelse ".", // affects the --git-dir argument
+ "-C", b.fmt("{f}", .{b.root}), // affects the --git-dir argument
"--git-dir", ".git", // affected by the -C argument
"describe", "--match", "*.*.*", //
"--tags", "--abbrev=9",
@@ -307,7 +310,7 @@ pub fn build(b: *std.Build) !void {
},
}
};
- const version = try b.allocator.dupeSentinel(u8, version_slice, 0);
+ const version = try arena.dupeSentinel(u8, version_slice, 0);
exe_options.addOption([:0]const u8, "version", version);
if (enable_llvm) {
@@ -315,7 +318,7 @@ pub fn build(b: *std.Build) !void {
const io = b.graph.io;
const cwd: Io.Dir = .cwd();
if (findConfigH(b, config_h_path_option)) |config_h_path| {
- const file_contents = cwd.readFileAlloc(io, config_h_path, b.allocator, .limited(max_config_h_bytes)) catch unreachable;
+ const file_contents = cwd.readFileAlloc(io, config_h_path, arena, .limited(max_config_h_bytes)) catch unreachable;
break :blk parseConfigH(b, file_contents);
} else {
std.log.warn("config.h could not be located automatically. Consider providing it explicitly via \"-Dconfig_h\"", .{});
@@ -424,8 +427,8 @@ pub fn build(b: *std.Build) !void {
else
null;
- const fmt_include_paths = &.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" };
- const fmt_exclude_paths = &.{ "test/cases", "test/behavior/zon" };
+ const fmt_include_paths = b.pathList(&.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" });
+ const fmt_exclude_paths = b.pathList(&.{ "test/cases", "test/behavior/zon" });
const do_fmt = b.addFmt(.{
.paths = fmt_include_paths,
.exclude_paths = fmt_exclude_paths,
@@ -975,11 +978,12 @@ fn addCxxKnownPath(
errtxt: ?[]const u8,
need_cpp_includes: bool,
) !void {
- if (!std.process.can_spawn)
- return error.RequiredLibraryNotFound;
+ if (!std.process.can_spawn) return error.RequiredLibraryNotFound;
+
+ const arena = b.graph.arena;
const path_padded = run: {
- var args = std.array_list.Managed([]const u8).init(b.allocator);
+ var args = std.array_list.Managed([]const u8).init(arena);
try args.append(ctx.cxx_compiler);
var it = std.mem.tokenizeAny(u8, ctx.cxx_compiler_arg1, &std.ascii.whitespace);
while (it.next()) |arg| try args.append(arg);
@@ -1048,6 +1052,7 @@ const CMakeConfig = struct {
const max_config_h_bytes = 1 * 1024 * 1024;
fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 {
+ const arena = b.graph.arena;
const io = b.graph.io;
const cwd: Io.Dir = .cwd();
@@ -1072,7 +1077,7 @@ fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 {
if (config_h_or_err) |*file| {
file.close(io);
return fs.path.join(
- b.allocator,
+ arena,
&[_][]const u8{ check_dir, "config.h" },
) catch unreachable;
} else |e| switch (e) {
@@ -1197,7 +1202,8 @@ fn parseConfigH(b: *std.Build, config_h_text: []const u8) ?CMakeConfig {
}
fn toNativePathSep(b: *std.Build, s: []const u8) []u8 {
- const duplicated = b.allocator.dupe(u8, s) catch unreachable;
+ const arena = b.graph.arena;
+ const duplicated = arena.dupe(u8, s) catch unreachable;
for (duplicated) |*byte| switch (byte.*) {
'/' => byte.* = fs.path.sep,
else => {},
@@ -1486,8 +1492,9 @@ const llvm_libs_xtensa = [_][]const u8{
"LLVMXtensaInfo",
};
-fn generateLangRef(b: *std.Build) std.Build.LazyPath {
+fn generateLangRef(b: *std.Build) !std.Build.LazyPath {
const io = b.graph.io;
+ const arena = b.graph.arena;
const doctest_exe = b.addExecutable(.{
.name = "doctest",
@@ -1498,11 +1505,10 @@ fn generateLangRef(b: *std.Build) std.Build.LazyPath {
}),
});
- var dir = b.build_root.handle.openDir(io, "doc/langref", .{ .iterate = true }) catch |err| {
- std.debug.panic("unable to open '{f}doc/langref' directory: {s}", .{
- b.build_root, @errorName(err),
- });
- };
+ const langref_path = try b.root.join(arena, "doc/langref");
+
+ var dir = langref_path.root_dir.handle.openDir(io, langref_path.sub_path, .{ .iterate = true }) catch |err|
+ std.debug.panic("unable to open directory {f}: {t}", .{ langref_path, err });
defer dir.close(io);
var wf = b.addWriteFiles();
@@ -1515,17 +1521,20 @@ fn generateLangRef(b: *std.Build) std.Build.LazyPath {
const out_basename = b.fmt("{s}.out", .{std.fs.path.stem(entry.name)});
const cmd = b.addRunArtifact(doctest_exe);
- cmd.addArgs(&.{
- "--zig", b.graph.zig_exe,
- // TODO: enhance doctest to use "--listen=-" rather than operating
- // in a temporary directory
- "--cache-root", b.cache_root.path orelse ".",
- });
- cmd.addArgs(&.{ "--zig-lib-dir", b.fmt("{f}", .{b.graph.zig_lib_directory}) });
- cmd.addArgs(&.{"-i"});
+
+ cmd.addArg("--zig");
+ cmd.addFileArg(.zig_exe);
+
+ cmd.addArg("--cache-root");
+ cmd.addDirectoryArg(.cache_root);
+
+ cmd.addArg("--zig-lib-dir");
+ cmd.addDirectoryArg(.zig_lib);
+
+ cmd.addArg("-i");
cmd.addFileArg(b.path(b.fmt("doc/langref/{s}", .{entry.name})));
- cmd.addArgs(&.{"-o"});
+ cmd.addArg("-o");
_ = wf.addCopyFile(cmd.addOutputFileArg(out_basename), out_basename);
}
diff --git a/ci/aarch64-freebsd-release.sh b/ci/aarch64-freebsd-release.sh
@@ -59,7 +59,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/aarch64-linux-release.sh b/ci/aarch64-linux-release.sh
@@ -64,7 +64,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/aarch64-macos-release.sh b/ci/aarch64-macos-release.sh
@@ -73,7 +73,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/aarch64-netbsd-release.sh b/ci/aarch64-netbsd-release.sh
@@ -59,7 +59,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/loongarch64-linux-release.sh b/ci/loongarch64-linux-release.sh
@@ -61,7 +61,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/powerpc64le-linux-release.sh b/ci/powerpc64le-linux-release.sh
@@ -63,7 +63,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/s390x-linux-release.sh b/ci/s390x-linux-release.sh
@@ -62,7 +62,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/x86_64-freebsd-release.sh b/ci/x86_64-freebsd-release.sh
@@ -69,7 +69,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/x86_64-linux-debug-llvm.sh b/ci/x86_64-linux-debug-llvm.sh
@@ -49,6 +49,7 @@ stage3-debug/bin/zig build \
-Dno-lib
stage3-debug/bin/zig build test docs \
+ --maker-opt=Debug \
--maxrss ${ZSF_MAX_RSS:-0} \
-Dlldb=$HOME/deps/lldb-zig/Debug-e0a42bb34/bin/lldb \
-Dlibc-test-path=$HOME/deps/libc-test-f2bac77 \
diff --git a/ci/x86_64-linux-release.sh b/ci/x86_64-linux-release.sh
@@ -85,7 +85,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/x86_64-netbsd-release.sh b/ci/x86_64-netbsd-release.sh
@@ -63,7 +63,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/ci/x86_64-openbsd-release.sh b/ci/x86_64-openbsd-release.sh
@@ -64,7 +64,6 @@ stage3-release/bin/zig build \
-Duse-zig-libcxx \
-Dversion-string="$(stage3-release/bin/zig version)"
-# diff returns an error code if the files differ.
echo "If the following command fails, it means nondeterminism has been"
echo "introduced, making stage3 and stage4 no longer byte-for-byte identical."
diff stage3-release/bin/zig stage4-release/bin/zig
diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig
@@ -0,0 +1,2051 @@
+const Maker = @This();
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Cache = std.Build.Cache;
+const Configuration = std.Build.Configuration;
+const File = std.Io.File;
+const Io = std.Io;
+const Dir = std.Io.Dir;
+const Path = std.Build.Cache.Path;
+const Writer = std.Io.Writer;
+const assert = std.debug.assert;
+const fatal = std.process.fatal;
+const fmt = std.fmt;
+const log = std.log;
+const mem = std.mem;
+const process = std.process;
+
+const Fuzz = @import("Maker/Fuzz.zig");
+const Graph = @import("Maker/Graph.zig");
+const Step = @import("Maker/Step.zig");
+const Watch = @import("Maker/Watch.zig");
+const WebServer = @import("Maker/WebServer.zig");
+const ScannedConfig = @import("Maker/ScannedConfig.zig");
+const PkgConfig = @import("Maker/PkgConfig.zig");
+
+pub const std_options: std.Options = .{
+ .side_channels_mitigations = .none,
+ .http_disable_tls = true,
+};
+
+gpa: Allocator,
+graph: *Graph,
+install_paths: InstallPaths,
+scanned_config: *const ScannedConfig,
+steps: []Step,
+generated_files: []Path,
+run_args: ?[]const []const u8,
+
+available_rss: usize,
+max_rss_is_default: bool,
+max_rss_mutex: Io.Mutex,
+skip_oom_steps: bool,
+unit_test_timeout_ns: ?u64,
+watch: bool,
+web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn,
+/// Allocated into `gpa`.
+memory_blocked_steps: std.ArrayList(Configuration.Step.Index),
+/// Allocated into `gpa`.
+step_stack: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void),
+pkg_config: PkgConfig,
+
+error_style: ErrorStyle,
+multiline_errors: MultilineErrors,
+summary: Summary,
+
+pub fn main(init: process.Init.Minimal) !void {
+ // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not
+ // always the case. So, we do need a true gpa for some things.
+ var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{});
+ defer _ = safe_gpa_state.deinit();
+ const gpa = safe_gpa_state.allocator();
+
+ var threaded: std.Io.Threaded = .init(gpa, .{
+ .environ = init.environ,
+ .argv0 = .init(init.args),
+ });
+ defer threaded.deinit();
+ const io = threaded.io();
+
+ // ...but we'll back our arena by `std.heap.page_allocator` for efficiency.
+ var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
+ defer arena_instance.deinit();
+ defer if (debugMakerLeaks()) log.debug("used {Bi} of arena", .{arena_instance.queryCapacity()});
+ const arena = arena_instance.allocator();
+
+ const args = try init.args.toSlice(arena);
+
+ // skip my own exe name
+ var arg_idx: usize = 1;
+
+ const zig_exe = expectArgOrFatal(args, &arg_idx, "--zig");
+ const zig_lib_dir = expectArgOrFatal(args, &arg_idx, "--zig-lib-dir");
+ const build_root = expectArgOrFatal(args, &arg_idx, "--build-root");
+ const local_cache_root = expectArgOrFatal(args, &arg_idx, "--local-cache");
+ const global_cache_root = expectArgOrFatal(args, &arg_idx, "--global-cache");
+ const configure_path = expectArgOrFatal(args, &arg_idx, "--configuration");
+
+ const cwd: Dir = .cwd();
+
+ const zig_lib_directory: Cache.Directory = .{
+ .path = zig_lib_dir,
+ .handle = try cwd.openDir(io, zig_lib_dir, .{}),
+ };
+
+ const build_root_directory: Cache.Directory = .{
+ .path = build_root,
+ .handle = try cwd.openDir(io, build_root, .{}),
+ };
+
+ const local_cache_directory: Cache.Directory = .{
+ .path = local_cache_root,
+ .handle = try cwd.createDirPathOpen(io, local_cache_root, .{}),
+ };
+
+ const global_cache_directory: Cache.Directory = .{
+ .path = global_cache_root,
+ .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}),
+ };
+
+ var graph: Graph = .{
+ .io = io,
+ .arena = arena,
+ .cache = .{
+ .io = io,
+ .gpa = gpa,
+ .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}),
+ .cwd = try process.currentPathAlloc(io, arena),
+ },
+ .zig_exe = zig_exe,
+ .environ_map = try init.environ.createMap(arena),
+ .global_cache_root = global_cache_directory,
+ .local_cache_root = local_cache_directory,
+ .zig_lib_directory = zig_lib_directory,
+ .build_root_directory = build_root_directory,
+ };
+
+ graph.cache.addPrefix(.{ .path = null, .handle = cwd });
+ graph.cache.addPrefix(build_root_directory);
+ graph.cache.addPrefix(local_cache_directory);
+ graph.cache.addPrefix(global_cache_directory);
+ graph.cache.hash.addBytes(builtin.zig_version_string);
+
+ var step_names: std.ArrayList([]const u8) = .empty;
+ var help_menu = false;
+ var steps_menu = false;
+ var print_configuration = false;
+ var override_install_prefix: ?[]const u8 = null;
+ var override_lib_dir: ?[]const u8 = null;
+ var override_bin_dir: ?[]const u8 = null;
+ var override_include_dir: ?[]const u8 = null;
+ var error_style: ErrorStyle = .verbose;
+ var multiline_errors: MultilineErrors = .indent;
+ var summary: ?Summary = null;
+ var max_rss: u64 = 0;
+ var skip_oom_steps = false;
+ var test_timeout_ns: ?u64 = null;
+ var color: Color = .auto;
+ var watch = false;
+ var fuzz: ?Fuzz.Mode = null;
+ var debounce_interval_ms: u16 = 50;
+ var webui_listen: ?Io.net.IpAddress = null;
+ var debug_pkg_config = false;
+ var run_args: ?[]const []const u8 = null;
+
+ if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| {
+ if (std.meta.stringToEnum(ErrorStyle, str)) |style| {
+ error_style = style;
+ }
+ }
+
+ if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| {
+ if (std.meta.stringToEnum(MultilineErrors, str)) |style| {
+ multiline_errors = style;
+ }
+ }
+
+ while (nextArg(args, &arg_idx)) |arg| {
+ if (mem.startsWith(u8, arg, "-")) {
+ if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
+ help_menu = true;
+ } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
+ steps_menu = true;
+ } else if (mem.eql(u8, arg, "--print-configuration")) {
+ print_configuration = true;
+ } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
+ override_install_prefix = nextArgOrFatal(args, &arg_idx);
+ } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
+ override_lib_dir = nextArgOrFatal(args, &arg_idx);
+ } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
+ override_bin_dir = nextArgOrFatal(args, &arg_idx);
+ } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
+ override_include_dir = nextArgOrFatal(args, &arg_idx);
+ } else if (mem.eql(u8, arg, "--sysroot")) {
+ graph.sysroot = nextArgOrFatal(args, &arg_idx);
+ } else if (mem.eql(u8, arg, "--maxrss")) {
+ const max_rss_text = nextArgOrFatal(args, &arg_idx);
+ max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err|
+ fatal("invalid byte size {q}: {t}", .{ max_rss_text, err });
+ } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
+ skip_oom_steps = true;
+ } else if (mem.eql(u8, arg, "--test-timeout")) {
+ const units: []const struct { []const u8, u64 } = &.{
+ .{ "ns", 1 },
+ .{ "nanosecond", 1 },
+ .{ "us", std.time.ns_per_us },
+ .{ "microsecond", std.time.ns_per_us },
+ .{ "ms", std.time.ns_per_ms },
+ .{ "millisecond", std.time.ns_per_ms },
+ .{ "s", std.time.ns_per_s },
+ .{ "second", std.time.ns_per_s },
+ .{ "m", std.time.ns_per_min },
+ .{ "minute", std.time.ns_per_min },
+ .{ "h", std.time.ns_per_hour },
+ .{ "hour", std.time.ns_per_hour },
+ };
+ const timeout_str = nextArgOrFatal(args, &arg_idx);
+ const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal(
+ "invalid timeout {q}: expected unit (ns, us, ms, s, m, h)",
+ .{timeout_str},
+ );
+ const num_str = timeout_str[0 .. num_end_idx + 1];
+ const unit_str = timeout_str[num_end_idx + 1 ..];
+ const unit_factor: f64 = for (units) |unit_and_factor| {
+ if (std.mem.eql(u8, unit_str, unit_and_factor[0])) {
+ break @floatFromInt(unit_and_factor[1]);
+ }
+ } else fatal(
+ "invalid timeout {q}: invalid unit {q} (expected ns, us, ms, s, m, h)",
+ .{ timeout_str, unit_str },
+ );
+ const num_parsed = std.fmt.parseFloat(f64, num_str) catch |err| fatal(
+ "invalid timeout {q}: invalid number {q} ({t})",
+ .{ timeout_str, num_str, err },
+ );
+ test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed);
+ } else if (mem.eql(u8, arg, "--search-prefix")) {
+ try graph.search_prefixes.append(arena, nextArgOrFatal(args, &arg_idx));
+ } else if (mem.eql(u8, arg, "--libc")) {
+ graph.libc_file = nextArgOrFatal(args, &arg_idx);
+ } else if (mem.eql(u8, arg, "--color")) {
+ const next_arg = nextArg(args, &arg_idx) orelse
+ fatalWithHint("expected [auto|on|off] after {q}", .{arg});
+ color = std.meta.stringToEnum(Color, next_arg) orelse {
+ fatalWithHint("expected [auto|on|off] after {q}, found {q}", .{
+ arg, next_arg,
+ });
+ };
+ } else if (mem.eql(u8, arg, "--error-style")) {
+ const next_arg = nextArg(args, &arg_idx) orelse
+ fatalWithHint("expected style after {q}", .{arg});
+ error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse {
+ fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg });
+ };
+ } else if (mem.eql(u8, arg, "--multiline-errors")) {
+ const next_arg = nextArg(args, &arg_idx) orelse
+ fatalWithHint("expected style after {q}", .{arg});
+ multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse {
+ fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg });
+ };
+ } else if (mem.eql(u8, arg, "--summary")) {
+ const next_arg = nextArg(args, &arg_idx) orelse
+ fatalWithHint("expected [all|new|failures|line|none] after {q}", .{arg});
+ summary = std.meta.stringToEnum(Summary, next_arg) orelse {
+ fatalWithHint("expected [all|new|failures|line|none] after {q}, found {q}", .{
+ arg, next_arg,
+ });
+ };
+ } else if (mem.eql(u8, arg, "--seed")) {
+ const next_arg = nextArg(args, &arg_idx) orelse
+ fatalWithHint("expected u32 after {q}", .{arg});
+ graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
+ fatal("unable to parse seed {q} as unsigned 32-bit integer: {t}", .{ next_arg, err });
+ };
+ } else if (mem.eql(u8, arg, "--build-id")) {
+ graph.build_id = .fast;
+ } else if (mem.cutPrefix(u8, arg, "--build-id=")) |style| {
+ graph.build_id = std.zig.BuildId.parse(style) catch |err|
+ fatal("unable to parse --build-id style {q}: {t}", .{ style, err });
+ } else if (mem.eql(u8, arg, "--debounce")) {
+ const next_arg = nextArg(args, &arg_idx) orelse
+ fatalWithHint("expected u16 after {q}", .{arg});
+ debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
+ fatal("unable to parse debounce interval {q} as unsigned 16-bit integer: {t}", .{
+ next_arg, err,
+ });
+ };
+ } else if (mem.eql(u8, arg, "--webui")) {
+ if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
+ } else if (mem.startsWith(u8, arg, "--webui=")) {
+ const addr_str = arg["--webui=".len..];
+ if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{});
+ webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| {
+ fatal("invalid web UI address {q}: {t}", .{ addr_str, err });
+ };
+ } else if (mem.eql(u8, arg, "--debug-log")) {
+ const next_arg = nextArgOrFatal(args, &arg_idx);
+ try graph.debug_log_scopes.append(arena, next_arg);
+ } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
+ graph.debug_compile_errors = true;
+ } else if (mem.eql(u8, arg, "--debug-incremental")) {
+ graph.debug_incremental = true;
+ } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
+ debug_pkg_config = true;
+ } else if (mem.eql(u8, arg, "--debug-rt")) {
+ graph.debug_compiler_runtime_libs = .Debug;
+ } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| {
+ graph.debug_compiler_runtime_libs = std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse
+ fatal("unrecognized optimization mode: {s}", .{rest});
+ } else if (is_debug_mode and mem.eql(u8, arg, "--debug-maker-leaks")) {
+ debug_maker_leaks = true;
+ } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) {
+ // --glibc-runtimes was the old name of the flag; kept for compatibility for now.
+ graph.libc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
+ } else if (mem.eql(u8, arg, "--verbose")) {
+ graph.verbose = true;
+ } else if (mem.eql(u8, arg, "--verbose-air")) {
+ graph.verbose_air = true;
+ } else if (mem.eql(u8, arg, "--verbose-cc")) {
+ graph.verbose_cc = true;
+ } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
+ graph.verbose_llvm_ir = true;
+ } else if (mem.eql(u8, arg, "--watch")) {
+ watch = true;
+ } else if (mem.eql(u8, arg, "--time-report")) {
+ graph.time_report = true;
+ if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
+ } else if (mem.eql(u8, arg, "--fuzz")) {
+ fuzz = .{ .forever = undefined };
+ graph.fuzzing = true;
+ if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
+ } else if (mem.startsWith(u8, arg, "--fuzz=")) {
+ const value = arg["--fuzz=".len..];
+ if (value.len == 0) fatal("missing argument to --fuzz", .{});
+
+ const unit: u8 = value[value.len - 1];
+ const digits = switch (unit) {
+ '0'...'9' => value,
+ 'K', 'M', 'G' => value[0 .. value.len - 1],
+ else => fatal(
+ "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
+ .{},
+ ),
+ };
+
+ const amount = std.fmt.parseInt(u64, digits, 10) catch {
+ fatal(
+ "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
+ .{},
+ );
+ };
+
+ const normalized_amount = std.math.mul(u64, amount, switch (unit) {
+ else => unreachable,
+ '0'...'9' => 1,
+ 'K' => 1000,
+ 'M' => 1_000_000,
+ 'G' => 1_000_000_000,
+ }) catch fatal("fuzzing limit amount overflows u64", .{});
+
+ fuzz = .{
+ .limit = .{
+ .amount = normalized_amount,
+ },
+ };
+ graph.fuzzing = true;
+ } else if (mem.eql(u8, arg, "-fincremental")) {
+ graph.incremental = true;
+ } else if (mem.eql(u8, arg, "-fno-incremental")) {
+ graph.incremental = false;
+ } else if (mem.eql(u8, arg, "-fwine")) {
+ graph.enable_wine = true;
+ } else if (mem.eql(u8, arg, "-fno-wine")) {
+ graph.enable_wine = false;
+ } else if (mem.eql(u8, arg, "-fqemu")) {
+ graph.enable_qemu = true;
+ } else if (mem.eql(u8, arg, "-fno-qemu")) {
+ graph.enable_qemu = false;
+ } else if (mem.eql(u8, arg, "-fwasmtime")) {
+ graph.enable_wasmtime = true;
+ } else if (mem.eql(u8, arg, "-fno-wasmtime")) {
+ graph.enable_wasmtime = false;
+ } else if (mem.eql(u8, arg, "-frosetta")) {
+ graph.enable_rosetta = true;
+ } else if (mem.eql(u8, arg, "-fno-rosetta")) {
+ graph.enable_rosetta = false;
+ } else if (mem.eql(u8, arg, "-fdarling")) {
+ graph.enable_darling = true;
+ } else if (mem.eql(u8, arg, "-fno-darling")) {
+ graph.enable_darling = false;
+ } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
+ graph.allow_so_scripts = true;
+ } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
+ graph.allow_so_scripts = false;
+ } else if (mem.eql(u8, arg, "-freference-trace")) {
+ graph.reference_trace = 256;
+ } else if (mem.cutPrefix(u8, arg, "-freference-trace=")) |num| {
+ graph.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err|
+ fatal("unable to parse reference_trace count {q}: {t}", .{ num, err });
+ } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
+ graph.reference_trace = null;
+ } else if (mem.eql(u8, arg, "--error-limit")) {
+ const next_arg = nextArgOrFatal(args, &arg_idx);
+ graph.error_limit = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err|
+ fatal("unable to parse error limit {q}: {t}", .{ next_arg, err });
+ } else if (mem.cutPrefix(u8, arg, "-j")) |text| {
+ const n = std.fmt.parseUnsigned(u32, text, 10) catch |err|
+ fatal("unable to parse jobs count {q}: {t}", .{ text, err });
+ if (n < 1) fatal("number of jobs must be at least 1", .{});
+ threaded.setAsyncLimit(.limited(n));
+ graph.max_jobs = n;
+ } else if (mem.eql(u8, arg, "--")) {
+ run_args = argsRest(args, arg_idx);
+ break;
+ } else {
+ fatalWithHint("unrecognized argument: {s}", .{arg});
+ }
+ } else {
+ try step_names.append(arena, arg);
+ }
+ }
+
+ const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map);
+ const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
+
+ graph.stderr_mode = switch (color) {
+ .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE),
+ .on => .escape_codes,
+ .off => .no_color,
+ };
+
+ const scanned_config: ScannedConfig = sc: {
+ const configuration = c: {
+ var file = cwd.openFile(io, configure_path, .{}) catch |err|
+ fatal("failed to open configuration file {s}: {t}", .{ configure_path, err });
+ defer file.close(io);
+ break :c Configuration.loadFile(arena, io, file) catch |err|
+ fatal("failed to load configuration file {s}: {t}", .{ configure_path, err });
+ };
+ // Technically if the configuration is marked as poisoned, we could
+ // already delete the file now, but we leave it around in case the
+ // maker process fails or crashes and it's helpful to be able to repeat
+ // execution of the command line or otherwise inspect the configuration file.
+ const c = &configuration;
+ var top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index) = .empty;
+ for (configuration.steps, 0..) |*conf_step, step_index_usize| {
+ if (conf_step.owner != .root) continue;
+ const step_index: Configuration.Step.Index = @enumFromInt(step_index_usize);
+ const flags = conf_step.flags(c);
+ switch (flags.tag) {
+ .top_level => {
+ const name = step_index.ptr(c).name.slice(c);
+ try top_level_steps.put(arena, name, step_index);
+ },
+ else => {},
+ }
+ }
+ for (c.search_prefixes) |search_prefix| {
+ try graph.search_prefixes.append(arena, search_prefix.slice(c));
+ }
+ break :sc .{
+ .configuration = configuration,
+ .top_level_steps = top_level_steps,
+ .path = configure_path,
+ };
+ };
+
+ if (help_menu) {
+ var w = initStdoutWriter(io);
+ scanned_config.printUsage(&graph, w) catch |err| switch (err) {
+ error.WriteFailed => return stdout_writer_allocation.err.?,
+ else => |e| return e,
+ };
+ w.flush() catch return stdout_writer_allocation.err.?;
+ return cleanExit(io, &scanned_config);
+ } else if (steps_menu) {
+ var w = initStdoutWriter(io);
+ scanned_config.printSteps(&graph, w) catch |err| switch (err) {
+ error.WriteFailed => return stdout_writer_allocation.err.?,
+ else => |e| return e,
+ };
+ w.flush() catch return stdout_writer_allocation.err.?;
+ return cleanExit(io, &scanned_config);
+ } else if (print_configuration) {
+ var w = initStdoutWriter(io);
+ scanned_config.print(w) catch return stdout_writer_allocation.err.?;
+ w.flush() catch return stdout_writer_allocation.err.?;
+ return cleanExit(io, &scanned_config);
+ }
+
+ if (webui_listen != null) {
+ if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{});
+ if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{});
+ }
+
+ const main_progress_node = std.Progress.start(io, .{
+ .disable_printing = (color == .off),
+ });
+ defer main_progress_node.end();
+
+ const install_prefix_path: Path = if (graph.environ_map.get("DESTDIR")) |dest_dir| .{
+ .root_dir = .cwd(),
+ .sub_path = try Dir.path.join(arena, &.{ dest_dir, override_install_prefix orelse "/usr" }),
+ } else if (override_install_prefix) |cwd_relative| .{
+ .root_dir = .cwd(),
+ .sub_path = cwd_relative,
+ } else .{
+ .root_dir = build_root_directory,
+ .sub_path = "zig-out",
+ };
+
+ const install_lib_path: Path = if (override_lib_dir) |cwd_relative| .{
+ .root_dir = .cwd(),
+ .sub_path = cwd_relative,
+ } else try install_prefix_path.join(arena, "lib");
+
+ const install_bin_path: Path = if (override_bin_dir) |cwd_relative| .{
+ .root_dir = .cwd(),
+ .sub_path = cwd_relative,
+ } else try install_prefix_path.join(arena, "bin");
+
+ const install_include_path: Path = if (override_include_dir) |cwd_relative| .{
+ .root_dir = .cwd(),
+ .sub_path = cwd_relative,
+ } else try install_prefix_path.join(arena, "include");
+
+ var maker: Maker = .{
+ .gpa = gpa,
+ .graph = &graph,
+ .scanned_config = &scanned_config,
+ .install_paths = .{
+ .prefix = install_prefix_path,
+ .lib = install_lib_path,
+ .bin = install_bin_path,
+ .include = install_include_path,
+ },
+
+ .steps = try arena.alloc(Step, scanned_config.configuration.steps.len),
+ .generated_files = try arena.alloc(Path, scanned_config.configuration.generated_files_len),
+ .run_args = run_args,
+
+ .available_rss = max_rss,
+ .max_rss_is_default = false,
+ .max_rss_mutex = .init,
+ .skip_oom_steps = skip_oom_steps,
+ .unit_test_timeout_ns = test_timeout_ns,
+
+ .watch = watch,
+ .web_server = undefined, // set after `prepare`
+ .memory_blocked_steps = .empty,
+ .step_stack = .empty,
+ .pkg_config = .{ .debug = debug_pkg_config },
+
+ .error_style = error_style,
+ .multiline_errors = multiline_errors,
+ .summary = summary orelse if (watch or webui_listen != null) .line else .failures,
+ };
+ defer {
+ maker.memory_blocked_steps.deinit(gpa);
+ maker.step_stack.deinit(gpa);
+ }
+
+ if (maker.available_rss == 0) {
+ maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
+ maker.max_rss_is_default = true;
+ }
+
+ maker.prepare(step_names.items) catch |err| switch (err) {
+ error.DependencyLoopDetected, error.InsufficientMemory => {
+ _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
+ process.exit(1);
+ },
+ else => |e| return e,
+ };
+
+ var w: Watch = w: {
+ if (!watch) break :w undefined;
+ if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag});
+ break :w try .init(&maker);
+ };
+
+ const now = Io.Clock.Timestamp.now(io, .awake);
+
+ maker.web_server = if (webui_listen) |listen_address| ws: {
+ if (builtin.single_threaded) unreachable; // `fatal` above
+ break :ws .init(.{
+ .maker = &maker,
+ .root_prog_node = main_progress_node,
+ .listen_address = listen_address,
+ .base_timestamp = now,
+ });
+ } else null;
+
+ if (maker.web_server) |*ws| {
+ ws.start() catch |err| fatal("failed to start web server: {t}", .{err});
+ }
+
+ rebuild: while (true) : (if (maker.error_style.clearOnUpdate()) {
+ const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
+ defer io.unlockStderr();
+ stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H") catch |err| switch (err) {
+ error.WriteFailed => return stderr.file_writer.err.?,
+ };
+ }) {
+ if (maker.web_server) |*ws| ws.startBuild();
+
+ try maker.makeStepNames(step_names.items, main_progress_node, fuzz);
+
+ if (maker.web_server) |*web_server| {
+ if (fuzz) |mode| if (mode != .forever) fatal(
+ "error: limited fuzzing is not implemented yet for --webui",
+ .{},
+ );
+
+ web_server.finishBuild(.{ .fuzz = fuzz != null });
+ }
+
+ if (maker.web_server) |*web_server| {
+ const c = &scanned_config.configuration;
+ assert(!watch); // fatal error after CLI parsing
+ while (true) switch (try web_server.wait()) {
+ .rebuild => {
+ for (maker.step_stack.keys()) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ step.state = .precheck_done;
+ const deps = step_index.ptr(c).deps.slice(c);
+ step.pending_deps = @intCast(deps.len);
+ step.reset(&maker);
+ }
+ continue :rebuild;
+ },
+ };
+ }
+
+ if (!maker.watch) return;
+
+ // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
+ if (!Watch.have_impl) unreachable;
+
+ try w.update(maker.step_stack.keys());
+
+ // Wait until a file system notification arrives. Read all such events
+ // until the buffer is empty. Then wait for a debounce interval, resetting
+ // if any more events come in. After the debounce interval has passed,
+ // trigger a rebuild on all steps with modified inputs, as well as their
+ // recursive dependants.
+ var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
+ const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
+ w.dir_count, countSubProcesses(&maker),
+ }) catch &caption_buf;
+ var debouncing_node = main_progress_node.start(caption, 0);
+ var in_debounce = false;
+ while (true) switch (try w.wait(if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) {
+ .timeout => {
+ assert(in_debounce);
+ debouncing_node.end();
+ markFailedStepsDirty(&maker);
+ continue :rebuild;
+ },
+ .dirty => if (!in_debounce) {
+ in_debounce = true;
+ debouncing_node.end();
+ debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
+ },
+ .clean => {},
+ };
+ }
+}
+
+fn markFailedStepsDirty(maker: *Maker) void {
+ const all_steps = maker.step_stack.keys();
+
+ for (all_steps) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ switch (step.state) {
+ .dependency_failure, .failure, .skipped => _ = maker.invalidateResult(step),
+ else => continue,
+ }
+ }
+ // Now that all dirty steps have been found, the remaining steps that
+ // succeeded from last run shall be marked "cached".
+ for (all_steps) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ switch (step.state) {
+ .success => step.result_cached = true,
+ else => continue,
+ }
+ }
+}
+
+fn countSubProcesses(maker: *Maker) usize {
+ const all_steps = maker.step_stack.keys();
+ var count: usize = 0;
+ for (all_steps) |step_index| {
+ const s = maker.stepByIndex(step_index);
+ count += @intFromBool(s.getZigProcess() != null);
+ }
+ return count;
+}
+
+const InstallPaths = struct {
+ prefix: Path,
+ lib: Path,
+ bin: Path,
+ include: Path,
+};
+
+pub fn stepByIndex(maker: *const Maker, i: Configuration.Step.Index) *Step {
+ return &maker.steps[@intFromEnum(i)];
+}
+
+fn prepare(maker: *Maker, step_names: []const []const u8) !void {
+ const gpa = maker.gpa;
+ const graph = maker.graph;
+ const arena = graph.arena;
+ const seed: u32 = graph.random_seed;
+ const step_stack = &maker.step_stack;
+ const c = &maker.scanned_config.configuration;
+
+ for (maker.steps, 0..) |*step, step_index_usize| {
+ const step_index: Configuration.Step.Index = @enumFromInt(step_index_usize);
+ step.* = .{ .extended = .init(step_index.ptr(c).flags(c).tag) };
+ }
+
+ if (step_names.len == 0) {
+ try step_stack.put(gpa, c.default_step, {});
+ } else {
+ try step_stack.ensureUnusedCapacity(gpa, step_names.len);
+ for (0..step_names.len) |i| {
+ const step_name = step_names[step_names.len - i - 1];
+ const s = maker.scanned_config.top_level_steps.get(step_name) orelse {
+ log.info("to list available steps: zig build -l", .{});
+ fatal("no such step: {s}", .{step_name});
+ };
+ step_stack.putAssumeCapacity(s, {});
+ }
+ }
+
+ const starting_steps = try arena.dupe(Configuration.Step.Index, step_stack.keys());
+
+ var rng = std.Random.DefaultPrng.init(seed);
+ const rand = rng.random();
+ rand.shuffle(Configuration.Step.Index, starting_steps);
+
+ for (starting_steps) |s| {
+ try constructGraphAndCheckForDependencyLoop(maker, s, &maker.step_stack, rand);
+ }
+
+ {
+ // Check that we have enough memory to complete the build.
+ var any_problems = false;
+ var max_needed: usize = 0;
+ for (step_stack.keys()) |step_index| {
+ const make_step = maker.stepByIndex(step_index);
+ const conf_step = step_index.ptr(c);
+ const max_rss = conf_step.max_rss.toBytes();
+ if (max_rss == 0) continue;
+ max_needed = @max(max_needed, max_rss);
+ if (max_rss > maker.available_rss) {
+ if (maker.skip_oom_steps) {
+ make_step.state = .skipped_oom;
+ for (make_step.dependants.items) |dependant| {
+ maker.stepByIndex(dependant).pending_deps -= 1;
+ }
+ } else {
+ log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{
+ conf_step.owner.depPrefixSlice(c),
+ conf_step.name.slice(c),
+ max_rss,
+ maker.available_rss,
+ });
+ any_problems = true;
+ }
+ }
+ }
+ if (any_problems) {
+ if (maker.max_rss_is_default) {
+ std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{
+ max_needed,
+ });
+ }
+ return error.InsufficientMemory;
+ }
+ }
+}
+
+fn makeStepNames(
+ maker: *Maker,
+ step_names: []const []const u8,
+ parent_prog_node: std.Progress.Node,
+ fuzz: ?Fuzz.Mode,
+) !void {
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const io = graph.io;
+ const step_stack = &maker.step_stack;
+ const top_level_steps = &maker.scanned_config.top_level_steps;
+ const c = &maker.scanned_config.configuration;
+
+ {
+ // Collect the initial set of tasks (those with no outstanding dependencies) into a buffer,
+ // then spawn them. The buffer is so that we don't race with `makeStep` and end up thinking
+ // a step is initial when it actually became ready due to an earlier initial step.
+ var initial_set: std.ArrayList(Configuration.Step.Index) = .empty;
+ defer initial_set.deinit(gpa);
+ try initial_set.ensureUnusedCapacity(gpa, step_stack.count());
+ for (step_stack.keys()) |step_index| {
+ const s = maker.stepByIndex(step_index);
+ if (s.state == .precheck_done and s.pending_deps == 0) {
+ initial_set.appendAssumeCapacity(step_index);
+ }
+ }
+
+ const step_prog = parent_prog_node.start("steps", step_stack.count());
+ defer step_prog.end();
+
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+ // Start working on all of the initial steps...
+ for (initial_set.items) |step_index| try stepReady(maker, &group, step_index, step_prog);
+ // ...and `makeStep` will trigger every other step when their last dependency finishes.
+ try group.await(io);
+ }
+
+ assert(maker.memory_blocked_steps.items.len == 0);
+
+ var test_pass_count: usize = 0;
+ var test_skip_count: usize = 0;
+ var test_fail_count: usize = 0;
+ var test_crash_count: usize = 0;
+ var test_timeout_count: usize = 0;
+
+ var test_count: usize = 0;
+
+ var success_count: usize = 0;
+ var skipped_count: usize = 0;
+ var failure_count: usize = 0;
+ var pending_count: usize = 0;
+ var total_compile_errors: usize = 0;
+
+ var cleanup_task = io.async(cleanTmpFiles, .{ maker, step_stack.keys() });
+ defer cleanup_task.await(io);
+
+ for (step_stack.keys()) |step_index| {
+ const make_step = maker.stepByIndex(step_index);
+ test_pass_count += make_step.test_results.passCount();
+ test_skip_count += make_step.test_results.skip_count;
+ test_fail_count += make_step.test_results.fail_count;
+ test_crash_count += make_step.test_results.crash_count;
+ test_timeout_count += make_step.test_results.timeout_count;
+
+ test_count += make_step.test_results.test_count;
+
+ switch (make_step.state) {
+ .precheck_unstarted => unreachable,
+ .precheck_started => unreachable,
+ .precheck_done => unreachable,
+ .dependency_failure => pending_count += 1,
+ .success => success_count += 1,
+ .skipped, .skipped_oom => skipped_count += 1,
+ .failure => {
+ failure_count += 1;
+ const compile_errors_len = make_step.result_error_bundle.errorMessageCount();
+ if (compile_errors_len > 0) {
+ total_compile_errors += compile_errors_len;
+ }
+ },
+ }
+ }
+
+ if (fuzz) |mode| blk: {
+ switch (builtin.os.tag) {
+ // Current implementation depends on two things that need to be ported to Windows:
+ // * Memory-mapping to share data between the fuzzer and build runner.
+ // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
+ // many addresses to source locations).
+ .windows => fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
+ else => {},
+ }
+ if (@bitSizeOf(usize) != 64) {
+ // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
+ // being compatible with file system's u64 return value. This is not the case
+ // on 32-bit platforms.
+ // Affects or affected by issues #5185, #22523, and #22464.
+ fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
+ }
+
+ switch (mode) {
+ .forever => break :blk,
+ .limit => {},
+ }
+
+ assert(mode == .limit);
+ var f = Fuzz.init(maker, step_stack.keys(), parent_prog_node, mode) catch |err|
+ fatal("failed to start fuzzer: {t}", .{err});
+ defer f.deinit();
+
+ f.start();
+ try f.waitAndPrintReport();
+ }
+
+ // Every test has a state
+ assert(test_pass_count + test_skip_count + test_fail_count + test_crash_count + test_timeout_count == test_count);
+
+ if (failure_count == 0) {
+ std.Progress.setStatus(.success);
+ } else {
+ std.Progress.setStatus(.failure);
+ }
+
+ summary: {
+ switch (maker.summary) {
+ .all, .new, .line => {},
+ .failures => if (failure_count == 0) break :summary,
+ .none => break :summary,
+ }
+
+ const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
+ defer io.unlockStderr();
+ const t = stderr.terminal();
+ const w = &stderr.file_writer.interface;
+
+ const total_count = success_count + failure_count + pending_count + skipped_count;
+ t.setColor(.cyan) catch {};
+ t.setColor(.bold) catch {};
+ w.writeAll("Build Summary: ") catch {};
+ t.setColor(.reset) catch {};
+ w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
+ {
+ t.setColor(.dim) catch {};
+ var first = true;
+ if (skipped_count > 0) {
+ w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {};
+ first = false;
+ }
+ if (failure_count > 0) {
+ w.print("{s}{d} failed", .{ if (first) " (" else ", ", failure_count }) catch {};
+ first = false;
+ }
+ if (!first) w.writeByte(')') catch {};
+ t.setColor(.reset) catch {};
+ }
+
+ if (test_count > 0) {
+ w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {};
+ t.setColor(.dim) catch {};
+ var first = true;
+ if (test_skip_count > 0) {
+ w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {};
+ first = false;
+ }
+ if (test_fail_count > 0) {
+ w.print("{s}{d} failed", .{ if (first) " (" else ", ", test_fail_count }) catch {};
+ first = false;
+ }
+ if (test_crash_count > 0) {
+ w.print("{s}{d} crashed", .{ if (first) " (" else ", ", test_crash_count }) catch {};
+ first = false;
+ }
+ if (test_timeout_count > 0) {
+ w.print("{s}{d} timed out", .{ if (first) " (" else ", ", test_timeout_count }) catch {};
+ first = false;
+ }
+ if (!first) w.writeByte(')') catch {};
+ t.setColor(.reset) catch {};
+ }
+
+ w.writeAll("\n") catch {};
+
+ if (maker.summary == .line) break :summary;
+
+ // Print a fancy tree with build results.
+ var step_stack_copy = try step_stack.clone(gpa);
+ defer step_stack_copy.deinit(gpa);
+
+ var print_node: PrintNode = .{ .parent = null };
+ if (step_names.len == 0) {
+ print_node.last = true;
+ printTreeStep(maker, c.default_step, t, &print_node, &step_stack_copy) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ else => {},
+ };
+ } else {
+ const last_index = if (maker.summary == .all) top_level_steps.count() else blk: {
+ var i: usize = step_names.len;
+ while (i > 0) {
+ i -= 1;
+ const step_index = top_level_steps.get(step_names[i]).?;
+ const step = maker.stepByIndex(step_index);
+ const found = switch (maker.summary) {
+ .all, .line, .none => unreachable,
+ .failures => step.state != .success,
+ .new => !step.result_cached,
+ };
+ if (found) break :blk i;
+ }
+ break :blk top_level_steps.count();
+ };
+ for (step_names, 0..) |step_name, i| {
+ const step_index = top_level_steps.get(step_name).?;
+ print_node.last = i + 1 == last_index;
+ printTreeStep(maker, step_index, t, &print_node, &step_stack_copy) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ else => {},
+ };
+ }
+ }
+ w.writeByte('\n') catch {};
+ }
+
+ if (maker.watch or maker.web_server != null) return;
+
+ const code: u8 = code: {
+ if (failure_count == 0) break :code 0; // success
+ if (maker.error_style.verboseContext()) break :code 1; // failure; print build command
+ break :code 2; // failure; do not print build command
+ };
+ if (code == 0) {
+ removePoisonedConfiguration(io, maker.scanned_config);
+ if (debugMakerLeaks()) return deinit(maker);
+ }
+ cleanup_task.await(io); // There is a defer above but an exit below.
+ _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
+ process.exit(code);
+}
+
+fn deinit(maker: *Maker) void {
+ const gpa = maker.gpa;
+ for (maker.steps) |*step| {
+ step.clearFailedCommand(gpa);
+ step.clearErrorBundle(gpa);
+ step.inputs.deinit(gpa);
+ }
+}
+
+fn stepReady(
+ maker: *Maker,
+ group: *Io.Group,
+ step_index: Configuration.Step.Index,
+ root_prog_node: std.Progress.Node,
+) Io.Cancelable!void {
+ const graph = maker.graph;
+ const io = graph.io;
+ const c = &maker.scanned_config.configuration;
+ const max_rss = step_index.ptr(c).max_rss.toBytes();
+ if (max_rss != 0) {
+ try maker.max_rss_mutex.lock(io);
+ defer maker.max_rss_mutex.unlock(io);
+ if (maker.available_rss < max_rss) {
+ // Running this step right now could possibly exceed the allotted RSS.
+ maker.memory_blocked_steps.append(maker.gpa, step_index) catch
+ @panic("TODO eliminate memory allocation here");
+ return;
+ }
+ maker.available_rss -= max_rss;
+ }
+ group.async(io, makeStep, .{ maker, group, step_index, root_prog_node });
+}
+
+/// Runs the "make" function of the single step `s`, updates its state, and then spawns newly-ready
+/// dependant steps in `group`. If `s` makes an RSS claim (i.e. `s.max_rss != 0`), the caller must
+/// have already subtracted this value from `maker.available_rss`. This function will release the RSS
+/// claim (i.e. add `s.max_rss` back into `maker.available_rss`) and queue any viable memory-blocked
+/// steps after "make" completes for `s`.
+fn makeStep(
+ maker: *Maker,
+ group: *Io.Group,
+ step_index: Configuration.Step.Index,
+ root_prog_node: std.Progress.Node,
+) Io.Cancelable!void {
+ const graph = maker.graph;
+ const io = graph.io;
+ const gpa = maker.gpa;
+ const c = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(c);
+ const step_name = conf_step.name.slice(c);
+ const deps = conf_step.deps.slice(c);
+ const make_step = maker.stepByIndex(step_index);
+
+ {
+ const step_prog_node = root_prog_node.start(step_name, 0);
+ defer step_prog_node.end();
+
+ if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .wip);
+
+ const new_state: Step.State = for (deps) |dep_index| {
+ const dep_make_step = maker.stepByIndex(dep_index);
+ switch (@atomicLoad(Step.State, &dep_make_step.state, .monotonic)) {
+ .precheck_unstarted => unreachable,
+ .precheck_started => unreachable,
+ .precheck_done => unreachable,
+
+ .failure,
+ .dependency_failure,
+ .skipped_oom,
+ => break .dependency_failure,
+
+ .success, .skipped => {},
+ }
+ } else if (Step.make(step_index, maker, step_prog_node)) state: {
+ break :state .success;
+ } else |err| switch (err) {
+ error.MakeFailed => .failure,
+ error.MakeSkipped => .skipped,
+ error.Canceled => |e| return e,
+ };
+
+ @atomicStore(Step.State, &make_step.state, new_state, .monotonic);
+
+ switch (new_state) {
+ .precheck_unstarted => unreachable,
+ .precheck_started => unreachable,
+ .precheck_done => unreachable,
+
+ .failure,
+ .dependency_failure,
+ .skipped_oom,
+ => {
+ if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .failure);
+ std.Progress.setStatus(.failure_working);
+ },
+
+ .success,
+ .skipped,
+ => {
+ if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .success);
+ },
+ }
+ }
+
+ // No matter the result, we want to display error/warning messages.
+ if (make_step.result_error_bundle.errorMessageCount() > 0 or
+ make_step.result_error_msgs.items.len > 0 or
+ make_step.result_stderr.len > 0)
+ {
+ const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
+ defer io.unlockStderr();
+ printErrorMessages(maker, step_index, .{}, stderr.terminal(), maker.error_style, maker.multiline_errors) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ error.WriteFailed => switch (stderr.file_writer.err.?) {
+ error.Canceled => |e| return e,
+ else => {},
+ },
+ else => {},
+ };
+ }
+
+ const max_rss = conf_step.max_rss.toBytes();
+ if (max_rss != 0) {
+ var dispatch_set: std.ArrayList(Configuration.Step.Index) = .empty;
+ defer dispatch_set.deinit(gpa);
+
+ // Release our RSS claim and kick off some blocked steps if possible. We use `dispatch_set`
+ // as a staging buffer to avoid recursing into `makeStep` while `maker.max_rss_mutex` is held.
+ {
+ try maker.max_rss_mutex.lock(io);
+ defer maker.max_rss_mutex.unlock(io);
+ maker.available_rss += max_rss;
+ dispatch_set.ensureUnusedCapacity(gpa, maker.memory_blocked_steps.items.len) catch
+ @panic("TODO eliminate memory allocation here");
+ while (maker.memory_blocked_steps.getLast()) |candidate_index| {
+ const candidate_max_rss = candidate_index.ptr(c).max_rss.toBytes();
+ if (maker.available_rss < candidate_max_rss) break;
+ assert(maker.memory_blocked_steps.pop() == candidate_index);
+ dispatch_set.appendAssumeCapacity(candidate_index);
+ }
+ }
+ for (dispatch_set.items) |candidate| {
+ group.async(io, makeStep, .{ maker, group, candidate, root_prog_node });
+ }
+ }
+
+ for (make_step.dependants.items) |dependant_index| {
+ const dependant = maker.stepByIndex(dependant_index);
+ // `.acq_rel` synchronizes with itself to ensure all dependencies' final states are visible when this hits 0.
+ if (@atomicRmw(u32, &dependant.pending_deps, .Sub, 1, .acq_rel) == 1) {
+ try stepReady(maker, group, dependant_index, root_prog_node);
+ }
+ }
+}
+
+fn printTreeStep(
+ maker: *Maker,
+ step_index: Configuration.Step.Index,
+ stderr: Io.Terminal,
+ parent_node: *PrintNode,
+ step_stack: *std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void),
+) !void {
+ const writer = stderr.writer;
+ const first = step_stack.swapRemove(step_index);
+ const summary = maker.summary;
+ const c = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(c);
+ const make_step = maker.stepByIndex(step_index);
+ const skip = switch (summary) {
+ .none, .line => unreachable,
+ .all => false,
+ .new => make_step.result_cached,
+ .failures => make_step.state == .success,
+ };
+ if (skip) return;
+ try printPrefix(parent_node, stderr);
+
+ if (parent_node.parent != null) {
+ if (parent_node.last) {
+ try printChildNodePrefix(stderr);
+ } else {
+ try writer.writeAll(switch (stderr.mode) {
+ .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─
+ else => "+- ",
+ });
+ }
+ }
+
+ if (!first) try stderr.setColor(.dim);
+
+ // dep_prefix omitted here because it is redundant with the tree.
+ try writer.writeAll(conf_step.name.slice(c));
+
+ const deps = conf_step.deps.slice(c);
+
+ if (first) {
+ try printStepStatus(maker, step_index, stderr);
+
+ const last_index = if (summary == .all) deps.len -| 1 else blk: {
+ var i: usize = deps.len;
+ while (i > 0) {
+ i -= 1;
+
+ const dep_index = deps[i];
+ const dep = maker.stepByIndex(dep_index);
+ const found = switch (summary) {
+ .all, .line, .none => unreachable,
+ .failures => dep.state != .success,
+ .new => !dep.result_cached,
+ };
+ if (found) break :blk i;
+ }
+ break :blk deps.len -| 1;
+ };
+ for (deps, 0..) |dep, i| {
+ var print_node: PrintNode = .{
+ .parent = parent_node,
+ .last = i == last_index,
+ };
+ try printTreeStep(maker, dep, stderr, &print_node, step_stack);
+ }
+ } else {
+ if (deps.len == 0) {
+ try writer.writeAll(" (reused)\n");
+ } else {
+ try writer.print(" (+{d} more reused dependencies)\n", .{deps.len});
+ }
+ try stderr.setColor(.reset);
+ }
+}
+
+fn printStepStatus(maker: *Maker, step_index: Configuration.Step.Index, stderr: Io.Terminal) !void {
+ const s = maker.stepByIndex(step_index);
+ const writer = stderr.writer;
+ switch (s.state) {
+ .precheck_unstarted => unreachable,
+ .precheck_started => unreachable,
+ .precheck_done => unreachable,
+
+ .dependency_failure => {
+ try stderr.setColor(.dim);
+ try writer.writeAll(" transitive failure\n");
+ try stderr.setColor(.reset);
+ },
+
+ .success => {
+ try stderr.setColor(.green);
+ if (s.result_cached) {
+ try writer.writeAll(" cached");
+ } else if (s.test_results.test_count > 0) {
+ const pass_count = s.test_results.passCount();
+ assert(s.test_results.test_count == pass_count + s.test_results.skip_count);
+ try writer.print(" {d} pass", .{pass_count});
+ if (s.test_results.skip_count > 0) {
+ try stderr.setColor(.reset);
+ try writer.writeAll(", ");
+ try stderr.setColor(.yellow);
+ try writer.print("{d} skip", .{s.test_results.skip_count});
+ }
+ try stderr.setColor(.reset);
+ try writer.print(" ({d} total)", .{s.test_results.test_count});
+ } else {
+ try writer.writeAll(" success");
+ }
+ try stderr.setColor(.reset);
+ if (s.result_duration_ns) |ns| {
+ try stderr.setColor(.dim);
+ if (ns >= std.time.ns_per_min) {
+ try writer.print(" {d}m", .{ns / std.time.ns_per_min});
+ } else if (ns >= std.time.ns_per_s) {
+ try writer.print(" {d}s", .{ns / std.time.ns_per_s});
+ } else if (ns >= std.time.ns_per_ms) {
+ try writer.print(" {d}ms", .{ns / std.time.ns_per_ms});
+ } else if (ns >= std.time.ns_per_us) {
+ try writer.print(" {d}us", .{ns / std.time.ns_per_us});
+ } else {
+ try writer.print(" {d}ns", .{ns});
+ }
+ try stderr.setColor(.reset);
+ }
+ if (s.result_peak_rss != 0) {
+ const rss = s.result_peak_rss;
+ try stderr.setColor(.dim);
+ if (rss >= 1000_000_000) {
+ try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000});
+ } else if (rss >= 1000_000) {
+ try writer.print(" MaxRSS:{d}M", .{rss / 1000_000});
+ } else if (rss >= 1000) {
+ try writer.print(" MaxRSS:{d}K", .{rss / 1000});
+ } else {
+ try writer.print(" MaxRSS:{d}B", .{rss});
+ }
+ try stderr.setColor(.reset);
+ }
+ try writer.writeAll("\n");
+ },
+ .skipped => {
+ try stderr.setColor(.yellow);
+ try writer.writeAll(" skipped\n");
+ try stderr.setColor(.reset);
+ },
+ .skipped_oom => {
+ const c = &maker.scanned_config.configuration;
+ const max_rss = step_index.ptr(c).max_rss.toBytes();
+ try stderr.setColor(.yellow);
+ try writer.writeAll(" skipped (not enough memory)");
+ try stderr.setColor(.dim);
+ try writer.print(" upper bound of {d} exceeded runner limit ({d})\n", .{
+ max_rss, maker.available_rss,
+ });
+ try stderr.setColor(.reset);
+ },
+ .failure => {
+ try printStepFailure(maker, step_index, stderr, false);
+ try stderr.setColor(.reset);
+ },
+ }
+}
+
+fn printStepFailure(
+ maker: *Maker,
+ step_index: Configuration.Step.Index,
+ stderr: Io.Terminal,
+ dim: bool,
+) !void {
+ const w = stderr.writer;
+ const s = maker.stepByIndex(step_index);
+ if (s.result_error_bundle.errorMessageCount() > 0) {
+ try stderr.setColor(.red);
+ try w.print(" {d} errors\n", .{
+ s.result_error_bundle.errorMessageCount(),
+ });
+ } else if (!s.test_results.isSuccess()) {
+ // These first values include all of the test "statuses". Every test is either passsed,
+ // skipped, failed, crashed, or timed out.
+ try stderr.setColor(.green);
+ try w.print(" {d} pass", .{s.test_results.passCount()});
+ try stderr.setColor(.reset);
+ if (dim) try stderr.setColor(.dim);
+ if (s.test_results.skip_count > 0) {
+ try w.writeAll(", ");
+ try stderr.setColor(.yellow);
+ try w.print("{d} skip", .{s.test_results.skip_count});
+ try stderr.setColor(.reset);
+ if (dim) try stderr.setColor(.dim);
+ }
+ if (s.test_results.fail_count > 0) {
+ try w.writeAll(", ");
+ try stderr.setColor(.red);
+ try w.print("{d} fail", .{s.test_results.fail_count});
+ try stderr.setColor(.reset);
+ if (dim) try stderr.setColor(.dim);
+ }
+ if (s.test_results.crash_count > 0) {
+ try w.writeAll(", ");
+ try stderr.setColor(.red);
+ try w.print("{d} crash", .{s.test_results.crash_count});
+ try stderr.setColor(.reset);
+ if (dim) try stderr.setColor(.dim);
+ }
+ if (s.test_results.timeout_count > 0) {
+ try w.writeAll(", ");
+ try stderr.setColor(.red);
+ try w.print("{d} timeout", .{s.test_results.timeout_count});
+ try stderr.setColor(.reset);
+ if (dim) try stderr.setColor(.dim);
+ }
+ try w.print(" ({d} total)", .{s.test_results.test_count});
+
+ // Memory leaks are intentionally written after the total, because is isn't a test *status*,
+ // but just a flag that any tests -- even passed ones -- can have. We also use a different
+ // separator, so it looks like:
+ // 2 pass, 1 skip, 2 fail (5 total); 2 leaks
+ if (s.test_results.leak_count > 0) {
+ try w.writeAll("; ");
+ try stderr.setColor(.red);
+ try w.print("{d} leaks", .{s.test_results.leak_count});
+ try stderr.setColor(.reset);
+ if (dim) try stderr.setColor(.dim);
+ }
+
+ // It's usually not helpful to know how many error logs there were because they tend to
+ // just come with other errors (e.g. crashes and leaks print stack traces, and clean
+ // failures print error traces). So only mention them if they're the only thing causing
+ // the failure.
+ const show_err_logs: bool = show: {
+ var alt_results = s.test_results;
+ alt_results.log_err_count = 0;
+ break :show alt_results.isSuccess();
+ };
+ if (show_err_logs) {
+ try w.writeAll("; ");
+ try stderr.setColor(.red);
+ try w.print("{d} error logs", .{s.test_results.log_err_count});
+ try stderr.setColor(.reset);
+ if (dim) try stderr.setColor(.dim);
+ }
+
+ try w.writeAll("\n");
+ } else if (s.result_error_msgs.items.len > 0) {
+ try stderr.setColor(.red);
+ try w.writeAll(" failure\n");
+ } else {
+ assert(s.result_stderr.len > 0);
+ try stderr.setColor(.red);
+ try w.writeAll(" w\n");
+ }
+}
+
+const PrintNode = struct {
+ parent: ?*PrintNode,
+ last: bool = false,
+};
+
+fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void {
+ const parent = node.parent orelse return;
+ const writer = stderr.writer;
+ if (parent.parent == null) return;
+ try printPrefix(parent, stderr);
+ if (parent.last) {
+ try writer.writeAll(" ");
+ } else {
+ try writer.writeAll(switch (stderr.mode) {
+ .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │
+ else => "| ",
+ });
+ }
+}
+
+fn printChildNodePrefix(stderr: Io.Terminal) !void {
+ try stderr.writer.writeAll(switch (stderr.mode) {
+ .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─
+ else => "+- ",
+ });
+}
+
+/// Traverse the dependency graph depth-first and make it undirected by having
+/// steps know their dependants (they only know dependencies at start).
+/// Along the way, check that there is no dependency loop, and record the steps
+/// in traversal order in `step_stack`.
+/// Each step has its dependencies traversed in random order, this accomplishes
+/// two things:
+/// - `step_stack` will be in randomized-depth-first order, so the build runner
+/// spawns initial steps in a random order
+/// - each step's `dependants` list is also filled in a random order, so that
+/// when it finishes executing in `makeStep`, it spawns next steps to run in
+/// random order
+fn constructGraphAndCheckForDependencyLoop(
+ maker: *Maker,
+ step_index: Configuration.Step.Index,
+ step_stack: *std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void),
+ rand: std.Random,
+) error{ DependencyLoopDetected, OutOfMemory }!void {
+ const c = &maker.scanned_config.configuration;
+ const gpa = maker.gpa;
+ const arena = maker.graph.arena;
+ const make_step = maker.stepByIndex(step_index);
+ switch (make_step.state) {
+ .precheck_started => {
+ log.err("dependency loop detected: {s}", .{step_index.ptr(c).name.slice(c)});
+ return error.DependencyLoopDetected;
+ },
+ .precheck_unstarted => {
+ make_step.state = .precheck_started;
+
+ const step = step_index.ptr(c);
+ const dependencies = step.deps.slice(c);
+ try step_stack.ensureUnusedCapacity(gpa, dependencies.len);
+
+ // We dupe to avoid shuffling the steps in the summary, it depends
+ // on dependencies' order.
+ const deps = try gpa.dupe(Configuration.Step.Index, dependencies);
+ defer gpa.free(deps);
+
+ rand.shuffle(Configuration.Step.Index, deps);
+
+ for (deps) |dep| {
+ const dep_step = maker.stepByIndex(dep);
+ try step_stack.put(gpa, dep, {});
+ try dep_step.dependants.append(arena, step_index);
+ constructGraphAndCheckForDependencyLoop(maker, dep, step_stack, rand) catch |err| switch (err) {
+ error.DependencyLoopDetected => {
+ log.info("needed by: {s}", .{step_index.ptr(c).name.slice(c)});
+ return err;
+ },
+ else => return err,
+ };
+ }
+
+ make_step.state = .precheck_done;
+ make_step.pending_deps = @intCast(dependencies.len);
+ },
+ .precheck_done => {},
+
+ // These don't happen until we actually run the step graph.
+ .dependency_failure => unreachable,
+ .success => unreachable,
+ .failure => unreachable,
+ .skipped => unreachable,
+ .skipped_oom => unreachable,
+ }
+}
+
+/// When file watching, prepares the step for being re-evaluated. Returns
+/// `true` if the step was newly invalidated, `false` if it was already
+/// invalidated.
+pub fn invalidateResult(maker: *Maker, step: *Step) bool {
+ if (step.state == .precheck_done) return false;
+ assert(step.pending_deps == 0);
+ step.state = .precheck_done;
+ step.reset(maker);
+ for (step.dependants.items) |dependant_index| {
+ const dependant = maker.stepByIndex(dependant_index);
+ _ = invalidateResult(maker, dependant);
+ dependant.pending_deps += 1;
+ }
+ return true;
+}
+
+pub fn printErrorMessages(
+ maker: *Maker,
+ failing_step_index: Configuration.Step.Index,
+ options: std.zig.ErrorBundle.RenderOptions,
+ stderr: Io.Terminal,
+ error_style: ErrorStyle,
+ multiline_errors: MultilineErrors,
+) !void {
+ const c = &maker.scanned_config.configuration;
+ const gpa = maker.gpa;
+ const writer = stderr.writer;
+ if (error_style.verboseContext()) {
+ // Provide context for where these error messages are coming from by
+ // printing the corresponding Step subtree.
+ var step_stack: std.ArrayList(Configuration.Step.Index) = .empty;
+ defer step_stack.deinit(gpa);
+ try step_stack.append(gpa, failing_step_index);
+ while (true) {
+ const last_step = maker.stepByIndex(step_stack.items[step_stack.items.len - 1]);
+ if (last_step.dependants.items.len == 0) break;
+ try step_stack.append(gpa, last_step.dependants.items[0]);
+ }
+
+ // Now, `step_stack` has the subtree that we want to print, in reverse order.
+ try stderr.setColor(.dim);
+ var indent: usize = 0;
+ while (step_stack.pop()) |step_index| : (indent += 1) {
+ if (indent > 0) {
+ try writer.splatByteAll(' ', (indent - 1) * 3);
+ try printChildNodePrefix(stderr);
+ }
+
+ try writer.writeAll(step_index.ptr(c).name.slice(c));
+
+ if (step_index == failing_step_index) {
+ try printStepFailure(maker, step_index, stderr, true);
+ } else {
+ try writer.writeAll("\n");
+ }
+ }
+ try stderr.setColor(.reset);
+ } else {
+ // Just print the failing step itself.
+ try stderr.setColor(.dim);
+ try writer.writeAll(failing_step_index.ptr(c).name.slice(c));
+ try printStepFailure(maker, failing_step_index, stderr, true);
+ try stderr.setColor(.reset);
+ }
+
+ const failing_step = maker.stepByIndex(failing_step_index);
+
+ if (failing_step.result_stderr.len > 0) {
+ try writer.writeAll(failing_step.result_stderr);
+ if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) {
+ try writer.writeAll("\n");
+ }
+ }
+
+ try failing_step.result_error_bundle.renderToTerminal(options, stderr);
+
+ for (failing_step.result_error_msgs.items) |msg| {
+ try stderr.setColor(.red);
+ try writer.writeAll("error:");
+ try stderr.setColor(.reset);
+ if (std.mem.indexOfScalar(u8, msg, '\n') == null) {
+ try writer.print(" {s}\n", .{msg});
+ } else switch (multiline_errors) {
+ .indent => {
+ var it = std.mem.splitScalar(u8, msg, '\n');
+ try writer.print(" {s}\n", .{it.first()});
+ while (it.next()) |line| {
+ try writer.print(" {s}\n", .{line});
+ }
+ },
+ .newline => try writer.print("\n{s}\n", .{msg}),
+ .none => try writer.print(" {s}\n", .{msg}),
+ }
+ }
+
+ if (error_style.verboseContext()) {
+ if (failing_step.result_failed_command) |cmd_str| {
+ try stderr.setColor(.red);
+ try writer.writeAll("failed command: ");
+ try stderr.setColor(.reset);
+ try writer.writeAll(cmd_str);
+ try writer.writeByte('\n');
+ }
+ }
+
+ if (failing_step.result_oom) {
+ try stderr.setColor(.red);
+ try writer.writeAll("error information missing due to allocation failure");
+ try stderr.setColor(.reset);
+ try writer.writeByte('\n');
+ }
+
+ try writer.writeByte('\n');
+}
+
+fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
+ if (idx.* >= args.len) return null;
+ defer idx.* += 1;
+ return args[idx.*];
+}
+
+fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
+ return nextArg(args, idx) orelse {
+ fatalWithHint("expected argument after {q}", .{args[idx.* - 1]});
+ };
+}
+
+fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {
+ const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});
+ if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg });
+ const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first});
+ return arg;
+}
+
+fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
+ if (idx >= args.len) return null;
+ return args[idx..];
+}
+
+const Color = std.zig.Color;
+const ErrorStyle = enum {
+ verbose,
+ minimal,
+ verbose_clear,
+ minimal_clear,
+ fn verboseContext(s: ErrorStyle) bool {
+ return switch (s) {
+ .verbose, .verbose_clear => true,
+ .minimal, .minimal_clear => false,
+ };
+ }
+ fn clearOnUpdate(s: ErrorStyle) bool {
+ return switch (s) {
+ .verbose, .minimal => false,
+ .verbose_clear, .minimal_clear => true,
+ };
+ }
+};
+const MultilineErrors = enum { indent, newline, none };
+const Summary = enum { all, new, failures, line, none };
+
+fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
+ log.info("to access the help menu: zig build -h", .{});
+ fatal(f, args);
+}
+
+fn cleanTmpFiles(maker: *Maker, steps: []const Configuration.Step.Index) void {
+ const graph = maker.graph;
+ const io = graph.io;
+ const conf = &maker.scanned_config.configuration;
+
+ for (steps) |step_index| {
+ const conf_step = step_index.ptr(conf);
+ const wf = conf_step.extended.cast(conf, Configuration.Step.WriteFile) orelse continue;
+ if (wf.flags.mode != .tmp) continue;
+ const step = maker.stepByIndex(step_index);
+ if (step.state != .success) continue;
+ const tmp_path = generatedPath(maker, wf.generated_directory).*;
+ tmp_path.root_dir.handle.deleteTree(io, tmp_path.subPathOrDot()) catch |err|
+ log.warn("failed to delete temporary path {f}: {t}", .{ tmp_path, err });
+ }
+}
+
+var stdio_buffer_allocation: [256]u8 = undefined;
+var stdout_writer_allocation: Io.File.Writer = undefined;
+
+fn initStdoutWriter(io: Io) *Writer {
+ stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation);
+ return &stdout_writer_allocation.interface;
+}
+
+/// `asking_step` is only used for debugging purposes; it's the step being run
+/// that is asking for the path.
+pub fn resolveLazyPath(
+ maker: *const Maker,
+ arena: Allocator,
+ lazy_path: Configuration.LazyPath,
+ asking_step_index: Configuration.Step.Index,
+) error{ OutOfMemory, MakeFailed }!Path {
+ const c = &maker.scanned_config.configuration;
+ return switch (lazy_path) {
+ .source_path => |sp| try packagePath(maker, arena, sp.owner, sp.sub_path.slice(c)),
+ .relative => |relative| relativePath(maker, arena, relative),
+ .generated => |gen| {
+ const base = generatedPath(maker, gen.index).*;
+ var file_path = base;
+ for (0..gen.flags.up) |_| {
+ file_path.sub_path = Dir.path.dirname(file_path.sub_path) orelse {
+ const s = stepByIndex(maker, asking_step_index);
+ return s.fail(maker, "invalid LazyPath traversal: up {d} times from {f}", .{
+ gen.flags.up, base,
+ });
+ };
+ }
+ return file_path.join(arena, gen.sub_path.slice(c));
+ },
+ };
+}
+
+pub fn resolveLazyPathIndex(
+ maker: *const Maker,
+ arena: Allocator,
+ lazy_path_index: Configuration.LazyPath.Index,
+ asking_step_index: Configuration.Step.Index,
+) error{ OutOfMemory, MakeFailed }!Path {
+ const c = &maker.scanned_config.configuration;
+ return resolveLazyPath(maker, arena, lazy_path_index.get(c), asking_step_index);
+}
+
+/// `resolveLazyPath` is preferred, but this can be necessary when passing Path
+/// objects to child processes.
+pub fn resolveLazyPathAbs(
+ maker: *const Maker,
+ arena: Allocator,
+ lazy_path: Configuration.LazyPath,
+ asking_step_index: Configuration.Step.Index,
+) error{ OutOfMemory, MakeFailed }![]const u8 {
+ const p = try resolveLazyPath(maker, arena, lazy_path, asking_step_index);
+ const root_dir_path = p.root_dir.path orelse return p.subPathOrDot();
+ if (p.sub_path.len == 0) return root_dir_path;
+ return Dir.path.join(arena, &.{ root_dir_path, p.sub_path });
+}
+
+/// `resolveLazyPath` is preferred, but this can be necessary when passing Path
+/// objects to child processes.
+pub fn resolveLazyPathIndexAbs(
+ maker: *const Maker,
+ arena: Allocator,
+ lazy_path_index: Configuration.LazyPath.Index,
+ asking_step_index: Configuration.Step.Index,
+) error{ OutOfMemory, MakeFailed }![]const u8 {
+ const c = &maker.scanned_config.configuration;
+ return resolveLazyPathAbs(maker, arena, lazy_path_index.get(c), asking_step_index);
+}
+
+pub fn generatedPath(maker: *const Maker, index: Configuration.GeneratedFileIndex) *Path {
+ return &maker.generated_files[@intFromEnum(index)];
+}
+
+pub fn packagePath(
+ maker: *const Maker,
+ arena: Allocator,
+ package_index: Configuration.Package.Index,
+ sub_path: []const u8,
+) Allocator.Error!Path {
+ const c = &maker.scanned_config.configuration;
+ const graph = maker.graph;
+ const package = package_index.get(c) orelse return .{
+ .root_dir = graph.build_root_directory,
+ .sub_path = sub_path,
+ };
+ // Currently, neither configurer nor Maker is aware of the standard zig
+ // package path, and the root path is stored as a bare string rather than
+ // relative to a known base directory. Without changing that, we must
+ // construct a cwd relative path here.
+ return .{
+ .root_dir = .cwd(),
+ .sub_path = try Dir.path.join(arena, &.{ package.root_path.slice(c), sub_path }),
+ };
+}
+
+pub fn relativePath(maker: *const Maker, arena: Allocator, relative: Configuration.LazyPath.Relative) Allocator.Error!Path {
+ const graph = maker.graph;
+ const c = &maker.scanned_config.configuration;
+ const sub_path = relative.sub_path.slice(c);
+ return switch (relative.flags.base) {
+ .cwd => .{
+ .root_dir = .cwd(),
+ .sub_path = sub_path,
+ },
+ .local_cache => .{
+ .root_dir = graph.local_cache_root,
+ .sub_path = sub_path,
+ },
+ .global_cache => .{
+ .root_dir = graph.global_cache_root,
+ .sub_path = sub_path,
+ },
+ .build_root => .{
+ .root_dir = graph.build_root_directory,
+ .sub_path = sub_path,
+ },
+ .zig_exe => .{
+ .root_dir = .cwd(),
+ .sub_path = if (sub_path.len == 0)
+ graph.zig_exe
+ else
+ try Io.Dir.path.join(arena, &.{ graph.zig_exe, sub_path }),
+ },
+ .zig_lib => .{
+ .root_dir = graph.zig_lib_directory,
+ .sub_path = sub_path,
+ },
+ .install_prefix => maker.install_paths.prefix,
+ .install_lib => maker.install_paths.lib,
+ .install_bin => maker.install_paths.bin,
+ .install_include => maker.install_paths.include,
+ };
+}
+
+pub fn resolveInstallDir(
+ maker: *Maker,
+ arena: Allocator,
+ dest_dir: Configuration.InstallDestDir,
+) Allocator.Error!Path {
+ const c = &maker.scanned_config.configuration;
+ return switch (dest_dir.unpack().?) {
+ .prefix => maker.install_paths.prefix,
+ .lib => maker.install_paths.lib,
+ .bin => maker.install_paths.bin,
+ .header => maker.install_paths.include,
+ .sub_path => |s| try maker.install_paths.prefix.join(arena, s.slice(c)),
+ };
+}
+
+pub fn installLazyPathSub(
+ maker: *Maker,
+ arena: Allocator,
+ source: Configuration.LazyPath.Index,
+ dest_dir: Configuration.InstallDestDir,
+ sub_path: []const u8,
+ asking_step_index: Configuration.Step.Index,
+) !Dir.PrevStatus {
+ const src_path = try resolveLazyPathIndex(maker, arena, source, asking_step_index);
+ const dest_dir_path = try resolveInstallDir(maker, arena, dest_dir);
+ const dest_path = try dest_dir_path.join(arena, sub_path);
+ return installPath(maker, arena, src_path, dest_path, asking_step_index);
+}
+
+pub fn installLazyPath(
+ maker: *Maker,
+ arena: Allocator,
+ source: Configuration.LazyPath.Index,
+ dest_dir: Configuration.InstallDestDir,
+ asking_step_index: Configuration.Step.Index,
+) !Dir.PrevStatus {
+ const src_path = try resolveLazyPathIndex(maker, arena, source, asking_step_index);
+ const dest_dir_path = try resolveInstallDir(maker, arena, dest_dir);
+ const dest_path = try dest_dir_path.join(arena, src_path.basename());
+ return installPath(maker, arena, src_path, dest_path, asking_step_index);
+}
+
+pub fn installGenerated(
+ maker: *Maker,
+ arena: Allocator,
+ source: Configuration.GeneratedFileIndex,
+ dest_dir: Configuration.InstallDestDir,
+ asking_step_index: Configuration.Step.Index,
+) !Dir.PrevStatus {
+ const src_path = generatedPath(maker, source).*;
+ const dest_dir_path = try resolveInstallDir(maker, arena, dest_dir);
+ const dest_path = try dest_dir_path.join(arena, src_path.basename());
+ return installPath(maker, arena, src_path, dest_path, asking_step_index);
+}
+
+pub fn truncatePath(
+ maker: *Maker,
+ arena: Allocator,
+ dest_path: Path,
+ asking_step_index: Configuration.Step.Index,
+) Step.ExtendedMakeError!void {
+ const graph = maker.graph;
+ const io = graph.io;
+ if (graph.verbose) try graph.handleVerbose(null, null, &.{
+ "truncate", try dest_path.toString(arena),
+ });
+ const err = e: {
+ var file = f: {
+ break :f dest_path.root_dir.handle.createFile(io, dest_path.sub_path, .{}) catch |err| switch (err) {
+ error.FileNotFound => {
+ const parent_path = dest_path.dirname() orelse break :e err;
+ parent_path.root_dir.handle.createDirPath(io, parent_path.sub_path) catch |in| switch (in) {
+ error.Canceled => |e| return e,
+ else => |e| {
+ const s = stepByIndex(maker, asking_step_index);
+ return s.fail(maker, "failed creating directory {f}: {t}", .{ parent_path, e });
+ },
+ };
+ break :f dest_path.root_dir.handle.createFile(io, dest_path.sub_path, .{}) catch |in| break :e in;
+ },
+ error.Canceled => |e| return e,
+ else => |e| break :e e,
+ };
+ };
+ file.close(io);
+ return;
+ };
+ const s = stepByIndex(maker, asking_step_index);
+ return s.fail(maker, "failed truncating file {f}: {t}", .{ dest_path, err });
+}
+
+pub fn installPath(
+ maker: *Maker,
+ arena: Allocator,
+ src_path: Path,
+ dest_path: Path,
+ asking_step_index: Configuration.Step.Index,
+) Step.ExtendedMakeError!Dir.PrevStatus {
+ const graph = maker.graph;
+ const io = graph.io;
+ if (graph.verbose) try graph.handleVerbose(null, null, &.{
+ "install", "-C", try src_path.toString(arena), try dest_path.toString(arena),
+ });
+ return Dir.updateFile(
+ src_path.root_dir.handle,
+ io,
+ src_path.sub_path,
+ dest_path.root_dir.handle,
+ dest_path.sub_path,
+ .{},
+ ) catch |err| {
+ const s = stepByIndex(maker, asking_step_index);
+ return s.fail(maker, "failed updating file from {f} to {f}: {t}", .{ src_path, dest_path, err });
+ };
+}
+
+/// Wrapper around `Dir.createDirPathStatus` that handles verbose and error output.
+pub fn installDir(
+ maker: *Maker,
+ arena: Allocator,
+ dest_path: Path,
+ asking_step_index: Configuration.Step.Index,
+) Step.ExtendedMakeError!Dir.CreatePathStatus {
+ const graph = maker.graph;
+ const io = graph.io;
+ if (graph.verbose) try graph.handleVerbose(null, null, &.{
+ "install", "-d", try dest_path.toString(arena),
+ });
+ return dest_path.root_dir.handle.createDirPathStatus(io, dest_path.sub_path, .default_dir) catch |err| {
+ const s = stepByIndex(maker, asking_step_index);
+ return s.fail(maker, "failed creating dir {f}: {t}", .{ dest_path, err });
+ };
+}
+
+pub fn installSymLinks(
+ maker: *Maker,
+ arena: Allocator,
+ output_path: Path,
+ compile_step_index: Configuration.Step.Index,
+ asking_step_index: Configuration.Step.Index,
+) !void {
+ const c = &maker.scanned_config.configuration;
+ const conf_step = compile_step_index.ptr(c);
+ const conf_comp = conf_step.extended.get(c.extra).compile;
+ const root_module = conf_comp.root_module.get(c);
+ const target = root_module.resolved_target.get(c).?.result.get(c);
+ const os_tag = target.flags.os_tag.unwrap().?;
+
+ assert(conf_comp.flags3.kind == .lib);
+ assert(conf_comp.flags2.linkage == .dynamic);
+ assert(os_tag != .windows);
+
+ const version = std.SemanticVersion.parse(conf_comp.version.value.?.slice(c)) catch unreachable;
+ const name = conf_comp.root_name.slice(c);
+
+ const filename_major_only, const filename_name_only = if (os_tag.isDarwin()) .{
+ try std.fmt.allocPrint(arena, "lib{s}.{d}.dylib", .{ name, version.major }),
+ try std.fmt.allocPrint(arena, "lib{s}.dylib", .{name}),
+ } else .{
+ try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ name, version.major }),
+ try std.fmt.allocPrint(arena, "lib{s}.so", .{name}),
+ };
+
+ return installSymLinksInner(maker, arena, output_path, asking_step_index, filename_major_only, filename_name_only);
+}
+
+fn installSymLinksInner(
+ maker: *Maker,
+ arena: Allocator,
+ output_path: Path,
+ asking_step_index: Configuration.Step.Index,
+ filename_major_only: []const u8,
+ filename_name_only: []const u8,
+) !void {
+ const io = maker.graph.io;
+ const step = stepByIndex(maker, asking_step_index);
+ const out_basename = Io.Dir.path.basename(output_path.sub_path);
+
+ const out_dir = output_path.dirname().?;
+ const major_only_path = try out_dir.join(arena, filename_major_only);
+ const name_only_path = try out_dir.join(arena, filename_name_only);
+
+ // libfoo.so.1 to libfoo.so.1.2.3
+ major_only_path.root_dir.handle.symLinkAtomic(io, out_basename, major_only_path.sub_path, .{}) catch |err|
+ return step.fail(maker, "failed symlinking {f} to {s}: {t}", .{ output_path, out_basename, err });
+
+ // libfoo.so to libfoo.so.1
+ name_only_path.root_dir.handle.symLinkAtomic(io, filename_major_only, name_only_path.sub_path, .{}) catch |err|
+ return step.fail(maker, "failed symlinking {f} to {s}: {t}", .{ name_only_path, filename_major_only, err });
+}
+
+fn cleanExit(io: Io, scanned_config: *const ScannedConfig) void {
+ removePoisonedConfiguration(io, scanned_config);
+ return process.cleanExit(io);
+}
+
+fn removePoisonedConfiguration(io: Io, scanned_config: *const ScannedConfig) void {
+ if (scanned_config.configuration.poisoned) {
+ // This configuration file was good for only 1 invocation of the maker
+ // process. Delete it to save space on disk.
+ Io.Dir.cwd().deleteFile(io, scanned_config.path) catch |err|
+ log.warn("failed deleting poisoned configuration file {s}: {t}", .{ scanned_config.path, err });
+ }
+}
+
+const is_debug_mode = builtin.mode == .Debug;
+var debug_maker_leaks: bool = false;
+inline fn debugMakerLeaks() bool {
+ if (!is_debug_mode) return false;
+ return debug_maker_leaks;
+}
diff --git a/lib/compiler/Maker/Fuzz.zig b/lib/compiler/Maker/Fuzz.zig
@@ -0,0 +1,685 @@
+const Fuzz = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Build = std.Build;
+const Cache = std.Build.Cache;
+const Coverage = std.debug.Coverage;
+const Configuration = std.Build.Configuration;
+const Io = std.Io;
+const abi = std.Build.abi.fuzz;
+const assert = std.debug.assert;
+const fatal = std.process.fatal;
+const log = std.log;
+
+const Maker = @import("../Maker.zig");
+const WebServer = @import("WebServer.zig");
+
+maker: *Maker,
+mode: Mode,
+
+/// Allocated into `gpa`.
+run_steps: []const Configuration.Step.Index,
+
+group: Io.Group,
+root_prog_node: std.Progress.Node,
+prog_node: std.Progress.Node,
+
+/// Protects `coverage_files`.
+coverage_mutex: Io.Mutex,
+coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
+
+queue_mutex: Io.Mutex,
+queue_cond: Io.Condition,
+msg_queue: std.ArrayList(Msg),
+
+pub const Mode = union(enum) {
+ forever: struct { ws: *WebServer },
+ limit: Limited,
+
+ pub const Limited = struct {
+ amount: u64,
+ };
+};
+
+const Msg = union(enum) {
+ coverage: struct {
+ id: u64,
+ cumulative: struct {
+ runs: u64,
+ unique: u64,
+ coverage: u64,
+ },
+ run: Configuration.Step.Index,
+ },
+ entry_point: struct {
+ coverage_id: u64,
+ addr: u64,
+ },
+};
+
+const CoverageMap = struct {
+ mapped_memory: []align(std.heap.page_size_min) const u8,
+ coverage: Coverage,
+ source_locations: []Coverage.SourceLocation,
+ /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
+ entry_points: std.ArrayList(u32),
+ start_timestamp: i64,
+ start_n_runs: u64,
+
+ fn deinit(cm: *CoverageMap, gpa: Allocator) void {
+ std.posix.munmap(cm.mapped_memory);
+ cm.coverage.deinit(gpa);
+ cm.* = undefined;
+ }
+};
+
+pub fn init(
+ maker: *Maker,
+ all_steps: []const Configuration.Step.Index,
+ root_prog_node: std.Progress.Node,
+ mode: Mode,
+) error{ OutOfMemory, Canceled }!Fuzz {
+ const graph = maker.graph;
+ const gpa = graph.cache.gpa;
+ const io = graph.io;
+ const conf = &maker.scanned_config.configuration;
+
+ const run_steps: []const Configuration.Step.Index = steps: {
+ var steps: std.ArrayList(Configuration.Step.Index) = .empty;
+ defer steps.deinit(gpa);
+ const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0);
+ defer rebuild_node.end();
+ var rebuild_group: Io.Group = .init;
+ defer rebuild_group.cancel(io);
+
+ for (all_steps) |step_index| {
+ const conf_run = step_index.ptr(conf).extended.cast(conf, Configuration.Step.Run) orelse continue;
+ if (conf_run.producer.value == null) continue;
+ const run = &maker.stepByIndex(step_index).extended.run;
+ if (run.fuzz_tests.items.len == 0) continue;
+ try steps.append(gpa, step_index);
+ rebuild_group.async(io, rebuildTestsWorkerRun, .{ maker, step_index, rebuild_node });
+ }
+
+ if (steps.items.len == 0) fatal("no fuzz tests found", .{});
+ rebuild_node.setEstimatedTotalItems(steps.items.len);
+ const run_steps = try gpa.dupe(Configuration.Step.Index, steps.items);
+ try rebuild_group.await(io);
+ break :steps run_steps;
+ };
+ errdefer gpa.free(run_steps);
+
+ for (run_steps) |run_index| {
+ const run = &maker.stepByIndex(run_index).extended.run;
+ assert(run.fuzz_tests.items.len > 0);
+ if (run.rebuilt_executable == null)
+ fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
+ }
+
+ return .{
+ .maker = maker,
+ .mode = mode,
+ .run_steps = run_steps,
+ .group = .init,
+ .root_prog_node = root_prog_node,
+ .prog_node = .none,
+ .coverage_files = .empty,
+ .coverage_mutex = .init,
+ .queue_mutex = .init,
+ .queue_cond = .init,
+ .msg_queue = .empty,
+ };
+}
+
+pub fn start(fuzz: *Fuzz) void {
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+
+ fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0);
+
+ if (fuzz.mode == .forever) {
+ // For polling messages and sending updates to subscribers.
+ fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err|
+ fatal("unable to spawn coverage task: {t}", .{err});
+ }
+
+ for (fuzz.run_steps) |run_index| {
+ const run = &maker.stepByIndex(run_index).extended.run;
+ assert(run.rebuilt_executable != null);
+ fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run_index });
+ }
+}
+
+pub fn deinit(fuzz: *Fuzz) void {
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+ const gpa = maker.gpa;
+
+ fuzz.group.cancel(io);
+ fuzz.prog_node.end();
+ gpa.free(fuzz.run_steps);
+}
+
+fn rebuildTestsWorkerRun(
+ maker: *Maker,
+ run_index: Configuration.Step.Index,
+ parent_prog_node: std.Progress.Node,
+) void {
+ rebuildTestsWorkerRunFallible(maker, run_index, parent_prog_node) catch |err| {
+ const conf = &maker.scanned_config.configuration;
+ const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run).?;
+ const comp_index = conf_run.producer.value.?;
+ const step_name = comp_index.ptr(conf).name.slice(conf);
+ log.err("step {s}: failed to rebuild in fuzz mode: {t}", .{ step_name, err });
+ };
+}
+
+fn rebuildTestsWorkerRunFallible(
+ maker: *Maker,
+ run_index: Configuration.Step.Index,
+ parent_prog_node: std.Progress.Node,
+) !void {
+ const graph = maker.graph;
+ const io = graph.io;
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+ const run = &maker.stepByIndex(run_index).extended.run;
+ const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run).?;
+ const comp_index = conf_run.producer.value.?;
+ const comp_step = maker.stepByIndex(comp_index);
+ const comp = &comp_step.extended.compile;
+ const conf_comp_step = comp_index.ptr(conf);
+ const conf_comp = conf_comp_step.extended.cast(conf, Configuration.Step.Compile).?;
+ const root_module = conf_comp.root_module.get(conf);
+ const target = root_module.resolved_target.get(conf).?.result.get(conf);
+
+ const prog_node = parent_prog_node.start(conf_comp_step.name.slice(conf), 0);
+ defer prog_node.end();
+
+ const result = comp.rebuildInFuzzMode(maker, comp_index, prog_node);
+
+ const show_compile_errors = comp_step.result_error_bundle.errorMessageCount() > 0;
+ const show_error_msgs = comp_step.result_error_msgs.items.len > 0;
+ const show_stderr = comp_step.result_stderr.len > 0;
+
+ if (show_error_msgs or show_compile_errors or show_stderr) {
+ var buf: [256]u8 = undefined;
+ const stderr = try io.lockStderr(&buf, graph.stderr_mode);
+ defer io.unlockStderr();
+ maker.printErrorMessages(comp_index, .{}, stderr.terminal(), .verbose, .indent) catch {};
+ }
+
+ const rebuilt_bin_path = result catch |err| switch (err) {
+ error.MakeFailed => return,
+ else => |other| return other,
+ };
+ const compile_filename = try std.zig.binNameAlloc(gpa, .{
+ .root_name = conf_comp.root_name.slice(conf),
+ .cpu_arch = target.flags.cpu_arch.unwrap().?,
+ .os_tag = target.flags.os_tag.unwrap().?,
+ .ofmt = target.flags.object_format.unwrap().?,
+ .abi = target.flags.abi.unwrap().?,
+ .output_mode = switch (conf_comp.flags3.kind) {
+ .lib => .Lib,
+ .obj, .test_obj => .Obj,
+ .exe, .@"test" => .Exe,
+ },
+ .link_mode = conf_comp.flags2.linkage.unwrap(),
+ .version = if (conf_comp.version.value) |v|
+ std.SemanticVersion.parse(v.slice(conf)) catch unreachable
+ else
+ null,
+ });
+ defer gpa.free(compile_filename);
+
+ run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile_filename);
+}
+
+fn fuzzWorkerRun(fuzz: *Fuzz, run_index: Configuration.Step.Index) void {
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+ const conf = &maker.scanned_config.configuration;
+ const run = &maker.stepByIndex(run_index).extended.run;
+
+ run.rerunInFuzzMode(run_index, fuzz, fuzz.prog_node) catch |err| switch (err) {
+ error.MakeFailed => {
+ var buf: [256]u8 = undefined;
+ const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) {
+ error.Canceled => return,
+ };
+ defer io.unlockStderr();
+ maker.printErrorMessages(run_index, .{}, stderr.terminal(), .verbose, .indent) catch {};
+ return;
+ },
+ else => {
+ const step_name = run_index.ptr(conf).name.slice(conf);
+ log.err("step {s}: failed to rerun in fuzz mode: {t}", .{ step_name, err });
+ return;
+ },
+ };
+}
+
+pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void {
+ assert(fuzz.mode == .forever);
+ const maker = fuzz.maker;
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+
+ var arena_state: std.heap.ArenaAllocator = .init(gpa);
+ defer arena_state.deinit();
+ const arena = arena_state.allocator();
+
+ const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
+ var dedup_table: DedupTable = .empty;
+ defer dedup_table.deinit(gpa);
+
+ for (fuzz.run_steps) |run_index| {
+ const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run) orelse continue;
+ const comp_index = conf_run.producer.value.?;
+ const comp_step = maker.stepByIndex(comp_index);
+ const compile_inputs = comp_step.inputs.table;
+ for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| {
+ try dedup_table.ensureUnusedCapacity(gpa, file_list.items.len);
+ for (file_list.items) |sub_path| {
+ if (!std.mem.endsWith(u8, sub_path, ".zig")) continue;
+ const joined_path = try dir_path.join(arena, sub_path);
+ dedup_table.putAssumeCapacity(joined_path, {});
+ }
+ }
+ }
+
+ const deduped_paths = dedup_table.keys();
+ const SortContext = struct {
+ pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
+ _ = this;
+ return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
+ .lt => true,
+ .gt => false,
+ .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
+ };
+ }
+ };
+ std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
+ return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths);
+}
+
+pub const Previous = struct {
+ unique_runs: usize,
+ entry_points: usize,
+ sent_source_index: bool,
+ pub const init: Previous = .{
+ .unique_runs = 0,
+ .entry_points = 0,
+ .sent_source_index = false,
+ };
+};
+pub fn sendUpdate(
+ fuzz: *Fuzz,
+ socket: *std.http.Server.WebSocket,
+ prev: *Previous,
+) !void {
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+
+ try fuzz.coverage_mutex.lock(io);
+ defer fuzz.coverage_mutex.unlock(io);
+
+ const coverage_maps = fuzz.coverage_files.values();
+ if (coverage_maps.len == 0) return;
+ // TODO: handle multiple fuzz steps in the WebSocket packets
+ const coverage_map = &coverage_maps[0];
+ const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
+ // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the
+ // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the
+ // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass
+ // this data straight to the socket with sendfile...
+ const seen_pcs = cov_header.seenBits();
+ const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
+ const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
+ {
+ if (!prev.sent_source_index) {
+ prev.sent_source_index = true;
+ // We need to send initial context.
+ const header: abi.SourceIndexHeader = .{
+ .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
+ .files_len = @intCast(coverage_map.coverage.files.entries.len),
+ .source_locations_len = @intCast(coverage_map.source_locations.len),
+ .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
+ .start_timestamp = coverage_map.start_timestamp,
+ .start_n_runs = coverage_map.start_n_runs,
+ };
+ var iovecs: [5][]const u8 = .{
+ @ptrCast(&header),
+ @ptrCast(coverage_map.coverage.directories.keys()),
+ @ptrCast(coverage_map.coverage.files.keys()),
+ @ptrCast(coverage_map.source_locations),
+ coverage_map.coverage.string_bytes.items,
+ };
+ try socket.writeMessageVec(&iovecs, .binary);
+ }
+
+ const header: abi.CoverageUpdateHeader = .{
+ .n_runs = n_runs,
+ .unique_runs = unique_runs,
+ };
+ var iovecs: [2][]const u8 = .{
+ @ptrCast(&header),
+ @ptrCast(seen_pcs),
+ };
+ try socket.writeMessageVec(&iovecs, .binary);
+
+ prev.unique_runs = unique_runs;
+ }
+
+ if (prev.entry_points != coverage_map.entry_points.items.len) {
+ const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len));
+ var iovecs: [2][]const u8 = .{
+ @ptrCast(&header),
+ @ptrCast(coverage_map.entry_points.items),
+ };
+ try socket.writeMessageVec(&iovecs, .binary);
+
+ prev.entry_points = coverage_map.entry_points.items.len;
+ }
+}
+
+fn coverageRun(fuzz: *Fuzz) void {
+ coverageRunCancelable(fuzz) catch |err| switch (err) {
+ error.Canceled => return,
+ };
+}
+
+fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void {
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+
+ try fuzz.queue_mutex.lock(io);
+ defer fuzz.queue_mutex.unlock(io);
+
+ while (true) {
+ try fuzz.queue_cond.wait(io, &fuzz.queue_mutex);
+ for (fuzz.msg_queue.items) |msg| switch (msg) {
+ .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) {
+ error.AlreadyReported => continue,
+ error.Canceled => return,
+ else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
+ },
+ .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
+ error.AlreadyReported => continue,
+ error.Canceled => return,
+ else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
+ },
+ };
+ fuzz.msg_queue.clearRetainingCapacity();
+ }
+}
+fn prepareTables(fuzz: *Fuzz, run_index: Configuration.Step.Index, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void {
+ assert(fuzz.mode == .forever);
+ const ws = fuzz.mode.forever.ws;
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+ const cache_root = graph.local_cache_root;
+
+ try fuzz.coverage_mutex.lock(io);
+ defer fuzz.coverage_mutex.unlock(io);
+
+ const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id);
+ if (gop.found_existing) {
+ // We are fuzzing the same executable with multiple threads.
+ // Perhaps the same unit test; perhaps a different one. In any
+ // case, since the coverage file is the same, we only have to
+ // notice changes to that one file in order to learn coverage for
+ // this particular executable.
+ return;
+ }
+ errdefer _ = fuzz.coverage_files.pop();
+
+ gop.value_ptr.* = .{
+ .coverage = std.debug.Coverage.init,
+ .mapped_memory = undefined, // populated below
+ .source_locations = undefined, // populated below
+ .entry_points = .empty,
+ .start_timestamp = ws.now(),
+ .start_n_runs = undefined, // populated below
+ };
+ errdefer gop.value_ptr.coverage.deinit(gpa);
+
+ const run_step = maker.stepByIndex(run_index);
+ const conf_run_step = run_index.ptr(conf);
+ const conf_run = conf_run_step.extended.cast(conf, Configuration.Step.Run).?;
+ const comp_index = conf_run.producer.value.?;
+ const conf_comp_step = comp_index.ptr(conf);
+ const conf_comp = conf_comp_step.extended.cast(conf, Configuration.Step.Compile).?;
+ const rebuilt_exe_path = run_step.extended.run.rebuilt_executable.?;
+ const root_module = conf_comp.root_module.get(conf);
+ const target = root_module.resolved_target.get(conf).?.result.get(conf);
+
+ var debug_info = std.debug.Info.load(
+ gpa,
+ io,
+ rebuilt_exe_path,
+ &gop.value_ptr.coverage,
+ target.flags.object_format.unwrap().?,
+ target.flags.cpu_arch.unwrap().?,
+ ) catch |err| {
+ log.err("step {s}: failed to load debug information for {f}: {t}", .{
+ conf_run_step.name.slice(conf), rebuilt_exe_path, err,
+ });
+ return error.AlreadyReported;
+ };
+ defer debug_info.deinit(gpa);
+
+ const coverage_file_path: Build.Cache.Path = .{
+ .root_dir = cache_root,
+ .sub_path = "v/" ++ std.fmt.hex(coverage_id),
+ };
+ var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
+ log.err("step {s}: failed to load coverage file {f}: {t}", .{
+ conf_run_step.name.slice(conf), coverage_file_path, err,
+ });
+ return error.AlreadyReported;
+ };
+ defer coverage_file.close(io);
+
+ const file_size = coverage_file.length(io) catch |err| {
+ log.err("unable to check len of coverage file {f}: {t}", .{ coverage_file_path, err });
+ return error.AlreadyReported;
+ };
+
+ const mapped_memory = std.posix.mmap(
+ null,
+ file_size,
+ .{ .READ = true },
+ .{ .TYPE = .SHARED },
+ coverage_file.handle,
+ 0,
+ ) catch |err| {
+ log.err("failed to map coverage file {f}: {t}", .{ coverage_file_path, err });
+ return error.AlreadyReported;
+ };
+ gop.value_ptr.mapped_memory = mapped_memory;
+
+ const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
+ const pcs = header.pcAddrs();
+ const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
+ errdefer gpa.free(source_locations);
+
+ // Unfortunately the PCs array that LLVM gives us from the 8-bit PC
+ // counters feature is not sorted.
+ var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty;
+ defer sorted_pcs.deinit(gpa);
+ try sorted_pcs.resize(gpa, pcs.len);
+ @memcpy(sorted_pcs.items(.pc), pcs);
+ for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i);
+ sorted_pcs.sortUnstable(struct {
+ addrs: []const u64,
+
+ pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
+ return ctx.addrs[a_index] < ctx.addrs[b_index];
+ }
+ }{ .addrs = sorted_pcs.items(.pc) });
+
+ debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
+ log.err("failed to resolve addresses to source locations: {t}", .{err});
+ return error.AlreadyReported;
+ };
+
+ for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl;
+ gop.value_ptr.source_locations = source_locations;
+ gop.value_ptr.start_n_runs = header.n_runs;
+
+ ws.notifyUpdate();
+}
+
+fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void {
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+ const gpa = maker.gpa;
+
+ try fuzz.coverage_mutex.lock(io);
+ defer fuzz.coverage_mutex.unlock(io);
+
+ const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?;
+ const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
+ const pcs = header.pcAddrs();
+
+ // Since this pcs list is unsorted, we must linear scan for the best index.
+ const index = i: {
+ var best: usize = 0;
+ for (pcs[1..], 1..) |elem_addr, i| {
+ if (elem_addr == addr) break :i i;
+ if (elem_addr > addr) continue;
+ if (elem_addr > pcs[best]) best = i;
+ }
+ break :i best;
+ };
+ if (index >= pcs.len) {
+ log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
+ addr, pcs[0], pcs[pcs.len - 1],
+ });
+ return error.AlreadyReported;
+ }
+ if (false) {
+ const sl = coverage_map.source_locations[index];
+ const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
+ if (pcs.len == 1) {
+ log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{
+ addr, file_name, sl.line, sl.column,
+ });
+ } else if (index == 0) {
+ log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{
+ addr, file_name, sl.line, sl.column, pcs[index + 1],
+ });
+ } else if (index == pcs.len - 1) {
+ log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{
+ addr, file_name, sl.line, sl.column, index, pcs[index - 1],
+ });
+ } else {
+ log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
+ addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
+ });
+ }
+ }
+ try coverage_map.entry_points.append(gpa, @intCast(index));
+}
+
+pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
+ assert(fuzz.mode == .limit);
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+ const cache_root = graph.local_cache_root;
+ const conf = &maker.scanned_config.configuration;
+
+ try fuzz.group.await(io);
+ fuzz.group = .init;
+
+ std.debug.print("======= FUZZING REPORT =======\n", .{});
+ for (fuzz.msg_queue.items) |msg| {
+ if (msg != .coverage) continue;
+
+ const cov = msg.coverage;
+ const run_step_name = cov.run.ptr(conf).name.slice(conf);
+ const run = &maker.stepByIndex(cov.run).extended.run;
+ const coverage_file_path: std.Build.Cache.Path = .{
+ .root_dir = cache_root,
+ .sub_path = "v/" ++ std.fmt.hex(cov.id),
+ };
+ var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
+ fatal("step {s}: failed to load coverage file {f}: {t}", .{
+ run_step_name, coverage_file_path, err,
+ });
+ };
+ defer coverage_file.close(io);
+
+ const fuzz_abi = std.Build.abi.fuzz;
+ var rbuf: [0x1000]u8 = undefined;
+ var r = coverage_file.reader(io, &rbuf);
+
+ var header: fuzz_abi.SeenPcsHeader = undefined;
+ r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
+ fatal("step {s}: failed to read from coverage file {f}: {t}", .{
+ run_step_name, coverage_file_path, err,
+ });
+ };
+
+ if (header.pcs_len == 0) {
+ fatal("step {s}: corrupted coverage file {f}: pcs_len was zero", .{
+ run_step_name, coverage_file_path,
+ });
+ }
+
+ var seen_count: usize = 0;
+ const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
+ for (0..chunk_count) |_| {
+ const seen = r.interface.takeInt(usize, .little) catch |err| {
+ fatal("step {s}: failed to read from coverage file {f}: {t}", .{
+ run_step_name, coverage_file_path, err,
+ });
+ };
+ seen_count += @popCount(seen);
+ }
+
+ const seen_f: f64 = @floatFromInt(seen_count);
+ const total_f: f64 = @floatFromInt(header.pcs_len);
+ const ratio = seen_f / total_f;
+ std.debug.print(
+ \\Step: {s}
+ \\Fuzz test: "{s}" ({x})
+ \\Runs: {} -> {}
+ \\Unique runs: {} -> {}
+ \\Coverage: {}/{} -> {}/{} ({:.02}%)
+ \\
+ , .{
+ run_step_name,
+ run.fuzz_tests.items[0],
+ cov.id,
+ cov.cumulative.runs,
+ header.n_runs,
+ cov.cumulative.unique,
+ header.unique_runs,
+ cov.cumulative.coverage,
+ header.pcs_len,
+ seen_count,
+ header.pcs_len,
+ ratio * 100,
+ });
+
+ std.debug.print("------------------------------\n", .{});
+ }
+ std.debug.print(
+ \\Values are accumulated across multiple runs when preserving the cache.
+ \\==============================
+ \\
+ , .{});
+}
diff --git a/lib/compiler/Maker/Graph.zig b/lib/compiler/Maker/Graph.zig
@@ -0,0 +1,85 @@
+//! Shared maker state among all steps.
+const Graph = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Allocator = std.mem.Allocator;
+const Configuration = std.Build.Configuration;
+const Path = std.Build.Cache.Path;
+const Directory = std.Build.Cache.Directory;
+
+io: Io,
+/// Process lifetime.
+arena: Allocator,
+cache: std.Build.Cache,
+zig_exe: []const u8,
+environ_map: std.process.Environ.Map,
+global_cache_root: Directory,
+local_cache_root: Directory,
+zig_lib_directory: Directory,
+build_root_directory: Directory,
+
+debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null,
+incremental: ?bool = null,
+random_seed: u32 = 0,
+allow_so_scripts: ?bool = null,
+time_report: bool = false,
+/// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also
+/// respects the '--color' flag.
+stderr_mode: ?Io.Terminal.Mode = null,
+reference_trace: ?u32 = null,
+debug_log_scopes: std.ArrayList([]const u8) = .empty,
+debug_compile_errors: bool = false,
+debug_incremental: bool = false,
+fuzzing: bool = false,
+verbose: bool = false,
+verbose_air: bool = false,
+verbose_cc: bool = false,
+verbose_link: bool = false,
+verbose_llvm_cpu_features: bool = false,
+verbose_llvm_ir: bool = false,
+libc_file: ?[]const u8 = null,
+/// What does this do? Nobody bothered to document it, and I think it's a
+/// smelly option. So unless somebody deletes these passive aggressive comments
+/// and replaces them with actual documentation, I'm going to delete this
+/// option from the build system in a future release. In other words, this is
+/// deprecated due to lack of test coverage, lack of documentation, and a hunch
+/// that it's a bad option that should be avoided.
+sysroot: ?[]const u8 = null,
+search_prefixes: std.ArrayList([]const u8) = .empty,
+build_id: ?std.zig.BuildId = null,
+error_limit: ?u32 = null,
+/// Steps should use `io` to limit the number of jobs, however in the case of
+/// a single step spawning a fixed number of processes this can be used.
+max_jobs: ?u32 = null,
+
+/// After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md,
+/// this will be the directory $glibc-build-dir/install/glibcs
+/// Given the example of the aarch64 target, this is the directory
+/// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`.
+/// Also works for dynamic musl.
+libc_runtimes_dir: ?[]const u8 = null,
+enable_wine: bool = false,
+enable_qemu: bool = false,
+enable_wasmtime: bool = false,
+enable_darling: bool = false,
+enable_rosetta: bool = false,
+
+/// Intention of verbose is to print all sub-process command lines to stderr
+/// before spawning them.
+pub fn handleVerbose(
+ graph: *const Graph,
+ cwd: ?[]const u8,
+ opt_env: ?*const std.process.Environ.Map,
+ argv: []const []const u8,
+) error{OutOfMemory}!void {
+ if (!graph.verbose) return;
+ const arena = graph.arena;
+ const text = try std.zig.allocPrintCmd(arena, argv, .{
+ .cwd = cwd,
+ .parent_env = &graph.environ_map,
+ .child_env = opt_env,
+ });
+ defer arena.free(text);
+ std.log.scoped(.verbose).info("{s}", .{text});
+}
diff --git a/lib/compiler/Maker/PkgConfig.zig b/lib/compiler/Maker/PkgConfig.zig
@@ -0,0 +1,114 @@
+const std = @import("std");
+const Io = std.Io;
+const mem = std.mem;
+const assert = std.debug.assert;
+
+const Maker = @import("../Maker.zig");
+const Step = @import("Step.zig");
+const Graph = @import("Graph.zig");
+
+mutex: Io.Mutex = .init,
+pkgs: ?std.zig.PkgConfig = null,
+debug: bool = false,
+
+pub const RunError = error{
+ PackageNotFound,
+ PkgConfigUnavailable,
+} || Step.ExtendedMakeError;
+
+pub const Result = std.zig.PkgConfig.Parsed;
+
+/// Run pkg-config for the given library name and parse the output, returning the arguments
+/// that should be passed to zig to link the given library.
+pub fn run(
+ maker: *Maker,
+ step: *Step,
+ progress_node: std.Progress.Node,
+ lib_name: []const u8,
+ /// If true, reports failure error messages on step rather than returning
+ /// error.PackageNotFound or error.PkgConfigUnavailable,
+ force: bool,
+) RunError!Result {
+ const pc = &maker.pkg_config;
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into process arena
+
+ const pkg_config_exe = getExe(graph);
+ const pkgs = try getPkgs(maker, step, progress_node, force);
+ const found_index = pkgs.find(lib_name) orelse {
+ if (force) return step.fail(maker, "{s}: package not found: {s}", .{ pkg_config_exe, lib_name });
+ return error.PackageNotFound;
+ };
+ const pkg = pkgs.all[found_index];
+
+ const stdout = try captureChildProcess(maker, step, .{
+ .argv = &.{ pkg_config_exe, pkg.name, "--cflags", "--libs" },
+ .progress_node = progress_node,
+ .allow_failure = !force,
+ });
+
+ const parsed = std.zig.PkgConfig.parse(arena, stdout) catch |err| switch (err) {
+ error.InvalidPkgConfigOutput => {
+ if (force) return step.fail(maker, "{s} package {s} invalid output: {s}", .{
+ pkg_config_exe, lib_name, stdout,
+ });
+ return error.PkgConfigUnavailable;
+ },
+ else => |e| return e,
+ };
+ if (force or pc.debug) {
+ for (parsed.unknown_flags) |unknown_flag| {
+ return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, unknown_flag });
+ }
+ }
+
+ return parsed;
+}
+
+fn getExe(graph: *const Graph) []const u8 {
+ return std.zig.PkgConfig.exe(&graph.environ_map);
+}
+
+fn getPkgs(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError!std.zig.PkgConfig {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const pc = &maker.pkg_config;
+
+ try pc.mutex.lock(io);
+ defer pc.mutex.unlock(io);
+
+ if (pc.pkgs) |pkgs| return pkgs;
+
+ const pkg_config_exe = getExe(graph);
+ const stdout = try captureChildProcess(maker, step, .{
+ .argv = &.{ pkg_config_exe, "--list-all" },
+ .progress_node = progress_node,
+ .allow_failure = !force,
+ });
+
+ var diagnostic: std.zig.PkgConfig.Diagnostic = undefined;
+ const result = std.zig.PkgConfig.init(arena, stdout, &diagnostic) catch |err| switch (err) {
+ error.InvalidPkgConfigOutput => {
+ if (force) return step.fail(maker, "{s}: invalid line({d}): {s}", .{
+ pkg_config_exe, diagnostic.invalid_line_index + 1, diagnostic.invalid_line,
+ });
+ return error.PkgConfigUnavailable;
+ },
+ else => |e| return e,
+ };
+
+ pc.pkgs = result;
+ return result;
+}
+
+fn captureChildProcess(maker: *Maker, step: *Step, options: Step.CaptureChildProcessOptions) ![]const u8 {
+ const captured = step.captureChildProcess(maker, options) catch |err| switch (err) {
+ error.FileNotFound => return error.PkgConfigUnavailable,
+ else => |e| return e,
+ };
+ assert(step.result_failed_command != null);
+ if (captured.term.success()) return captured.stdout;
+ if (!options.allow_failure) return step.fail(maker, "{s} {f}", .{ options.argv[0], captured.term });
+ return error.PkgConfigUnavailable;
+}
diff --git a/lib/compiler/Maker/ScannedConfig.zig b/lib/compiler/Maker/ScannedConfig.zig
@@ -0,0 +1,370 @@
+const ScannedConfig = @This();
+
+const std = @import("std");
+const Configuration = std.Build.Configuration;
+const Writer = std.Io.Writer;
+const Serializer = std.zon.Serializer;
+
+const Graph = @import("Graph.zig");
+
+configuration: Configuration,
+top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index),
+path: []const u8,
+
+pub fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void {
+ std.log.err("TODO also print paths", .{});
+ std.log.err("TODO also print unlazy deps", .{});
+ std.log.err("TODO also print system integrations", .{});
+ std.log.err("TODO also print available options", .{});
+ const c = &sc.configuration;
+ var serializer: Serializer = .{ .writer = w };
+ var s = try serializer.beginStruct(.{});
+
+ {
+ var tf = try s.beginTupleField("search_prefixes", .{});
+ for (c.search_prefixes) |string| try tf.field(string.slice(c), .{});
+ try tf.end();
+ }
+
+ try s.field("default_step", @intFromEnum(c.default_step), .{});
+ {
+ var sf = try s.beginStructField("top_level_steps", .{});
+ for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step| {
+ try sf.field(name, @intFromEnum(step), .{});
+ }
+ try sf.end();
+ }
+
+ {
+ var tf = try s.beginTupleField("steps", .{});
+ for (c.steps) |step| {
+ var step_field = try tf.beginStructField(.{});
+ try printStruct(sc, &step_field, Configuration.Step, step);
+ try step_field.end();
+ }
+ try tf.end();
+ }
+
+ try s.end();
+}
+
+fn printStruct(sc: *const ScannedConfig, s: *Serializer.Struct, comptime S: type, v: S) !void {
+ inline for (@typeInfo(S).@"struct".fields) |field| {
+ try s.fieldPrefix(field.name);
+ try printValue(sc, s.container.serializer, field.type, @field(v, field.name));
+ }
+}
+
+fn printValue(sc: *const ScannedConfig, s: *Serializer, comptime Field: type, field_value: Field) !void {
+ const c = &sc.configuration;
+ switch (Field) {
+ Configuration.String => {
+ try s.value(field_value.slice(c), .{});
+ },
+ Configuration.Deps.Index => {
+ try printValue(sc, s, []const Configuration.Step.Index, field_value.get(c).steps.slice);
+ },
+ Configuration.MaxRss => {
+ try s.value(field_value.toBytes(), .{});
+ },
+ Configuration.Step.Run.Arg.Index => {
+ var sub_struct = try s.beginStruct(.{});
+ try printStruct(sc, &sub_struct, Configuration.Step.Run.Arg, field_value.get(c));
+ try sub_struct.end();
+ },
+ Configuration.Step.ObjCopy.UpdateSection.Flags => {
+ var sub_struct = try s.beginStruct(.{});
+ try printStruct(sc, &sub_struct, Field, field_value);
+ try sub_struct.end();
+ },
+ Configuration.LazyPath.Index => {
+ switch (field_value.get(c)) {
+ inline else => |u| {
+ var sub_struct = try s.beginStruct(.{});
+ try printStruct(sc, &sub_struct, @TypeOf(u), u);
+ try sub_struct.end();
+ },
+ }
+ },
+ else => switch (@typeInfo(Field)) {
+ .int => try s.int(field_value),
+ .pointer => |info| switch (info.size) {
+ .slice => {
+ var slice_field = try s.beginTuple(.{});
+ for (field_value) |elem| {
+ try slice_field.fieldPrefix();
+ try printValue(sc, s, info.child, elem);
+ }
+ try slice_field.end();
+ },
+ else => comptime unreachable,
+ },
+ .@"enum" => {
+ if (@hasDecl(Field, "storage")) switch (Field.storage) {
+ .extended => {
+ var sub_struct = try s.beginStruct(.{});
+ switch (field_value.get(c.extra)) {
+ inline else => |u| {
+ try printStruct(sc, &sub_struct, @TypeOf(u), u);
+ },
+ }
+ try sub_struct.end();
+ },
+ .flag_optional => comptime unreachable,
+ .flag_length_prefixed_list => comptime unreachable,
+ .enum_optional => comptime unreachable,
+ .union_list => comptime unreachable,
+ .length_prefixed_list => comptime unreachable,
+ .flag_list => comptime unreachable,
+ .flag_union => comptime unreachable,
+ .multi_list => comptime unreachable,
+ } else if (std.enums.tagName(Field, field_value)) |name| {
+ try s.ident(name);
+ } else {
+ try s.int(@intFromEnum(field_value));
+ }
+ },
+ .@"struct" => |info| switch (info.layout) {
+ .@"packed" => {
+ try s.value(field_value, .{});
+ },
+ .@"extern" => {
+ var sub_struct = try s.beginStruct(.{});
+ try printStruct(sc, &sub_struct, Field, field_value);
+ try sub_struct.end();
+ },
+ .auto => switch (Field.storage) {
+ .flag_optional, .enum_optional => {
+ if (field_value.value) |some| {
+ try printValue(sc, s, Field.Value, some);
+ } else {
+ try s.value(null, .{});
+ }
+ },
+ .length_prefixed_list, .flag_length_prefixed_list, .flag_list => {
+ try printValue(sc, s, @TypeOf(field_value.slice), field_value.slice);
+ },
+ .extended => @compileError("TODO"),
+ .union_list => {
+ var slice_field = try s.beginTuple(.{});
+ for (field_value.slice(c.extra), 0..) |elem, i| switch (field_value.tag(c.extra, i)) {
+ inline else => |tag| {
+ var sub_struct = try s.beginStruct(.{});
+ try sub_struct.fieldPrefix(@tagName(tag));
+ try printValue(sc, s, @FieldType(Field.Union, @tagName(tag)), @enumFromInt(elem));
+ try sub_struct.end();
+ },
+ };
+ try slice_field.end();
+ },
+ .flag_union => try printValue(sc, s, Field.Union, field_value.u),
+ .multi_list => @compileError("TODO"),
+ },
+ },
+ .@"union" => {
+ try printTaggedUnion(sc, s, field_value);
+ },
+ else => @compileError("not implemented: " ++ @typeName(Field)),
+ },
+ }
+}
+
+fn printTaggedUnion(sc: *const ScannedConfig, s: *Serializer, value: anytype) !void {
+ switch (value) {
+ inline else => |u, tag| {
+ if (@TypeOf(u) == void) {
+ try s.ident(@tagName(tag));
+ } else {
+ var sub_struct = try s.beginStruct(.{});
+ try sub_struct.fieldPrefix(@tagName(tag));
+ try printValue(sc, s, @TypeOf(u), u);
+ try sub_struct.end();
+ }
+ },
+ }
+}
+
+pub fn printSteps(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void {
+ const arena = graph.arena;
+ const c = &sc.configuration;
+ for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step_index| {
+ const step = step_index.ptr(c);
+ const decorated_name = if (step_index == c.default_step)
+ try std.fmt.allocPrint(arena, "{s} (default)", .{name})
+ else
+ name;
+ const top_level = step.extended.get(c.extra).top_level;
+ const description = top_level.description.slice(c);
+ try w.print(" {s:<28} {s}\n", .{ decorated_name, description });
+ }
+}
+
+pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void {
+ const arena = graph.arena;
+
+ try w.print(
+ \\Usage: {s} build [steps] [options]
+ \\
+ \\Steps:
+ \\
+ , .{graph.zig_exe});
+ try printSteps(sc, graph, w);
+ try w.writeAll(
+ \\
+ \\Project-Specific Options:
+ \\
+ );
+
+ const available_options = sc.configuration.available_options;
+ if (available_options.len == 0) {
+ try w.print(" (none)\n", .{});
+ } else {
+ for (available_options) |option| {
+ const name = option.name.slice(&sc.configuration);
+ const description = option.description.slice(&sc.configuration);
+ const help = try std.fmt.allocPrint(arena, " -D{s}=[{t}]", .{ name, option.type });
+ try w.print("{s:<30} {s}\n", .{ help, description });
+ if (option.enum_options.slice(&sc.configuration)) |enum_options| {
+ const padding: [33]u8 = @splat(' ');
+ try w.writeAll(padding ++ "Supported Values:\n");
+ for (enum_options) |enum_option_index| {
+ const enum_option = enum_option_index.slice(&sc.configuration);
+ try w.print(padding ++ " {s}\n", .{enum_option});
+ }
+ }
+ }
+ }
+
+ try w.writeAll(
+ \\
+ \\System Integration Options:
+ \\ --search-prefix [path] Add a path to look for binaries, libraries, headers
+ \\ --sysroot [path] Set the system root directory (usually /)
+ \\ --libc [file] Provide a file which specifies libc paths
+ \\
+ \\ --system [pkgdir] Disable package fetching; enable all integrations
+ \\ -fsys=[name] Enable a system integration
+ \\ -fno-sys=[name] Disable a system integration
+ \\
+ \\ -fdarling, -fno-darling Integration with system-installed Darling to
+ \\ execute macOS programs on Linux hosts
+ \\ (default: no)
+ \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute
+ \\ foreign-architecture programs on Linux hosts
+ \\ (default: no)
+ \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc
+ \\ (e.g. glibc or musl) built for multiple foreign
+ \\ architectures, allowing execution of non-native
+ \\ programs that link with libc.
+ \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
+ \\ ARM64 macOS hosts. (default: no)
+ \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
+ \\ execute WASI binaries. (default: no)
+ \\ -fwine, -fno-wine Integration with system-installed Wine to execute
+ \\ Windows programs on Linux hosts. (default: no)
+ \\
+ \\ Available System Integrations: Enabled:
+ \\
+ );
+ if (sc.configuration.system_integrations.len == 0) {
+ try w.writeAll(" (none) -\n");
+ } else {
+ for (sc.configuration.system_integrations) |system_integration| {
+ const name = system_integration.name.slice(&sc.configuration);
+ const status = switch (system_integration.status) {
+ .disabled => "no",
+ .enabled => "yes",
+ };
+ try w.print(" {s:<43} {s}\n", .{ name, status });
+ }
+ }
+
+ try w.writeAll(
+ \\
+ \\General Options:
+ \\ -h, --help Print this help to stdout and exit
+ \\ -l, --list-steps Print available steps to stdout and exit
+ \\
+ \\ -p, --prefix [path] Where to install files (default: zig-out)
+ \\ --prefix-lib-dir [path] Where to install libraries
+ \\ --prefix-exe-dir [path] Where to install executables
+ \\ --prefix-include-dir [path] Where to install C header files
+ \\ --release[=mode] Request release mode, optionally specifying a
+ \\ preferred optimization mode: fast, safe, small
+ \\
+ \\ --verbose Print commands before executing them
+ \\ --color [auto|off|on] Enable or disable colored error messages
+ \\ --error-style [style] Control how build errors are printed
+ \\ verbose (Default) Report errors with full context
+ \\ minimal Report errors after summary, excluding context like command lines
+ \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update
+ \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update
+ \\ --multiline-errors [style] Control how multi-line error messages are printed
+ \\ indent (Default) Indent non-initial lines to align with initial line
+ \\ newline Include a leading newline so that the error message is on its own lines
+ \\ none Print as usual so the first line is misaligned
+ \\ --summary [mode] Control the printing of the build summary
+ \\ all Print the build summary in its entirety
+ \\ new Omit cached steps
+ \\ failures (Default if short-lived) Only print failed steps
+ \\ line (Default if long-lived) Only print the single-line summary
+ \\ none Do not print the build summary
+ \\ -j<N> Limit concurrent jobs (default is to use all CPU cores)
+ \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
+ \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
+ \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded.
+ \\ The timeout must include a unit: ns, us, ms, s, m, h
+ \\ --watch Continuously rebuild when source files are modified
+ \\ --debounce <ms> Delay before rebuilding after changed file detected
+ \\ --webui[=ip] Enable the web interface on the given IP address
+ \\ --fuzz[=limit] Continuously search for unit test failures with an optional
+ \\ limit to the max number of iterations. The argument supports
+ \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
+ \\ '--webui' when no limit is specified.
+ \\ --time-report Force full rebuild and provide detailed information on
+ \\ compilation time of Zig source code (implies '--webui')
+ \\ -fincremental Enable incremental compilation
+ \\ -fno-incremental Disable incremental compilation
+ \\
+ \\Package Management Options:
+ \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit
+ \\ needed (Default) Lazy dependencies are fetched as needed
+ \\ all Lazy dependencies are always fetched
+ \\ --fork=[path], --fork [path] Override one or more projects from dependency tree
+ \\
+ \\Advanced Options:
+ \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error
+ \\ -fno-reference-trace Disable reference trace
+ \\ -fallow-so-scripts Allows .so files to be GNU ld scripts
+ \\ -fno-allow-so-scripts (default) .so files must be ELF files
+ \\ --error-limit [num] Set the maximum amount of distinct error values
+ \\ --build-file [file] Override path to build.zig
+ \\ --cache-dir [path] Override path to local Zig cache directory
+ \\ --global-cache-dir [path] Override path to global Zig cache directory
+ \\ --zig-lib-dir [arg] Override path to Zig lib directory
+ \\ --seed [integer] For shuffling dependency traversal order (default: random)
+ \\ --cache-poison[=mode] Override configuration caching behavior
+ \\ pure (default) Avoid false positive cache hits
+ \\ poisoned Don't cache the configuration
+ \\ disallowed Panics when cache would be poisoned
+ \\ ignored A little poison never hurt anybody
+ \\ --print-configuration Render configuration as .zon to stdout
+ \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries
+ \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM)
+ \\ sha1, tree 20-byte cryptographic hash (ELF, WASM)
+ \\ md5 16-byte cryptographic hash (ELF)
+ \\ uuid 16-byte random UUID (ELF, WASM)
+ \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM)
+ \\ none (default) No build ID
+ \\ --debug-log [scope] Enable debugging the compiler
+ \\ --debug-pkg-config Fail if unknown pkg-config flags encountered
+ \\ --maker-opt=[mode] Change maker executable optimization mode (default: ReleaseSafe)
+ \\ --verbose-link Enable compiler debug output for linking
+ \\ --verbose-air Enable compiler debug output for Zig AIR
+ \\ --verbose-llvm-ir Enable compiler debug output for LLVM IR
+ \\ --verbose-cimport Enable compiler debug output for C imports
+ \\ --verbose-cc Enable compiler debug output for C compilation
+ \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
+ \\
+ );
+}
diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig
@@ -0,0 +1,863 @@
+//! The *mutable* state that `Maker` needs in order to process one node from
+//! the build graph.
+const Step = @This();
+
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Cache = std.Build.Cache;
+const Io = std.Io;
+const Dir = std.Io.Dir;
+const LazyPath = std.Build.Configuration.LazyPath;
+const Package = std.Build.Configuration.Package;
+const Path = std.Build.Cache.Path;
+const Configuration = std.Build.Configuration;
+const assert = std.debug.assert;
+
+const WebServer = @import("WebServer.zig");
+const Maker = @import("../Maker.zig");
+
+pub const CheckFile = @import("Step/CheckFile.zig");
+pub const Compile = @import("Step/Compile.zig");
+pub const ConfigHeader = @import("Step/ConfigHeader.zig");
+pub const FindProgram = @import("Step/FindProgram.zig");
+pub const Fmt = @import("Step/Fmt.zig");
+pub const InstallArtifact = @import("Step/InstallArtifact.zig");
+pub const InstallDir = @import("Step/InstallDir.zig");
+pub const InstallFile = @import("Step/InstallFile.zig");
+pub const ObjCopy = @import("Step/ObjCopy.zig");
+pub const Options = @import("Step/Options.zig");
+pub const Run = @import("Step/Run.zig");
+pub const TranslateC = @import("Step/TranslateC.zig");
+pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
+pub const WriteFile = @import("Step/WriteFile.zig");
+
+/// Avoid false sharing.
+_: void align(std.atomic.cache_line) = {},
+
+/// Extra data for specific types of steps.
+extended: Extended,
+
+/// This field is atomically accessed multi-threaded.
+state: State = .precheck_unstarted,
+
+dependants: std.ArrayList(Configuration.Step.Index) = .empty,
+/// Collects the set of files that retrigger this step to run.
+///
+/// This is used by the build system's implementation of `--watch` but it can
+/// also be potentially useful for IDEs to know what effects editing a
+/// particular file has.
+///
+/// Populated within `make`. Implementation may choose to clear and repopulate,
+/// retain previous value, or update.
+inputs: Inputs = .init,
+pending_deps: u32 = undefined,
+
+result_error_msgs: std.ArrayList([]const u8) = .empty,
+result_error_bundle: std.zig.ErrorBundle = .empty,
+result_stderr: []const u8 = "",
+result_cached: bool = false,
+/// Indicates error information is missing due to allocation failure.
+result_oom: bool = false,
+result_duration_ns: ?u64 = null,
+/// 0 means unavailable or not reported.
+result_peak_rss: usize = 0,
+/// If the step is failed and this field is populated, this is the command which failed.
+/// This field may be populated even if the step succeeded.
+/// Memory owned by `Maker.gpa`.
+result_failed_command: ?[]const u8 = null,
+test_results: TestResults = .{},
+
+comptime {
+ // Common cache line size is 128. This check prevents accidentally crossing
+ // an additional cache line. In the future it might be nice to try to fit
+ // this struct in 128 bytes or less.
+ if (std.atomic.cache_line <= 128) assert(@sizeOf(@This()) <= 128 * 3);
+}
+
+pub const Extended = union(enum) {
+ check_file: CheckFile,
+ compile: Compile,
+ config_header: ConfigHeader,
+ fail: Fail,
+ find_program: FindProgram,
+ fmt: Fmt,
+ install_artifact: InstallArtifact,
+ install_dir: InstallDir,
+ install_file: InstallFile,
+ obj_copy: ObjCopy,
+ options: Options,
+ run: Run,
+ top_level: TopLevel,
+ translate_c: TranslateC,
+ update_source_files: UpdateSourceFiles,
+ write_file: WriteFile,
+
+ pub fn init(tag: Configuration.Step.Tag) Extended {
+ return switch (tag) {
+ .check_file => .{ .check_file = .{} },
+ .compile => .{ .compile = .{} },
+ .config_header => .{ .config_header = .{} },
+ .fail => .{ .fail = .{} },
+ .find_program => .{ .find_program = .{} },
+ .fmt => .{ .fmt = .{} },
+ .install_artifact => .{ .install_artifact = .{} },
+ .install_dir => .{ .install_dir = .{} },
+ .install_file => .{ .install_file = .{} },
+ .obj_copy => .{ .obj_copy = .{} },
+ .options => .{ .options = .{} },
+ .run => .{ .run = .{} },
+ .top_level => .{ .top_level = .{} },
+ .translate_c => .{ .translate_c = .{} },
+ .update_source_files => .{ .update_source_files = .{} },
+ .write_file => .{ .write_file = .{} },
+ };
+ }
+
+ pub const TopLevel = struct {
+ pub fn make(
+ top_level: *TopLevel,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+ ) Step.ExtendedMakeError!void {
+ _ = top_level;
+ _ = step_index;
+ _ = maker;
+ _ = progress_node;
+ }
+ };
+
+ pub const Fail = struct {
+ pub fn make(
+ this: *@This(),
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+ ) Step.ExtendedMakeError!void {
+ _ = this;
+ _ = progress_node;
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const conf = &maker.scanned_config.configuration;
+ const step = maker.stepByIndex(step_index);
+ const conf_step = step_index.ptr(conf);
+ const conf_fail = conf_step.extended.get(conf.extra).fail;
+
+ try step.result_error_msgs.append(arena, conf_fail.msg.slice(conf));
+ return error.MakeFailed;
+ }
+ };
+};
+
+pub const State = enum {
+ precheck_unstarted,
+ precheck_started,
+ /// This is also used to indicate "dirty" steps that have been modified
+ /// after a previous build completed, in which case, the step may or may
+ /// not have been completed before. Either way, one or more of its direct
+ /// file system inputs have been modified, meaning that the step needs to
+ /// be re-evaluated.
+ precheck_done,
+ dependency_failure,
+ success,
+ failure,
+ /// This state indicates that the step did not complete, however, it also did not fail,
+ /// and it is safe to continue executing its dependencies.
+ skipped,
+ /// This step was skipped because it specified a max_rss that exceeded the runner's maximum.
+ /// It is not safe to run its dependencies.
+ skipped_oom,
+};
+
+pub const Inputs = struct {
+ table: Table,
+
+ pub const init: Inputs = .{
+ .table = .{},
+ };
+
+ pub const Table = std.ArrayHashMapUnmanaged(Path, Files, Path.TableAdapter, false);
+ /// The special file name "." means any changes inside the directory.
+ pub const Files = std.ArrayList([]const u8);
+
+ pub fn populated(inputs: *Inputs) bool {
+ return inputs.table.count() != 0;
+ }
+
+ pub fn clear(inputs: *Inputs, gpa: Allocator) void {
+ for (inputs.table.values()) |*files| files.deinit(gpa);
+ inputs.table.clearRetainingCapacity();
+ }
+
+ pub fn deinit(inputs: *Inputs, gpa: Allocator) void {
+ clear(inputs, gpa);
+ inputs.table.deinit(gpa);
+ }
+};
+
+pub const TestResults = struct {
+ /// The total number of tests in the step. Every test has a "status" from the following:
+ /// * passed
+ /// * skipped
+ /// * failed cleanly
+ /// * crashed
+ /// * timed out
+ test_count: u32 = 0,
+
+ /// The number of tests which were skipped (`error.SkipZigTest`).
+ skip_count: u32 = 0,
+ /// The number of tests which failed cleanly.
+ fail_count: u32 = 0,
+ /// The number of tests which terminated unexpectedly, i.e. crashed.
+ crash_count: u32 = 0,
+ /// The number of tests which timed out.
+ timeout_count: u32 = 0,
+
+ /// The number of detected memory leaks. The associated test may still have passed; indeed, *all*
+ /// individual tests may have passed. However, the step as a whole fails if any test has leaks.
+ leak_count: u32 = 0,
+ /// The number of detected error logs. The associated test may still have passed; indeed, *all*
+ /// individual tests may have passed. However, the step as a whole fails if any test logs errors.
+ log_err_count: u32 = 0,
+
+ pub fn isSuccess(tr: TestResults) bool {
+ // all steps are success or skip
+ return tr.fail_count == 0 and
+ tr.crash_count == 0 and
+ tr.timeout_count == 0 and
+ // no (otherwise successful) step leaked memory or logged errors
+ tr.leak_count == 0 and
+ tr.log_err_count == 0;
+ }
+
+ /// Computes the number of tests which passed from the other values.
+ pub fn passCount(tr: TestResults) u32 {
+ return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count;
+ }
+};
+
+pub const MakeError = error{
+ /// Indicates the error is already reported.
+ MakeFailed,
+ MakeSkipped,
+} || Io.Cancelable;
+
+pub const ExtendedMakeError = MakeError || Allocator.Error;
+
+pub fn make(
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) MakeError!void {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const io = graph.io;
+ const c = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(c);
+ const s = maker.stepByIndex(step_index);
+
+ var start_ts: ?Io.Timestamp = t: {
+ if (!graph.time_report) break :t null;
+ const flags = conf_step.flags(c);
+ switch (flags.tag) {
+ .compile => break :t null,
+ .run => {
+ const run_flags: Configuration.Step.Run.Flags = @bitCast(flags);
+ if (run_flags.stdio == .zig_test) break :t null;
+ },
+ else => {},
+ }
+ break :t Io.Clock.awake.now(io);
+ };
+ const make_result = switch (s.extended) {
+ inline else => |*extended| extended.make(step_index, maker, progress_node),
+ };
+ if (start_ts) |*ts| {
+ const duration = ts.untilNow(io, .awake);
+ maker.web_server.?.updateTimeReportGeneric(step_index, duration);
+ }
+
+ make_result catch |err| switch (err) {
+ error.MakeFailed, error.MakeSkipped => |e| return e,
+ error.OutOfMemory => {
+ s.result_oom = true;
+ return error.MakeFailed;
+ },
+ error.Canceled => |e| return e,
+ };
+
+ if (!s.test_results.isSuccess()) {
+ return error.MakeFailed;
+ }
+
+ const max_rss = conf_step.max_rss.toBytes();
+ if (max_rss != 0 and s.result_peak_rss > max_rss) {
+ if (std.fmt.allocPrint(
+ arena,
+ "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)",
+ .{ s.result_peak_rss, max_rss },
+ )) |msg| {
+ s.oomWrap(s.result_error_msgs.append(arena, msg));
+ } else |_| s.result_oom = true;
+ }
+}
+
+/// Prepares the step for being re-evaluated.
+pub fn reset(step: *Step, maker: *Maker) void {
+ assert(step.state == .precheck_done);
+ const gpa = maker.gpa;
+
+ clearFailedCommand(step, gpa);
+
+ step.result_error_msgs.clearRetainingCapacity();
+ step.result_stderr = "";
+ step.result_cached = false;
+ step.result_duration_ns = null;
+ step.result_peak_rss = 0;
+ step.test_results = .{};
+ clearWatchInputs(step, maker);
+ clearErrorBundle(step, gpa);
+}
+
+pub const CaptureChildProcessError = error{
+ FileNotFound,
+} || ExtendedMakeError;
+
+pub const CaptureChildProcessOptions = struct {
+ argv: []const []const u8,
+ progress_node: std.Progress.Node = .none,
+ environ_map: ?*const std.process.Environ.Map = null,
+ allow_failure: bool = false,
+};
+
+/// Populates `s.result_failed_command`.
+pub fn captureChildProcess(s: *Step, maker: *Maker, options: CaptureChildProcessOptions) !std.process.RunResult {
+ const gpa = maker.gpa;
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO stop leaking into process arena
+ const io = graph.io;
+
+ clearFailedCommand(s, gpa);
+ s.result_failed_command = try std.zig.allocPrintCmd(gpa, options.argv, .{});
+
+ try handleChildProcUnsupported(s, maker);
+ try graph.handleVerbose(null, null, options.argv);
+
+ const result = std.process.run(arena, io, .{
+ .argv = options.argv,
+ .environ_map = options.environ_map orelse &graph.environ_map,
+ .progress_node = options.progress_node,
+ }) catch |err| {
+ switch (err) {
+ error.OutOfMemory, error.Canceled => |e| return e,
+ error.FileNotFound => |e| if (options.allow_failure) return e,
+ else => {},
+ }
+ return s.fail(maker, "failed to run {s}: {t}", .{ options.argv[0], err });
+ };
+
+ if (result.stderr.len > 0) try s.result_error_msgs.append(arena, result.stderr);
+
+ return result;
+}
+
+pub fn clearErrorBundle(s: *Step, gpa: Allocator) void {
+ s.result_error_bundle.deinit(gpa);
+ s.result_error_bundle = .empty;
+}
+
+pub fn clearFailedCommand(s: *Step, gpa: Allocator) void {
+ if (s.result_failed_command) |cmd| {
+ gpa.free(cmd);
+ s.result_failed_command = null;
+ }
+}
+
+pub const FailError = error{ OutOfMemory, MakeFailed };
+
+pub fn fail(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) FailError {
+ try step.addError(maker, fmt, args);
+ return error.MakeFailed;
+}
+
+pub fn addError(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into the process_arena
+ const msg = try std.fmt.allocPrint(arena, fmt, args);
+ try step.result_error_msgs.append(arena, msg);
+}
+
+pub const ZigProcess = struct {
+ child: std.process.Child,
+ multi_reader_buffer: Io.File.MultiReader.Buffer(2),
+ multi_reader: Io.File.MultiReader,
+ progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn,
+
+ pub const StreamEnum = enum { stdout, stderr };
+
+ pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void {
+ zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null;
+ }
+
+ pub fn deinit(zp: *ZigProcess, io: Io) void {
+ zp.child.kill(io);
+ zp.multi_reader.deinit();
+ zp.* = undefined;
+ }
+};
+
+/// Assumes that argv contains `--listen=-` and that the process being spawned
+/// is the zig compiler - the same version that compiled the build runner.
+/// Populates `s.result_failed_command`.
+pub fn evalZigProcess(
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ argv: []const []const u8,
+ prog_node: std.Progress.Node,
+ watch: bool,
+) (Step.ExtendedMakeError || error{NeedCompileErrorCheck})!?Path {
+ const s = maker.stepByIndex(step_index);
+ const gpa = maker.gpa;
+ const graph = maker.graph;
+ const io = graph.io;
+
+ // If an error occurs, it's happened in this command:
+ clearFailedCommand(s, gpa);
+ s.result_failed_command = try std.zig.allocPrintCmd(gpa, argv, .{});
+
+ if (s.getZigProcess()) |zp| update: {
+ assert(watch);
+ if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index);
+ zp.progress_ipc_index = null;
+ var exited = false;
+ defer if (exited) {
+ s.extended.compile.zig_process = null;
+ zp.deinit(io);
+ gpa.destroy(zp);
+ } else zp.saveState(prog_node);
+ const result = zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) {
+ error.BrokenPipe, error.EndOfStream => |reason| {
+ // Process restart required.
+ std.log.info("{s} restart required: {t}", .{ argv[0], reason });
+ _ = zp.child.wait(io) catch |e| return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e });
+ exited = true;
+ break :update;
+ },
+ error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e,
+ else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}),
+ };
+
+ if (s.result_error_bundle.errorMessageCount() > 0)
+ return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
+
+ if (s.result_error_msgs.items.len > 0 and result == null) {
+ // Crash detected.
+ const term = zp.child.wait(io) catch |e| {
+ return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e });
+ };
+ s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
+ exited = true;
+ try handleChildProcessTerm(s, maker, term);
+ return error.MakeFailed;
+ }
+
+ return result;
+ }
+ assert(argv.len != 0);
+
+ try handleChildProcUnsupported(s, maker);
+ try graph.handleVerbose(null, null, argv);
+
+ const zp = try gpa.create(ZigProcess);
+ defer if (!watch) gpa.destroy(zp);
+
+ zp.child = std.process.spawn(io, .{
+ .argv = argv,
+ .environ_map = &graph.environ_map,
+ .stdin = .pipe,
+ .stdout = .pipe,
+ .stderr = .pipe,
+ .request_resource_usage_statistics = true,
+ .progress_node = prog_node,
+ }) catch |err| return s.fail(maker, "failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
+
+ zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{
+ zp.child.stdout.?, zp.child.stderr.?,
+ });
+ if (watch) s.extended.compile.zig_process = zp;
+ defer if (!watch) zp.deinit(io);
+
+ const result = result: {
+ defer if (watch) zp.saveState(prog_node);
+ break :result zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) {
+ error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e,
+ else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}),
+ };
+ };
+
+ if (!watch) {
+ // Send EOF to stdin.
+ zp.child.stdin.?.close(io);
+ zp.child.stdin = null;
+
+ const term = zp.child.wait(io) catch |err| {
+ return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], err });
+ };
+ s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
+
+ // Special handling for compile step that is expecting compile errors.
+ const conf = &maker.scanned_config.configuration;
+ if (term == .exited) switch (step_index.ptr(conf).extended.get(conf.extra)) {
+ .compile => |compile| if (compile.flags4.expect_errors != .none) {
+ // Note that the exit code may be 0 in this case due to the
+ // compiler server protocol.
+ return error.NeedCompileErrorCheck;
+ },
+ else => {},
+ };
+ try handleChildProcessTerm(s, maker, term);
+ }
+
+ if (s.result_error_bundle.errorMessageCount() > 0) {
+ return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
+ }
+
+ return result;
+}
+
+fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *ZigProcess, watch: bool) !?Path {
+ const s = maker.stepByIndex(step_index);
+ const gpa = maker.gpa;
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const io = graph.io;
+
+ const start_ts = Io.Clock.awake.now(io);
+
+ try sendMessage(io, zp.child.stdin.?, .update);
+ if (!watch) try sendMessage(io, zp.child.stdin.?, .exit);
+
+ var result: ?Path = null;
+ var eos_err: error{EndOfStream}!void = {};
+
+ const stdout = zp.multi_reader.fileReader(0);
+
+ while (true) {
+ const Header = std.zig.Server.Message.Header;
+ const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
+ error.EndOfStream => break,
+ error.ReadFailed => return stdout.err.?,
+ };
+ const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
+ error.EndOfStream => |e| {
+ // Better to report the crash with stderr below, but we set
+ // this in case the child exits successfully while violating
+ // this protocol.
+ eos_err = e;
+ break;
+ },
+ error.ReadFailed => return stdout.err.?,
+ };
+ switch (header.tag) {
+ .zig_version => {
+ if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
+ return s.fail(
+ maker,
+ "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
+ .{ builtin.zig_version_string, body },
+ );
+ }
+ },
+ .error_bundle => {
+ s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
+ // This message indicates the end of the update.
+ if (watch) break;
+ },
+ .emit_digest => {
+ const EmitDigest = std.zig.Server.Message.EmitDigest;
+ const emit_digest: *align(1) const EmitDigest = @ptrCast(body);
+ s.result_cached = emit_digest.flags.cache_hit;
+ const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
+ result = .{
+ .root_dir = graph.local_cache_root,
+ .sub_path = try arena.dupe(u8, "o" ++ Dir.path.sep_str ++ Cache.binToHex(digest.*)),
+ };
+ },
+ .file_system_inputs => {
+ clearWatchInputs(s, maker);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ var it = std.mem.splitScalar(u8, body, 0);
+ while (it.next()) |prefixed_path| {
+ const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1);
+ const sub_path = try arena.dupe(u8, prefixed_path[1..]);
+ const sub_path_dirname = Dir.path.dirname(sub_path) orelse "";
+ switch (prefix_index) {
+ .cwd => {
+ const path: Path = .{
+ .root_dir = .cwd(),
+ .sub_path = sub_path_dirname,
+ };
+ try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
+ },
+ .zig_lib => zl: {
+ switch (conf_step.extended.get(conf.extra)) {
+ .compile => |compile| if (compile.zig_lib_dir.value) |zig_lib_dir| {
+ const resolved = try maker.resolveLazyPathIndex(arena, zig_lib_dir, step_index);
+ const appended = try resolved.join(arena, sub_path);
+ try addWatchInputPath(s, maker, appended);
+ break :zl;
+ },
+ else => {},
+ }
+ const path: Path = .{
+ .root_dir = graph.zig_lib_directory,
+ .sub_path = sub_path_dirname,
+ };
+ try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
+ },
+ .local_cache => {
+ const path: Path = .{
+ .root_dir = graph.local_cache_root,
+ .sub_path = sub_path_dirname,
+ };
+ try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
+ },
+ .global_cache => {
+ const path: Path = .{
+ .root_dir = graph.global_cache_root,
+ .sub_path = sub_path_dirname,
+ };
+ try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path));
+ },
+ }
+ }
+ },
+ .time_report => if (maker.web_server) |*ws| {
+ const TimeReport = std.zig.Server.Message.TimeReport;
+ const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]);
+ ws.updateTimeReportCompile(.{
+ .compile_step = step_index,
+ .use_llvm = tr.flags.use_llvm,
+ .stats = tr.stats,
+ .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()),
+ .llvm_pass_timings_len = tr.llvm_pass_timings_len,
+ .files_len = tr.files_len,
+ .decls_len = tr.decls_len,
+ .trailing = body[@sizeOf(TimeReport)..],
+ });
+ },
+ else => {}, // ignore other messages
+ }
+ }
+
+ s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds());
+
+ const stderr_contents = zp.multi_reader.reader(1).buffered();
+ if (stderr_contents.len > 0) {
+ try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents));
+ }
+
+ try eos_err;
+
+ return result;
+}
+
+pub fn getZigProcess(s: *Step) ?*ZigProcess {
+ return switch (s.extended) {
+ .compile => |*compile| compile.zig_process,
+ else => null,
+ };
+}
+
+fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
+ const header: std.zig.Client.Message.Header = .{
+ .tag = tag,
+ .bytes_len = 0,
+ };
+ var w = file.writer(io, &.{});
+ w.interface.writeStruct(header, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+}
+
+/// Asserts that the caller has already populated `s.result_failed_command`.
+pub inline fn handleChildProcUnsupported(s: *Step, maker: *Maker) FailError!void {
+ assert(s.result_failed_command != null);
+ if (!std.process.can_spawn)
+ return s.fail(maker, "unable to spawn process: host cannot spawn child processes", .{});
+}
+
+/// Asserts that the caller has already populated `s.result_failed_command`.
+pub fn handleChildProcessTerm(s: *Step, maker: *Maker, term: std.process.Child.Term) FailError!void {
+ assert(s.result_failed_command != null);
+ if (!term.success()) return s.fail(maker, "process {f}", .{term});
+}
+
+/// Prefer `cacheHitAndWatch` unless you already added watch inputs
+/// separately from using the cache system.
+pub fn cacheHit(s: *Step, maker: *Maker, man: *Cache.Manifest) !bool {
+ s.result_cached = man.hit() catch |err| return failWithCacheError(s, maker, man, err);
+ return s.result_cached;
+}
+
+/// Clears previous watch inputs, if any, and then populates watch inputs from
+/// the full set of files picked up by the cache manifest.
+///
+/// Must be accompanied with `writeManifestAndWatch`.
+pub fn cacheHitAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !bool {
+ const is_hit = man.hit() catch |err| return failWithCacheError(s, maker, man, err);
+ s.result_cached = is_hit;
+ // The above call to hit() populates the manifest with files, so in case of
+ // a hit, we need to populate watch inputs.
+ if (is_hit) try setWatchInputsFromManifest(s, maker, man);
+ return is_hit;
+}
+
+fn failWithCacheError(
+ s: *Step,
+ maker: *Maker,
+ man: *const Cache.Manifest,
+ err: Cache.Manifest.HitError,
+) error{ OutOfMemory, Canceled, MakeFailed } {
+ switch (err) {
+ error.CacheCheckFailed => switch (man.diagnostic) {
+ .none => unreachable,
+ .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail(maker, "failed checking cache: {t} {t}", .{
+ man.diagnostic, e,
+ }),
+ .file_open, .file_stat, .file_read, .file_hash => |op| {
+ const pp = man.files.keys()[op.file_index].prefixed_path;
+ const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
+ return s.fail(maker, "failed checking cache: {s}{c}{s} {t} {t}", .{
+ prefix, Dir.path.sep, pp.sub_path, man.diagnostic, op.err,
+ });
+ },
+ },
+ error.OutOfMemory, error.Canceled => |e| return e,
+ error.InvalidFormat => return s.fail(maker, "failed checking cache: invalid manifest file format", .{}),
+ }
+}
+
+/// Prefer `writeManifestAndWatch` unless you already added watch inputs
+/// separately from using the cache system.
+pub fn writeManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
+ if (s.test_results.isSuccess()) {
+ man.writeManifest() catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ else => |e| try s.addError(maker, "failed writing cache manifest: {t}", .{e}),
+ };
+ }
+}
+
+/// Clears previous watch inputs, if any, and then populates watch inputs from
+/// the full set of files picked up by the cache manifest.
+///
+/// Must be accompanied with `cacheHitAndWatch`.
+pub fn writeManifestAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
+ try writeManifest(s, maker, man);
+ try setWatchInputsFromManifest(s, maker, man);
+}
+
+fn setWatchInputsFromManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into process arena
+ const prefixes = man.cache.prefixes();
+ clearWatchInputs(s, maker);
+ for (man.files.keys()) |file| {
+ // The file path data is freed when the cache manifest is cleaned up at the end of `make`.
+ const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path);
+ try addWatchInputFromPath(s, maker, .{
+ .root_dir = prefixes[file.prefixed_path.prefix],
+ .sub_path = Dir.path.dirname(sub_path) orelse "",
+ }, Dir.path.basename(sub_path));
+ }
+}
+
+/// For steps that have a single input that never changes when re-running `make`.
+pub fn singleUnchangingWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_path: LazyPath) Allocator.Error!void {
+ if (!step.inputs.populated()) try step.addWatchInput(maker, arena, lazy_path);
+}
+
+pub fn clearWatchInputs(step: *Step, maker: *Maker) void {
+ step.inputs.clear(maker.gpa);
+}
+
+/// Places a *file* dependency on the path.
+pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: LazyPath) Allocator.Error!void {
+ const conf = &maker.scanned_config.configuration;
+ switch (lazy_file) {
+ .source_path => |source_path| {
+ const sub_path = source_path.sub_path.slice(conf);
+ const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path);
+ try addWatchInputPath(step, maker, pkg_path);
+ },
+ .relative => |relative| {
+ const resolved_path = try maker.relativePath(arena, relative);
+ try addWatchInputPath(step, maker, resolved_path);
+ },
+ // Nothing to watch because this dependency edge is modeled instead via `dependants`.
+ .generated => {},
+ }
+}
+
+/// Any changes inside the directory will trigger invalidation.
+///
+/// See also `addDirectoryWatchInputFromPath` which takes a `Path` instead.
+///
+/// Paths derived from this directory should also be manually added via
+/// `addDirectoryWatchInputFromPath` if and only if this function returns
+/// `true`.
+pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPath) Allocator.Error!bool {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ switch (lazy_directory) {
+ .source_path => |source_path| {
+ const conf = &maker.scanned_config.configuration;
+ const sub_path = source_path.sub_path.slice(conf);
+ const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path);
+ try addDirectoryWatchInputFromPath(step, maker, pkg_path);
+ },
+ .relative => |relative| {
+ const resolved_path = try maker.relativePath(arena, relative);
+ try addDirectoryWatchInputFromPath(step, maker, resolved_path);
+ },
+ // Nothing to watch because this dependency edge is modeled instead via `dependants`.
+ .generated => return false,
+ }
+ return true;
+}
+
+/// Any changes inside the directory will trigger invalidation.
+///
+/// See also `addDirectoryWatchInput` which takes a `LazyPath` instead.
+///
+/// This function should only be called when it has been verified that the
+/// dependency on `path` is not already accounted for by a `Step` dependency.
+/// In other words, before calling this function, first check that the
+/// `LazyPath` which this `path` is derived from is not `generated`.
+pub fn addDirectoryWatchInputFromPath(step: *Step, maker: *Maker, path: Path) !void {
+ return addWatchInputFromPath(step, maker, path, ".");
+}
+
+fn addWatchInputPath(step: *Step, maker: *Maker, path: Path) Allocator.Error!void {
+ return addWatchInputFromPath(step, maker, .{
+ .root_dir = path.root_dir,
+ .sub_path = Dir.path.dirname(path.sub_path) orelse "",
+ }, Dir.path.basename(path.sub_path));
+}
+
+fn addWatchInputFromPath(step: *Step, maker: *Maker, directory: Path, basename: []const u8) Allocator.Error!void {
+ const gpa = maker.gpa;
+ const gop = try step.inputs.table.getOrPut(gpa, directory);
+ if (!gop.found_existing) gop.value_ptr.* = .empty;
+ try gop.value_ptr.append(gpa, basename);
+}
+
+fn oomWrap(s: *Step, result: error{OutOfMemory}!void) void {
+ result catch {
+ s.result_oom = true;
+ };
+}
diff --git a/lib/compiler/Maker/Step/CheckFile.zig b/lib/compiler/Maker/Step/CheckFile.zig
@@ -0,0 +1,63 @@
+const CheckFile = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Configuration = std.Build.Configuration;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ check_file: *CheckFile,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = check_file;
+ _ = progress_node;
+ const graph = maker.graph;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_cf = conf_step.extended.get(conf.extra).check_file;
+ const lazy_path = conf_cf.file.get(conf);
+
+ try step.singleUnchangingWatchInput(maker, arena, lazy_path);
+
+ const src_path = try maker.resolveLazyPath(arena, lazy_path, step_index);
+ const limit: Io.Limit = if (conf_cf.max_bytes.value) |x| .limited(x) else .unlimited;
+
+ const contents = src_path.root_dir.handle.readFileAlloc(io, src_path.sub_path, arena, limit) catch |err|
+ return step.fail(maker, "failed to read {f}: {t}", .{ src_path, err });
+
+ for (conf_cf.expected_matches.slice) |expected_match_index| {
+ const expected_match = expected_match_index.slice(conf);
+ if (std.mem.find(u8, contents, expected_match) == null) {
+ return step.fail(maker,
+ \\
+ \\========= expected to find: ===================
+ \\{s}
+ \\========= but file does not contain it: =======
+ \\{s}
+ \\===============================================
+ , .{ expected_match, contents });
+ }
+ }
+
+ if (conf_cf.expected_exact.value) |expected_exact_index| {
+ const expected_exact = expected_exact_index.slice(conf);
+ if (!std.mem.eql(u8, expected_exact, contents)) {
+ return step.fail(maker,
+ \\
+ \\========= expected: =====================
+ \\{s}
+ \\========= but found: ====================
+ \\{s}
+ \\========= from the following file: ======
+ \\{f}
+ , .{ expected_exact, contents, src_path });
+ }
+ }
+}
diff --git a/lib/compiler/Maker/Step/Compile.zig b/lib/compiler/Maker/Step/Compile.zig
@@ -0,0 +1,1390 @@
+const Compile = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const mem = std.mem;
+const Configuration = std.Build.Configuration;
+const Dir = std.Io.Dir;
+const Path = std.Build.Cache.Path;
+const Module = std.Build.Configuration.Module;
+const Io = std.Io;
+const Sha256 = std.crypto.hash.sha2.Sha256;
+const assert = std.debug.assert;
+const allocPrint = std.fmt.allocPrint;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+const PkgConfig = @import("../PkgConfig.zig");
+
+/// Populated when there is compiler process that lives across multiple calls
+/// to `make`.
+zig_process: ?*Step.ZigProcess = null,
+/// Populated by InstallArtifact.
+installed_path: ?Path = null,
+/// Populated by `make`, used by `Run`.
+is_linking_libc: bool = false,
+
+pub fn make(
+ compile: *Compile,
+ compile_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = compile_index.ptr(conf);
+ const conf_comp = conf_step.extended.get(conf.extra).compile;
+
+ var arena_allocator: std.heap.ArenaAllocator = .init(gpa);
+ defer arena_allocator.deinit();
+ const arena = arena_allocator.allocator();
+
+ var argv: std.ArrayList([]const u8) = .empty;
+ defer argv.deinit(gpa);
+
+ try lowerZigArgs(arena, compile, compile_index, maker, progress_node, &argv, false);
+
+ const maybe_output_dir = Step.evalZigProcess(
+ compile_index,
+ maker,
+ argv.items,
+ progress_node,
+ (graph.incremental == true) and (maker.watch or maker.web_server != null),
+ ) catch |err| switch (err) {
+ error.NeedCompileErrorCheck => {
+ try checkCompileErrors(arena, maker, compile_index);
+ return;
+ },
+ else => |e| return e,
+ };
+
+ const root_module = conf_comp.root_module.get(conf);
+ const target = root_module.resolved_target.get(conf).?.result.get(conf);
+
+ // Update generated files
+ if (maybe_output_dir) |output_dir| {
+ if (conf_comp.emit_directory.value) |gf| maker.generatedPath(gf).* = output_dir;
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_bin.value, .bin);
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_pdb.value, .pdb);
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_implib.value, .implib);
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_h.value, .h);
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_docs.value, .docs);
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_asm.value, .@"asm");
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_llvm_ir.value, .llvm_ir);
+ try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_llvm_bc.value, .llvm_bc);
+ }
+
+ if (conf_comp.flags3.kind == .lib and conf_comp.flags2.linkage == .dynamic and
+ conf_comp.version.value != null and target.flags.os_tag != .windows)
+ {
+ if (conf_comp.generated_bin.value) |generated_bin| {
+ const full_dest_path = maker.generatedPath(generated_bin).*;
+ try maker.installSymLinks(arena, full_dest_path, compile_index, compile_index);
+ }
+ }
+}
+
+fn updateGeneratedFile(
+ maker: *Maker,
+ arena: Allocator,
+ conf_comp: *const Configuration.Step.Compile,
+ out_path: std.Build.Cache.Path,
+ target: *const Configuration.TargetQuery,
+ opt_gf: ?Configuration.GeneratedFileIndex,
+ ea: std.zig.EmitArtifact,
+) Allocator.Error!void {
+ const gf = opt_gf orelse return;
+ const graph = maker.graph;
+ const conf = &maker.scanned_config.configuration;
+ const name = try ea.cacheName(arena, .{
+ .root_name = conf_comp.root_name.slice(conf),
+ .cpu_arch = target.flags.cpu_arch.unwrap().?,
+ .os_tag = target.flags.os_tag.unwrap().?,
+ .ofmt = target.flags.object_format.unwrap().?,
+ .abi = target.flags.abi.unwrap().?,
+ .output_mode = switch (conf_comp.flags3.kind) {
+ .lib => .Lib,
+ .obj, .test_obj => .Obj,
+ .exe, .@"test" => .Exe,
+ },
+ .link_mode = conf_comp.flags2.linkage.unwrap(),
+ .version = if (conf_comp.version.value) |v|
+ std.SemanticVersion.parse(v.slice(conf)) catch unreachable
+ else
+ null,
+ });
+ maker.generatedPath(gf).* = try out_path.join(graph.arena, name);
+}
+
+/// List of importable modules in a compilation's module graph, including
+/// the root module. The root module is guaranteed to be first.
+const ModuleList = std.AutoArrayHashMapUnmanaged(Configuration.Module.Index, Configuration.String);
+/// Keyed on the first key in the module list.
+pub const ModuleGraph = std.ArrayHashMapUnmanaged(ModuleList, void, ModuleListContext, false);
+
+const ModuleListContext = struct {
+ pub fn eql(ctx: @This(), a: ModuleList, b: ModuleList) bool {
+ _ = ctx;
+ return a.keys()[0] == b.keys()[0];
+ }
+
+ pub fn hash(ctx: @This(), key: ModuleList) u32 {
+ _ = ctx;
+ return std.hash.int(@intFromEnum(key.keys()[0]));
+ }
+
+ const Adapter = struct {
+ pub fn eql(ctx: @This(), a: Configuration.Module.Index, b: ModuleList, b_index: usize) bool {
+ _ = ctx;
+ _ = b_index;
+ return a == b.keys()[0];
+ }
+
+ pub fn hash(ctx: @This(), key: Configuration.Module.Index) u32 {
+ _ = ctx;
+ return std.hash.int(@intFromEnum(key));
+ }
+ };
+};
+
+fn lowerZigArgs(
+ arena: Allocator,
+ compile: *Compile,
+ compile_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+ zig_args: *std.ArrayList([]const u8),
+ fuzz: bool,
+) Step.ExtendedMakeError!void {
+ const step = maker.stepByIndex(compile_index);
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = compile_index.ptr(conf);
+ const conf_comp = conf_step.extended.get(conf.extra).compile;
+ const root_module_target = conf_comp.rootModuleTarget(conf);
+
+ try zig_args.append(gpa, graph.zig_exe);
+
+ const cmd = switch (conf_comp.flags3.kind) {
+ .lib => "build-lib",
+ .exe => "build-exe",
+ .obj => "build-obj",
+ .@"test" => "test",
+ .test_obj => "test-obj",
+ };
+ try zig_args.append(gpa, cmd);
+
+ if (graph.reference_trace) |some| {
+ try zig_args.append(gpa, try allocPrint(arena, "-freference-trace={d}", .{some}));
+ }
+ try addFlag(gpa, zig_args, "allow-so-scripts", conf_comp.flags2.allow_so_scripts.toBool() orelse graph.allow_so_scripts);
+
+ try addFlag(gpa, zig_args, "llvm", conf_comp.flags2.use_llvm.toBool());
+ try addFlag(gpa, zig_args, "lld", conf_comp.flags2.use_lld.toBool());
+ try addFlag(gpa, zig_args, "new-linker", conf_comp.flags2.use_new_linker.toBool());
+
+ const root_module = conf_comp.root_module.get(conf);
+
+ if (root_module.resolved_target.get(conf).?.query.unwrap()) |query| {
+ if (query.get(conf).flags.object_format.unwrap()) |ofmt| {
+ try zig_args.append(gpa, try allocPrint(arena, "-ofmt={t}", .{ofmt}));
+ }
+ }
+
+ switch (conf_comp.flags3.entry) {
+ .default => {},
+ .disabled => try zig_args.append(gpa, "-fno-entry"),
+ .enabled => try zig_args.append(gpa, "-fentry"),
+ .symbol_name => {
+ const symbol_name = conf_comp.entry.value.?.slice(conf);
+ try zig_args.append(gpa, try allocPrint(arena, "-fentry={s}", .{symbol_name}));
+ },
+ }
+
+ for (conf_comp.force_undefined_symbols.slice) |symbol_name| {
+ try zig_args.appendSlice(gpa, &.{ "--force_undefined", symbol_name.slice(conf) });
+ }
+
+ if (conf_comp.stack_size.value) |stack_size| {
+ try zig_args.appendSlice(gpa, &.{ "--stack", try allocPrint(arena, "{d}", .{stack_size}) });
+ }
+
+ try addBool(gpa, zig_args, "-ffuzz", fuzz);
+
+ {
+ var is_linking_libc = conf_comp.flags3.is_linking_libc;
+ var is_linking_libcpp = conf_comp.flags3.is_linking_libcpp;
+
+ // Stores system libraries that have already been seen for at least one
+ // module, along with any C compiler arguments that need to be passed
+ // to the compiler for each module individually as reported by
+ // pkg-config.
+ var seen_system_libs: std.AutoArrayHashMapUnmanaged(Configuration.String, []const []const u8) = .empty;
+ var frameworks: std.AutoArrayHashMapUnmanaged(Configuration.String, Configuration.Module.Framework.Flags) = .empty;
+ var module_graph: ModuleGraph = .empty;
+
+ var prev_has_cflags = false;
+ var prev_has_rcflags = false;
+ var prev_search_strategy: Configuration.SystemLib.SearchStrategy = .paths_first;
+ var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic;
+ // Track the number of positional arguments so that a nice error can be
+ // emitted if there is nothing to link.
+ var total_linker_objects: usize = @intFromBool(root_module.root_source_file != .none);
+
+ // Fully recursive iteration including dynamic libraries to detect
+ // libc and libc++ linkage.
+ for (try getCompileDependencies(arena, &module_graph, conf, compile_index, true)) |some_compile_index| {
+ const some_compile = some_compile_index.ptr(conf).extended.get(conf.extra).compile;
+ const modules = try getModuleList(arena, &module_graph, some_compile.root_module, conf);
+ for (modules.keys()) |mod_index| {
+ const mod = mod_index.get(conf);
+ is_linking_libc = is_linking_libc or mod.flags2.link_libc == .true;
+ is_linking_libcpp = is_linking_libcpp or mod.flags2.link_libcpp == .true;
+ }
+ }
+
+ var cli_named_modules = try CliNamedModules.init(arena, &module_graph, compile_index, maker);
+
+ // For this loop, don't chase dynamic libraries because their link
+ // objects are already linked.
+ for (try getCompileDependencies(arena, &module_graph, conf, compile_index, false)) |dep_compile_index| {
+ const dep_compile = dep_compile_index.ptr(conf).extended.get(conf.extra).compile;
+ const modules = try getModuleList(arena, &module_graph, dep_compile.root_module, conf);
+ for (modules.keys()) |mod_index| {
+ const mod = mod_index.get(conf);
+ // While walking transitive dependencies, if a given link object is
+ // already included in a library, it should not redundantly be
+ // placed on the linker line of the dependee.
+ const my_responsibility = dep_compile_index == compile_index;
+ const already_linked = !my_responsibility and dep_compile.isDynamicLibrary();
+
+ // Inherit dependencies on darwin frameworks.
+ if (!already_linked) {
+ for (mod.frameworks.slice) |framework| {
+ try frameworks.put(arena, framework.name, framework.flags);
+ }
+ }
+
+ // Inherit dependencies on system libraries and static libraries.
+ for (0..mod.link_objects.len) |lo_i| switch (mod.link_objects.get(conf.extra, lo_i)) {
+ .static_path => |static_path| {
+ if (my_responsibility) {
+ try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, static_path, compile_index));
+ total_linker_objects += 1;
+ }
+ },
+ .system_lib => |system_lib_index| {
+ const system_lib = system_lib_index.get(conf);
+ const system_lib_name = system_lib.name.slice(conf);
+ const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
+ if (system_lib_gop.found_existing) {
+ try zig_args.appendSlice(gpa, system_lib_gop.value_ptr.*);
+ continue;
+ } else {
+ system_lib_gop.value_ptr.* = &.{};
+ }
+
+ if (already_linked)
+ continue;
+
+ if ((system_lib.flags.search_strategy != prev_search_strategy or
+ system_lib.flags.preferred_link_mode != prev_preferred_link_mode) and
+ conf_comp.flags2.linkage != .static)
+ {
+ try zig_args.ensureUnusedCapacity(gpa, 1);
+ switch (system_lib.flags.search_strategy) {
+ .no_fallback => switch (system_lib.flags.preferred_link_mode) {
+ .dynamic => zig_args.appendAssumeCapacity("-search_dylibs_only"),
+ .static => zig_args.appendAssumeCapacity("-search_static_only"),
+ },
+ .paths_first => switch (system_lib.flags.preferred_link_mode) {
+ .dynamic => zig_args.appendAssumeCapacity("-search_paths_first"),
+ .static => zig_args.appendAssumeCapacity("-search_paths_first_static"),
+ },
+ .mode_first => switch (system_lib.flags.preferred_link_mode) {
+ .dynamic => zig_args.appendAssumeCapacity("-search_dylibs_first"),
+ .static => zig_args.appendAssumeCapacity("-search_static_first"),
+ },
+ }
+ prev_search_strategy = system_lib.flags.search_strategy;
+ prev_preferred_link_mode = system_lib.flags.preferred_link_mode;
+ }
+
+ const prefix: []const u8 = prefix: {
+ if (system_lib.flags.needed) break :prefix "-needed-l";
+ if (system_lib.flags.weak) break :prefix "-weak-l";
+ break :prefix "-l";
+ };
+ l: {
+ pc: {
+ const force = switch (system_lib.flags.use_pkg_config) {
+ .no => break :pc,
+ .yes => false,
+ .force => true,
+ };
+
+ const pkg_conf_node = progress_node.start("pkg-config", 0);
+ defer pkg_conf_node.end();
+
+ if (PkgConfig.run(maker, step, pkg_conf_node, system_lib_name, force)) |result| {
+ try zig_args.appendSlice(gpa, result.cflags);
+ try zig_args.appendSlice(gpa, result.libs);
+ try seen_system_libs.put(arena, system_lib.name, result.cflags);
+ break :l;
+ } else |err| switch (err) {
+ error.PkgConfigUnavailable,
+ error.PackageNotFound,
+ => {
+ // pkg-config failed, so fall back to linking the library by name directly.
+ assert(!force);
+ break :pc;
+ },
+ else => |e| return e,
+ }
+ }
+ try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
+ prefix, system_lib_name,
+ }));
+ }
+ },
+ .other_step => |other_step_index| {
+ const other = other_step_index.ptr(conf);
+ const other_compile = other.extended.get(conf.extra).compile;
+ switch (other_compile.flags3.kind) {
+ .exe => return step.fail(maker, "cannot link with an executable build artifact", .{}),
+ .@"test" => return step.fail(maker, "cannot link with a test", .{}),
+ .obj, .test_obj => {
+ const included_in_lib_or_obj = switch (dep_compile.flags3.kind) {
+ .lib, .obj, .test_obj => !my_responsibility,
+ else => false,
+ };
+ if (!already_linked and !included_in_lib_or_obj) {
+ try zig_args.append(gpa, try maker.resolveLazyPathAbs(
+ arena,
+ .{ .generated = .{ .index = other_compile.generated_bin.value.? } },
+ compile_index,
+ ));
+ total_linker_objects += 1;
+ }
+ },
+ .lib => l: {
+ const other_produces_implib = other_compile.producesImplib(conf);
+ const other_is_static = other_produces_implib or other_compile.isStaticLibrary();
+
+ if (conf_comp.isStaticLibrary() and other_is_static) {
+ // Avoid putting a static library inside a static library.
+ break :l;
+ }
+
+ // For DLLs, we must link against the implib.
+ // For everything else, we directly link
+ // against the library file.
+ const full_path_lib = try maker.resolveLazyPathAbs(
+ arena,
+ .{ .generated = .{
+ .index = if (other_produces_implib)
+ other_compile.generated_implib.value.?
+ else
+ other_compile.generated_bin.value.?,
+ } },
+ compile_index,
+ );
+
+ try zig_args.append(gpa, full_path_lib);
+ total_linker_objects += 1;
+
+ if (other_compile.flags2.linkage == .dynamic and
+ root_module_target.flags.os_tag != .windows)
+ {
+ if (Dir.path.dirname(full_path_lib)) |dirname| {
+ try zig_args.appendSlice(gpa, &.{ "-rpath", dirname });
+ }
+ }
+ },
+ }
+ },
+ .assembly_file => |asm_file| l: {
+ if (!my_responsibility) break :l;
+
+ if (prev_has_cflags) {
+ try zig_args.appendSlice(gpa, &.{ "-cflags", "--" });
+ prev_has_cflags = false;
+ }
+ try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, asm_file, compile_index));
+ total_linker_objects += 1;
+ },
+
+ .c_source_file => |c_source_file_index| l: {
+ if (!my_responsibility) break :l;
+
+ const c_source_file = c_source_file_index.get(conf);
+
+ if (prev_has_cflags or c_source_file.args.slice.len != 0) {
+ try zig_args.ensureUnusedCapacity(gpa, 2 + c_source_file.args.slice.len);
+ zig_args.appendAssumeCapacity("-cflags");
+ for (c_source_file.args.slice) |arg| {
+ zig_args.appendAssumeCapacity(arg.slice(conf));
+ }
+ zig_args.appendAssumeCapacity("--");
+ }
+ prev_has_cflags = (c_source_file.args.slice.len != 0);
+
+ if (c_source_file.flags.lang.get()) |lang|
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", lang.clangIdentifier() };
+
+ try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, c_source_file.file, compile_index));
+
+ if (c_source_file.flags.lang != .default)
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", "none" };
+
+ total_linker_objects += 1;
+ },
+
+ .c_source_files => |c_source_files_index| l: {
+ if (!my_responsibility) break :l;
+
+ const c_source_files = c_source_files_index.get(conf);
+
+ if (prev_has_cflags or c_source_files.args.slice.len != 0) {
+ try zig_args.ensureUnusedCapacity(gpa, 2 + c_source_files.args.slice.len);
+ zig_args.appendAssumeCapacity("-cflags");
+ for (c_source_files.args.slice) |arg| {
+ zig_args.appendAssumeCapacity(arg.slice(conf));
+ }
+ zig_args.appendAssumeCapacity("--");
+ }
+ prev_has_cflags = (c_source_files.args.slice.len != 0);
+
+ if (c_source_files.flags.lang.get()) |lang|
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", lang.clangIdentifier() };
+
+ const root_path = try maker.resolveLazyPathIndexAbs(arena, c_source_files.root, compile_index);
+ try zig_args.ensureUnusedCapacity(gpa, c_source_files.sub_paths.slice.len);
+ for (c_source_files.sub_paths.slice) |sub_path| {
+ zig_args.appendAssumeCapacity(try Dir.path.join(arena, &.{
+ root_path, sub_path.slice(conf),
+ }));
+ }
+
+ if (c_source_files.flags.lang != .default)
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", "none" };
+
+ total_linker_objects += c_source_files.sub_paths.slice.len;
+ },
+
+ .win32_resource_file => |rc_source_file_index| l: {
+ if (!my_responsibility) break :l;
+
+ const rc_source_file = rc_source_file_index.get(conf);
+
+ if (rc_source_file.args.slice.len == 0 and rc_source_file.include_paths.slice.len == 0) {
+ if (prev_has_rcflags) {
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-rcflags", "--" };
+ prev_has_rcflags = false;
+ }
+ } else {
+ try zig_args.ensureUnusedCapacity(gpa, 1 + rc_source_file.args.slice.len);
+ zig_args.appendAssumeCapacity("-rcflags");
+ for (rc_source_file.args.slice) |arg| {
+ zig_args.appendAssumeCapacity(arg.slice(conf));
+ }
+ try zig_args.ensureUnusedCapacity(gpa, 1 + 2 * rc_source_file.include_paths.slice.len);
+ for (rc_source_file.include_paths.slice) |include_path| {
+ zig_args.appendAssumeCapacity("/I");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, include_path, compile_index));
+ }
+ zig_args.appendAssumeCapacity("--");
+ prev_has_rcflags = true;
+ }
+ try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, rc_source_file.file, compile_index));
+ total_linker_objects += 1;
+ },
+ };
+
+ // We need to emit the --mod argument here so that the above link objects
+ // have the correct parent module, but only if the module is part of
+ // this compilation.
+ if (!my_responsibility) continue;
+ if (cli_named_modules.modules.getIndex(mod_index)) |module_cli_index| {
+ const module_cli_name = cli_named_modules.names.keys()[module_cli_index];
+ const module_index = cli_named_modules.modules.keys()[module_cli_index];
+ try appendModuleFlags(arena, module_index, zig_args, compile_index, maker);
+
+ const imports = mod.import_table.get(conf).imports.mal;
+
+ // --dep arguments
+ try zig_args.ensureUnusedCapacity(gpa, imports.len * 2);
+ for (imports.items(.name), imports.items(.module)) |name, import| {
+ const import_index = cli_named_modules.modules.getIndex(import).?;
+ const import_cli_name = cli_named_modules.names.keys()[import_index];
+ zig_args.appendAssumeCapacity("--dep");
+ const name_slice = name.slice(conf);
+ if (mem.eql(u8, import_cli_name, name_slice)) {
+ zig_args.appendAssumeCapacity(import_cli_name);
+ } else {
+ zig_args.appendAssumeCapacity(try allocPrint(arena, "{s}={s}", .{
+ name_slice, import_cli_name,
+ }));
+ }
+ }
+
+ // When the CLI sees a -M argument, it determines whether it
+ // implies the existence of a Zig compilation unit based on
+ // whether there is a root source file. If there is no root
+ // source file, then this is not a zig compilation unit - it is
+ // perhaps a set of linker objects, or C source files instead.
+ // Linker objects are added to the CLI globally, while C source
+ // files must have a module parent.
+ try zig_args.ensureUnusedCapacity(gpa, 1);
+ if (mod.root_source_file.unwrap()) |lp| {
+ const src = try maker.resolveLazyPathIndexAbs(arena, lp, compile_index);
+ zig_args.appendAssumeCapacity(try allocPrint(arena, "-M{s}={s}", .{ module_cli_name, src }));
+ } else if (moduleNeedsCliArg(&mod, conf)) {
+ zig_args.appendAssumeCapacity(try allocPrint(arena, "-M{s}", .{module_cli_name}));
+ }
+ }
+ }
+ }
+
+ if (total_linker_objects == 0) {
+ return step.fail(maker, "the linker needs one or more objects to link", .{});
+ }
+
+ for (frameworks.keys(), frameworks.values()) |name, info| {
+ try zig_args.ensureUnusedCapacity(gpa, 2);
+ if (info.needed) {
+ zig_args.appendAssumeCapacity("-needed_framework");
+ } else if (info.weak) {
+ zig_args.appendAssumeCapacity("-weak_framework");
+ } else {
+ zig_args.appendAssumeCapacity("-framework");
+ }
+ zig_args.appendAssumeCapacity(name.slice(conf));
+ }
+
+ try zig_args.ensureUnusedCapacity(gpa, 2);
+ if (is_linking_libcpp) zig_args.appendAssumeCapacity("-lc++");
+ if (is_linking_libc) zig_args.appendAssumeCapacity("-lc");
+
+ compile.is_linking_libc = is_linking_libc;
+ }
+
+ if (conf_comp.win32_manifest.value) |manifest_file| {
+ try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, manifest_file, compile_index));
+ }
+
+ if (conf_comp.win32_module_definition.value) |module_file| {
+ try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, module_file, compile_index));
+ }
+
+ if (conf_comp.image_base.value) |image_base| {
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "--image-base", try allocPrint(arena, "0x{x}", .{image_base}),
+ };
+ }
+
+ for (conf_comp.filters.slice) |filter| {
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{ "--test-filter", filter.slice(conf) };
+ }
+
+ switch (conf_comp.test_runner.u) {
+ .default => {},
+ .simple, .server => |lp| (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "--test-runner", try maker.resolveLazyPathIndexAbs(arena, lp, compile_index),
+ },
+ }
+
+ for (graph.debug_log_scopes.items) |log_scope| {
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{ "--debug-log", log_scope };
+ }
+
+ try addBool(gpa, zig_args, "--debug-compile-errors", graph.debug_compile_errors);
+ try addBool(gpa, zig_args, "--debug-incremental", graph.debug_incremental);
+ try addBool(gpa, zig_args, "--verbose-air", graph.verbose_air);
+ try addBool(gpa, zig_args, "--verbose-llvm-ir", graph.verbose_llvm_ir);
+ try addBool(gpa, zig_args, "--verbose-link", graph.verbose_link or conf_comp.flags.verbose_link);
+ try addBool(gpa, zig_args, "--verbose-cc", graph.verbose_cc or conf_comp.flags.verbose_cc);
+ try addBool(gpa, zig_args, "--verbose-llvm-cpu-features", graph.verbose_llvm_cpu_features);
+ try addBool(gpa, zig_args, "--time-report", graph.time_report);
+
+ if (conf_comp.generated_bin.value == null) try zig_args.append(gpa, "-fno-emit-bin");
+ if (conf_comp.generated_asm.value != null) try zig_args.append(gpa, "-femit-asm");
+ if (conf_comp.generated_docs.value != null) try zig_args.append(gpa, "-femit-docs");
+ if (conf_comp.generated_implib.value != null) try zig_args.append(gpa, "-femit-implib");
+ if (conf_comp.generated_llvm_bc.value != null) try zig_args.append(gpa, "-femit-llvm-bc");
+ if (conf_comp.generated_llvm_ir.value != null) try zig_args.append(gpa, "-femit-llvm-ir");
+ if (conf_comp.generated_h.value != null) try zig_args.append(gpa, "-femit-h");
+
+ try addFlag(gpa, zig_args, "formatted-panics", conf_comp.flags2.formatted_panics.toBool());
+
+ switch (conf_comp.flags3.compress_debug_sections) {
+ .none => {},
+ .zlib => try zig_args.append(gpa, "--compress-debug-sections=zlib"),
+ .zstd => try zig_args.append(gpa, "--compress-debug-sections=zstd"),
+ }
+
+ try addBool(gpa, zig_args, "--eh-frame-hdr", conf_comp.flags.link_eh_frame_hdr);
+ try addBool(gpa, zig_args, "--emit-relocs", conf_comp.flags.link_emit_relocs);
+ try addBool(gpa, zig_args, "-ffunction-sections", conf_comp.flags.link_function_sections);
+ try addBool(gpa, zig_args, "-fdata-sections", conf_comp.flags.link_data_sections);
+
+ if (conf_comp.flags2.link_gc_sections.toBool()) |x|
+ try zig_args.append(gpa, if (x) "--gc-sections" else "--no-gc-sections");
+
+ if (!conf_comp.flags.linker_dynamicbase)
+ try zig_args.append(gpa, "--no-dynamicbase");
+
+ try addFlag(gpa, zig_args, "allow-shlib-undefined", conf_comp.flags2.linker_allow_shlib_undefined.toBool());
+ if (conf_comp.flags.link_z_notext) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "notext" };
+ if (!conf_comp.flags.link_z_relro) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "norelro" };
+ if (conf_comp.flags.link_z_lazy) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "lazy" };
+ if (conf_comp.link_z_common_page_size.value) |size| (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "-z", try allocPrint(arena, "common-page-size={d}", .{size}),
+ };
+ if (conf_comp.link_z_max_page_size.value) |size| (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "-z", try allocPrint(arena, "max-page-size={d}", .{size}),
+ };
+ if (conf_comp.flags.link_z_defs) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "defs" };
+
+ try zig_args.ensureUnusedCapacity(gpa, 2);
+ if (conf_comp.libc_file.value) |libc_file| {
+ zig_args.appendAssumeCapacity("--libc");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, libc_file, compile_index));
+ } else if (graph.libc_file) |libc_file| {
+ zig_args.appendAssumeCapacity("--libc");
+ zig_args.appendAssumeCapacity(libc_file);
+ }
+
+ (try zig_args.addManyAsArray(gpa, 4)).* = .{
+ "--cache-dir", graph.local_cache_root.path orelse ".",
+ "--global-cache-dir", graph.global_cache_root.path orelse ".",
+ };
+
+ try zig_args.ensureUnusedCapacity(gpa, 1);
+ if (graph.debug_compiler_runtime_libs) |mode| switch (mode) {
+ .Debug => zig_args.appendAssumeCapacity("--debug-rt"),
+ else => zig_args.appendAssumeCapacity(try allocPrint(arena, "--debug-rt={t}", .{mode})),
+ };
+
+ {
+ try zig_args.ensureUnusedCapacity(gpa, 7);
+
+ zig_args.addManyAsArrayAssumeCapacity(2).* = .{ "--name", conf_comp.root_name.slice(conf) };
+
+ switch (conf_comp.flags2.linkage) {
+ .dynamic => zig_args.appendAssumeCapacity("-dynamic"),
+ .static => zig_args.appendAssumeCapacity("-static"),
+ .default => {},
+ }
+
+ if (conf_comp.flags3.kind == .lib and conf_comp.flags2.linkage == .dynamic) {
+ if (conf_comp.version.value) |version| zig_args.addManyAsArrayAssumeCapacity(2).* = .{
+ "--version", version.slice(conf),
+ };
+
+ const os_tag = root_module_target.flags.os_tag.unwrap().?;
+ if (os_tag.isDarwin()) {
+ const abi = root_module_target.flags.abi.unwrap().?;
+ zig_args.addManyAsArrayAssumeCapacity(2).* = .{
+ "-install_name",
+ if (conf_comp.install_name.value) |s| s.slice(conf) else try allocPrint(
+ arena,
+ "@rpath/{s}{s}{s}",
+ .{
+ os_tag.libPrefix(abi),
+ conf_comp.root_name.slice(conf),
+ os_tag.dynamicLibSuffix(),
+ },
+ ),
+ };
+ }
+ }
+ }
+
+ if (conf_comp.entitlements.value) |entitlements| {
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "--entitlements", try maker.resolveLazyPathIndexAbs(arena, entitlements, compile_index),
+ };
+ }
+ if (conf_comp.pagezero_size.value) |pagezero_size| {
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "-pagezero_size", try allocPrint(arena, "{x}", .{pagezero_size}),
+ };
+ }
+ if (conf_comp.headerpad_size.value) |headerpad_size| {
+ (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "-headerpad", try allocPrint(arena, "{x}", .{headerpad_size}),
+ };
+ }
+ try addBool(gpa, zig_args, "-headerpad_max_install_names", conf_comp.flags.headerpad_max_install_names);
+ try addBool(gpa, zig_args, "-dead_strip_dylibs", conf_comp.flags.dead_strip_dylibs);
+ try addBool(gpa, zig_args, "-ObjC", conf_comp.flags.force_load_objc);
+ try addBool(gpa, zig_args, "--discard-all", conf_comp.flags.discard_local_symbols);
+
+ try addFlag(gpa, zig_args, "compiler-rt", conf_comp.flags2.bundle_compiler_rt.toBool());
+ try addFlag(gpa, zig_args, "ubsan-rt", conf_comp.flags2.bundle_ubsan_rt.toBool());
+ try addFlag(gpa, zig_args, "dll-export-fns", conf_comp.flags2.dll_export_fns.toBool());
+
+ try addBool(gpa, zig_args, "-rdynamic", conf_comp.flags.rdynamic);
+ try addBool(gpa, zig_args, "--import-memory", conf_comp.flags.import_memory);
+ try addBool(gpa, zig_args, "--export-memory", conf_comp.flags.export_memory);
+ try addBool(gpa, zig_args, "--import-symbols", conf_comp.flags.import_symbols);
+ try addBool(gpa, zig_args, "--import-table", conf_comp.flags.import_table);
+ try addBool(gpa, zig_args, "--export-table", conf_comp.flags.export_table);
+ try addBool(gpa, zig_args, "--shared-memory", conf_comp.flags.shared_memory);
+
+ {
+ try zig_args.ensureUnusedCapacity(gpa, 4);
+ if (conf_comp.initial_memory.value) |initial_memory| {
+ zig_args.appendAssumeCapacity(try allocPrint(arena, "--initial-memory={d}", .{initial_memory}));
+ }
+ if (conf_comp.max_memory.value) |max_memory| {
+ zig_args.appendAssumeCapacity(try allocPrint(arena, "--max-memory={d}", .{max_memory}));
+ }
+ if (conf_comp.global_base.value) |global_base| {
+ zig_args.appendAssumeCapacity(try allocPrint(arena, "--global-base={d}", .{global_base}));
+ }
+ switch (conf_comp.flags3.wasi_exec_model) {
+ .default => {},
+ .command => zig_args.appendAssumeCapacity("-mexec-model=command"),
+ .reactor => zig_args.appendAssumeCapacity("-mexec-model=reactor"),
+ }
+ }
+
+ if (conf_comp.linker_script.value) |linker_script| (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "--script", try maker.resolveLazyPathIndexAbs(arena, linker_script, compile_index),
+ };
+ if (conf_comp.version_script.value) |version_script| (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "--version-script", try maker.resolveLazyPathIndexAbs(arena, version_script, compile_index),
+ };
+ if (conf_comp.flags2.linker_allow_undefined_version.toBool()) |x| {
+ try zig_args.append(gpa, if (x) "--undefined-version" else "--no-undefined-version");
+ }
+
+ if (conf_comp.flags2.linker_enable_new_dtags.toBool()) |enabled| {
+ try zig_args.append(gpa, if (enabled) "--enable-new-dtags" else "--disable-new-dtags");
+ }
+
+ if (conf_comp.flags3.kind == .@"test" and conf_comp.exec_cmd_args.slice.len != 0) {
+ for (conf_comp.exec_cmd_args.slice) |cmd_arg| {
+ try zig_args.ensureUnusedCapacity(gpa, 2);
+ if (cmd_arg.slice(conf)) |arg| {
+ zig_args.appendAssumeCapacity("--test-cmd");
+ zig_args.appendAssumeCapacity(arg);
+ } else {
+ zig_args.appendAssumeCapacity("--test-cmd-bin");
+ }
+ }
+ }
+
+ if (graph.sysroot) |sysroot| try zig_args.appendSlice(gpa, &.{ "--sysroot", sysroot });
+
+ // -I and -L arguments that appear after the last --mod argument apply to all modules.
+ const cwd: Io.Dir = .cwd();
+ const io = graph.io;
+
+ for (graph.search_prefixes.items) |search_prefix| {
+ var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| {
+ return step.fail(maker, "unable to open prefix directory '{s}': {t}", .{ search_prefix, err });
+ };
+ defer prefix_dir.close(io);
+
+ // Avoid passing -L and -I flags for nonexistent directories.
+ // This prevents a warning, that should probably be upgraded to an error in Zig's
+ // CLI parsing code, when the linker sees an -L directory that does not exist.
+
+ if (prefix_dir.access(io, "lib", .{})) |_| {
+ try zig_args.appendSlice(gpa, &.{
+ "-L", try Dir.path.join(arena, &.{ search_prefix, "lib" }),
+ });
+ } else |err| switch (err) {
+ error.FileNotFound => {},
+ else => |e| return step.fail(maker, "unable to access '{s}/lib' directory: {t}", .{ search_prefix, e }),
+ }
+
+ if (prefix_dir.access(io, "include", .{})) |_| {
+ try zig_args.appendSlice(gpa, &.{
+ "-I", try Dir.path.join(arena, &.{ search_prefix, "include" }),
+ });
+ } else |err| switch (err) {
+ error.FileNotFound => {},
+ else => |e| return step.fail(maker, "unable to access '{s}/include' directory: {t}", .{ search_prefix, e }),
+ }
+ }
+
+ if (conf_comp.flags3.rc_includes != .any) (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "-rcincludes", @tagName(conf_comp.flags3.rc_includes),
+ };
+
+ try addFlag(gpa, zig_args, "each-lib-rpath", conf_comp.flags2.each_lib_rpath.toBool());
+
+ if (conf_comp.flags3.build_id.unwrap(conf_comp.build_id.value, conf) orelse graph.build_id) |build_id| {
+ try zig_args.append(gpa, switch (build_id) {
+ .hexstring => |hs| try allocPrint(arena, "--build-id=0x{x}", .{hs.toSlice()}),
+ .none, .fast, .uuid, .sha1, .md5 => try allocPrint(arena, "--build-id={t}", .{build_id}),
+ });
+ }
+
+ const opt_zig_lib_dir: ?[]const u8 = if (conf_comp.zig_lib_dir.value) |dir|
+ try maker.resolveLazyPathIndexAbs(arena, dir, compile_index)
+ else if (graph.zig_lib_directory.path) |_|
+ try allocPrint(arena, "{f}", .{graph.zig_lib_directory})
+ else
+ null;
+
+ if (opt_zig_lib_dir) |zig_lib_dir| (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "--zig-lib-dir", zig_lib_dir,
+ };
+
+ try addFlag(gpa, zig_args, "PIE", conf_comp.flags2.pie.toBool());
+
+ try zig_args.ensureUnusedCapacity(gpa, 1);
+ switch (conf_comp.flags3.lto) {
+ .full => zig_args.appendAssumeCapacity("-flto=full"),
+ .thin => zig_args.appendAssumeCapacity("-flto=thin"),
+ .none => zig_args.appendAssumeCapacity("-fno-lto"),
+ .default => {},
+ }
+
+ try addFlag(gpa, zig_args, "sanitize-coverage-trace-pc-guard", conf_comp.flags2.sanitize_coverage_trace_pc_guard.toBool());
+
+ switch (conf_comp.flags3.subsystem) {
+ .default => {},
+ else => |t| (try zig_args.addManyAsArray(gpa, 2)).* = .{ "--subsystem", @tagName(t) },
+ }
+
+ try addBool(gpa, zig_args, "-municode", conf_comp.flags.mingw_unicode_entry_point);
+
+ if (conf_comp.error_limit.value orelse graph.error_limit) |err_limit| (try zig_args.addManyAsArray(gpa, 2)).* = .{
+ "--error-limit", try allocPrint(arena, "{d}", .{err_limit}),
+ };
+
+ try addFlag(gpa, zig_args, "incremental", graph.incremental);
+
+ try zig_args.append(gpa, "--listen=-");
+
+ // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux
+ // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and
+ // pass that to zig, e.g. via 'zig build-lib @args.rsp'
+ // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html
+ var args_length: usize = 0;
+ for (zig_args.items) |arg| {
+ args_length += arg.len + 1; // +1 to account for null terminator
+ }
+ if (args_length >= 30 * 1024) {
+ const local_cache_root = graph.local_cache_root;
+ const args_path: Path = .{ .root_dir = local_cache_root, .sub_path = "args" };
+ args_path.root_dir.handle.createDirPath(io, args_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating directory {f}: {t}", .{ args_path, err });
+
+ const args_to_escape = zig_args.items[2..];
+ var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len);
+ arg_blk: for (args_to_escape) |arg| {
+ for (arg, 0..) |c, arg_idx| {
+ if (c == '\\' or c == '"') {
+ // Slow path for arguments that need to be escaped. We'll need to allocate and copy
+ var escaped: std.ArrayList(u8) = .empty;
+ try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1);
+ try escaped.appendSlice(arena, arg[0..arg_idx]);
+ for (arg[arg_idx..]) |to_escape| {
+ if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\');
+ try escaped.append(arena, to_escape);
+ }
+ escaped_args.appendAssumeCapacity(escaped.items);
+ continue :arg_blk;
+ }
+ }
+ escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument
+ }
+
+ // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with
+ // other zig build commands running in parallel.
+ const partially_quoted = try mem.join(arena, "\" \"", escaped_args.items);
+ const args = try mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" });
+
+ var args_hash: [Sha256.digest_length]u8 = undefined;
+ Sha256.hash(args, &args_hash, .{});
+ var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined;
+ _ = std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash}) catch unreachable;
+
+ const args_file = "args" ++ Dir.path.sep_str ++ args_hex_hash;
+ local_cache_root.handle.access(io, args_file, .{}) catch {
+ var af = local_cache_root.handle.createFileAtomic(io, args_file, .{
+ .replace = false,
+ .make_path = true,
+ }) catch |e| return step.fail(maker, "failed creating tmp args file {f}{s}: {t}", .{
+ local_cache_root, args_file, e,
+ });
+ defer af.deinit(io);
+
+ af.file.writeStreamingAll(io, args) catch |e| {
+ return step.fail(maker, "failed writing args data to tmp file {f}{s}: {t}", .{
+ local_cache_root, args_file, e,
+ });
+ };
+ // Note we can't clean up this file, not even after build
+ // success, because that might interfere with another build
+ // process that needs the same file.
+ af.link(io) catch |e| switch (e) {
+ error.PathAlreadyExists => {
+ // The args file was created by another concurrent build process.
+ },
+ else => |other_err| return step.fail(maker, "failed linking tmp file {f}{s}: {t}", .{
+ local_cache_root, args_file, other_err,
+ }),
+ };
+ };
+
+ const resolved_args_file = try mem.concat(arena, u8, &.{
+ "@", try local_cache_root.join(arena, &.{args_file}),
+ });
+
+ zig_args.shrinkRetainingCapacity(2);
+ try zig_args.append(gpa, resolved_args_file);
+ }
+}
+
+pub fn rebuildInFuzzMode(
+ compile: *Compile,
+ maker: *Maker,
+ compile_index: Configuration.Step.Index,
+ progress_node: std.Progress.Node,
+) !Path {
+ const gpa = maker.gpa;
+ const step = maker.stepByIndex(compile_index);
+
+ var arena_allocator: std.heap.ArenaAllocator = .init(gpa);
+ defer arena_allocator.deinit();
+ const arena = arena_allocator.allocator();
+
+ step.result_error_msgs.clearRetainingCapacity();
+ step.result_stderr = "";
+
+ step.result_error_bundle.deinit(gpa);
+ step.result_error_bundle = std.zig.ErrorBundle.empty;
+
+ step.clearFailedCommand(gpa);
+
+ var argv: std.ArrayList([]const u8) = .empty;
+ defer argv.deinit(gpa);
+
+ try lowerZigArgs(arena, compile, compile_index, maker, progress_node, &argv, true);
+ const maybe_output_bin_path = try Step.evalZigProcess(compile_index, maker, argv.items, progress_node, false);
+ return maybe_output_bin_path.?;
+}
+
+fn addBool(gpa: Allocator, args: *std.ArrayList([]const u8), arg: []const u8, opt: bool) !void {
+ if (opt) try args.append(gpa, arg);
+}
+
+fn addFlag(gpa: Allocator, args: *std.ArrayList([]const u8), comptime name: []const u8, opt: ?bool) !void {
+ const cond = opt orelse return;
+ try args.append(gpa, if (cond) "-f" ++ name else "-fno-" ++ name);
+}
+
+fn checkCompileErrors(arena: Allocator, maker: *Maker, step_index: Configuration.Step.Index) Step.ExtendedMakeError!void {
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_comp = conf_step.extended.get(conf.extra).compile;
+
+ // Clear this field so that it does not get printed by the build runner.
+ var actual_eb = step.result_error_bundle;
+ step.result_error_bundle = .empty;
+ defer actual_eb.deinit(maker.gpa);
+
+ const actual_errors = ae: {
+ var aw: std.Io.Writer.Allocating = .init(arena);
+ defer aw.deinit();
+ actual_eb.renderToWriter(.{
+ .include_reference_trace = false,
+ .include_source_line = false,
+ }, &aw.writer) catch |err| switch (err) {
+ error.WriteFailed => return error.OutOfMemory,
+ };
+ break :ae try aw.toOwnedSlice();
+ };
+
+ // Render the expected lines into a string that we can compare verbatim.
+ var expected_generated: std.ArrayList(u8) = .empty;
+ var actual_line_it = mem.splitScalar(u8, actual_errors, '\n');
+
+ switch (conf_comp.expect_errors.u) {
+ .none => unreachable,
+ .starts_with => |expect_starts_with_string| {
+ const expect_starts_with = expect_starts_with_string.slice(conf);
+ if (mem.startsWith(u8, actual_errors, expect_starts_with)) return;
+ return step.fail(maker,
+ \\
+ \\========= should start with: ============
+ \\{s}
+ \\========= but not found: ================
+ \\{s}
+ \\=========================================
+ , .{ expect_starts_with, actual_errors });
+ },
+ .contains => |expect_line_string| {
+ const expect_line = expect_line_string.slice(conf);
+ while (actual_line_it.next()) |actual_line| {
+ if (!matchCompileError(actual_line, expect_line)) continue;
+ return;
+ }
+
+ return step.fail(maker,
+ \\
+ \\========= should contain: ===============
+ \\{s}
+ \\========= but not found: ================
+ \\{s}
+ \\=========================================
+ , .{ expect_line, actual_errors });
+ },
+ .stderr_contains => |expect_line_string| {
+ const expect_line = expect_line_string.slice(conf);
+ const actual_stderr: []const u8 = if (step.result_error_msgs.items.len > 0)
+ step.result_error_msgs.items[0]
+ else
+ &.{};
+ step.result_error_msgs.clearRetainingCapacity();
+
+ var stderr_line_it = mem.splitScalar(u8, actual_stderr, '\n');
+
+ while (stderr_line_it.next()) |actual_line| {
+ if (!matchCompileError(actual_line, expect_line)) continue;
+ return;
+ }
+
+ return step.fail(maker,
+ \\
+ \\========= should contain: ===============
+ \\{s}
+ \\========= but not found: ================
+ \\{s}
+ \\=========================================
+ , .{ expect_line, actual_stderr });
+ },
+ .exact => |expect_lines| {
+ for (expect_lines.slice) |expect_line_string| {
+ const expect_line = expect_line_string.slice(conf);
+ const actual_line = actual_line_it.next() orelse {
+ try expected_generated.appendSlice(arena, expect_line);
+ try expected_generated.append(arena, '\n');
+ continue;
+ };
+ if (matchCompileError(actual_line, expect_line)) {
+ try expected_generated.appendSlice(arena, actual_line);
+ try expected_generated.append(arena, '\n');
+ continue;
+ }
+ try expected_generated.appendSlice(arena, expect_line);
+ try expected_generated.append(arena, '\n');
+ }
+
+ if (mem.eql(u8, expected_generated.items, actual_errors)) return;
+
+ return step.fail(maker,
+ \\
+ \\========= expected: =====================
+ \\{s}
+ \\========= but found: ====================
+ \\{s}
+ \\=========================================
+ , .{ expected_generated.items, actual_errors });
+ },
+ }
+}
+
+fn matchCompileError(actual: []const u8, expected: []const u8) bool {
+ if (mem.endsWith(u8, actual, expected)) return true;
+ if (mem.startsWith(u8, expected, ":?:?: ")) {
+ if (mem.endsWith(u8, actual, expected[":?:?: ".len..])) return true;
+ }
+ // We scan for /?/ in expected line and if there is a match, we match everything
+ // up to and after /?/.
+ const expected_trim = mem.trim(u8, expected, " ");
+ if (mem.find(u8, expected_trim, "/?/")) |index| {
+ const actual_trim = mem.trim(u8, actual, " ");
+ const lhs = expected_trim[0..index];
+ const rhs = expected_trim[index + "/?/".len ..];
+ if (mem.startsWith(u8, actual_trim, lhs) and mem.endsWith(u8, actual_trim, rhs)) return true;
+ }
+ return false;
+}
+
+fn moduleNeedsCliArg(mod: *const Configuration.Module, conf: *const Configuration) bool {
+ return for (0..mod.link_objects.len) |i| switch (mod.link_objects.tag(conf.extra, i)) {
+ .c_source_file, .c_source_files, .assembly_file, .win32_resource_file => break true,
+ else => continue,
+ } else false;
+}
+
+const CliNamedModules = struct {
+ modules: std.AutoArrayHashMapUnmanaged(Configuration.Module.Index, void),
+ names: std.StringArrayHashMapUnmanaged(void),
+
+ /// Traverse the whole dependency graph and give every module a unique
+ /// name, ideally one named after what it's called somewhere in the graph.
+ /// It will help here to have both a mapping from module to name and a set
+ /// of all the currently-used names.
+ fn init(
+ arena: Allocator,
+ module_graph: *ModuleGraph,
+ compile_index: Configuration.Step.Index,
+ maker: *const Maker,
+ ) Allocator.Error!CliNamedModules {
+ const conf = &maker.scanned_config.configuration;
+ const conf_compile = compile_index.ptr(conf).extended.get(conf.extra).compile;
+
+ var result: CliNamedModules = .{
+ .modules = .{},
+ .names = .{},
+ };
+ const modules = try getModuleList(arena, module_graph, conf_compile.root_module, conf);
+ {
+ assert(conf_compile.root_module == modules.keys()[0]);
+ try result.modules.put(arena, conf_compile.root_module, {});
+ try result.names.put(arena, "root", {});
+ }
+ for (modules.keys()[1..], modules.values()[1..]) |mod, orig_name| {
+ const orig_name_slice = orig_name.slice(conf);
+ var name: []const u8 = orig_name_slice;
+ var n: usize = 0;
+ while (true) {
+ const gop = try result.names.getOrPut(arena, name);
+ if (!gop.found_existing) {
+ try result.modules.putNoClobber(arena, mod, {});
+ break;
+ }
+ name = try allocPrint(arena, "{s}{d}", .{ orig_name_slice, n });
+ n += 1;
+ }
+ }
+ return result;
+ }
+};
+
+pub fn getCompileDependencies(
+ arena: Allocator,
+ module_graph: *ModuleGraph,
+ conf: *const Configuration,
+ start: Configuration.Step.Index,
+ chase_dynamic: bool,
+) ![]const Configuration.Step.Index {
+ var compiles: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void) = .empty;
+ var compiles_i: usize = 0;
+
+ try compiles.putNoClobber(arena, start, {});
+
+ while (compiles_i < compiles.count()) : (compiles_i += 1) {
+ const step = compiles.keys()[compiles_i].ptr(conf);
+ const compile = step.extended.get(conf.extra).compile;
+ const modules = try getModuleList(arena, module_graph, compile.root_module, conf);
+
+ for (modules.keys()) |mod_index| {
+ const mod = mod_index.get(conf);
+ for (0..mod.link_objects.len) |i| {
+ switch (mod.link_objects.get(conf.extra, i)) {
+ .other_step => |other_compile_index| {
+ const other_compile = other_compile_index.ptr(conf).extended.get(conf.extra).compile;
+ if (!chase_dynamic and other_compile.isDynamicLibrary()) continue;
+ try compiles.put(arena, other_compile_index, {});
+ },
+ else => {},
+ }
+ }
+ }
+ }
+
+ return compiles.keys();
+}
+
+/// Returned pointer expires upon next call to `getModuleList`.
+fn getModuleList(
+ arena: Allocator,
+ module_graph: *ModuleGraph,
+ root_module: Configuration.Module.Index,
+ conf: *const Configuration,
+) !*ModuleList {
+ const gop = try module_graph.getOrPutAdapted(arena, root_module, @as(ModuleListContext.Adapter, .{}));
+ const modules = gop.key_ptr;
+
+ if (gop.found_existing) return modules;
+ modules.* = .empty;
+ try modules.putNoClobber(arena, root_module, .root);
+
+ var i: usize = 0;
+
+ while (i < modules.entries.len) : (i += 1) {
+ const dep_index = modules.keys()[i];
+ const dep = dep_index.get(conf);
+ const imports = dep.import_table.get(conf).imports;
+ try modules.ensureUnusedCapacity(arena, imports.mal.len);
+ for (imports.mal.items(.name), imports.mal.items(.module)) |import_name, other_mod|
+ modules.putAssumeCapacity(other_mod, import_name);
+ }
+
+ return modules;
+}
+
+fn appendModuleFlags(
+ arena: Allocator,
+ module_index: Configuration.Module.Index,
+ zig_args: *std.ArrayList([]const u8),
+ asking_step: Configuration.Step.Index,
+ maker: *const Maker,
+) !void {
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+ const m = module_index.get(conf);
+
+ try addFlag(gpa, zig_args, "strip", m.flags.strip.toBool());
+ try addFlag(gpa, zig_args, "single-threaded", m.flags.single_threaded.toBool());
+ try addFlag(gpa, zig_args, "stack-check", m.flags.stack_check.toBool());
+ try addFlag(gpa, zig_args, "stack-protector", m.flags.stack_protector.toBool());
+ try addFlag(gpa, zig_args, "omit-frame-pointer", m.flags2.omit_frame_pointer.toBool());
+ try addFlag(gpa, zig_args, "error-tracing", m.flags2.error_tracing.toBool());
+ try addFlag(gpa, zig_args, "sanitize-thread", m.flags.sanitize_thread.toBool());
+ try addFlag(gpa, zig_args, "fuzz", m.flags.fuzz.toBool());
+ try addFlag(gpa, zig_args, "valgrind", m.flags2.valgrind.toBool());
+ try addFlag(gpa, zig_args, "PIC", m.flags2.pic.toBool());
+ try addFlag(gpa, zig_args, "red-zone", m.flags2.red_zone.toBool());
+ try addFlag(gpa, zig_args, "no-builtin", m.flags2.no_builtin.toBool());
+
+ {
+ try zig_args.ensureUnusedCapacity(gpa, 6);
+
+ switch (m.flags.sanitize_c) {
+ .off => zig_args.appendAssumeCapacity("-fno-sanitize-c"),
+ .trap => zig_args.appendAssumeCapacity("-fsanitize-c=trap"),
+ .full => zig_args.appendAssumeCapacity("-fsanitize-c=full"),
+ .default => {},
+ }
+
+ switch (m.flags.dwarf_format) {
+ .@"32" => zig_args.appendAssumeCapacity("-gdwarf32"),
+ .@"64" => zig_args.appendAssumeCapacity("-gdwarf64"),
+ .default => {},
+ }
+
+ switch (m.flags.unwind_tables) {
+ .none => zig_args.appendAssumeCapacity("-fno-unwind-tables"),
+ .sync => zig_args.appendAssumeCapacity("-funwind-tables"),
+ .async => zig_args.appendAssumeCapacity("-fasync-unwind-tables"),
+ .default => {},
+ }
+
+ switch (m.flags.optimize) {
+ .debug => zig_args.appendAssumeCapacity("-ODebug"),
+ .safe => zig_args.appendAssumeCapacity("-OReleaseSafe"),
+ .fast => zig_args.appendAssumeCapacity("-OReleaseFast"),
+ .small => zig_args.appendAssumeCapacity("-OReleaseSmall"),
+ .default => {},
+ }
+
+ if (m.flags.code_model != .default) {
+ zig_args.appendAssumeCapacity("-mcmodel");
+ zig_args.appendAssumeCapacity(@tagName(m.flags.code_model));
+ }
+ }
+
+ if (m.resolved_target.get(conf)) |resolved_target| {
+ // Communicate the query via CLI since it's more compact.
+ if (resolved_target.query.get(conf)) |compact_query| {
+ try zig_args.ensureUnusedCapacity(gpa, 6);
+
+ const query = compact_query.unwrap(conf);
+
+ zig_args.appendAssumeCapacity("-target");
+ zig_args.appendAssumeCapacity(try query.zigTriple(arena));
+
+ zig_args.appendAssumeCapacity("-mcpu");
+ zig_args.appendAssumeCapacity(try query.serializeCpuAlloc(arena));
+
+ if (query.dynamic_linker) |*dynamic_linker| {
+ if (dynamic_linker.get()) |dynamic_linker_path| {
+ zig_args.appendAssumeCapacity("--dynamic-linker");
+ zig_args.appendAssumeCapacity(dynamic_linker_path);
+ } else {
+ zig_args.appendAssumeCapacity("--no-dynamic-linker");
+ }
+ }
+ }
+ }
+
+ for (m.export_symbol_names.slice) |symbol_name| {
+ try zig_args.append(gpa, try allocPrint(arena, "--export={s}", .{symbol_name.slice(conf)}));
+ }
+
+ try zig_args.ensureUnusedCapacity(gpa, 2 * m.include_dirs.len);
+ for (0..m.include_dirs.len) |i|
+ try appendIncludeDirFlags(arena, m.include_dirs.get(conf.extra, i), zig_args, asking_step, maker);
+
+ try zig_args.ensureUnusedCapacity(gpa, m.c_macros.slice.len);
+ for (m.c_macros.slice) |c_macro|
+ zig_args.appendAssumeCapacity(c_macro.slice(conf));
+
+ try zig_args.ensureUnusedCapacity(gpa, 2 * m.lib_paths.slice.len);
+ for (m.lib_paths.slice) |lib_path| {
+ zig_args.appendAssumeCapacity("-L");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lib_path, asking_step));
+ }
+
+ try zig_args.ensureUnusedCapacity(gpa, 2 * m.rpaths.len);
+ for (0..m.rpaths.len) |i| switch (m.rpaths.get(conf.extra, i)) {
+ .lazy_path => |lp| {
+ zig_args.appendAssumeCapacity("-rpath");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step));
+ },
+ .special => |string| {
+ zig_args.appendAssumeCapacity("-rpath");
+ zig_args.appendAssumeCapacity(string.slice(conf));
+ },
+ };
+}
+
+/// Assumes unused capacity for at least 2 items.
+pub fn appendIncludeDirFlags(
+ arena: Allocator,
+ include_dir: Configuration.Module.IncludeDir,
+ zig_args: *std.ArrayList([]const u8),
+ asking_step: Configuration.Step.Index,
+ maker: *const Maker,
+) !void {
+ const conf = &maker.scanned_config.configuration;
+
+ switch (include_dir) {
+ .path => |lp| {
+ zig_args.appendAssumeCapacity("-I");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step));
+ },
+ .path_system => |lp| {
+ zig_args.appendAssumeCapacity("-isystem");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step));
+ },
+ .path_after => |lp| {
+ zig_args.appendAssumeCapacity("-idirafter");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step));
+ },
+ .framework_path => |lp| {
+ zig_args.appendAssumeCapacity("-F");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step));
+ },
+ .framework_path_system => |lp| {
+ zig_args.appendAssumeCapacity("-iframework");
+ zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step));
+ },
+ .config_header_step => |ch_index| {
+ const conf_ch = ch_index.ptr(conf).extended.get(conf.extra).config_header;
+ const path = maker.generatedPath(conf_ch.generated_dir).*;
+ zig_args.appendAssumeCapacity("-I");
+ zig_args.appendAssumeCapacity(try path.toString(arena));
+ },
+ .embed_path => |lazy_path| {
+ zig_args.appendAssumeCapacity(try allocPrint(arena, "--embed-dir={f}", .{
+ try maker.resolveLazyPathIndex(arena, lazy_path, asking_step),
+ }));
+ },
+ }
+}
diff --git a/lib/compiler/Maker/Step/ConfigHeader.zig b/lib/compiler/Maker/Step/ConfigHeader.zig
@@ -0,0 +1,610 @@
+const ConfigHeader = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Configuration = std.Build.Configuration;
+const Writer = std.Io.Writer;
+const Path = std.Build.Cache.Path;
+const Allocator = std.mem.Allocator;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+const header_text = "This file was generated by ConfigHeader using the Zig Build System.";
+const c_generated_line = "/* " ++ header_text ++ " */\n";
+const asm_generated_line = "; " ++ header_text ++ "\n";
+
+/// Table value is whether the value is used.
+const ValueMap = std.array_hash_map.String(bool);
+const Value = Configuration.Step.ConfigHeader.Value;
+
+pub fn make(
+ config_header: *ConfigHeader,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = config_header;
+ _ = progress_node;
+ const graph = maker.graph;
+ const step = maker.stepByIndex(step_index);
+ const io = graph.io;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_ch = conf_step.extended.get(conf.extra).config_header;
+ const cache_root = graph.local_cache_root;
+
+ const input_size_limit: Io.Limit = if (conf_ch.input_size_limit.value) |x| .limited64(x) else .unlimited;
+ const include_guard_override: ?[]const u8 = if (conf_ch.include_guard.value) |s| s.slice(conf) else null;
+ const include_path: []const u8 = conf_ch.include_path.slice(conf);
+ const template_file = if (conf_ch.template_file.value) |lp|
+ try maker.resolveLazyPathIndex(arena, lp, step_index)
+ else
+ null;
+ const value_pairs = conf_ch.values.slice;
+
+ if (conf_ch.template_file.value) |lp| try step.singleUnchangingWatchInput(maker, arena, lp.get(conf));
+
+ var value_map: ValueMap = .empty;
+ try value_map.ensureTotalCapacity(arena, value_pairs.len);
+ for (value_pairs) |pair| value_map.putAssumeCapacityNoClobber(pair.key.slice(conf), false);
+
+ var man = graph.cache.obtain();
+ defer man.deinit();
+
+ // Random bytes to make ConfigHeader unique. Refresh this with new
+ // random bytes when ConfigHeader implementation is modified in a
+ // non-backwards-compatible way.
+ man.hash.add(@as(u32, 0xdef08d23));
+ man.hash.add(@as(u32, @bitCast(conf_ch.flags)));
+ man.hash.addBytes(include_path);
+ man.hash.addOptionalBytes(include_guard_override);
+
+ var aw: Writer.Allocating = .init(arena);
+ defer aw.deinit();
+
+ switch (conf_ch.flags.style) {
+ .autoconf_undef => {
+ const tf = template_file.?;
+ const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
+ return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err });
+ renderAutoConfUndef(maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) {
+ error.WriteFailed => return error.OutOfMemory,
+ else => |e| return e,
+ };
+ },
+ .autoconf_at => {
+ const tf = template_file.?;
+ const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
+ return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err });
+ renderAutoconfAt(maker, step, contents, &aw, value_pairs, &value_map, tf) catch |err| switch (err) {
+ error.WriteFailed => return error.OutOfMemory,
+ else => |e| return e,
+ };
+ },
+ .cmake => {
+ const tf = template_file.?;
+ const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
+ return step.fail(maker, "unable to read cmake input file {f}: {t}", .{ tf, err });
+ renderCmake(arena, maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) {
+ error.WriteFailed => return error.OutOfMemory,
+ else => |e| return e,
+ };
+ },
+ .blank => {
+ renderBlank(conf, &aw.writer, value_pairs, &value_map, include_path, include_guard_override) catch |err| switch (err) {
+ error.WriteFailed => return error.OutOfMemory,
+ else => |e| return e,
+ };
+ },
+ .nasm => {
+ renderNasm(conf, &aw.writer, value_pairs, &value_map) catch |err| switch (err) {
+ error.WriteFailed => return error.OutOfMemory,
+ else => |e| return e,
+ };
+ },
+ }
+
+ const output = aw.written();
+ man.hash.addBytes(output);
+
+ if (try step.cacheHit(maker, &man)) {
+ const digest = man.final();
+ maker.generatedPath(conf_ch.generated_dir).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
+ };
+ return;
+ }
+
+ const digest = man.final();
+
+ // If output_path has directory parts, deal with them. Example:
+ // output_dir is zig-cache/o/HASH
+ // output_path is libavutil/avconfig.h
+ // We want to open directory zig-cache/o/HASH/libavutil/
+ // but keep output_dir as zig-cache/o/HASH for -I include
+ const out_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, conf_ch.include_path.slice(conf) }),
+ };
+ const out_path_dirname = out_path.dirname().?;
+
+ out_path_dirname.root_dir.handle.createDirPath(io, out_path_dirname.sub_path) catch |err|
+ return step.fail(maker, "unable to make path {f}: {t}", .{ out_path_dirname, err });
+
+ out_path.root_dir.handle.writeFile(io, .{ .sub_path = out_path.sub_path, .data = output }) catch |err|
+ return step.fail(maker, "unable to write file {f}: {t}", .{ out_path, err });
+
+ maker.generatedPath(conf_ch.generated_dir).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
+ };
+
+ try step.writeManifest(maker, &man);
+}
+
+fn ensureAllValuesUsed(
+ maker: *Maker,
+ step: *Step,
+ value_map: *const ValueMap,
+ src_path: Path,
+) Step.ExtendedMakeError!void {
+ var any_errors = false;
+ for (value_map.keys(), value_map.values()) |name, used| {
+ if (used) continue;
+ try step.addError(maker, "{f}: config header value unused: {s}", .{ src_path, name });
+ any_errors = true;
+ }
+ if (any_errors) return error.MakeFailed;
+}
+
+fn renderAutoConfUndef(
+ maker: *Maker,
+ step: *Step,
+ contents: []const u8,
+ w: *Writer,
+ value_pairs: []const Value.Pair,
+ value_map: *ValueMap,
+ src_path: Path,
+) !void {
+ const conf = &maker.scanned_config.configuration;
+
+ try w.writeAll(c_generated_line);
+
+ var any_errors = false;
+ var line_index: u32 = 0;
+ var line_it = std.mem.splitScalar(u8, contents, '\n');
+ while (line_it.next()) |line| : (line_index += 1) {
+ if (!std.mem.startsWith(u8, line, "#")) {
+ try w.writeAll(line);
+ try w.writeByte('\n');
+ continue;
+ }
+ var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
+ const undef = it.next().?;
+ if (!std.mem.eql(u8, undef, "undef")) {
+ try w.writeAll(line);
+ try w.writeByte('\n');
+ continue;
+ }
+ const name = it.next().?;
+ const index = value_map.getIndex(name) orelse {
+ try step.addError(maker, "{f}:{d}: unspecified config header value: {s}", .{
+ src_path, line_index + 1, name,
+ });
+ any_errors = true;
+ continue;
+ };
+ value_map.values()[index] = true; // Set to used.
+ try renderValueC(conf, w, name, value_pairs[index].index);
+ }
+
+ try ensureAllValuesUsed(maker, step, value_map, src_path);
+ if (any_errors) return error.MakeFailed;
+}
+
+fn renderAutoconfAt(
+ maker: *Maker,
+ step: *Step,
+ contents: []const u8,
+ aw: *Writer.Allocating,
+ value_pairs: []const Value.Pair,
+ value_map: *const ValueMap,
+ src_path: Path,
+) !void {
+ const w = &aw.writer;
+ const conf = &maker.scanned_config.configuration;
+
+ try w.writeAll(c_generated_line);
+
+ var any_errors = false;
+ var line_index: u32 = 0;
+ var line_it = std.mem.splitScalar(u8, contents, '\n');
+ while (line_it.next()) |line| : (line_index += 1) {
+ const last_line = line_it.index == line_it.buffer.len;
+
+ const old_len = aw.written().len;
+ expandVariablesAutoconfAt(w, line, conf, value_pairs, value_map) catch |err| switch (err) {
+ error.MissingValue => {
+ const name = aw.written()[old_len..];
+ defer aw.shrinkRetainingCapacity(old_len);
+ try step.addError(maker, "{f}:{d}: error: unspecified config header value: {s}", .{
+ src_path, line_index + 1, name,
+ });
+ any_errors = true;
+ continue;
+ },
+ else => {
+ try step.addError(maker, "{f}:{d}: unable to substitute variable: error: {t}", .{
+ src_path, line_index + 1, err,
+ });
+ any_errors = true;
+ continue;
+ },
+ };
+ if (!last_line) try w.writeByte('\n');
+ }
+
+ try ensureAllValuesUsed(maker, step, value_map, src_path);
+ if (any_errors) return error.MakeFailed;
+}
+
+fn renderCmake(
+ arena: Allocator,
+ maker: *Maker,
+ step: *Step,
+ contents: []const u8,
+ w: *Writer,
+ value_pairs: []const Value.Pair,
+ value_map: *ValueMap,
+ src_path: Path,
+) !void {
+ const conf = &maker.scanned_config.configuration;
+
+ try w.writeAll(c_generated_line);
+
+ var any_errors = false;
+ var line_index: u32 = 0;
+ var line_it = std.mem.splitScalar(u8, contents, '\n');
+ while (line_it.next()) |raw_line| : (line_index += 1) {
+ const last_line = line_it.index == line_it.buffer.len;
+
+ const line = expandVariablesCmake(arena, raw_line, conf, value_pairs, value_map) catch |err| switch (err) {
+ error.InvalidCharacter => {
+ try step.addError(maker, "{f}:{d}: invalid character in a variable name", .{
+ src_path, line_index + 1,
+ });
+ any_errors = true;
+ continue;
+ },
+ else => {
+ try step.addError(maker, "{f}:{d}: failed substituting variable: {t}", .{
+ src_path, line_index + 1, err,
+ });
+ any_errors = true;
+ continue;
+ },
+ };
+
+ const line_start = std.mem.findNone(u8, line, " \t\r") orelse {
+ try w.writeAll(line);
+ if (!last_line) try w.writeByte('\n');
+ continue;
+ };
+ const whitespace_prefix = line[0..line_start];
+ const trimmed_line = line[line_start..];
+
+ if (!std.mem.startsWith(u8, trimmed_line, "#")) {
+ try w.writeAll(line);
+ if (!last_line) try w.writeByte('\n');
+ continue;
+ }
+
+ var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r");
+ const cmakedefine = it.next().?;
+
+ const booldefine = if (std.mem.eql(u8, cmakedefine, "cmakedefine01"))
+ true
+ else if (std.mem.eql(u8, cmakedefine, "cmakedefine"))
+ false
+ else {
+ try w.writeAll(line);
+ if (!last_line) try w.writeByte('\n');
+ continue;
+ };
+
+ const name = it.next() orelse {
+ try step.addError(maker, "{f}:{d}: error: missing define name", .{ src_path, line_index + 1 });
+ any_errors = true;
+ continue;
+ };
+ const orig_value: Value.Index = v: {
+ const index = value_map.getIndex(name) orelse break :v if (booldefine) .int_0 else .undef;
+ value_map.values()[index] = true; // Mark as used.
+ break :v value_pairs[index].index;
+ };
+ const value = switch (orig_value.unpack(conf)) {
+ .bool => |b| if (!b) .undef else orig_value,
+ inline .i64, .u64 => |i| if (i == 0) .undef else orig_value,
+ .string => |s| if (s.len == 0) .undef else orig_value,
+ else => orig_value,
+ };
+
+ try w.writeAll(whitespace_prefix);
+
+ if (booldefine) {
+ try renderValueCBool(w, name, switch (value.unpack(conf)) {
+ .undef, .defined => false,
+ .bool => |b| b,
+ inline .u64, .i64 => |i| i != 0,
+ .string => |s| s.len != 0,
+ .ident => false,
+ });
+ } else if (value != .undef) {
+ try renderValueCIdent(w, name, it.rest());
+ } else {
+ try renderValueC(conf, w, name, value);
+ }
+ }
+
+ try ensureAllValuesUsed(maker, step, value_map, src_path);
+ if (any_errors) return error.MakeFailed;
+}
+
+fn renderBlank(
+ conf: *const Configuration,
+ w: *Writer,
+ value_pairs: []const Value.Pair,
+ value_map: *const ValueMap,
+ include_path: []const u8,
+ include_guard_override: ?[]const u8,
+) !void {
+ try w.writeAll(c_generated_line);
+
+ const include_guard_fmt: IncludeGuardFmt = .{
+ .include_path = include_path,
+ .override = include_guard_override,
+ };
+
+ try w.print(
+ \\#ifndef {[0]f}
+ \\#define {[0]f}
+ \\
+ , .{include_guard_fmt});
+
+ for (value_map.keys(), value_pairs) |name, pair| try renderValueC(conf, w, name, pair.index);
+
+ try w.print(
+ \\#endif /* {f} */
+ \\
+ , .{include_guard_fmt});
+}
+
+const IncludeGuardFmt = struct {
+ include_path: []const u8,
+ override: ?[]const u8,
+
+ pub fn format(this: @This(), w: *Writer) Writer.Error!void {
+ if (this.override) |s| return w.writeAll(s);
+ for (this.include_path) |byte| switch (byte) {
+ 'a'...'z' => try w.writeByte(byte - 'a' + 'A'),
+ 'A'...'Z', '0'...'9' => continue,
+ else => try w.writeByte('_'),
+ };
+ }
+};
+
+fn renderNasm(
+ conf: *const Configuration,
+ w: *Writer,
+ value_pairs: []const Value.Pair,
+ value_map: *const ValueMap,
+) !void {
+ try w.writeAll(asm_generated_line);
+ for (value_map.keys(), value_pairs) |name, pair| try renderValueNasm(conf, w, name, pair.index);
+}
+
+fn renderValueC(conf: *const Configuration, w: *Writer, name: []const u8, value: Value.Index) !void {
+ switch (value.unpack(conf)) {
+ .undef => try w.print("/* #undef {s} */\n", .{name}),
+ .defined => try w.print("#define {s}\n", .{name}),
+ .bool => |b| return renderValueCBool(w, name, b),
+ inline .u64, .i64 => |i| try w.print("#define {s} {d}\n", .{ name, i }),
+ .ident => |ident| return renderValueCIdent(w, name, ident),
+ .string => |string| try w.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
+ }
+}
+
+fn renderValueCIdent(w: *Writer, name: []const u8, ident: []const u8) Writer.Error!void {
+ return w.print("#define {s} {s}\n", .{ name, ident });
+}
+
+fn renderValueCBool(w: *Writer, name: []const u8, b: bool) Writer.Error!void {
+ return w.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) });
+}
+
+fn renderValueNasm(conf: *const Configuration, w: *Writer, name: []const u8, value: Value.Index) !void {
+ switch (value.unpack(conf)) {
+ .undef => try w.print("; %undef {s}\n", .{name}),
+ .defined => try w.print("%define {s}\n", .{name}),
+ .bool => |b| try w.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }),
+ inline .u64, .i64 => |i| try w.print("%define {s} {d}\n", .{ name, i }),
+ .ident => |ident| try w.print("%define {s} {s}\n", .{ name, ident }),
+ .string => |string| try w.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
+ }
+}
+
+fn expandVariablesAutoconfAt(
+ w: *Writer,
+ contents: []const u8,
+ conf: *const Configuration,
+ value_pairs: []const Value.Pair,
+ value_map: *const ValueMap,
+) !void {
+ const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
+
+ var curr: usize = 0;
+ var source_offset: usize = 0;
+ while (curr < contents.len) : (curr += 1) {
+ if (contents[curr] != '@') continue;
+ if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
+ if (close_pos == curr + 1) {
+ // closed immediately, preserve as a literal
+ continue;
+ }
+ const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
+ if (valid_varname_end != close_pos) {
+ // contains invalid characters, preserve as a literal
+ continue;
+ }
+
+ const key = contents[curr + 1 .. close_pos];
+ const index = value_map.getIndex(key) orelse {
+ // Report the missing key to the caller.
+ try w.writeAll(key);
+ return error.MissingValue;
+ };
+ const value = value_pairs[index].index;
+ value_map.values()[index] = true; // Mark as used.
+ try w.writeAll(contents[source_offset..curr]);
+ switch (value.unpack(conf)) {
+ .undef, .defined => {},
+ .bool => |b| try w.writeByte(@as(u8, '0') + @intFromBool(b)),
+ inline .u64, .i64 => |i| try w.print("{d}", .{i}),
+ .ident, .string => |s| try w.writeAll(s),
+ }
+
+ curr = close_pos;
+ source_offset = close_pos + 1;
+ }
+ }
+
+ try w.writeAll(contents[source_offset..]);
+}
+
+fn expandVariablesCmake(
+ arena: Allocator,
+ contents: []const u8,
+ conf: *const Configuration,
+ value_pairs: []const Value.Pair,
+ value_map: *const ValueMap,
+) ![]const u8 {
+ var result: std.ArrayList(u8) = .empty;
+
+ const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-";
+ const open_var = "${";
+
+ var curr: usize = 0;
+ var source_offset: usize = 0;
+ const Position = struct {
+ source: usize,
+ target: usize,
+ };
+ var var_stack: std.ArrayList(Position) = .empty;
+ loop: while (curr < contents.len) : (curr += 1) {
+ switch (contents[curr]) {
+ '@' => blk: {
+ if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
+ if (close_pos == curr + 1) {
+ // closed immediately, preserve as a literal
+ break :blk;
+ }
+ const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
+ if (valid_varname_end != close_pos) {
+ // contains invalid characters, preserve as a literal
+ break :blk;
+ }
+
+ const key = contents[curr + 1 .. close_pos];
+ const index = value_map.getIndex(key) orelse return error.MissingValue;
+ value_map.values()[index] = true; // Mark as used.
+ const value = value_pairs[index].index;
+ const missing = contents[source_offset..curr];
+ try result.appendSlice(arena, missing);
+ switch (value.unpack(conf)) {
+ .undef, .defined => {},
+ .bool => |b| try result.append(arena, if (b) '1' else '0'),
+ inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}),
+ .ident, .string => |s| try result.appendSlice(arena, s),
+ }
+
+ curr = close_pos;
+ source_offset = close_pos + 1;
+
+ continue :loop;
+ }
+ },
+ '$' => blk: {
+ const next = curr + 1;
+ if (next == contents.len or contents[next] != '{') {
+ // no open bracket detected, preserve as a literal
+ break :blk;
+ }
+ const missing = contents[source_offset..curr];
+ try result.appendSlice(arena, missing);
+ try result.appendSlice(arena, open_var);
+
+ source_offset = curr + open_var.len;
+ curr = next;
+ try var_stack.append(arena, .{
+ .source = curr,
+ .target = result.items.len - open_var.len,
+ });
+
+ continue :loop;
+ },
+ '}' => blk: {
+ if (var_stack.items.len == 0) {
+ // no open bracket, preserve as a literal
+ break :blk;
+ }
+ const open_pos = var_stack.pop().?;
+ if (source_offset == open_pos.source) {
+ source_offset += open_var.len;
+ }
+ const missing = contents[source_offset..curr];
+ try result.appendSlice(arena, missing);
+
+ const key_start = open_pos.target + open_var.len;
+ const key = result.items[key_start..];
+ if (key.len == 0) {
+ return error.MissingKey;
+ }
+ const index = value_map.getIndex(key) orelse return error.MissingValue;
+ value_map.values()[index] = true; // Mark as used.
+ const value = value_pairs[index].index;
+ result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len);
+ switch (value.unpack(conf)) {
+ .undef, .defined => {},
+ .bool => |b| try result.append(arena, if (b) '1' else '0'),
+ inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}),
+ .ident, .string => |s| try result.appendSlice(arena, s),
+ }
+
+ source_offset = curr + 1;
+
+ continue :loop;
+ },
+ '\\' => {
+ // backslash is not considered a special character
+ continue :loop;
+ },
+ else => {},
+ }
+
+ if (var_stack.items.len > 0 and std.mem.findScalar(u8, valid_varname_chars, contents[curr]) == null) {
+ return error.InvalidCharacter;
+ }
+ }
+
+ if (source_offset != contents.len) {
+ const missing = contents[source_offset..];
+ try result.appendSlice(arena, missing);
+ }
+
+ try result.shrinkToLen(arena);
+
+ return result.toOwnedSliceAssert();
+}
diff --git a/lib/compiler/Maker/Step/FindProgram.zig b/lib/compiler/Maker/Step/FindProgram.zig
@@ -0,0 +1,120 @@
+const FindProgram = @This();
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Io = std.Io;
+const Configuration = std.Build.Configuration;
+const assert = std.debug.assert;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ find_program: *FindProgram,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = find_program;
+ _ = progress_node;
+ const graph = maker.graph;
+ const step = maker.stepByIndex(step_index);
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_fp = conf_step.extended.get(conf.extra).find_program;
+ const found_path = conf_fp.found_path;
+ const names = conf_fp.names.slice(conf);
+
+ // In case we fail at the end.
+ var err_msg: std.ArrayList(u8) = .empty;
+ try err_msg.appendSlice(arena, "program not found. searched paths:\n");
+
+ for (names) |name_index| {
+ const name = name_index.slice(conf);
+
+ if (Io.Dir.path.isAbsolute(name)) {
+ if (try checkCandidate(maker, step, found_path, &err_msg, name)) return;
+
+ continue;
+ }
+
+ for (graph.search_prefixes.items) |search_prefix| {
+ const full_path = try Io.Dir.path.join(arena, &.{ search_prefix, "bin", name });
+
+ if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return;
+ }
+ }
+
+ if (graph.environ_map.get("PATH")) |PATH| {
+ for (names) |name_index| {
+ const name = name_index.slice(conf);
+
+ var it = std.mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter);
+ while (it.next()) |p| {
+ const full_path = try Io.Dir.path.join(arena, &.{ p, name });
+
+ if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return;
+ }
+ }
+ }
+
+ assert(err_msg.items[err_msg.items.len - 1] == '\n');
+ const chopped = err_msg.items[0 .. err_msg.items.len - 1];
+ try step.result_error_msgs.append(arena, chopped);
+ return error.MakeFailed;
+}
+
+fn checkCandidate(
+ maker: *Maker,
+ step: *Step,
+ found_path: Configuration.GeneratedFileIndex,
+ err_msg: *std.ArrayList(u8),
+ full_path: []const u8,
+) !bool {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+
+ if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| {
+ maker.generatedPath(found_path).* = .initCwd(full_path);
+ return true;
+ } else |err| switch (err) {
+ error.Canceled => |e| return e,
+ error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
+ try err_msg.print(arena, "{t} {s}\n", .{ e, full_path });
+ },
+ else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ full_path, e }),
+ }
+
+ if (builtin.os.tag == .windows) {
+ if (graph.environ_map.get("PATHEXT")) |PATHEXT| {
+ var it = std.mem.tokenizeScalar(u8, PATHEXT, Io.Dir.path.delimiter);
+ while (it.next()) |ext| {
+ if (!supportedWindowsProgramExtension(ext)) continue;
+
+ const extended_path = try std.mem.concat(arena, u8, &.{ full_path, ext });
+
+ if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| {
+ maker.generatedPath(found_path).* = .initCwd(extended_path);
+ return true;
+ } else |err| switch (err) {
+ error.Canceled => |e| return e,
+ error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
+ try err_msg.print(arena, "{t} {s}\n", .{ e, extended_path });
+ },
+ else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }),
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+fn supportedWindowsProgramExtension(ext: []const u8) bool {
+ inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| {
+ if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true;
+ }
+ return false;
+}
diff --git a/lib/compiler/Maker/Step/Fmt.zig b/lib/compiler/Maker/Step/Fmt.zig
@@ -0,0 +1,65 @@
+const Fmt = @This();
+
+const std = @import("std");
+const Configuration = std.Build.Configuration;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+/// Persisted to reuse memory on subsequent calls to `make`.
+argv: std.ArrayList([]const u8) = .empty,
+
+pub fn make(
+ fmt: *Fmt,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ const graph = maker.graph;
+ const step = maker.stepByIndex(step_index);
+ const gpa = maker.gpa;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const argv = &fmt.argv;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_fmt = conf_step.extended.get(conf.extra).fmt;
+ const paths = conf_fmt.paths.slice;
+ const exclude_paths = conf_fmt.exclude_paths.slice;
+
+ argv.clearRetainingCapacity();
+ try argv.ensureUnusedCapacity(gpa, 2 + 1 + paths.len + 2 * exclude_paths.len);
+
+ argv.appendAssumeCapacity(graph.zig_exe);
+ argv.appendAssumeCapacity("fmt");
+
+ if (conf_fmt.flags.check)
+ argv.appendAssumeCapacity("--check");
+
+ for (paths) |lp|
+ argv.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, step_index));
+
+ for (exclude_paths) |lp| {
+ argv.appendAssumeCapacity("--exclude");
+ argv.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, step_index));
+ }
+
+ const run_result = step.captureChildProcess(maker, .{
+ .progress_node = progress_node,
+ .argv = argv.items,
+ .allow_failure = false,
+ }) catch |err| switch (err) {
+ error.FileNotFound => unreachable,
+ else => |e| return e,
+ };
+
+ if (conf_fmt.flags.check) switch (run_result.term) {
+ .exited => |code| if (code != 0 and run_result.stdout.len != 0) {
+ var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n');
+ while (it.next()) |bad_file_name| {
+ try step.addError(maker, "{s}: non-conforming formatting", .{bad_file_name});
+ }
+ },
+ else => {},
+ };
+ try step.handleChildProcessTerm(maker, run_result.term);
+}
diff --git a/lib/compiler/Maker/Step/InstallArtifact.zig b/lib/compiler/Maker/Step/InstallArtifact.zig
@@ -0,0 +1,141 @@
+const InstallArtifact = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Configuration = std.Build.Configuration;
+const assert = std.debug.assert;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ install_artifact: *InstallArtifact,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = install_artifact;
+ _ = progress_node;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const arena = graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const conf_step = step_index.ptr(conf);
+ const conf_ia = conf_step.extended.get(conf.extra).install_artifact;
+ const compile_step_index = conf_step.deps.get(conf).steps.slice[0];
+ const conf_comp_step = compile_step_index.ptr(conf);
+ const conf_comp = conf_comp_step.extended.get(conf.extra).compile;
+ const root_module = conf_comp.root_module.get(conf);
+ const target = root_module.resolved_target.get(conf).?.result.get(conf);
+
+ var all_cached = true;
+
+ if (conf_ia.bin_dir.value) |bin_dir| {
+ if (conf_comp.generated_bin.value) |generated_bin| {
+ const bin_sub_path = if (conf_ia.bin_sub_path.value) |s| s.slice(conf) else try std.zig.binNameAlloc(arena, .{
+ .root_name = conf_comp.root_name.slice(conf),
+ .cpu_arch = target.flags.cpu_arch.unwrap().?,
+ .os_tag = target.flags.os_tag.unwrap().?,
+ .ofmt = target.flags.object_format.unwrap().?,
+ .abi = target.flags.abi.unwrap().?,
+ .output_mode = conf_comp.flags3.kind.toOutputMode(),
+ .link_mode = conf_comp.flags2.linkage.unwrap(),
+ .version = v: {
+ const string = conf_comp.version.value orelse break :v null;
+ const slice = string.slice(conf);
+ break :v std.SemanticVersion.parse(slice) catch @panic("bad semver string");
+ },
+ });
+ const dest_dir = try maker.resolveInstallDir(arena, bin_dir);
+ const dest_path = try dest_dir.join(arena, bin_sub_path);
+ const src_path = maker.generatedPath(generated_bin).*;
+ const p = try maker.installPath(arena, src_path, dest_path, step_index);
+ all_cached = all_cached and p == .fresh;
+
+ if (conf_ia.flags.dylib_symlinks)
+ try maker.installSymLinks(arena, dest_path, compile_step_index, step_index);
+
+ const make_comp_step = maker.stepByIndex(compile_step_index);
+ const make_comp = &make_comp_step.extended.compile;
+ make_comp.installed_path = dest_path;
+ }
+ }
+
+ if (conf_ia.implib_dir.value) |implib_dir| {
+ if (conf_comp.generated_implib.value) |generated_implib| {
+ const p = try maker.installGenerated(arena, generated_implib, implib_dir, step_index);
+ all_cached = all_cached and p == .fresh;
+ }
+ }
+
+ if (conf_ia.pdb_dir.value) |pdb_dir| {
+ if (conf_comp.generated_pdb.value) |generated_pdb| {
+ const p = try maker.installGenerated(arena, generated_pdb, pdb_dir, step_index);
+ all_cached = all_cached and p == .fresh;
+ }
+ }
+
+ if (conf_ia.h_dir.value) |h_dir| {
+ const h_prefix = try maker.resolveInstallDir(arena, h_dir);
+
+ if (conf_comp.generated_h.value) |generated_h| {
+ const p = try maker.installGenerated(arena, generated_h, h_dir, step_index);
+ all_cached = all_cached and p == .fresh;
+ }
+
+ for (conf_comp.installed_headers.slice) |installation| switch (installation.get(conf.extra)) {
+ .file => |file| {
+ const src_path = try maker.resolveLazyPathIndex(arena, file.source, step_index);
+ const dest_path = try h_prefix.join(arena, file.dest_sub_path.slice(conf));
+ const p = try maker.installPath(arena, src_path, dest_path, step_index);
+ all_cached = all_cached and p == .fresh;
+ },
+ .directory => |dir| {
+ const src_dir_path = try maker.resolveLazyPathIndex(arena, dir.source, step_index);
+ const full_h_prefix = try h_prefix.join(arena, dir.dest_sub_path.slice(conf));
+
+ var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
+ return step.fail(maker, "unable to open source directory {f}: {t}", .{ src_dir_path, err });
+ };
+ defer src_dir.close(io);
+
+ var it = try src_dir.walk(gpa);
+ defer it.deinit();
+ next_entry: while (it.next(io) catch |err| switch (err) {
+ error.Canceled, error.OutOfMemory => |e| return e,
+ else => |e| return step.fail(maker, "failed to iterate directory {f}: {t}", .{ src_dir_path, e }),
+ }) |entry| {
+ for (dir.exclude_extensions.slice) |ext| {
+ if (std.mem.endsWith(u8, entry.path, ext.slice(conf))) continue :next_entry;
+ }
+ if (dir.flags.include_extensions) {
+ for (dir.include_extensions.slice) |inc| {
+ if (std.mem.endsWith(u8, entry.path, inc.slice(conf))) break;
+ } else {
+ continue :next_entry;
+ }
+ }
+
+ const full_dest_path = try full_h_prefix.join(arena, entry.path);
+ switch (entry.kind) {
+ .directory => {
+ const p = try maker.installDir(arena, full_dest_path, step_index);
+ all_cached = all_cached and p == .existed;
+ },
+ .file => {
+ const entry_dir_path = try maker.resolveLazyPathIndex(arena, dir.source, step_index);
+ const entry_path = try entry_dir_path.join(arena, entry.path);
+ const p = try maker.installPath(arena, entry_path, full_dest_path, step_index);
+ all_cached = all_cached and p == .fresh;
+ },
+ else => continue,
+ }
+ }
+ },
+ };
+ }
+
+ step.result_cached = all_cached;
+}
diff --git a/lib/compiler/Maker/Step/InstallDir.zig b/lib/compiler/Maker/Step/InstallDir.zig
@@ -0,0 +1,99 @@
+const InstallDir = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const log = std.log;
+const Configuration = std.Build.Configuration;
+const endsWith = std.mem.endsWith;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ install_dir: *InstallDir,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = install_dir;
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_id = conf_step.extended.get(conf.extra).install_dir;
+
+ step.clearWatchInputs(maker);
+
+ const dest_parent_path = try maker.resolveInstallDir(arena, conf_id.dest_dir);
+ const dest_prefix = if (conf_id.dest_sub_path.value) |s|
+ try dest_parent_path.join(arena, s.slice(conf))
+ else
+ dest_parent_path;
+ const src_dir_lazy_path = conf_id.source_dir.get(conf);
+ const src_dir_path = try maker.resolveLazyPath(arena, src_dir_lazy_path, step_index);
+ const need_derived_inputs = try step.addDirectoryWatchInput(maker, src_dir_lazy_path);
+
+ var src_dir = src_dir_path.root_dir.handle.openDir(
+ io,
+ src_dir_path.subPathOrDot(),
+ .{ .iterate = true },
+ ) catch |err| return step.fail(maker, "failed opening source directory {f}: {t}", .{ src_dir_path, err });
+ defer src_dir.close(io);
+
+ const exclude_extensions = conf_id.exclude_extensions.slice;
+ const include_extensions: ?[]const Configuration.String = if (conf_id.flags.include_extensions_active)
+ conf_id.include_extensions.slice
+ else
+ null;
+ const blank_extensions = conf_id.blank_extensions.slice;
+
+ var all_cached = true;
+ var it = try src_dir.walk(gpa);
+ defer it.deinit();
+ next_entry: while (it.next(io) catch |err| switch (err) {
+ error.Canceled, error.OutOfMemory => |e| return e,
+ else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }),
+ }) |entry| {
+ for (exclude_extensions) |ext| {
+ if (endsWith(u8, entry.path, ext.slice(conf))) continue :next_entry;
+ }
+ if (include_extensions) |includes| {
+ for (includes) |inc| {
+ if (endsWith(u8, entry.path, inc.slice(conf))) break;
+ } else {
+ continue :next_entry;
+ }
+ }
+
+ const dest_path = try dest_prefix.join(arena, entry.path);
+ switch (entry.kind) {
+ .directory => {
+ if (need_derived_inputs) {
+ const entry_path = try src_dir_path.join(arena, entry.path);
+ try step.addDirectoryWatchInputFromPath(maker, entry_path);
+ }
+ const p = try maker.installDir(arena, dest_path, step_index);
+ all_cached = all_cached and p == .existed;
+ },
+ .file => {
+ for (blank_extensions) |ext| {
+ if (endsWith(u8, entry.path, ext.slice(conf))) {
+ try maker.truncatePath(arena, dest_path, step_index);
+ continue :next_entry;
+ }
+ }
+
+ const entry_path = try src_dir_path.join(arena, entry.path);
+ const p = try maker.installPath(arena, entry_path, dest_path, step_index);
+ all_cached = all_cached and p == .fresh;
+ progress_node.completeOne();
+ },
+ else => continue,
+ }
+ }
+
+ step.result_cached = all_cached;
+}
diff --git a/lib/compiler/Maker/Step/InstallFile.zig b/lib/compiler/Maker/Step/InstallFile.zig
@@ -0,0 +1,26 @@
+const InstallFile = @This();
+
+const std = @import("std");
+const Configuration = std.Build.Configuration;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ install_file: *InstallFile,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = install_file;
+ _ = progress_node;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_if = conf_step.extended.get(conf.extra).install_file;
+
+ try step.singleUnchangingWatchInput(maker, arena, conf_if.source.get(conf));
+ const p = try maker.installLazyPathSub(arena, conf_if.source, conf_if.dest_dir, conf_if.dest_sub_path.slice(conf), step_index);
+ step.result_cached = p == .fresh;
+}
diff --git a/lib/compiler/Maker/Step/ObjCopy.zig b/lib/compiler/Maker/Step/ObjCopy.zig
@@ -0,0 +1,173 @@
+const ObjCopy = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Path = std.Build.Cache.Path;
+const allocPrint = std.fmt.allocPrint;
+const Configuration = std.Build.Configuration;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ obj_copy: *ObjCopy,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = obj_copy;
+ const graph = maker.graph;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_oc = conf_step.extended.get(conf.extra).obj_copy;
+ const cache_root = graph.local_cache_root;
+ const input_lazy_path = conf_oc.input_file.get(conf);
+ const only_section: ?[]const u8 = if (conf_oc.only_section.value) |s| s.slice(conf) else null;
+ const opt_basename: ?[]const u8 = if (conf_oc.basename.value) |s| s.slice(conf) else null;
+ const opt_debug_basename: ?[]const u8 = if (conf_oc.debug_basename.value) |s| s.slice(conf) else null;
+
+ try step.singleUnchangingWatchInput(maker, arena, input_lazy_path);
+
+ var man = graph.cache.obtain();
+ defer man.deinit();
+
+ const input_path = try maker.resolveLazyPath(arena, input_lazy_path, step_index);
+ _ = try man.addFilePath(input_path, null);
+ man.hash.addOptionalBytes(only_section);
+ man.hash.addOptionalBytes(opt_basename);
+ man.hash.addOptionalBytes(opt_debug_basename);
+ man.hash.addOptional(conf_oc.pad_to.value);
+ man.hash.add(conf_oc.flags.format);
+ man.hash.add(conf_oc.flags.compress_debug);
+ man.hash.add(conf_oc.flags.strip);
+ man.hash.add(conf_oc.debug_file.value != null);
+
+ const basename = opt_basename orelse Io.Dir.path.basename(input_path.sub_path);
+
+ if (try step.cacheHit(maker, &man)) {
+ // Cache hit, skip subprocess execution.
+ const digest = man.final();
+ maker.generatedPath(conf_oc.output_file).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }),
+ };
+ if (conf_oc.debug_file.value) |debug_file| {
+ const debug_basename = opt_debug_basename orelse try allocPrint(arena, "{s}.debug", .{
+ Io.Dir.path.basename(input_path.sub_path),
+ });
+ maker.generatedPath(debug_file).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, debug_basename }),
+ };
+ }
+ return;
+ }
+
+ // We don't find out more input files while executing objcopy so we can
+ // already obtain the digest and use it directly as the output path.
+ const digest = man.final();
+ const dest_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }),
+ };
+ const dest_dirname = dest_path.dirname().?;
+ dest_dirname.root_dir.handle.createDirPath(io, dest_dirname.sub_path) catch |err|
+ return step.fail(maker, "failed to create path {f}: {t}", .{ dest_dirname, err });
+
+ var argv: std.ArrayList([]const u8) = .empty;
+ try argv.ensureUnusedCapacity(arena, 11);
+
+ argv.addManyAsArrayAssumeCapacity(2).* = .{ graph.zig_exe, "objcopy" };
+
+ if (only_section) |s| argv.addManyAsArrayAssumeCapacity(2).* = .{ "-j", s };
+
+ switch (conf_oc.flags.strip) {
+ .none => {},
+ .debug => argv.appendAssumeCapacity("--strip-debug"),
+ .debug_and_symbols => argv.appendAssumeCapacity("--strip-all"),
+ }
+
+ if (conf_oc.pad_to.value) |pad_to| {
+ argv.addManyAsArrayAssumeCapacity(2).* = .{
+ "--pad-to", try allocPrint(arena, "{d}", .{pad_to}),
+ };
+ }
+
+ switch (conf_oc.flags.format) {
+ .default => {},
+ else => |t| argv.addManyAsArrayAssumeCapacity(2).* = .{ "-O", @tagName(t) },
+ }
+
+ if (conf_oc.flags.compress_debug)
+ argv.appendAssumeCapacity("--compress-debug-sections");
+
+ if (conf_oc.debug_file.value) |debug_file| {
+ const debug_basename = opt_debug_basename orelse try allocPrint(arena, "{s}.debug", .{
+ Io.Dir.path.basename(input_path.sub_path),
+ });
+ const debug_dest_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, debug_basename }),
+ };
+ argv.appendAssumeCapacity(try allocPrint(arena, "--extract-to={f}", .{debug_dest_path}));
+ maker.generatedPath(debug_file).* = debug_dest_path;
+ }
+
+ try argv.ensureUnusedCapacity(arena, conf_oc.add_section.slice.len * 2);
+
+ for (conf_oc.add_section.slice) |section| {
+ argv.appendAssumeCapacity("--add-section");
+ argv.appendAssumeCapacity(try allocPrint(arena, "{s}={f}", .{
+ section.section_name.slice(conf),
+ try maker.resolveLazyPathIndex(arena, section.file_path, step_index),
+ }));
+ }
+
+ for (conf_oc.update_section.slice) |update| {
+ const name = update.section_name.slice(conf);
+
+ try argv.ensureUnusedCapacity(arena, 4);
+
+ if (update.flags.alignment.toBytes()) |a| {
+ argv.appendAssumeCapacity("--set-section-alignment");
+ argv.appendAssumeCapacity(try allocPrint(arena, "{s}={d}", .{ name, a }));
+ }
+
+ const f = update.flags.section_flags;
+ if (f != Configuration.Step.ObjCopy.SectionFlags.default) {
+ // trailing comma is allowed
+ argv.appendAssumeCapacity("--set-section-flags");
+ argv.appendAssumeCapacity(try allocPrint(arena, "{s}={s}{s}{s}{s}{s}{s}{s}{s}{s}", .{
+ name,
+ if (f.alloc) "alloc," else "",
+ if (f.contents) "contents," else "",
+ if (f.load) "load," else "",
+ if (f.readonly) "readonly," else "",
+ if (f.code) "code," else "",
+ if (f.exclude) "exclude," else "",
+ if (f.large) "large," else "",
+ if (f.merge) "merge," else "",
+ if (f.strings) "strings," else "",
+ }));
+ }
+ }
+
+ argv.appendAssumeCapacity(try allocPrint(arena, "{f}", .{input_path}));
+ argv.appendAssumeCapacity(try allocPrint(arena, "{f}", .{dest_path}));
+
+ argv.appendAssumeCapacity("--listen=-");
+ _ = Step.evalZigProcess(step_index, maker, argv.items, progress_node, false) catch |err| switch (err) {
+ error.NeedCompileErrorCheck => unreachable,
+ else => |e| return e,
+ };
+
+ maker.generatedPath(conf_oc.output_file).* = dest_path;
+
+ step.writeManifest(maker, &man) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ else => |e| try step.addError(maker, "failed writing cache manifest: {t}", .{e}),
+ };
+}
diff --git a/lib/compiler/Maker/Step/Options.zig b/lib/compiler/Maker/Step/Options.zig
@@ -0,0 +1,104 @@
+const Options = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Configuration = std.Build.Configuration;
+const Cache = std.Build.Cache;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ options: *Options,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = options;
+
+ // This step completes so quickly that no progress reporting is necessary.
+ _ = progress_node;
+
+ const graph = maker.graph;
+ const step = maker.stepByIndex(step_index);
+ const io = graph.io;
+ const cache_root = graph.local_cache_root;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_options = conf_step.extended.get(conf.extra).options;
+ const contents = conf_options.contents.slice(conf);
+
+ // This step operates under the assumption that all contents of the
+ // generated zig file are observable by dependant steps, as well as the
+ // contents of files added via Options.Arg.
+
+ step.clearWatchInputs(maker);
+
+ var man = graph.cache.obtain();
+ defer man.deinit();
+
+ var args_bytes: std.ArrayList(u8) = .empty;
+
+ for (conf_options.args.slice) |arg| {
+ const name = arg.name.slice(conf);
+ const lazy_path = arg.path.get(conf);
+ try step.addWatchInput(maker, arena, lazy_path);
+ const arg_path = try maker.resolveLazyPath(arena, lazy_path, step_index);
+ _ = try man.addFilePath(arg_path, null);
+ try args_bytes.print(arena, "pub const {f}: []const u8 = \"{f}\";\n", .{
+ std.zig.fmtId(name), arg_path.fmtEscapeString(),
+ });
+ }
+
+ man.hash.addBytes(contents);
+ man.hash.addBytes(args_bytes.items);
+
+ const basename = "options.zig";
+
+ if (try step.cacheHitAndWatch(maker, &man)) {
+ const digest = man.final();
+ maker.generatedPath(conf_options.generated_file).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }),
+ };
+ step.result_cached = true;
+ return;
+ }
+
+ const digest = man.final();
+ const out_path: Cache.Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }),
+ };
+
+ var file: Io.File = out_path.root_dir.handle.createFile(io, out_path.sub_path, .{}) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ error.FileNotFound => f: {
+ out_path.root_dir.handle.createDirPath(io, Io.Dir.path.dirname(out_path.sub_path).?) catch |inner| switch (inner) {
+ error.Canceled => |e| return e,
+ else => |e| return step.fail(maker, "failed to create {f}: {t}", .{ out_path, e }),
+ };
+ break :f out_path.root_dir.handle.createFile(io, out_path.sub_path, .{}) catch |inner| switch (inner) {
+ error.Canceled => |e| return e,
+ else => |e| return step.fail(maker, "failed to create {f}: {t}", .{ out_path, e }),
+ };
+ },
+ else => |e| return step.fail(maker, "failed to create {f}: {t}", .{ out_path, e }),
+ };
+ defer file.close(io);
+
+ // No buffer because we already have all contents buffered.
+ var file_writer = file.writer(io, &.{});
+ var data: [2][]const u8 = .{ contents, args_bytes.items };
+ file_writer.interface.writeVecAll(&data) catch |write_err| switch (write_err) {
+ error.WriteFailed => switch (file_writer.err.?) {
+ error.Canceled => |e| return e,
+ else => |e| return step.fail(maker, "failed to write to {f}: {t}", .{ out_path, e }),
+ },
+ };
+
+ try step.writeManifestAndWatch(maker, &man);
+
+ maker.generatedPath(conf_options.generated_file).* = out_path;
+}
diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig
@@ -0,0 +1,2372 @@
+const Run = @This();
+
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Cache = std.Build.Cache;
+const Configuration = std.Build.Configuration;
+const Dir = std.Io.Dir;
+const EnvMap = std.process.Environ.Map;
+const Io = std.Io;
+const Path = std.Build.Cache.Path;
+const assert = std.debug.assert;
+const mem = std.mem;
+const process = std.process;
+const allocPrint = std.fmt.allocPrint;
+const Allocator = std.mem.Allocator;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+const Fuzz = @import("../../Maker/Fuzz.zig");
+
+/// If this is a Zig unit test binary, this tracks the names of the unit
+/// tests that are also fuzz tests. Indexes cannot be used as they may
+/// change between reruns.
+fuzz_tests: std.ArrayList([]const u8) = .empty,
+cached_test_metadata: ?CachedTestMetadata = null,
+
+/// Populated during the fuzz phase if this run step corresponds to a unit test
+/// executable that contains fuzz tests.
+rebuilt_executable: ?Path = null,
+
+pub fn make(
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const step = maker.stepByIndex(run_index);
+ const io = graph.io;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = run_index.ptr(conf);
+ const conf_run = conf_step.extended.get(conf.extra).run;
+ const cache_root = graph.local_cache_root;
+
+ var arena_allocator: std.heap.ArenaAllocator = .init(gpa);
+ defer arena_allocator.deinit();
+ const arena = arena_allocator.allocator();
+
+ var argv_list: std.ArrayList([]const u8) = .empty;
+ defer argv_list.deinit(gpa);
+
+ var output_placeholders: std.ArrayList(IndexedOutput) = .empty;
+ defer output_placeholders.deinit(gpa);
+
+ var man = graph.cache.obtain();
+ defer man.deinit();
+
+ if (conf_run.environ_map.value) |environ_map_index| {
+ const environ_map = environ_map_index.get(conf);
+ for (environ_map.keys.slice(conf), environ_map.values.slice(conf)) |key, value| {
+ man.hash.addBytesZ(key.slice(conf));
+ man.hash.addBytesZ(value.slice(conf));
+ }
+ }
+
+ man.hash.add(graph.fuzzing);
+ man.hash.add(conf_run.flags.color);
+ man.hash.add(conf_run.flags.disable_zig_progress);
+
+ var any_dep_files = false;
+ var any_output_args = false;
+ var any_cli_positionals = false;
+
+ for (conf_run.args.slice) |arg_index| {
+ const arg = arg_index.get(conf);
+ try argv_list.ensureUnusedCapacity(gpa, 1);
+ switch (arg.flags.tag) {
+ .string => {
+ const prefix = arg.prefix.value.?.slice(conf);
+ argv_list.appendAssumeCapacity(prefix);
+ man.hash.addBytesZ(prefix);
+ },
+ .path_file => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
+ argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
+ prefix, try convertPathArg(arena, run_index, maker, file_path), suffix,
+ }));
+ man.hash.addBytesZ(prefix);
+ man.hash.addBytesZ(suffix);
+ _ = try man.addFilePath(file_path, null);
+ },
+ .path_directory => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
+ const resolved_arg = try mem.concat(arena, u8, &.{
+ prefix, try convertPathArg(arena, run_index, maker, file_path), suffix,
+ });
+ argv_list.appendAssumeCapacity(resolved_arg);
+ man.hash.addBytes(resolved_arg);
+ },
+ .file_content => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
+
+ var result: std.Io.Writer.Allocating = .init(arena);
+ result.writer.writeAll(prefix) catch return error.OutOfMemory;
+
+ const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err|
+ return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err });
+ defer file.close(io);
+
+ var file_reader = file.reader(io, &.{});
+ _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
+ error.ReadFailed => switch (file_reader.err.?) {
+ error.Canceled => |e| return e,
+ else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }),
+ },
+ error.WriteFailed => return error.OutOfMemory,
+ };
+ result.writer.writeAll(suffix) catch return error.OutOfMemory;
+
+ argv_list.appendAssumeCapacity(result.written());
+ man.hash.addBytesZ(prefix);
+ man.hash.addBytesZ(suffix);
+ _ = try man.addFilePath(file_path, null);
+ },
+ .artifact => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const producer_index = arg.producer.value.?;
+ const producer_step = producer_index.ptr(conf);
+ const producer = producer_step.extended.get(conf.extra).compile;
+ const producer_make_comp_step = maker.stepByIndex(producer_index);
+ const producer_make_comp = &producer_make_comp_step.extended.compile;
+
+ const file_path = producer_make_comp.installed_path orelse maker.generatedPath(producer.generated_bin.value.?).*;
+
+ argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
+ prefix, try convertPathArg(arena, run_index, maker, file_path), suffix,
+ }));
+
+ _ = try man.addFilePath(file_path, null);
+ },
+ .output_file, .output_directory => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const basename = arg.basename.value.?.slice(conf);
+
+ man.hash.addBytesZ(prefix);
+ man.hash.addBytesZ(basename);
+ man.hash.addBytesZ(suffix);
+ man.hash.add(arg.flags.dep_file);
+
+ any_dep_files = any_dep_files or arg.flags.dep_file;
+ any_output_args = true;
+
+ // Add a placeholder into the argument list because we need the
+ // manifest hash to be updated with all arguments before the
+ // object directory is computed.
+ try output_placeholders.append(gpa, .{
+ .index = @intCast(argv_list.items.len),
+ .arg_index = arg_index,
+ });
+ argv_list.items.len += 1;
+ },
+ .passthru => {
+ any_cli_positionals = true;
+ if (maker.run_args) |run_args| {
+ try argv_list.appendSlice(gpa, run_args);
+ man.hash.addListOfBytes(run_args);
+ }
+ },
+ }
+ }
+
+ man.hash.add(conf_run.flags.test_runner_mode);
+ if (conf_run.flags.test_runner_mode) {
+ const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root });
+
+ try argv_list.ensureUnusedCapacity(gpa, 3);
+ argv_list.appendAssumeCapacity(try allocPrint(arena, "--cache-dir={s}", .{cache_dir_string}));
+ argv_list.appendAssumeCapacity(try allocPrint(arena, "--seed=0x{x}", .{graph.random_seed}));
+ argv_list.appendAssumeCapacity("--listen=-");
+ }
+
+ switch (conf_run.stdin.u) {
+ .bytes => |bytes| {
+ man.hash.addBytes(bytes.slice(conf));
+ },
+ .lazy_path => |lazy_path| {
+ const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
+ _ = try man.addFilePath(file_path, null);
+ },
+ .none => {},
+ }
+
+ if (conf_run.captured_stdout.value) |captured| {
+ man.hash.addBytes(captured.basename.slice(conf));
+ man.hash.add(conf_run.flags.stdout_trim_whitespace);
+ }
+
+ if (conf_run.captured_stderr.value) |captured| {
+ man.hash.addBytes(captured.basename.slice(conf));
+ man.hash.add(conf_run.flags.stderr_trim_whitespace);
+ }
+
+ switch (conf_run.flags.stdio) {
+ .infer_from_args, .inherit, .zig_test => {},
+ .check => {
+ man.hash.addBytes(if (conf_run.expect_stderr_exact.value) |bytes| bytes.slice(conf) else "");
+ man.hash.addBytes(if (conf_run.expect_stdout_exact.value) |bytes| bytes.slice(conf) else "");
+ for (conf_run.expect_stderr_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf));
+ for (conf_run.expect_stdout_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf));
+ man.hash.add(conf_run.flags2.expect_term_status);
+ man.hash.addOptional(conf_run.expect_term_value.value);
+ },
+ }
+
+ for (conf_run.file_inputs.slice) |lazy_path| {
+ const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
+ _ = try man.addFilePath(file_path, null);
+ }
+
+ if (conf_run.cwd.value) |lazy_path| {
+ const cwd_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
+ _ = man.hash.addBytes(try cwd_path.toString(arena));
+ }
+
+ // Whether the Run step has side effects *other than* updating the output arguments.
+ // When fuzzing we need to always run the test runner to populate fuzz_tests.
+ const has_side_effects = graph.fuzzing or conf_run.flags.has_side_effects or any_cli_positionals or
+ switch (conf_run.flags.stdio) {
+ .infer_from_args => !any_output_args and
+ conf_run.captured_stdout.value == null and
+ conf_run.captured_stderr.value == null,
+ .inherit => true,
+ .check, .zig_test => false,
+ };
+
+ if (!has_side_effects and try step.cacheHitAndWatch(maker, &man)) {
+ // Cache hit; skip running command.
+ const digest = man.final();
+ try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest);
+ try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest);
+ step.result_cached = true;
+ return;
+ }
+
+ if (!any_dep_files) {
+ // We already know the final output paths; use them directly.
+ const digest = if (has_side_effects) man.hash.final() else man.final();
+ const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest;
+ try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest);
+ try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, argv_list.items);
+ try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, output_dir_path, null);
+ if (!has_side_effects) try step.writeManifestAndWatch(maker, &man);
+ return;
+ }
+
+ // We do not know the final output paths yet; use temporary directory to run the command.
+ var rand_int: u64 = undefined;
+ io.random(@ptrCast(&rand_int));
+ const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
+
+ try populateGeneratedPathsCreateDirs(arena, run_index, maker, tmp_dir_path, output_placeholders.items, argv_list.items);
+ try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, tmp_dir_path, null);
+
+ for (output_placeholders.items) |placeholder| {
+ const arg = placeholder.arg_index.get(conf);
+ switch (arg.flags.tag) {
+ .output_file => if (arg.flags.dep_file) {
+ const generated_path = maker.generatedPath(arg.generated.value.?).*;
+ const result = if (has_side_effects)
+ man.addDepFile(generated_path.root_dir.handle, generated_path.sub_path)
+ else
+ man.addDepFilePost(generated_path.root_dir.handle, generated_path.sub_path);
+ result catch |err| switch (err) {
+ error.OutOfMemory, error.Canceled => |e| return e,
+ else => |e| return step.fail(maker, "failed adding to cache the file {f}: {t}", .{
+ generated_path, e,
+ }),
+ };
+ },
+ .output_directory => continue,
+ else => unreachable,
+ }
+ }
+
+ const digest = if (has_side_effects) man.hash.final() else man.final();
+
+ const any_output = output_placeholders.items.len > 0 or
+ conf_run.captured_stdout.value != null or conf_run.captured_stderr.value != null;
+
+ if (any_output) {
+ // Rename into place.
+ const tmp_path: Path = .{ .root_dir = cache_root, .sub_path = tmp_dir_path };
+ const dst_path: Path = .{ .root_dir = cache_root, .sub_path = "o" ++ Dir.path.sep_str ++ &digest };
+ Dir.rename(
+ tmp_path.root_dir.handle,
+ tmp_path.sub_path,
+ dst_path.root_dir.handle,
+ dst_path.sub_path,
+ io,
+ ) catch |err| switch (err) {
+ error.DirNotEmpty => {
+ dst_path.root_dir.handle.deleteTree(io, dst_path.sub_path) catch |del_err|
+ return step.fail(maker, "failed to remove tree {f}: {t}", .{ dst_path, del_err });
+
+ Dir.rename(
+ tmp_path.root_dir.handle,
+ tmp_path.sub_path,
+ dst_path.root_dir.handle,
+ dst_path.sub_path,
+ io,
+ ) catch |retry_err| return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{
+ tmp_path, dst_path, retry_err,
+ });
+ },
+ else => return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{
+ tmp_path, dst_path, err,
+ }),
+ };
+ }
+
+ if (!has_side_effects) try step.writeManifestAndWatch(maker, &man);
+
+ try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest);
+ try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest);
+}
+
+/// Reads stdout of a Zig test process until a termination condition is reached:
+/// * A write fails, indicating the child unexpectedly closed stdin
+/// * A test (or a response from the test runner) times out
+/// * The wait fails, indicating the child closed stdout and stderr
+fn waitZigTest(
+ arena: Allocator,
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ maker: *Maker,
+ child: *process.Child,
+ progress_node: std.Progress.Node,
+ multi_reader: *Io.File.MultiReader,
+ opt_metadata: *?TestMetadata,
+ results: *Step.TestResults,
+) !union(enum) {
+ write_failed: anyerror,
+ no_poll: struct {
+ active_test_index: ?u32,
+ ns_elapsed: u64,
+ },
+ timeout: struct {
+ active_test_index: ?u32,
+ ns_elapsed: u64,
+ },
+} {
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const io = graph.io;
+ const step = maker.stepByIndex(run_index);
+
+ var sub_prog_node: ?std.Progress.Node = null;
+ defer if (sub_prog_node) |n| n.end();
+
+ if (opt_metadata.*) |*md| {
+ // Previous unit test process died or was killed; we're continuing where it left off
+ requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
+ } else {
+ // Running unit tests normally
+ run.fuzz_tests.clearRetainingCapacity();
+ sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err };
+ }
+
+ var active_test_index: ?u32 = null;
+
+ var last_update: Io.Clock.Timestamp = .now(io, .awake);
+
+ // This timeout is used when we're waiting on the test runner itself rather than a user-specified
+ // test. For instance, if the test runner leaves this much time between us requesting a test to
+ // start and it acknowledging the test starting, we terminate the child and raise an error. This
+ // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
+ const response_timeout: Io.Clock.Duration = t: {
+ const ns = @max(maker.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
+ break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) };
+ };
+ const test_timeout: ?Io.Clock.Duration = if (maker.unit_test_timeout_ns) |ns| .{
+ .clock = .awake,
+ .raw = .fromNanoseconds(ns),
+ } else null;
+
+ const stdout = multi_reader.reader(0);
+ const stderr = multi_reader.reader(1);
+ const Header = std.zig.Server.Message.Header;
+
+ while (true) {
+ const timeout: Io.Timeout = t: {
+ const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
+ const duration = opt_duration orelse break :t .none;
+ break :t .{ .deadline = last_update.addDuration(duration) };
+ };
+
+ // This block is exited when `stdout` contains enough bytes for a `Header`.
+ header_ready: {
+ if (stdout.buffered().len >= @sizeOf(Header)) {
+ // We already have one, no need to poll!
+ break :header_ready;
+ }
+
+ multi_reader.fill(64, timeout) catch |err| switch (err) {
+ error.Timeout => return .{ .timeout = .{
+ .active_test_index = active_test_index,
+ .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
+ } },
+ error.EndOfStream => return .{ .no_poll = .{
+ .active_test_index = active_test_index,
+ .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
+ } },
+ else => |e| return e,
+ };
+
+ continue;
+ }
+ // There is definitely a header available now -- read it.
+ const header = stdout.takeStruct(Header, .little) catch unreachable;
+
+ while (stdout.buffered().len < header.bytes_len) {
+ multi_reader.fill(64, timeout) catch |err| switch (err) {
+ error.Timeout => return .{ .timeout = .{
+ .active_test_index = active_test_index,
+ .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
+ } },
+ error.EndOfStream => return .{ .no_poll = .{
+ .active_test_index = active_test_index,
+ .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
+ } },
+ else => |e| return e,
+ };
+ }
+
+ const body = stdout.take(header.bytes_len) catch unreachable;
+ var body_r: std.Io.Reader = .fixed(body);
+ switch (header.tag) {
+ .zig_version => {
+ if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail(
+ maker,
+ "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
+ .{ builtin.zig_version_string, body },
+ );
+ },
+ .test_metadata => {
+ // `metadata` would only be populated if we'd already seen a `test_metadata`, but we
+ // only request it once (and importantly, we don't re-request it if we kill and
+ // restart the test runner).
+ assert(opt_metadata.* == null);
+
+ const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable;
+ results.test_count = tm_hdr.tests_len;
+
+ const names = try arena.alloc(u32, results.test_count);
+ for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
+
+ const expected_panic_msgs = try arena.alloc(u32, results.test_count);
+ for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
+
+ const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable;
+
+ progress_node.setEstimatedTotalItems(names.len);
+ opt_metadata.* = .{
+ .string_bytes = try arena.dupe(u8, string_bytes),
+ .ns_per_test = try arena.alloc(u64, results.test_count),
+ .names = names,
+ .expected_panic_msgs = expected_panic_msgs,
+ .next_index = 0,
+ .prog_node = progress_node,
+ };
+ @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
+
+ active_test_index = null;
+ last_update = .now(io, .awake);
+
+ requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
+ },
+ .test_started => {
+ active_test_index = opt_metadata.*.?.next_index - 1;
+ last_update = .now(io, .awake);
+ },
+ .test_results => {
+ const md = &opt_metadata.*.?;
+
+ const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable;
+ assert(tr_hdr.index == active_test_index);
+
+ switch (tr_hdr.flags.status) {
+ .pass => {},
+ .skip => results.skip_count +|= 1,
+ .fail => results.fail_count +|= 1,
+ }
+ const leak_count = tr_hdr.flags.leak_count;
+ const log_err_count = tr_hdr.flags.log_err_count;
+ results.leak_count +|= leak_count;
+ results.log_err_count +|= log_err_count;
+
+ if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
+
+ if (tr_hdr.flags.status == .fail) {
+ const name = md.testName(tr_hdr.index);
+ const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
+ stderr.tossBuffered();
+ if (stderr_bytes.len == 0) {
+ try step.addError(maker, "'{s}' failed without output", .{name});
+ } else {
+ try step.addError(maker, "'{s}' failed:\n{s}", .{ name, stderr_bytes });
+ }
+ } else if (leak_count > 0) {
+ const name = md.testName(tr_hdr.index);
+ const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
+ stderr.tossBuffered();
+ try step.addError(maker, "'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
+ } else if (log_err_count > 0) {
+ const name = md.testName(tr_hdr.index);
+ const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
+ stderr.tossBuffered();
+ try step.addError(maker, "'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
+ }
+
+ active_test_index = null;
+
+ const now: Io.Clock.Timestamp = .now(io, .awake);
+ md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
+ last_update = now;
+
+ requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
+ },
+ else => {}, // ignore other messages
+ }
+ }
+}
+
+const FuzzTestRunner = struct {
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ ctx: FuzzContext,
+ coverage_id: ?u64,
+
+ instances: []Instance,
+ /// The indexes of this are layed out such that it is effectively an array
+ /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr.
+ batch: Io.Batch,
+ /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter.
+ pending_broadcasts: std.ArrayList(u8),
+ broadcast: std.ArrayList(u8),
+ broadcast_undelivered: u32,
+
+ const Instance = struct {
+ child: process.Child,
+ message: std.ArrayListAligned(u8, .@"4"),
+ broadcast_written: usize,
+ stderr: std.ArrayList(u8),
+ stdin_vec: [1][]u8,
+ stdout_vec: [1][]u8,
+ stderr_vec: [1][]u8,
+ progress_node: std.Progress.Node,
+
+ fn messageHeader(instance: *Instance) InHeader {
+ assert(instance.message.items.len >= @sizeOf(InHeader));
+ const header_ptr: *InHeader = @ptrCast(instance.message.items);
+ var header = header_ptr.*;
+ if (std.builtin.Endian.native != .little) {
+ std.mem.byteSwapAllFields(InHeader, &header);
+ }
+ return header;
+ }
+ };
+
+ const PendingBroadcastFooter = struct {
+ from_id: u32,
+ body_len: u32,
+ };
+
+ const InHeader = std.zig.Server.Message.Header;
+ const OutHeader = std.zig.Client.Message.Header;
+
+ const stdin_i = 0;
+ const stdout_i = 1;
+ const stderr_i = 2;
+
+ fn init(
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ ctx: FuzzContext,
+ progress_node: std.Progress.Node,
+ spawn_options: process.SpawnOptions,
+ ) !FuzzTestRunner {
+ const maker = ctx.fuzz.maker;
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const io = graph.io;
+
+ const n_instances = switch (ctx.fuzz.mode) {
+ .forever => graph.max_jobs orelse @min(
+ std.Thread.getCpuCount() catch 1,
+ (std.math.maxInt(u32) - 2) / 3,
+ ),
+ .limit => 1,
+ };
+ const instances = try gpa.alloc(Instance, n_instances);
+ errdefer gpa.free(instances);
+ const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3);
+ errdefer gpa.free(batch_storage);
+
+ @memset(instances, .{
+ .child = undefined,
+ .message = .empty,
+ .broadcast_written = undefined,
+ .stderr = .empty,
+ .stdin_vec = undefined,
+ .stdout_vec = undefined,
+ .stderr_vec = undefined,
+ .progress_node = undefined,
+ });
+ for (0.., instances) |id, *instance| {
+ errdefer for (instances[0..id]) |*spawned| {
+ spawned.child.kill(io);
+ spawned.progress_node.end();
+ };
+ instance.child = try process.spawn(io, spawn_options);
+ instance.progress_node = progress_node.start("starting fuzzer", 0);
+ }
+
+ return .{
+ .run = run,
+ .run_index = run_index,
+ .ctx = ctx,
+ .coverage_id = null,
+
+ .instances = instances,
+ .batch = .init(batch_storage),
+ .pending_broadcasts = .empty,
+ .broadcast = .empty,
+ .broadcast_undelivered = 0,
+ };
+ }
+
+ fn deinit(f: *FuzzTestRunner) void {
+ const maker = f.ctx.fuzz.maker;
+ const run_index = f.run_index;
+
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const io = graph.io;
+ const step = maker.stepByIndex(run_index);
+
+ f.batch.cancel(io);
+ gpa.free(f.batch.storage);
+ var total_rss: usize = 0;
+ for (f.instances) |*instance| {
+ instance.child.kill(io);
+ instance.message.deinit(gpa);
+ instance.stderr.deinit(gpa);
+ instance.progress_node.end();
+ total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0;
+ }
+ step.result_peak_rss = @max(step.result_peak_rss, total_rss);
+ gpa.free(f.instances);
+ }
+
+ fn startInstances(f: *FuzzTestRunner) !void {
+ const maker = f.ctx.fuzz.maker;
+ const run_index = f.run_index;
+ const run = f.run;
+
+ const graph = maker.graph;
+ const io = graph.io;
+ const step = maker.stepByIndex(run_index);
+
+ for (0.., f.instances) |id, *instance| {
+ const id32: u32 = @intCast(id);
+ (switch (f.ctx.fuzz.mode) {
+ .forever => sendRunFuzzTestMessage(
+ io,
+ instance.child.stdin.?,
+ run.fuzz_tests.items,
+ .forever,
+ id32,
+ ),
+ .limit => |limit| sendRunFuzzTestMessage(
+ io,
+ instance.child.stdin.?,
+ run.fuzz_tests.items,
+ .iterations,
+ limit.amount,
+ ),
+ }) catch |write_err| {
+ // The runner unexpectedly closed stdin, which means it crashed during initialization.
+ // Clean up everything and wait for the child to exit.
+ instance.child.stdin.?.close(io);
+ instance.child.stdin = null;
+ const term = try instance.child.wait(io);
+ return step.fail(
+ maker,
+ "unable to write stdin ({t}); test process unexpectedly {f}",
+ .{ write_err, fmtTerm(term) },
+ );
+ };
+
+ try f.addStdoutRead(id32, @sizeOf(InHeader));
+ try f.addStderrRead(id32);
+ }
+ }
+
+ fn listen(f: *FuzzTestRunner, arena: Allocator) !void {
+ const maker = f.ctx.fuzz.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+
+ while (true) {
+ try f.batch.awaitConcurrent(io, .none);
+ while (f.batch.next()) |completion| {
+ const id = completion.index / 3;
+ const result = completion.result;
+ switch (completion.index % 3) {
+ 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) {
+ // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
+ // that all stderr is collected.
+ error.BrokenPipe => continue,
+ else => |write_e| return write_e,
+ }),
+ 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) {
+ // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
+ // that all stderr is collected.
+ error.EndOfStream => continue,
+ else => |read_e| return read_e,
+ }),
+ 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) {
+ error.EndOfStream => return f.instanceEos(arena, id),
+ else => |read_e| return read_e,
+ }),
+ else => unreachable,
+ }
+ }
+ }
+ }
+
+ fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
+ const maker = f.ctx.fuzz.maker;
+ const instance = &f.instances[id];
+ const run_index = f.run_index;
+ const run = f.run;
+
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const io = graph.io;
+ const step = maker.stepByIndex(run_index);
+
+ instance.message.items.len += n;
+ const total_read = instance.message.items.len;
+ if (total_read < @sizeOf(InHeader)) {
+ try f.addStdoutRead(id, @sizeOf(InHeader));
+ return;
+ }
+
+ const header = instance.messageHeader();
+ const body = instance.message.items[@sizeOf(InHeader)..];
+ if (body.len != header.bytes_len) {
+ try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len);
+ return;
+ }
+
+ switch (header.tag) {
+ .zig_version => {
+ if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail(
+ maker,
+ "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
+ .{ builtin.zig_version_string, body },
+ );
+ },
+ .coverage_id => {
+ var body_r: Io.Reader = .fixed(body);
+ f.coverage_id = body_r.takeInt(u64, .little) catch unreachable;
+ const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable;
+ const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable;
+ const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable;
+
+ const fuzz = f.ctx.fuzz;
+ fuzz.queue_mutex.lockUncancelable(io);
+ defer fuzz.queue_mutex.unlock(io);
+ try fuzz.msg_queue.append(gpa, .{ .coverage = .{
+ .id = f.coverage_id.?,
+ .cumulative = .{
+ .runs = cumulative_runs,
+ .unique = cumulative_unique,
+ .coverage = cumulative_coverage,
+ },
+ .run = run_index,
+ } });
+ fuzz.queue_cond.signal(io);
+ },
+ .fuzz_start_addr => {
+ var body_r: Io.Reader = .fixed(body);
+ const fuzz = f.ctx.fuzz;
+ const addr = body_r.takeInt(u64, .little) catch unreachable;
+
+ fuzz.queue_mutex.lockUncancelable(io);
+ defer fuzz.queue_mutex.unlock(io);
+ try fuzz.msg_queue.append(gpa, .{ .entry_point = .{
+ .addr = addr,
+ .coverage_id = f.coverage_id.?,
+ } });
+ fuzz.queue_cond.signal(io);
+ },
+ .fuzz_test_change => {
+ const test_i = std.mem.readInt(u32, body[0..4], .little);
+ instance.progress_node.setName(run.fuzz_tests.items[test_i]);
+ },
+ .broadcast_fuzz_input => {
+ if (f.instances.len == 1) {
+ // No other processes to broadcast to.
+ } else if (f.broadcast_undelivered == 0) {
+ try f.instanceBroadcast(id, body);
+ } else {
+ const footer: PendingBroadcastFooter = .{
+ .from_id = id,
+ .body_len = @intCast(body.len),
+ };
+ // There is another broadcast in progress so add this one to the queue.
+ const size = @sizeOf(PendingBroadcastFooter) + body.len;
+ try f.pending_broadcasts.ensureUnusedCapacity(gpa, size);
+ f.pending_broadcasts.appendSliceAssumeCapacity(body);
+ f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer));
+ }
+ },
+ else => {}, // ignore other messages
+ }
+
+ instance.message.clearRetainingCapacity();
+ try f.addStdoutRead(id, @sizeOf(InHeader));
+ }
+
+ fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
+ const instance = &f.instances[id];
+ instance.stderr.items.len += n;
+ try f.addStderrRead(id);
+ }
+
+ fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void {
+ const instance = &f.instances[id];
+
+ instance.broadcast_written += n;
+ if (instance.broadcast_written == f.broadcast.items.len) {
+ f.broadcast_undelivered -= 1;
+ if (f.broadcast_undelivered == 0) {
+ try f.broadcastComplete();
+ }
+ } else {
+ f.addStdinWrite(id);
+ }
+ }
+
+ fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void {
+ const maker = f.ctx.fuzz.maker;
+ const gpa = maker.gpa;
+ const instance = &f.instances[id];
+
+ try instance.message.ensureTotalCapacity(gpa, end);
+ const start = instance.message.items.len;
+ instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]};
+ f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{
+ .file = instance.child.stdout.?,
+ .data = &instance.stdout_vec,
+ } });
+ }
+
+ fn addStderrRead(f: *FuzzTestRunner, id: u32) !void {
+ const maker = f.ctx.fuzz.maker;
+ const gpa = maker.gpa;
+ const instance = &f.instances[id];
+
+ try instance.stderr.ensureUnusedCapacity(gpa, 1);
+ instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()};
+ f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{
+ .file = instance.child.stderr.?,
+ .data = &instance.stderr_vec,
+ } });
+ }
+
+ fn addStdinWrite(f: *FuzzTestRunner, id: u32) void {
+ const instance = &f.instances[id];
+
+ assert(f.broadcast.items.len != instance.broadcast_written);
+ instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]};
+ f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{
+ .file = instance.child.stdin.?,
+ .data = &instance.stdin_vec,
+ } });
+ }
+
+ fn instanceEos(f: *FuzzTestRunner, arena: Allocator, id: u32) !void {
+ const maker = f.ctx.fuzz.maker;
+ const instance = &f.instances[id];
+ const run_index = f.run_index;
+
+ const graph = maker.graph;
+ const io = graph.io;
+ const step = maker.stepByIndex(run_index);
+
+ instance.child.stdin.?.close(io);
+ instance.child.stdin = null;
+ const term = try instance.child.wait(io);
+ if (!termMatches(.{ .exited = 0 }, term)) {
+ step.result_stderr = try f.mergedStderr(arena);
+ try f.saveCrash(id, term);
+ return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)});
+ }
+ }
+
+ fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void {
+ const fuzz = f.ctx.fuzz;
+ const run_index = f.run_index;
+ const run = f.run;
+
+ const maker = fuzz.maker;
+ const step = maker.stepByIndex(run_index);
+ const graph = maker.graph;
+ const io = graph.io;
+ const cache_root = graph.local_cache_root;
+
+ if (f.coverage_id == null) return;
+
+ // Search for the input file corresponding to the instance
+ const InputHeader = std.Build.abi.fuzz.MmapInputHeader;
+ var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined;
+ var in_r: Io.File.Reader = undefined;
+ var in_f: Io.File = undefined;
+ var in_name_buf: [12]u8 = undefined;
+ var in_name: []const u8 = undefined;
+ var i: u32 = 0;
+ const header: InputHeader = while (true) : ({
+ if (i == std.math.maxInt(u32)) return;
+ i += 1;
+ }) {
+ const name_prefix = "f" ++ Dir.path.sep_str ++ "in";
+ in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable;
+ in_f = cache_root.handle.openFile(io, in_name, .{
+ .lock = .exclusive,
+ .lock_nonblocking = true,
+ }) catch |e| switch (e) {
+ error.FileNotFound => return,
+ error.WouldBlock => continue, // Can not be from
+ // the crashed instance since it is still locked.
+ else => return step.fail(maker, "failed to open file '{f}{s}': {t}", .{
+ cache_root, in_name, e,
+ }),
+ };
+
+ in_r = in_f.readerStreaming(io, &in_r_buf);
+ const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| {
+ in_f.close(io);
+ switch (e) {
+ error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{
+ cache_root, in_name, in_r.err.?,
+ }),
+ error.EndOfStream => continue,
+ }
+ };
+
+ if (header.pc_digest == f.coverage_id.? and
+ header.instance_id == id and
+ header.test_i < run.fuzz_tests.items.len)
+ {
+ break header;
+ }
+
+ in_f.close(io);
+ };
+ defer in_f.close(io);
+
+ // Save it to a seperate file
+ const crash_name = "f" ++ Dir.path.sep_str ++ "crash";
+ const out = cache_root.handle.createFile(io, crash_name, .{
+ .lock = .exclusive, // Multiple run steps could have found a crash at the same time
+ }) catch |e| return step.fail(maker, "failed to create file '{f}{s}': {t}", .{
+ cache_root, crash_name, e,
+ });
+ defer out.close(io);
+
+ var out_w_buf: [512]u8 = undefined;
+ var out_w = out.writerStreaming(io, &out_w_buf);
+ _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) {
+ error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{
+ cache_root, in_name, in_r.err.?,
+ }),
+ error.WriteFailed => return step.fail(maker, "failed to write file '{f}{s}': {t}", .{
+ cache_root, crash_name, out_w.err.?,
+ }),
+ };
+
+ return step.fail(maker, "test '{s}' {f}; input saved to '{f}{s}'", .{
+ run.fuzz_tests.items[header.test_i],
+ fmtTerm(term),
+ cache_root,
+ crash_name,
+ });
+ }
+
+ fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void {
+ assert(f.instances.len > 1);
+ assert(f.broadcast_undelivered == 0); // no other broadcast is progress
+ assert(f.broadcast.items.len == 0);
+ assert(from_id < f.instances.len);
+
+ const maker = f.ctx.fuzz.maker;
+ const gpa = maker.gpa;
+
+ var out_header: OutHeader = .{
+ .tag = .new_fuzz_input,
+ .bytes_len = @intCast(bytes.len),
+ };
+ if (std.builtin.Endian.native != .little) {
+ std.mem.byteSwapAllFields(OutHeader, &out_header);
+ }
+ try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len);
+ f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header));
+ f.broadcast.appendSliceAssumeCapacity(bytes);
+
+ f.broadcast_undelivered = @intCast(f.instances.len - 1);
+ for (0.., f.instances) |to_id, *instance| {
+ if (to_id == from_id) continue;
+ instance.broadcast_written = 0;
+ f.addStdinWrite(@intCast(to_id));
+ }
+ }
+
+ fn broadcastComplete(f: *FuzzTestRunner) !void {
+ assert(f.instances.len > 1);
+ assert(f.broadcast_undelivered == 0);
+ f.broadcast.clearRetainingCapacity();
+
+ const pending = &f.pending_broadcasts;
+ if (pending.items.len != 0) {
+ // Another broadcast is pending; copy it over to `broadcast`
+
+ const footer_len = @sizeOf(PendingBroadcastFooter);
+ const footer_bytes = pending.items[pending.items.len - footer_len ..];
+ const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes);
+ pending.items.len -= footer_len;
+
+ const body = pending.items[pending.items.len - footer.body_len ..];
+ try f.instanceBroadcast(footer.from_id, body);
+ pending.items.len -= body.len;
+ }
+ }
+
+ fn mergedStderr(f: *FuzzTestRunner, arena: Allocator) Allocator.Error![]const u8 {
+ // Collect any available stderr
+ while (f.batch.next()) |completion| {
+ if (completion.index % 3 != 2) continue;
+ const len = completion.result.file_read_streaming catch continue;
+ f.instances[completion.index / 3].stderr.items.len += len;
+ }
+
+ var stderr_len: usize = 0;
+ for (f.instances) |*instance| stderr_len += instance.stderr.items.len;
+ const stderr = try arena.alloc(u8, stderr_len);
+
+ stderr_len = 0;
+ for (f.instances) |*instance| {
+ @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items);
+ stderr_len += instance.stderr.items.len;
+ }
+ return stderr;
+ }
+};
+
+fn evalFuzzTest(
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ progress_node: std.Progress.Node,
+ spawn_options: process.SpawnOptions,
+ fuzz_context: FuzzContext,
+) !void {
+ var f: FuzzTestRunner = try .init(run, run_index, fuzz_context, progress_node, spawn_options);
+ defer f.deinit();
+ try f.startInstances();
+ try f.listen(fuzz_context.fuzz.maker.graph.arena);
+}
+
+const StdioPollEnum = enum { stdout, stderr };
+
+fn evalZigTest(
+ arena: Allocator,
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+ spawn_options: process.SpawnOptions,
+ fuzz_context: ?FuzzContext,
+) !void {
+ if (fuzz_context != null) {
+ try evalFuzzTest(run, run_index, progress_node, spawn_options, fuzz_context.?);
+ return;
+ }
+
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const io = graph.io;
+ const step = maker.stepByIndex(run_index);
+
+ // We will update this every time a child runs.
+ step.result_peak_rss = 0;
+
+ var test_results: Step.TestResults = .{
+ .test_count = 0,
+ .skip_count = 0,
+ .fail_count = 0,
+ .crash_count = 0,
+ .timeout_count = 0,
+ .leak_count = 0,
+ .log_err_count = 0,
+ };
+ var test_metadata: ?TestMetadata = null;
+
+ while (true) {
+ var child = try process.spawn(io, spawn_options);
+ var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
+ var multi_reader: Io.File.MultiReader = undefined;
+ multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
+ var child_killed = false;
+ defer if (!child_killed) {
+ child.kill(io);
+ multi_reader.deinit();
+ step.result_peak_rss = @max(
+ step.result_peak_rss,
+ child.resource_usage_statistics.getMaxRss() orelse 0,
+ );
+ };
+
+ switch (try waitZigTest(
+ arena,
+ run,
+ run_index,
+ maker,
+ &child,
+ progress_node,
+ &multi_reader,
+ &test_metadata,
+ &test_results,
+ )) {
+ .write_failed => |err| {
+ // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured
+ // all available stderr to make our error output as useful as possible.
+ const stderr_fr = multi_reader.fileReader(1);
+ while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) {
+ error.ReadFailed => return stderr_fr.err.?,
+ error.EndOfStream => {},
+ }
+ step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered());
+
+ // Clean up everything and wait for the child to exit.
+ child.stdin.?.close(io);
+ child.stdin = null;
+ multi_reader.deinit();
+ child_killed = true;
+ const term = try child.wait(io);
+ step.result_peak_rss = @max(
+ step.result_peak_rss,
+ child.resource_usage_statistics.getMaxRss() orelse 0,
+ );
+
+ // The individual unit test results are irrelevant: the test runner itself broke!
+ // Fail immediately without populating `s.test_results`.
+ return step.fail(maker, "unable to write stdin ({t}); test process unexpectedly {f}", .{
+ err, fmtTerm(term),
+ });
+ },
+ .no_poll => |no_poll| {
+ // This might be a success (we requested exit and the child dutifully closed stdout) or
+ // a crash of some kind. Either way, the child will terminate by itself -- wait for it.
+ const stderr_reader = multi_reader.reader(1);
+ const stderr_owned = try arena.dupe(u8, stderr_reader.buffered());
+
+ // Clean up everything and wait for the child to exit.
+ child.stdin.?.close(io);
+ child.stdin = null;
+ multi_reader.deinit();
+ child_killed = true;
+ const term = try child.wait(io);
+ step.result_peak_rss = @max(
+ step.result_peak_rss,
+ child.resource_usage_statistics.getMaxRss() orelse 0,
+ );
+
+ if (no_poll.active_test_index) |test_index| {
+ // A test was running, so this is definitely a crash. Report it against that
+ // test, and continue to the next test.
+ test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed;
+ test_results.crash_count += 1;
+ try step.addError(maker, "'{s}' {f}{s}{s}", .{
+ test_metadata.?.testName(test_index),
+ fmtTerm(term),
+ if (stderr_owned.len != 0) " with stderr:\n" else "",
+ std.mem.trim(u8, stderr_owned, "\n"),
+ });
+ continue;
+ }
+
+ // Report an error if the child terminated uncleanly or if we were still trying to run more tests.
+ step.result_stderr = stderr_owned;
+ const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32);
+ if (!tests_done or !termMatches(.{ .exited = 0 }, term)) {
+ // The individual unit test results are irrelevant: the test runner itself broke!
+ // Fail immediately without populating `s.test_results`.
+ return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)});
+ }
+
+ // We're done with all of the tests! Commit the test results and return.
+ step.test_results = test_results;
+ if (test_metadata) |tm| {
+ run.cached_test_metadata = tm.toCachedTestMetadata();
+ if (maker.web_server) |*ws| {
+ if (graph.time_report) {
+ ws.updateTimeReportRunTest(
+ run_index,
+ &run.cached_test_metadata.?,
+ tm.ns_per_test,
+ );
+ }
+ }
+ }
+ return;
+ },
+ .timeout => |timeout| {
+ const stderr_reader = multi_reader.reader(1);
+ const stderr = stderr_reader.buffered();
+ stderr_reader.tossBuffered();
+ if (timeout.active_test_index) |test_index| {
+ // A test was running. Report the timeout against that test, and continue on to
+ // the next test.
+ test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed;
+ test_results.timeout_count += 1;
+ try step.addError(maker, "'{s}' timed out after {f}{s}{s}", .{
+ test_metadata.?.testName(test_index),
+ Io.Duration{ .nanoseconds = timeout.ns_elapsed },
+ if (stderr.len != 0) " with stderr:\n" else "",
+ std.mem.trim(u8, stderr, "\n"),
+ });
+ continue;
+ }
+ // Just log an error and let the child be killed.
+ step.result_stderr = try arena.dupe(u8, stderr);
+ // The individual unit test results in `results` are irrelevant: the test runner
+ // is broken! Fail immediately without populating `s.test_results`.
+ return step.fail(maker, "test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }});
+ },
+ }
+ comptime unreachable;
+ }
+}
+
+const TestMetadata = struct {
+ names: []const u32,
+ ns_per_test: []u64,
+ expected_panic_msgs: []const u32,
+ string_bytes: []const u8,
+ next_index: u32,
+ prog_node: std.Progress.Node,
+
+ fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata {
+ return .{
+ .names = tm.names,
+ .string_bytes = tm.string_bytes,
+ };
+ }
+
+ fn testName(tm: TestMetadata, index: u32) []const u8 {
+ return tm.toCachedTestMetadata().testName(index);
+ }
+};
+
+pub const CachedTestMetadata = struct {
+ names: []const u32,
+ string_bytes: []const u8,
+
+ pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 {
+ return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0);
+ }
+};
+
+fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
+ while (metadata.next_index < metadata.names.len) {
+ const i = metadata.next_index;
+ metadata.next_index += 1;
+
+ if (metadata.expected_panic_msgs[i] != 0) continue;
+
+ const name = metadata.testName(i);
+ if (sub_prog_node.*) |n| n.end();
+ sub_prog_node.* = metadata.prog_node.start(name, 0);
+
+ try sendRunTestMessage(io, in, .run_test, i);
+ return;
+ } else {
+ metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done
+ try sendMessage(io, in, .exit);
+ }
+}
+
+fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
+ const header: std.zig.Client.Message.Header = .{
+ .tag = tag,
+ .bytes_len = 0,
+ };
+ var w = file.writerStreaming(io, &.{});
+ w.interface.writeStruct(header, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+}
+
+fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void {
+ const header: std.zig.Client.Message.Header = .{
+ .tag = tag,
+ .bytes_len = 4,
+ };
+ var w = file.writerStreaming(io, &.{});
+ w.interface.writeStruct(header, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+}
+
+fn sendRunFuzzTestMessage(
+ io: Io,
+ file: Io.File,
+ test_names: []const []const u8,
+ kind: std.Build.abi.fuzz.LimitKind,
+ amount_or_instance: u64,
+) !void {
+ const header: std.zig.Client.Message.Header = .{
+ .tag = .start_fuzzing,
+ .bytes_len = 1 + 8 + 4 + count: {
+ var c: u32 = @intCast(test_names.len * 4);
+ for (test_names) |name| {
+ c += @intCast(name.len);
+ }
+ break :count c;
+ },
+ };
+ var w = file.writerStreaming(io, &.{});
+ w.interface.writeStruct(header, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ for (test_names) |test_name| {
+ w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ w.interface.writeAll(test_name) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ }
+}
+
+/// Uses `arena` to allocate the result.
+fn evalGeneric(
+ arena: Allocator,
+ run_index: Configuration.Step.Index,
+ maker: *Maker,
+ spawn_options: process.SpawnOptions,
+) !EvalGenericResult {
+ const graph = maker.graph;
+ const io = graph.io;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = run_index.ptr(conf);
+ const conf_run = conf_step.extended.get(conf.extra).run;
+ const step = maker.stepByIndex(run_index);
+
+ var child = try process.spawn(io, spawn_options);
+ defer child.kill(io);
+
+ switch (conf_run.stdin.u) {
+ .bytes => |bytes| {
+ child.stdin.?.writeStreamingAll(io, bytes.slice(conf)) catch |err| {
+ return step.fail(maker, "failed to write stdin: {t}", .{err});
+ };
+ child.stdin.?.close(io);
+ child.stdin = null;
+ },
+ .lazy_path => |lazy_path| {
+ const path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index);
+ const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| {
+ return step.fail(maker, "failed to open stdin file: {t}", .{err});
+ };
+ defer file.close(io);
+ // TODO https://github.com/ziglang/zig/issues/23955
+ var read_buffer: [1024]u8 = undefined;
+ var file_reader = file.reader(io, &read_buffer);
+ var write_buffer: [1024]u8 = undefined;
+ var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer);
+ _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
+ error.ReadFailed => return step.fail(maker, "failed to read from {f}: {t}", .{
+ path, file_reader.err.?,
+ }),
+ error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{
+ stdin_writer.err.?,
+ }),
+ };
+ stdin_writer.interface.flush() catch |err| switch (err) {
+ error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{
+ stdin_writer.err.?,
+ }),
+ };
+ child.stdin.?.close(io);
+ child.stdin = null;
+ },
+ .none => {},
+ }
+
+ var stdout_bytes: ?[]const u8 = null;
+ var stderr_bytes: ?[]const u8 = null;
+
+ if (child.stdout) |stdout| {
+ if (child.stderr) |stderr| {
+ var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
+ var multi_reader: Io.File.MultiReader = undefined;
+ multi_reader.init(arena, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr });
+
+ const stdout_reader = multi_reader.reader(0);
+ const stderr_reader = multi_reader.reader(1);
+
+ while (multi_reader.fill(64, .none)) |_| {
+ if (conf_run.stdio_limit.value) |limit| {
+ if (stdout_reader.buffered().len > limit)
+ return error.StdoutStreamTooLong;
+ if (stderr_reader.buffered().len > limit)
+ return error.StderrStreamTooLong;
+ }
+ } else |err| switch (err) {
+ error.Timeout => unreachable,
+ error.EndOfStream => {},
+ else => |e| return e,
+ }
+
+ try multi_reader.checkAnyError();
+
+ stdout_bytes = try multi_reader.toOwnedSlice(0);
+ stderr_bytes = try multi_reader.toOwnedSlice(1);
+ } else {
+ var stdout_reader = stdout.readerStreaming(io, &.{});
+ const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited(x) else .unlimited;
+ stdout_bytes = stdout_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) {
+ error.OutOfMemory => |e| return e,
+ error.ReadFailed => return stdout_reader.err.?,
+ error.StreamTooLong => return error.StdoutStreamTooLong,
+ };
+ }
+ } else if (child.stderr) |stderr| {
+ var stderr_reader = stderr.readerStreaming(io, &.{});
+ const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited(x) else .unlimited;
+ stderr_bytes = stderr_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) {
+ error.OutOfMemory => |e| return e,
+ error.ReadFailed => return stderr_reader.err.?,
+ error.StreamTooLong => return error.StderrStreamTooLong,
+ };
+ }
+
+ if (stderr_bytes) |bytes| if (bytes.len > 0) {
+ // Treat stderr as an error message.
+ const stderr_is_diagnostic = conf_run.captured_stderr.value == null and switch (conf_run.flags.stdio) {
+ .check => !checksContainStderr(&conf_run),
+ else => true,
+ };
+ if (stderr_is_diagnostic) {
+ step.result_stderr = bytes;
+ }
+ };
+
+ step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0;
+
+ return .{
+ .term = try child.wait(io),
+ .stdout = stdout_bytes,
+ .stderr = stderr_bytes,
+ };
+}
+
+const IndexedOutput = struct {
+ index: u32,
+ arg_index: Configuration.Step.Run.Arg.Index,
+};
+
+pub fn rerunInFuzzMode(
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ fuzz: *Fuzz,
+ prog_node: std.Progress.Node,
+) !void {
+ const maker = fuzz.maker;
+ const graph = maker.graph;
+ const step = maker.stepByIndex(run_index);
+ const io = graph.io;
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = run_index.ptr(conf);
+ const conf_run = conf_step.extended.get(conf.extra).run;
+ const cache_root = graph.local_cache_root;
+
+ var arena_allocator: std.heap.ArenaAllocator = .init(gpa);
+ defer arena_allocator.deinit();
+ const arena = arena_allocator.allocator();
+
+ var argv_list: std.ArrayList([]const u8) = .empty;
+ defer argv_list.deinit(gpa);
+
+ for (conf_run.args.slice) |arg_index| {
+ const arg = arg_index.get(conf);
+ try argv_list.ensureUnusedCapacity(gpa, 1);
+ switch (arg.flags.tag) {
+ .string => {
+ const prefix = arg.prefix.value.?.slice(conf);
+ argv_list.appendAssumeCapacity(prefix);
+ },
+ .path_file => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
+ argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
+ prefix, try convertPathArg(arena, run_index, maker, file_path), suffix,
+ }));
+ },
+ .path_directory => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
+ const resolved_arg = try mem.concat(arena, u8, &.{
+ prefix, try convertPathArg(arena, run_index, maker, file_path), suffix,
+ });
+ argv_list.appendAssumeCapacity(resolved_arg);
+ },
+ .file_content => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index);
+
+ var result: std.Io.Writer.Allocating = .init(arena);
+ result.writer.writeAll(prefix) catch return error.OutOfMemory;
+
+ const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err|
+ return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err });
+ defer file.close(io);
+
+ var file_reader = file.reader(io, &.{});
+ _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
+ error.ReadFailed => switch (file_reader.err.?) {
+ error.Canceled => |e| return e,
+ else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }),
+ },
+ error.WriteFailed => return error.OutOfMemory,
+ };
+ result.writer.writeAll(suffix) catch return error.OutOfMemory;
+
+ argv_list.appendAssumeCapacity(result.written());
+ },
+ .artifact => {
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const producer_index = arg.producer.value.?;
+ const producer_step = producer_index.ptr(conf);
+ const producer = producer_step.extended.get(conf.extra).compile;
+ const producer_make_comp_step = maker.stepByIndex(producer_index);
+ const producer_make_comp = &producer_make_comp_step.extended.compile;
+ const file_path: Path = if (producer_index == conf_run.producer.value.?)
+ run.rebuilt_executable.?
+ else
+ producer_make_comp.installed_path orelse
+ maker.generatedPath(producer.generated_bin.value.?).*;
+ argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{
+ prefix, try convertPathArg(arena, run_index, maker, file_path), suffix,
+ }));
+ },
+ .output_file => unreachable,
+ .output_directory => unreachable,
+ .passthru => unreachable,
+ }
+ }
+
+ if (conf_run.flags.test_runner_mode) {
+ const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root });
+
+ try argv_list.ensureUnusedCapacity(gpa, 3);
+ argv_list.appendAssumeCapacity(try allocPrint(arena, "--cache-dir={s}", .{cache_dir_string}));
+ argv_list.appendAssumeCapacity(try allocPrint(arena, "--seed=0x{x}", .{graph.random_seed}));
+ argv_list.appendAssumeCapacity("--listen=-");
+ }
+
+ step.clearFailedCommand(gpa);
+
+ const has_side_effects = false;
+ var rand_int: u64 = undefined;
+ io.random(@ptrCast(&rand_int));
+ const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
+ try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, has_side_effects, tmp_dir_path, .{
+ .fuzz = fuzz,
+ });
+}
+
+fn populateGeneratedPaths(
+ maker: *Maker,
+ output_placeholders: []const IndexedOutput,
+ cache_root: Cache.Directory,
+ digest: *const Cache.HexDigest,
+) !void {
+ const conf = &maker.scanned_config.configuration;
+ const graph = maker.graph;
+
+ for (output_placeholders) |placeholder| {
+ const arg = placeholder.arg_index.get(conf);
+ maker.generatedPath(arg.generated.value.?).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Dir.path.join(graph.arena, &.{
+ "o", digest, arg.basename.value.?.slice(conf),
+ }),
+ };
+ }
+}
+
+fn populateGeneratedPathsCreateDirs(
+ arena: Allocator,
+ run_index: Configuration.Step.Index,
+ maker: *Maker,
+ output_dir_path: []const u8,
+ output_placeholders: []const IndexedOutput,
+ argv: [][]const u8,
+) !void {
+ const step = maker.stepByIndex(run_index);
+ const conf = &maker.scanned_config.configuration;
+ const graph = maker.graph;
+ const io = graph.io;
+ const cache_root = graph.local_cache_root;
+
+ for (output_placeholders) |placeholder| {
+ const arg = placeholder.arg_index.get(conf);
+ const prefix = if (arg.prefix.value) |p| p.slice(conf) else "";
+ const suffix = if (arg.suffix.value) |p| p.slice(conf) else "";
+ const basename = arg.basename.value.?.slice(conf);
+
+ const generated_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Dir.path.join(graph.arena, &.{ output_dir_path, basename }),
+ };
+ const create_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = switch (arg.flags.tag) {
+ .output_file => Dir.path.dirname(generated_path.sub_path).?,
+ .output_directory => generated_path.sub_path,
+ else => unreachable,
+ },
+ };
+ create_path.root_dir.handle.createDirPath(io, create_path.sub_path) catch |err|
+ return step.fail(maker, "unable to make path {f}: {t}", .{ create_path, err });
+
+ maker.generatedPath(arg.generated.value.?).* = generated_path;
+
+ const arg_output_path = try convertPathArg(arena, run_index, maker, generated_path);
+ argv[placeholder.index] = try mem.concat(arena, u8, &.{ prefix, arg_output_path, suffix });
+ }
+}
+
+fn populateGeneratedStdIo(
+ maker: *Maker,
+ conf_run: *const Configuration.Step.Run,
+ cache_root: Cache.Directory,
+ digest: *const Cache.HexDigest,
+) !void {
+ const conf = &maker.scanned_config.configuration;
+ const graph = maker.graph;
+
+ if (conf_run.captured_stdout.value) |captured| {
+ maker.generatedPath(captured.generated_file).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Dir.path.join(graph.arena, &.{
+ "o", digest, captured.basename.slice(conf),
+ }),
+ };
+ }
+
+ if (conf_run.captured_stderr.value) |captured| {
+ maker.generatedPath(captured.generated_file).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Dir.path.join(graph.arena, &.{
+ "o", digest, captured.basename.slice(conf),
+ }),
+ };
+ }
+}
+
+fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
+ if (term) |t| switch (t) {
+ .exited => |code| try w.print("exited with code {d}", .{code}),
+ .signal => |sig| try w.print("terminated with signal {t}", .{sig}),
+ .stopped => |sig| try w.print("stopped with signal {t}", .{sig}),
+ .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
+ } else {
+ try w.writeAll("exited with any code");
+ }
+}
+fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) {
+ return .{ .data = term };
+}
+
+const FuzzContext = struct {
+ fuzz: *Fuzz,
+};
+
+fn runCommand(
+ arena: Allocator,
+ run: *Run,
+ run_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+ argv: []const []const u8,
+ has_side_effects: bool,
+ output_dir_path: []const u8,
+ fuzz_context: ?FuzzContext,
+) Step.ExtendedMakeError!void {
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const step = maker.stepByIndex(run_index);
+ const io = graph.io;
+ const cache_root = graph.local_cache_root;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = run_index.ptr(conf);
+ const conf_run = conf_step.extended.get(conf.extra).run;
+
+ const cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd|
+ .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) }
+ else
+ .inherit;
+
+ const allow_skip = switch (conf_run.flags.stdio) {
+ .check, .zig_test => conf_run.flags.skip_foreign_checks,
+ else => false,
+ };
+
+ var interp_argv: std.ArrayList([]const u8) = .empty;
+
+ var environ_map: std.process.Environ.Map = .init(gpa);
+ defer environ_map.deinit();
+
+ // In either case we add to this mutatable data structure so that we can
+ // tweak the environment below.
+ if (conf_run.environ_map.value) |env_map_index| {
+ const conf_env_map = env_map_index.get(conf);
+ for (conf_env_map.keys.slice(conf), conf_env_map.values.slice(conf)) |k, v| {
+ try environ_map.put(k.slice(conf), v.slice(conf));
+ }
+ } else {
+ try environ_map.putAll(&graph.environ_map);
+ }
+
+ // Now that we have the environ map, we might need to mutate it to insert
+ // .dll search paths because Windows doesn't have rpaths.
+ const arg0 = conf_run.args.slice[0].get(conf);
+ if (arg0.producer.value) |producer_index| {
+ const producer_step = producer_index.ptr(conf);
+ const producer = producer_step.extended.get(conf.extra).compile;
+ const root_module = producer.root_module.get(conf);
+ const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf);
+ if (root_module_target.flags.os_tag == .windows) {
+ try addPathForDynLibs(maker, arena, producer_index, &environ_map, argv[0]);
+ }
+ }
+
+ const cwd_string = switch (cwd) {
+ .path => |p| p,
+ .dir => unreachable,
+ .inherit => null,
+ };
+ try graph.handleVerbose(cwd_string, &environ_map, argv);
+
+ const opt_generic_result = spawnChildAndCollect(
+ arena,
+ run_index,
+ run,
+ maker,
+ progress_node,
+ argv,
+ &environ_map,
+ has_side_effects,
+ fuzz_context,
+ ) catch |err| term: {
+ switch (err) {
+ error.InvalidExe, // cpu arch mismatch
+ error.FileNotFound, // can happen with a wrong dynamic linker path
+ => interpret: {
+ const producer_index = arg0.producer.value orelse break :interpret;
+ const producer_step = producer_index.ptr(conf);
+ const producer = producer_step.extended.get(conf.extra).compile;
+ switch (producer.flags3.kind) {
+ .exe, .@"test" => {},
+ else => break :interpret,
+ }
+ const root_module = producer.root_module.get(conf);
+ const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf);
+ const other_target_query = root_module_target.unwrap(conf);
+ const root_target = std.zig.system.resolveTargetQuery(io, other_target_query) catch unreachable;
+ const link_libc = maker.stepByIndex(producer_index).extended.compile.is_linking_libc;
+
+ const host: std.Target = std.zig.system.resolveTargetQuery(io, .{}) catch |he| switch (he) {
+ error.Canceled => |e| return e,
+ else => builtin.target,
+ };
+
+ const need_cross_libc = link_libc and root_target.os.tag == .linux and
+ switch (producer.flags2.linkage) {
+ .static => false,
+ .dynamic => true,
+ .default => root_target.isGnuLibC(),
+ };
+ switch (std.zig.system.getExternalExecutor(io, &root_target, .{
+ .host_cpu_arch = host.cpu.arch,
+ .host_os_tag = host.os.tag,
+ .qemu_fixes_dl = need_cross_libc and graph.libc_runtimes_dir != null,
+ .link_libc = link_libc,
+ })) {
+ .native, .rosetta => {
+ if (allow_skip) return error.MakeSkipped;
+ break :interpret;
+ },
+ .wine => |bin_name| {
+ if (graph.enable_wine) {
+ try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len);
+ interp_argv.appendAssumeCapacity(bin_name);
+ interp_argv.appendSliceAssumeCapacity(argv);
+
+ // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but
+ // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.
+ if (environ_map.get("WINEDEBUG") == null) {
+ try environ_map.put("WINEDEBUG", "-all");
+ }
+ } else {
+ return failForeign(arena, &conf_run, maker, run_index, "-fwine", argv[0], &root_target, &host);
+ }
+ },
+ .qemu => |bin_name| {
+ if (graph.enable_qemu) {
+ try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len);
+ interp_argv.appendAssumeCapacity(bin_name);
+
+ if (need_cross_libc) {
+ if (graph.libc_runtimes_dir) |dir| {
+ interp_argv.appendAssumeCapacity("-L");
+ interp_argv.appendAssumeCapacity(try Dir.path.join(arena, &.{
+ dir,
+ try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple(
+ arena,
+ root_target.cpu.arch,
+ root_target.os.tag,
+ root_target.abi,
+ ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple(
+ arena,
+ root_target.cpu.arch,
+ root_target.abi,
+ ) else unreachable,
+ }));
+ } else return failForeign(arena, &conf_run, maker, run_index, "--libc-runtimes", argv[0], &root_target, &host);
+ }
+
+ interp_argv.appendSliceAssumeCapacity(argv);
+ } else return failForeign(arena, &conf_run, maker, run_index, "-fqemu", argv[0], &root_target, &host);
+ },
+ .darling => |bin_name| {
+ if (graph.enable_darling) {
+ try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len);
+ interp_argv.appendAssumeCapacity(bin_name);
+ interp_argv.appendSliceAssumeCapacity(argv);
+ } else {
+ return failForeign(arena, &conf_run, maker, run_index, "-fdarling", argv[0], &root_target, &host);
+ }
+ },
+ .wasmtime => |bin_name| {
+ if (graph.enable_wasmtime) {
+ try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len);
+ interp_argv.appendAssumeCapacity(bin_name);
+ interp_argv.appendAssumeCapacity("--dir=.");
+ // Wasmtime doeesn't inherit environment variables from the parent process
+ // by default. '-S inherit-env' was added in Wasmtime version 20.
+ interp_argv.appendAssumeCapacity("-Sinherit-env");
+ interp_argv.appendSliceAssumeCapacity(argv);
+ } else {
+ return failForeign(arena, &conf_run, maker, run_index, "-fwasmtime", argv[0], &root_target, &host);
+ }
+ },
+ .bad_dl => |foreign_dl| {
+ if (allow_skip) return error.MakeSkipped;
+
+ const host_dl = host.dynamic_linker.get() orelse "(none)";
+
+ return step.fail(maker,
+ \\the host system is unable to execute binaries from the target
+ \\ because the host dynamic linker is '{s}',
+ \\ while the target dynamic linker is '{s}'.
+ \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step
+ , .{ host_dl, foreign_dl });
+ },
+ .bad_os_or_cpu => {
+ if (allow_skip) return error.MakeSkipped;
+
+ const host_name = try host.zigTriple(arena);
+ const foreign_name = try root_target.zigTriple(arena);
+
+ return step.fail(maker, "the host system ({s}) is unable to execute binaries from the target ({s})", .{
+ host_name, foreign_name,
+ });
+ },
+ }
+
+ step.clearFailedCommand(gpa);
+ try graph.handleVerbose(cwd_string, &environ_map, interp_argv.items);
+
+ break :term spawnChildAndCollect(
+ arena,
+ run_index,
+ run,
+ maker,
+ progress_node,
+ interp_argv.items,
+ &environ_map,
+ has_side_effects,
+ fuzz_context,
+ ) catch |e| {
+ if (!conf_run.flags.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
+ if (e == error.MakeFailed) return error.MakeFailed; // error already reported
+ return step.fail(maker, "unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e });
+ };
+ },
+ error.MakeFailed, error.OutOfMemory, error.Canceled => |e| return e,
+ else => {},
+ }
+ return step.fail(maker, "failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err });
+ };
+
+ const generic_result = opt_generic_result orelse {
+ assert(conf_run.flags.stdio == .zig_test);
+ // Specific errors have already been reported, and test results are populated. All we need
+ // to do is report step failure if any test failed.
+ if (!step.test_results.isSuccess()) return error.MakeFailed;
+ return;
+ };
+
+ assert(fuzz_context == null);
+ assert(conf_run.flags.stdio != .zig_test);
+
+ // Capture stdout and stderr to GeneratedFile objects.
+ const Stream = struct {
+ captured: ?Configuration.Step.Run.CapturedStream,
+ bytes: ?[]const u8,
+ trim_whitespace: Configuration.Step.Run.TrimWhitespace,
+ };
+ for (&[_]Stream{
+ .{
+ .captured = conf_run.captured_stdout.value,
+ .bytes = generic_result.stdout,
+ .trim_whitespace = conf_run.flags.stdout_trim_whitespace,
+ },
+ .{
+ .captured = conf_run.captured_stderr.value,
+ .bytes = generic_result.stderr,
+ .trim_whitespace = conf_run.flags.stderr_trim_whitespace,
+ },
+ }) |*stream| {
+ if (stream.captured) |captured| {
+ const output_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Dir.path.join(graph.arena, &.{
+ output_dir_path, captured.basename.slice(conf),
+ }),
+ };
+ maker.generatedPath(captured.generated_file).* = output_path;
+
+ const sub_path_parent = output_path.dirname().?;
+ sub_path_parent.root_dir.handle.createDirPath(io, sub_path_parent.sub_path) catch |err|
+ return step.fail(maker, "unable to make path {f}: {t}", .{ sub_path_parent, err });
+
+ const data = switch (stream.trim_whitespace) {
+ .none => stream.bytes.?,
+ .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace),
+ .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace),
+ .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace),
+ };
+ output_path.root_dir.handle.writeFile(io, .{
+ .sub_path = output_path.sub_path,
+ .data = data,
+ }) catch |err| return step.fail(maker, "unable to write file {f}: {t}", .{ output_path, err });
+ }
+ }
+
+ switch (conf_run.flags.stdio) {
+ .zig_test => unreachable,
+ .check => {
+ if (conf_run.expect_stderr_exact.value) |bytes| {
+ const expected_bytes = bytes.slice(conf);
+ if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) {
+ return step.fail(maker,
+ \\========= expected this stderr: =========
+ \\{s}
+ \\========= but found: ====================
+ \\{s}
+ , .{
+ expected_bytes,
+ generic_result.stderr.?,
+ });
+ }
+ }
+ if (conf_run.expect_stdout_exact.value) |bytes| {
+ const expected_bytes = bytes.slice(conf);
+ if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) {
+ return step.fail(maker,
+ \\========= expected this stdout: =========
+ \\{s}
+ \\========= but found: ====================
+ \\{s}
+ , .{
+ expected_bytes,
+ generic_result.stdout.?,
+ });
+ }
+ }
+ for (conf_run.expect_stderr_match.slice) |bytes| {
+ const match = bytes.slice(conf);
+ if (mem.find(u8, generic_result.stderr.?, match) == null) {
+ return step.fail(maker,
+ \\========= expected to find in stderr: =========
+ \\{s}
+ \\========= but stderr does not contain it: =====
+ \\{s}
+ , .{
+ match,
+ generic_result.stderr.?,
+ });
+ }
+ }
+ for (conf_run.expect_stdout_match.slice) |bytes| {
+ const match = bytes.slice(conf);
+ if (mem.find(u8, generic_result.stdout.?, match) == null) {
+ return step.fail(maker,
+ \\========= expected to find in stdout: =========
+ \\{s}
+ \\========= but stdout does not contain it: =====
+ \\{s}
+ , .{
+ match,
+ generic_result.stdout.?,
+ });
+ }
+ }
+ if (conf_run.expect_term_value.value) |expected_term_value| {
+ const expected_term: process.Child.Term = switch (conf_run.flags2.expect_term_status) {
+ .exited => .{ .exited = @intCast(expected_term_value) },
+ .signal => .{ .signal = @enumFromInt(expected_term_value) },
+ .stopped => .{ .stopped = @enumFromInt(expected_term_value) },
+ .unknown => .{ .unknown = expected_term_value },
+ };
+ if (!termMatches(expected_term, generic_result.term)) {
+ return step.fail(maker, "process {f} (expected {f})", .{
+ fmtTerm(generic_result.term),
+ fmtTerm(expected_term),
+ });
+ }
+ }
+ },
+ else => {
+ // On failure, report captured stderr like normal standard error output.
+ const bad_exit = switch (generic_result.term) {
+ .exited => |code| code != 0,
+ .signal, .stopped, .unknown => true,
+ };
+ if (bad_exit) {
+ if (generic_result.stderr) |bytes| {
+ step.result_stderr = bytes;
+ }
+ }
+
+ try step.handleChildProcessTerm(maker, generic_result.term);
+ },
+ }
+}
+
+const EvalGenericResult = struct {
+ term: process.Child.Term,
+ stdout: ?[]const u8,
+ stderr: ?[]const u8,
+};
+
+fn spawnChildAndCollect(
+ arena: Allocator,
+ run_index: Configuration.Step.Index,
+ run: *Run,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+ argv: []const []const u8,
+ environ_map: *EnvMap,
+ has_side_effects: bool,
+ fuzz_context: ?FuzzContext,
+) !?EvalGenericResult {
+ const step = maker.stepByIndex(run_index);
+ const graph = maker.graph;
+ const io = graph.io;
+ const gpa = maker.gpa;
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = run_index.ptr(conf);
+ const conf_run = conf_step.extended.get(conf.extra).run;
+
+ if (fuzz_context != null) {
+ assert(!has_side_effects);
+ assert(conf_run.flags.stdio == .zig_test);
+ }
+
+ const child_cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd|
+ .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) }
+ else
+ .inherit;
+
+ // If an error occurs, it's caused by this command:
+ step.clearFailedCommand(gpa);
+ step.result_failed_command = try std.zig.allocPrintCmd(gpa, argv, .{
+ .cwd = switch (child_cwd) {
+ .path => |p| p,
+ .dir => unreachable,
+ .inherit => null,
+ },
+ .child_env = environ_map,
+ .parent_env = &graph.environ_map,
+ });
+
+ try step.handleChildProcUnsupported(maker);
+
+ var spawn_options: process.SpawnOptions = .{
+ .argv = argv,
+ .cwd = child_cwd,
+ .environ_map = environ_map,
+ .request_resource_usage_statistics = true,
+ .stdin = if (conf_run.stdin.u != .none) s: {
+ assert(conf_run.flags.stdio != .inherit);
+ break :s .pipe;
+ } else switch (conf_run.flags.stdio) {
+ .infer_from_args => if (has_side_effects) .inherit else .ignore,
+ .inherit => .inherit,
+ .check => .ignore,
+ .zig_test => .pipe,
+ },
+ .stdout = if (conf_run.captured_stdout.value != null) .pipe else switch (conf_run.flags.stdio) {
+ .infer_from_args => if (has_side_effects) .inherit else .ignore,
+ .inherit => .inherit,
+ .check => if (checksContainStdout(&conf_run)) .pipe else .ignore,
+ .zig_test => .pipe,
+ },
+ .stderr = if (conf_run.captured_stderr.value != null) .pipe else switch (conf_run.flags.stdio) {
+ .infer_from_args => if (has_side_effects) .inherit else .pipe,
+ .inherit => .inherit,
+ .check => .pipe,
+ .zig_test => .pipe,
+ },
+ };
+
+ if (conf_run.flags.stdio == .zig_test) {
+ const started: Io.Clock.Timestamp = .now(io, .awake);
+ const result = evalZigTest(graph.arena, run, run_index, maker, progress_node, spawn_options, fuzz_context) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ else => |e| e,
+ };
+ step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
+ try result;
+ return null;
+ } else {
+ const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit;
+ if (!conf_run.flags.disable_zig_progress and !inherit) {
+ spawn_options.progress_node = progress_node;
+ }
+ const terminal_mode: Io.Terminal.Mode = if (inherit) m: {
+ const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
+ break :m stderr.terminal_mode;
+ } else .no_color;
+ defer if (inherit) io.unlockStderr();
+ try setColorEnvironmentVariables(&conf_run, environ_map, terminal_mode);
+
+ const started: Io.Clock.Timestamp = .now(io, .awake);
+ const result = evalGeneric(graph.arena, run_index, maker, spawn_options) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ else => |e| e,
+ };
+ step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
+ return try result;
+ }
+}
+
+fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
+ return if (expected) |e| switch (e) {
+ .exited => |expected_code| switch (actual) {
+ .exited => |actual_code| expected_code == actual_code,
+ else => false,
+ },
+ .signal => |expected_sig| switch (actual) {
+ .signal => |actual_sig| expected_sig == actual_sig,
+ else => false,
+ },
+ .stopped => |expected_sig| switch (actual) {
+ .stopped => |actual_sig| expected_sig == actual_sig,
+ else => false,
+ },
+ .unknown => |expected_code| switch (actual) {
+ .unknown => |actual_code| expected_code == actual_code,
+ else => false,
+ },
+ } else switch (actual) {
+ .exited => true,
+ else => false,
+ };
+}
+
+fn setColorEnvironmentVariables(
+ conf_run: *const Configuration.Step.Run,
+ environ_map: *EnvMap,
+ terminal_mode: Io.Terminal.Mode,
+) !void {
+ color: switch (conf_run.flags.color) {
+ .manual => {},
+ .enable => {
+ try environ_map.put("CLICOLOR_FORCE", "1");
+ _ = environ_map.swapRemove("NO_COLOR");
+ },
+ .disable => {
+ try environ_map.put("NO_COLOR", "1");
+ _ = environ_map.swapRemove("CLICOLOR_FORCE");
+ },
+ .inherit => switch (terminal_mode) {
+ .no_color, .windows_api => continue :color .disable,
+ .escape_codes => continue :color .enable,
+ },
+ .auto => {
+ const capture_stderr = conf_run.captured_stderr.value != null or switch (conf_run.flags.stdio) {
+ .check => checksContainStderr(conf_run),
+ .infer_from_args, .inherit, .zig_test => false,
+ };
+ if (capture_stderr) {
+ continue :color .disable;
+ } else {
+ continue :color .inherit;
+ }
+ },
+ }
+}
+
+fn checksContainStdout(conf_run: *const Configuration.Step.Run) bool {
+ return conf_run.expect_stdout_exact.value != null or conf_run.expect_stdout_match.slice.len != 0;
+}
+
+fn checksContainStderr(conf_run: *const Configuration.Step.Run) bool {
+ return conf_run.expect_stderr_exact.value != null or conf_run.expect_stderr_match.slice.len != 0;
+}
+
+/// If `path` is cwd-relative, make it relative to the cwd of the child instead.
+///
+/// Whenever a path is included in the argv of a child, it should be put through this function first
+/// to make sure the child doesn't see paths relative to a cwd other than its own.
+fn convertPathArg(arena: Allocator, run_index: Configuration.Step.Index, maker: *Maker, path: Path) ![]const u8 {
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = run_index.ptr(conf);
+ const conf_run = conf_step.extended.get(conf.extra).run;
+ const graph = maker.graph;
+
+ const path_str = try path.toString(arena);
+ if (Dir.path.isAbsolute(path_str)) {
+ // Absolute paths don't need changing.
+ return path_str;
+ }
+ const child_cwd_rel: []const u8 = rel: {
+ const child_lazy_cwd = conf_run.cwd.value orelse break :rel path_str;
+ const child_cwd = try maker.resolveLazyPathIndexAbs(arena, child_lazy_cwd, run_index);
+ // Convert it from relative to *our* cwd, to relative to the *child's* cwd.
+ break :rel try Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str);
+ };
+ // Not every path can be made relative, e.g. if the path and the child cwd are on different
+ // disk designators on Windows. In that case, `relative` will return an absolute path which we can
+ // just return.
+ if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel;
+
+ // We're not done yet. In some cases this path must be prefixed with './':
+ // * On POSIX, the executable name cannot be a single component like 'foo'
+ // * Some executables might treat a leading '-' like a flag, which we must avoid
+ // There's no harm in it, so just *always* apply this prefix.
+ return Dir.path.join(arena, &.{ ".", child_cwd_rel });
+}
+
+fn addPathForDynLibs(
+ maker: *Maker,
+ arena: Allocator,
+ artifact: Configuration.Step.Index,
+ environ_map: *process.Environ.Map,
+ argv0: []const u8,
+) !void {
+ const conf = &maker.scanned_config.configuration;
+ const graph = maker.graph;
+ const use_wine = graph.enable_wine and builtin.os.tag != .windows and std.ascii.endsWithIgnoreCase(argv0, ".exe");
+ const path_key = if (use_wine) "WINEPATH" else "PATH";
+ const path_delimiter: u8 = if (builtin.os.tag == .windows or use_wine)
+ Dir.path.delimiter_windows
+ else
+ Dir.path.delimiter;
+
+ var module_graph: Step.Compile.ModuleGraph = .empty;
+ const compile_deps = try Step.Compile.getCompileDependencies(arena, &module_graph, conf, artifact, true);
+
+ for (compile_deps) |dep_index| {
+ const conf_comp_step = dep_index.ptr(conf);
+ const conf_comp = conf_comp_step.extended.get(conf.extra).compile;
+ const root_module = conf_comp.root_module.get(conf);
+ const target = root_module.resolved_target.get(conf).?.result.get(conf);
+ if (target.flags.os_tag == .windows and conf_comp.isDynamicLibrary()) {
+ const dll_path = try maker.generatedPath(conf_comp.generated_bin.value.?).toString(arena);
+ const search_path = Dir.path.dirname(dll_path).?;
+ if (environ_map.get(path_key)) |prev_path| {
+ const new_path = try allocPrint(arena, "{s}{c}{s}", .{ prev_path, path_delimiter, search_path });
+ try environ_map.put(path_key, new_path);
+ } else {
+ try environ_map.put(path_key, search_path);
+ }
+ }
+ }
+}
+
+fn failForeign(
+ arena: Allocator,
+ conf_run: *const Configuration.Step.Run,
+ maker: *Maker,
+ step_index: Configuration.Step.Index,
+ suggested_flag: []const u8,
+ argv0: []const u8,
+ artifact_target: *const std.Target,
+ host_target: *const std.Target,
+) Step.ExtendedMakeError {
+ const step = maker.stepByIndex(step_index);
+ switch (conf_run.flags.stdio) {
+ .check, .zig_test => {
+ if (conf_run.flags.skip_foreign_checks) return error.MakeSkipped;
+
+ const host_name = try host_target.zigTriple(arena);
+ const foreign_name = try artifact_target.zigTriple(arena);
+
+ return step.fail(maker,
+ \\unable to spawn foreign binary '{s}' ({s}) on host system ({s})
+ \\ consider using {s} or enabling skip_foreign_checks in the Run step
+ , .{ argv0, foreign_name, host_name, suggested_flag });
+ },
+ else => {
+ return step.fail(maker, "unable to spawn foreign binary '{s}'", .{argv0});
+ },
+ }
+}
diff --git a/lib/compiler/Maker/Step/TranslateC.zig b/lib/compiler/Maker/Step/TranslateC.zig
@@ -0,0 +1,152 @@
+const TranslateC = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Configuration = std.Build.Configuration;
+const allocPrint = std.fmt.allocPrint;
+const assert = std.debug.assert;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+const PkgConfig = @import("../PkgConfig.zig");
+
+pub fn make(
+ translate_c: *TranslateC,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = translate_c;
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_tc = conf_step.extended.get(conf.extra).translate_c;
+ const cache_root = graph.local_cache_root;
+
+ var argv: std.ArrayList([]const u8) = .empty;
+
+ try argv.ensureUnusedCapacity(arena, 10);
+ argv.appendAssumeCapacity(graph.zig_exe);
+ argv.appendAssumeCapacity("translate-c");
+ if (conf_tc.flags.link_libc) {
+ argv.appendAssumeCapacity("-lc");
+ }
+
+ argv.appendAssumeCapacity("--cache-dir");
+ argv.appendAssumeCapacity(cache_root.path orelse ".");
+
+ argv.appendAssumeCapacity("--global-cache-dir");
+ argv.appendAssumeCapacity(graph.global_cache_root.path orelse ".");
+
+ if (conf_tc.target.get(conf).?.query.unwrap()) |compact_query| {
+ const query = compact_query.get(conf).unwrap(conf);
+ argv.appendAssumeCapacity("-target");
+ argv.appendAssumeCapacity(try query.zigTriple(arena));
+ }
+
+ switch (conf_tc.flags.optimize) {
+ .debug, .default => {}, // Skip since it's the default.
+ else => argv.appendAssumeCapacity(try allocPrint(arena, "-O{t}", .{conf_tc.flags.optimize})),
+ }
+
+ try argv.ensureUnusedCapacity(arena, conf_tc.include_dirs.len * 2);
+ for (0..conf_tc.include_dirs.len) |i|
+ try Step.Compile.appendIncludeDirFlags(arena, conf_tc.include_dirs.get(conf.extra, i), &argv, step_index, maker);
+
+ for (conf_tc.c_macros.slice) |c_macro| {
+ (try argv.addManyAsArray(arena, 2)).* = .{ "-D", c_macro.slice(conf) };
+ }
+
+ var prev_search_strategy: std.Build.Module.SystemLib.SearchStrategy = .paths_first;
+ var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic;
+ var seen_system_libs: std.AutoArrayHashMapUnmanaged(Configuration.String, []const []const u8) = .empty;
+
+ for (conf_tc.system_libs.slice) |system_lib_index| {
+ const system_lib = system_lib_index.get(conf);
+ const system_lib_name = system_lib.name.slice(conf);
+ const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
+ if (system_lib_gop.found_existing) {
+ try argv.appendSlice(arena, system_lib_gop.value_ptr.*);
+ continue;
+ } else {
+ system_lib_gop.value_ptr.* = &.{};
+ }
+
+ if ((system_lib.flags.search_strategy != prev_search_strategy or
+ system_lib.flags.preferred_link_mode != prev_preferred_link_mode))
+ {
+ try argv.ensureUnusedCapacity(arena, 1);
+ switch (system_lib.flags.search_strategy) {
+ .no_fallback => switch (system_lib.flags.preferred_link_mode) {
+ .dynamic => argv.appendAssumeCapacity("-search_dylibs_only"),
+ .static => argv.appendAssumeCapacity("-search_static_only"),
+ },
+ .paths_first => switch (system_lib.flags.preferred_link_mode) {
+ .dynamic => argv.appendAssumeCapacity("-search_paths_first"),
+ .static => argv.appendAssumeCapacity("-search_paths_first_static"),
+ },
+ .mode_first => switch (system_lib.flags.preferred_link_mode) {
+ .dynamic => argv.appendAssumeCapacity("-search_dylibs_first"),
+ .static => argv.appendAssumeCapacity("-search_static_first"),
+ },
+ }
+ prev_search_strategy = system_lib.flags.search_strategy;
+ prev_preferred_link_mode = system_lib.flags.preferred_link_mode;
+ }
+
+ const prefix: []const u8 = prefix: {
+ if (system_lib.flags.needed) break :prefix "-needed-l";
+ if (system_lib.flags.weak) break :prefix "-weak-l";
+ break :prefix "-l";
+ };
+ l: {
+ pc: {
+ const force = switch (system_lib.flags.use_pkg_config) {
+ .no => break :pc,
+ .yes => false,
+ .force => true,
+ };
+
+ const pkg_conf_node = progress_node.start("pkg-config", 0);
+ defer pkg_conf_node.end();
+
+ if (PkgConfig.run(maker, step, pkg_conf_node, system_lib_name, force)) |result| {
+ try argv.appendSlice(arena, result.cflags);
+ try argv.appendSlice(arena, result.libs);
+ try seen_system_libs.put(arena, system_lib.name, result.cflags);
+ break :l;
+ } else |err| switch (err) {
+ error.PkgConfigUnavailable,
+ error.PackageNotFound,
+ => {
+ // pkg-config failed, so fall back to linking the library by name directly.
+ assert(!force);
+ break :pc;
+ },
+ else => |e| return e,
+ }
+ }
+ try argv.append(arena, try allocPrint(arena, "{s}{s}", .{
+ prefix, system_lib_name,
+ }));
+ }
+ }
+
+ try argv.ensureUnusedCapacity(arena, 2);
+
+ const c_source_path = try maker.resolveLazyPathIndexAbs(arena, conf_tc.src_path, step_index);
+ argv.appendAssumeCapacity(c_source_path);
+
+ argv.appendAssumeCapacity("--listen=-");
+ const output_dir_path = (Step.evalZigProcess(step_index, maker, argv.items, progress_node, false) catch |err| switch (err) {
+ error.NeedCompileErrorCheck => unreachable,
+ else => |e| return e,
+ }).?;
+
+ const stem = Io.Dir.path.stem(Io.Dir.path.basename(c_source_path));
+ const out_basename = try allocPrint(arena, "{s}.zig", .{stem});
+
+ maker.generatedPath(conf_tc.output_file).* = try output_dir_path.join(arena, out_basename);
+}
diff --git a/lib/compiler/Maker/Step/UpdateSourceFiles.zig b/lib/compiler/Maker/Step/UpdateSourceFiles.zig
@@ -0,0 +1,89 @@
+const UpdateSourceFiles = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const Path = std.Build.Cache.Path;
+const allocPrint = std.fmt.allocPrint;
+const Configuration = std.Build.Configuration;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ usf: *UpdateSourceFiles,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = usf;
+ const graph = maker.graph;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_usf = conf_step.extended.get(conf.extra).update_source_files;
+ const build_root = graph.build_root_directory;
+
+ if (conf_step.owner != .root)
+ return step.fail(maker, "non-root package attempted to update its source files", .{});
+
+ var any_miss = false;
+
+ progress_node.setEstimatedTotalItems(conf_usf.embeds.slice.len + conf_usf.copies.slice.len);
+
+ step.clearWatchInputs(maker);
+
+ for (conf_usf.embeds.slice) |*embed| {
+ const dest_path: Path = .{
+ .root_dir = build_root,
+ .sub_path = embed.sub_path.slice(conf),
+ };
+ if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
+ const dirname_path: Path = .{
+ .root_dir = build_root,
+ .sub_path = dirname,
+ };
+ dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
+ }
+ dest_path.root_dir.handle.writeFile(io, .{
+ .sub_path = dest_path.sub_path,
+ .data = embed.contents.slice(conf),
+ }) catch |err| return step.fail(maker, "failed writing file {f}: {t}", .{ dest_path, err });
+ any_miss = true;
+ progress_node.completeOne();
+ }
+
+ for (conf_usf.copies.slice) |*copy| {
+ const dest_path: Path = .{
+ .root_dir = build_root,
+ .sub_path = copy.sub_path.slice(conf),
+ };
+ if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
+ const dirname_path: Path = .{
+ .root_dir = build_root,
+ .sub_path = dirname,
+ };
+ dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
+ }
+ const src_lazy_path = copy.src_file.get(conf);
+ const source_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
+ try step.addWatchInput(maker, arena, src_lazy_path);
+
+ const prev_status = source_path.root_dir.handle.updateFile(
+ io,
+ source_path.sub_path,
+ dest_path.root_dir.handle,
+ dest_path.sub_path,
+ .{},
+ ) catch |err| return step.fail(maker, "failed updating file from {f} to {f}: {t}", .{
+ source_path, dest_path, err,
+ });
+ any_miss = any_miss or prev_status == .stale;
+ progress_node.completeOne();
+ }
+
+ step.result_cached = !any_miss;
+}
diff --git a/lib/compiler/Maker/Step/WriteFile.zig b/lib/compiler/Maker/Step/WriteFile.zig
@@ -0,0 +1,294 @@
+const WriteFile = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const assert = std.debug.assert;
+const Path = std.Build.Cache.Path;
+const allocPrint = std.fmt.allocPrint;
+const Configuration = std.Build.Configuration;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ wf: *WriteFile,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = wf;
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_wf = conf_step.extended.get(conf.extra).write_file;
+ const cache_root = graph.local_cache_root;
+ const directories = conf_wf.directories.slice;
+
+ const open_dir_cache = try arena.alloc(Io.Dir, directories.len);
+ var open_dirs_count: u32 = 0;
+ defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]);
+
+ // Doesn't yet include contents of directories.
+ var total_items: usize = conf_wf.embeds.slice.len + conf_wf.copies.slice.len + conf_wf.directories.slice.len;
+ progress_node.setEstimatedTotalItems(total_items);
+
+ switch (conf_wf.flags.mode) {
+ .whole_cached => {
+ step.clearWatchInputs(maker);
+
+ // The cache is used here primarily as a way to find a canonical
+ // location to put build artifacts without parallel step execution
+ // clobbering each other.
+
+ var man = graph.cache.obtain();
+ defer man.deinit();
+
+ for (conf_wf.embeds.slice) |*embed| {
+ man.hash.addBytes(embed.sub_path.slice(conf));
+ man.hash.addBytes(embed.contents.slice(conf));
+ }
+
+ for (conf_wf.copies.slice) |*copy| {
+ man.hash.addBytes(copy.sub_path.slice(conf));
+ const src_lazy_path = copy.src_file.get(conf);
+ const source_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
+ _ = try man.addFilePath(source_path, null);
+ try step.addWatchInput(maker, arena, src_lazy_path);
+ }
+
+ for (directories, open_dir_cache) |conf_dir, *opened_dir| {
+ const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{};
+ const include_extensions = conf_dir.include_extensions.slice(conf);
+
+ man.hash.addBytes(conf_dir.sub_path.slice(conf));
+ for (exclude_extensions) |ext| man.hash.addBytes(ext.slice(conf));
+ if (include_extensions) |includes| for (includes) |inc| {
+ man.hash.addBytes(inc.slice(conf));
+ };
+
+ const src_lazy_path = conf_dir.src_path.get(conf);
+ const need_derived_inputs = try step.addDirectoryWatchInput(maker, src_lazy_path);
+ const src_dir_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
+
+ var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
+ return step.fail(maker, "failed opening source directory {f}: {t}", .{ src_dir_path, err });
+ };
+ opened_dir.* = src_dir;
+ open_dirs_count += 1;
+
+ var it = try src_dir.walk(gpa);
+ defer it.deinit();
+ while (it.next(io) catch |err| switch (err) {
+ error.Canceled, error.OutOfMemory => |e| return e,
+ else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }),
+ }) |entry| {
+ if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue;
+
+ switch (entry.kind) {
+ .directory => {
+ if (need_derived_inputs) {
+ const entry_path = try src_dir_path.join(arena, entry.path);
+ try step.addDirectoryWatchInputFromPath(maker, entry_path);
+ }
+ },
+ .file => {
+ const entry_path = try src_dir_path.join(arena, entry.path);
+ _ = try man.addFilePath(entry_path, null);
+ total_items += 1;
+ },
+ else => continue,
+ }
+ }
+ }
+
+ if (try step.cacheHit(maker, &man)) {
+ const digest = man.final();
+ maker.generatedPath(conf_wf.generated_directory).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
+ };
+ assert(step.result_cached);
+ return;
+ }
+
+ const digest = man.final();
+ const out_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
+ };
+
+ progress_node.setEstimatedTotalItems(total_items);
+ try operate(maker, step_index, open_dir_cache, out_path, progress_node);
+ try step.writeManifest(maker, &man);
+
+ maker.generatedPath(conf_wf.generated_directory).* = out_path;
+ },
+ .tmp => {
+ step.result_cached = false;
+
+ var rand_int: u64 = undefined;
+ io.random(@ptrCast(&rand_int));
+ const hex_digest = std.fmt.hex(rand_int);
+
+ const out_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "tmp", &hex_digest }),
+ };
+
+ try operate(maker, step_index, open_dir_cache, out_path, progress_node);
+
+ maker.generatedPath(conf_wf.generated_directory).* = out_path;
+ },
+ .mutate => {
+ step.result_cached = false;
+ const root_path = try maker.resolveLazyPathIndex(arena, conf_wf.mutate_path.value.?, step_index);
+ try operate(maker, step_index, open_dir_cache, root_path, progress_node);
+ maker.generatedPath(conf_wf.generated_directory).* = root_path;
+ },
+ }
+}
+
+fn operate(
+ maker: *Maker,
+ step_index: Configuration.Step.Index,
+ open_dir_cache: []const Io.Dir,
+ root_path: std.Build.Cache.Path,
+ progress_node: std.Progress.Node,
+) !void {
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_wf = conf_step.extended.get(conf.extra).write_file;
+
+ const root_directory: std.Build.Cache.Directory = .{
+ .handle = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ root_path, err }),
+ .path = try root_path.toString(arena),
+ };
+ defer root_directory.handle.close(io);
+
+ for (conf_wf.embeds.slice) |*embed| {
+ const dest_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = embed.sub_path.slice(conf),
+ };
+ if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
+ const dirname_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = dirname,
+ };
+ dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
+ }
+ dest_path.root_dir.handle.writeFile(io, .{
+ .sub_path = dest_path.sub_path,
+ .data = embed.contents.slice(conf),
+ }) catch |err| return step.fail(maker, "failed writing contents to file {f}: {t}", .{ dest_path, err });
+ progress_node.completeOne();
+ }
+
+ for (conf_wf.copies.slice) |*copy| {
+ const dest_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = copy.sub_path.slice(conf),
+ };
+ // Rather than passing make_path = true below, this optimizes for the
+ // more common case where the directory does not exist.
+ if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
+ const dirname_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = dirname,
+ };
+ dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
+ }
+ const source_path = try maker.resolveLazyPathIndex(arena, copy.src_file, step_index);
+ Io.Dir.copyFile(
+ source_path.root_dir.handle,
+ source_path.sub_path,
+ dest_path.root_dir.handle,
+ dest_path.sub_path,
+ io,
+ .{},
+ ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{
+ source_path, dest_path, err,
+ });
+ progress_node.completeOne();
+ }
+
+ for (conf_wf.directories.slice, open_dir_cache) |conf_dir, already_open_dir| {
+ const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{};
+ const include_extensions = conf_dir.include_extensions.slice(conf);
+
+ const src_dir_path = try maker.resolveLazyPathIndex(arena, conf_dir.src_path, step_index);
+ const dest_dir_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = conf_dir.sub_path.slice(conf),
+ };
+
+ if (dest_dir_path.sub_path.len != 0) {
+ dest_dir_path.root_dir.handle.createDirPath(io, dest_dir_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dest_dir_path, err });
+ }
+
+ var it = try already_open_dir.walk(gpa);
+ defer it.deinit();
+ while (it.next(io) catch |err| switch (err) {
+ error.Canceled, error.OutOfMemory => |e| return e,
+ else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }),
+ }) |entry| {
+ if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue;
+
+ const src_entry_path = try src_dir_path.join(arena, entry.path);
+ const dest_path = try dest_dir_path.join(arena, entry.path);
+ switch (entry.kind) {
+ .directory => dest_path.root_dir.handle.createDirPath(io, dest_path.sub_path) catch |err| {
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dest_path, err });
+ },
+ .file => {
+ Io.Dir.copyFile(
+ src_entry_path.root_dir.handle,
+ src_entry_path.sub_path,
+ dest_path.root_dir.handle,
+ dest_path.sub_path,
+ io,
+ .{ .make_path = true }, // Directory entry may be filtered out above.
+ ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{
+ src_entry_path, dest_path, err,
+ });
+ progress_node.completeOne();
+ },
+ else => continue,
+ }
+ }
+ }
+}
+
+fn pathIncluded(
+ conf: *const Configuration,
+ exclude_extensions: []const Configuration.String,
+ include_extensions: ?[]const Configuration.String,
+ path: []const u8,
+) bool {
+ for (exclude_extensions) |ext| {
+ if (std.mem.endsWith(u8, path, ext.slice(conf)))
+ return false;
+ }
+ if (include_extensions) |incs| {
+ for (incs) |inc| {
+ if (std.mem.endsWith(u8, path, inc.slice(conf)))
+ return true;
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/lib/compiler/Maker/Watch.zig b/lib/compiler/Maker/Watch.zig
@@ -0,0 +1,989 @@
+const Watch = @This();
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Io = std.Io;
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const fatal = std.process.fatal;
+const Configuration = std.Build.Configuration;
+
+const FsEvents = @import("Watch/FsEvents.zig");
+const Step = @import("Step.zig");
+const Maker = @import("../Maker.zig");
+
+os: Os,
+/// The number to show as the number of directories being watched.
+dir_count: usize,
+// These fields are common to most implementations so are kept here for simplicity.
+// They are `undefined` on implementations which do not utilize then.
+dir_table: DirTable,
+generation: Generation,
+maker: *Maker,
+
+pub const have_impl = Os != void;
+
+/// Key is the directory to watch which contains one or more files we are
+/// interested in noticing changes to.
+///
+/// Value is generation.
+const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false);
+
+/// Special key of "." means any changes in this directory trigger the steps.
+const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet);
+const StepSet = std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, Generation);
+
+const Generation = u8;
+
+const Hash = std.hash.Wyhash;
+const Cache = std.Build.Cache;
+
+const Os = switch (builtin.os.tag) {
+ .linux => struct {
+ const posix = std.posix;
+
+ /// Keyed differently but indexes correspond 1:1 with `dir_table`.
+ handle_table: HandleTable,
+ /// fanotify file descriptors are keyed by mount id since marks
+ /// are limited to a single filesystem.
+ poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd),
+
+ const MountId = i32;
+ const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false);
+
+ const fan_mask: std.os.linux.fanotify.MarkMask = .{
+ .CLOSE_WRITE = true,
+ .CREATE = true,
+ .DELETE = true,
+ .DELETE_SELF = true,
+ .EVENT_ON_CHILD = true,
+ .MOVED_FROM = true,
+ .MOVED_TO = true,
+ .MOVE_SELF = true,
+ .ONDIR = true,
+ };
+
+ const FileHandle = struct {
+ handle: *align(1) std.os.linux.file_handle,
+
+ fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
+ const bytes = lfh.slice();
+ const new_ptr = try gpa.alignedAlloc(
+ u8,
+ .of(std.os.linux.file_handle),
+ @sizeOf(std.os.linux.file_handle) + bytes.len,
+ );
+ const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
+ new_header.* = lfh.handle.*;
+ const new: FileHandle = .{ .handle = new_header };
+ @memcpy(new.slice(), lfh.slice());
+ return new;
+ }
+
+ fn destroy(lfh: FileHandle, gpa: Allocator) void {
+ const ptr: [*]u8 = @ptrCast(lfh.handle);
+ const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
+ return gpa.free(allocated_slice);
+ }
+
+ fn slice(lfh: FileHandle) []u8 {
+ const ptr: [*]u8 = &lfh.handle.f_handle;
+ return ptr[0..lfh.handle.handle_bytes];
+ }
+
+ const Adapter = struct {
+ pub fn hash(self: Adapter, a: FileHandle) u32 {
+ _ = self;
+ const unsigned_type: u32 = @bitCast(a.handle.handle_type);
+ return @truncate(Hash.hash(unsigned_type, a.slice()));
+ }
+ pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
+ _ = self;
+ _ = b_index;
+ return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
+ }
+ };
+ };
+
+ fn init(maker: *Maker) !Watch {
+ return .{
+ .dir_table = .{},
+ .dir_count = 0,
+ .os = switch (builtin.os.tag) {
+ .linux => .{
+ .handle_table = .{},
+ .poll_fds = .{},
+ },
+ else => {},
+ },
+ .generation = 0,
+ .maker = maker,
+ };
+ }
+
+ fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
+ var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
+ path.sub_path,
+ }) catch return error.NameTooLong;
+ const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
+ stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
+ try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID);
+ const stack_lfh: FileHandle = .{ .handle = stack_ptr };
+ return stack_lfh.clone(gpa);
+ }
+
+ fn markDirtySteps(w: *Watch, fan_fd: posix.fd_t) !bool {
+ const maker = w.maker;
+ const fanotify = std.os.linux.fanotify;
+ const M = fanotify.event_metadata;
+ var events_buf: [256 + 4096]u8 = undefined;
+ var any_dirty = false;
+ while (true) {
+ var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) {
+ error.WouldBlock => return any_dirty,
+ else => |e| return e,
+ };
+ var meta: [*]align(1) M = @ptrCast(&events_buf);
+ while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
+ len -= meta[0].event_len;
+ meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
+ }) {
+ assert(meta[0].vers == M.VERSION);
+ if (meta[0].mask.Q_OVERFLOW) {
+ any_dirty = true;
+ std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
+ markAllFilesDirty(w);
+ return true;
+ }
+ const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
+ switch (fid.hdr.info_type) {
+ .DFID_NAME => {
+ const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
+ const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
+ const file_name = std.mem.span(file_name_z);
+ const lfh: FileHandle = .{ .handle = file_handle };
+ if (w.os.handle_table.getPtr(lfh)) |value| {
+ if (value.reaction_set.getPtr(".")) |glob_set|
+ any_dirty = markStepSetDirty(maker, glob_set, any_dirty);
+ if (value.reaction_set.getPtr(file_name)) |step_set|
+ any_dirty = markStepSetDirty(maker, step_set, any_dirty);
+ }
+ },
+ else => |t| std.log.warn("unexpected fanotify event '{t}'", .{t}),
+ }
+ }
+ }
+ }
+
+ fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
+ const maker = w.maker;
+ const gpa = maker.gpa;
+
+ // Add missing marks and note persisted ones.
+ for (steps) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
+ const reaction_set = rs: {
+ const gop = try w.dir_table.getOrPut(gpa, path);
+ if (!gop.found_existing) {
+ var mount_id: MountId = undefined;
+ const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
+ error.FileNotFound => {
+ std.debug.assert(w.dir_table.swapRemove(path));
+ continue;
+ },
+ else => return err,
+ };
+ const fan_fd = blk: {
+ const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id);
+ if (!fd_gop.found_existing) {
+ const fan_fd = std.posix.fanotify_init(.{
+ .CLASS = .NOTIF,
+ .CLOEXEC = true,
+ .NONBLOCK = true,
+ .REPORT_NAME = true,
+ .REPORT_DIR_FID = true,
+ .REPORT_FID = true,
+ .REPORT_TARGET_FID = true,
+ }, 0) catch |err| switch (err) {
+ error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}),
+ else => |e| return e,
+ };
+ fd_gop.value_ptr.* = .{
+ .fd = fan_fd,
+ .events = std.posix.POLL.IN,
+ .revents = undefined,
+ };
+ }
+ break :blk fd_gop.value_ptr.*.fd;
+ };
+ // `dir_handle` may already be present in the table in
+ // the case that we have multiple Cache.Path instances
+ // that compare inequal but ultimately point to the same
+ // directory on the file system.
+ // In such case, we must revert adding this directory, but keep
+ // the additions to the step set.
+ const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle);
+ if (dh_gop.found_existing) {
+ _ = w.dir_table.pop();
+ } else {
+ assert(dh_gop.index == gop.index);
+ dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
+ posix.fanotify_mark(fan_fd, .{
+ .ADD = true,
+ .ONLYDIR = true,
+ }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err|
+ fatal("unable to watch {f}: {t}", .{ path, err });
+ }
+ break :rs &dh_gop.value_ptr.reaction_set;
+ }
+ break :rs &w.os.handle_table.values()[gop.index].reaction_set;
+ };
+ for (files.items) |basename| {
+ const gop = try reaction_set.getOrPut(gpa, basename);
+ if (!gop.found_existing) gop.value_ptr.* = .{};
+ try gop.value_ptr.put(gpa, step_index, w.generation);
+ }
+ }
+ }
+
+ {
+ // Remove marks for files that are no longer inputs.
+ var i: usize = 0;
+ while (i < w.os.handle_table.entries.len) {
+ {
+ const reaction_set = &w.os.handle_table.values()[i].reaction_set;
+ var step_set_i: usize = 0;
+ while (step_set_i < reaction_set.entries.len) {
+ const step_set = &reaction_set.values()[step_set_i];
+ var dirent_i: usize = 0;
+ while (dirent_i < step_set.entries.len) {
+ const generations = step_set.values();
+ if (generations[dirent_i] == w.generation) {
+ dirent_i += 1;
+ continue;
+ }
+ step_set.swapRemoveAt(dirent_i);
+ }
+ if (step_set.entries.len > 0) {
+ step_set_i += 1;
+ continue;
+ }
+ reaction_set.swapRemoveAt(step_set_i);
+ }
+ if (reaction_set.entries.len > 0) {
+ i += 1;
+ continue;
+ }
+ }
+
+ const path = w.dir_table.keys()[i];
+
+ const mount_id = w.os.handle_table.values()[i].mount_id;
+ const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd;
+ posix.fanotify_mark(fan_fd, .{
+ .REMOVE = true,
+ .ONLYDIR = true,
+ }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) {
+ error.FileNotFound => {}, // Expected, harmless.
+ else => |e| std.log.warn("unable to unwatch {f}: {t}", .{ path, e }),
+ };
+
+ w.dir_table.swapRemoveAt(i);
+ w.os.handle_table.swapRemoveAt(i);
+ }
+ w.generation +%= 1;
+ }
+ w.dir_count = w.dir_table.count();
+ }
+
+ fn wait(w: *Watch, timeout: Timeout) !WaitResult {
+ const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms());
+ if (events_len == 0)
+ return .timeout;
+ for (w.os.poll_fds.values()) |poll_fd| {
+ if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try markDirtySteps(w, poll_fd.fd))
+ return .dirty;
+ }
+ return .clean;
+ }
+ },
+ .windows => struct {
+ const windows = std.os.windows;
+
+ /// Keyed differently but indexes correspond 1:1 with `dir_table`.
+ handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false),
+ ready_dirs: std.DoublyLinkedList,
+
+ const FileId = struct {
+ volumeSerialNumber: windows.ULONG,
+ indexNumber: windows.LARGE_INTEGER,
+ };
+
+ const Directory = struct {
+ reaction_set: ReactionSet,
+ id: FileId,
+ file: Io.File,
+ state: enum { idle, listening, ready },
+ iosb: windows.IO_STATUS_BLOCK,
+ // 64 KB is the packet size limit when monitoring over a network.
+ // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks
+ buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)),
+ ready_node: std.DoublyLinkedList.Node,
+
+ /// Start listening for events, buffer field will be overwritten eventually.
+ fn startListening(dir: *Directory, w: *Watch) !void {
+ assert(dir.file.flags.nonblocking);
+ assert(dir.state == .idle);
+ switch (windows.ntdll.NtNotifyChangeDirectoryFileEx(
+ dir.file.handle,
+ null,
+ ¬ifyApc,
+ w,
+ &dir.iosb,
+ &dir.buffer,
+ dir.buffer.len,
+ .{
+ .FILE_NAME = true,
+ .DIR_NAME = true,
+ .SIZE = true,
+ .LAST_WRITE = true,
+ .CREATION = true,
+ },
+ .FALSE,
+ .Notify,
+ )) {
+ .SUCCESS, .PENDING => dir.state = .listening,
+ .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported,
+ else => |status| return windows.unexpectedStatus(status),
+ }
+ }
+
+ fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void {
+ const w: *Watch = @ptrCast(@alignCast(apc_context));
+ const dir: *Directory = @fieldParentPtr("iosb", iosb);
+ assert(iosb.u.Status != .PENDING);
+ assert(dir.state == .listening);
+ w.os.ready_dirs.append(&dir.ready_node);
+ dir.state = .ready;
+ }
+
+ fn init(gpa: Allocator, path: Cache.Path) !*Directory {
+ // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW)
+ // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW.
+ var dir_handle: windows.HANDLE = undefined;
+ const root_fd = path.root_dir.handle.handle;
+ const sub_path = path.subPathOrDot();
+ const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call
+ var iosb: windows.IO_STATUS_BLOCK = undefined;
+ switch (windows.ntdll.NtCreateFile(
+ &dir_handle,
+ .{
+ .SPECIFIC = .{ .FILE_DIRECTORY = .{
+ .LIST = true,
+ } },
+ .STANDARD = .{ .SYNCHRONIZE = true },
+ .GENERIC = .{ .READ = true },
+ },
+ &.{
+ .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd,
+ .ObjectName = @constCast(&sub_path_w.string()),
+ },
+ &iosb,
+ null,
+ .{},
+ .VALID_FLAGS,
+ .OPEN,
+ .{
+ .DIRECTORY_FILE = true,
+ .IO = .ASYNCHRONOUS,
+ .OPEN_FOR_BACKUP_INTENT = true,
+ },
+ null,
+ 0,
+ )) {
+ .SUCCESS => {},
+ .OBJECT_NAME_INVALID => return error.BadPathName,
+ .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
+ .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
+ .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
+ .NOT_A_DIRECTORY => return error.NotDir,
+ // This can happen if the directory has 'List folder contents' permission set to 'Deny'
+ .ACCESS_DENIED => return error.AccessDenied,
+ .INVALID_PARAMETER => unreachable,
+ else => |rc| return windows.unexpectedStatus(rc),
+ }
+ assert(dir_handle != windows.INVALID_HANDLE_VALUE);
+ errdefer windows.CloseHandle(dir_handle);
+
+ const dir_id = try getFileId(dir_handle);
+
+ const dir = try gpa.create(Directory);
+ dir.* = .{
+ .reaction_set = .empty,
+ .id = dir_id,
+ .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } },
+ .state = .idle,
+ .iosb = undefined,
+ .buffer = undefined,
+ .ready_node = undefined,
+ };
+ return dir;
+ }
+
+ fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void {
+ state: switch (dir.state) {
+ .idle => {},
+ .listening => {
+ var cancel_iosb: windows.IO_STATUS_BLOCK = undefined;
+ _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb);
+ while (switch (dir.state) {
+ .idle => unreachable,
+ .listening => true,
+ .ready => false,
+ }) Io.Threaded.waitForApcOrAlert();
+ continue :state .ready;
+ },
+ .ready => w.os.ready_dirs.remove(&dir.ready_node),
+ }
+ windows.CloseHandle(dir.file.handle);
+ gpa.destroy(dir);
+ }
+
+ /// Useful to make `*Directory` a key in `std.ArrayHashMap`.
+ const TableAdapter = struct {
+ pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 {
+ return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber)));
+ }
+ pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool {
+ _ = rhs_index;
+ return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and
+ lhs_dir.id.indexNumber == rhs_dir.id.indexNumber;
+ }
+ };
+ };
+
+ fn init(maker: *Maker) !Watch {
+ return .{
+ .dir_table = .{},
+ .dir_count = 0,
+ .os = switch (builtin.os.tag) {
+ .windows => .{
+ .handle_table = .empty,
+ .ready_dirs = .{},
+ },
+ else => {},
+ },
+ .generation = 0,
+ .maker = maker,
+ };
+ }
+
+ fn getFileId(handle: windows.HANDLE) !FileId {
+ var file_id: FileId = undefined;
+ var io_status: windows.IO_STATUS_BLOCK = undefined;
+ var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined;
+ switch (windows.ntdll.NtQueryVolumeInformationFile(
+ handle,
+ &io_status,
+ &volume_info,
+ @sizeOf(windows.FILE.FS_VOLUME_INFORMATION),
+ .Volume,
+ )) {
+ .SUCCESS => {},
+ // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
+ // size provided. This is treated as success because the type of variable-length information that this would be relevant for
+ // (name, volume name, etc) we don't care about.
+ .BUFFER_OVERFLOW => {},
+ else => |rc| return windows.unexpectedStatus(rc),
+ }
+ file_id.volumeSerialNumber = volume_info.VolumeSerialNumber;
+ var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined;
+ switch (windows.ntdll.NtQueryInformationFile(
+ handle,
+ &io_status,
+ &internal_info,
+ @sizeOf(windows.FILE.INTERNAL_INFORMATION),
+ .Internal,
+ )) {
+ .SUCCESS => {},
+ else => |rc| return windows.unexpectedStatus(rc),
+ }
+ file_id.indexNumber = internal_info.IndexNumber;
+ return file_id;
+ }
+
+ fn markDirtySteps(w: *Watch, dir: *Directory) !bool {
+ const maker = w.maker;
+
+ var any_dirty = false;
+ const bytes_returned = dir.iosb.Information;
+ if (bytes_returned == 0) {
+ std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
+ markAllFilesDirty(w);
+ try dir.startListening(w);
+ return true;
+ }
+ var file_name_buf: [std.fs.max_path_bytes]u8 = undefined;
+ var offset: usize = 0;
+ while (true) {
+ const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset]));
+ const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())];
+ if (dir.reaction_set.getPtr(".")) |glob_set|
+ any_dirty = markStepSetDirty(maker, glob_set, any_dirty);
+ if (dir.reaction_set.getPtr(file_name)) |step_set|
+ any_dirty = markStepSetDirty(maker, step_set, any_dirty);
+ if (notify.NextEntryOffset == 0)
+ break;
+
+ offset += notify.NextEntryOffset;
+ }
+
+ // We call this now since at this point we have finished reading dir.buffer.
+ try dir.startListening(w);
+ return any_dirty;
+ }
+
+ fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
+ const maker = w.maker;
+ const gpa = maker.gpa;
+ // Add missing marks and note persisted ones.
+ for (steps) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
+ const dir = dir: {
+ const gop = try w.dir_table.getOrPut(gpa, path);
+ if (!gop.found_existing) {
+ const dir: *Directory = try .init(gpa, path);
+ errdefer dir.deinit(gpa, w);
+ // `dir.id` may already be present in the table in
+ // the case that we have multiple Cache.Path instances
+ // that compare inequal but ultimately point to the same
+ // directory on the file system.
+ // In such case, we must revert adding this directory, but keep
+ // the additions to the step set.
+ const dh_gop = try w.os.handle_table.getOrPut(gpa, dir);
+ if (dh_gop.found_existing) {
+ dir.deinit(gpa, w);
+ _ = w.dir_table.pop();
+ break :dir w.os.handle_table.keys()[dh_gop.index];
+ } else {
+ assert(dh_gop.index == gop.index);
+ try dir.startListening(w);
+ break :dir dir;
+ }
+ }
+ break :dir w.os.handle_table.keys()[gop.index];
+ };
+ for (files.items) |basename| {
+ const gop = try dir.reaction_set.getOrPut(gpa, basename);
+ if (!gop.found_existing) gop.value_ptr.* = .{};
+ try gop.value_ptr.put(gpa, step_index, w.generation);
+ }
+ }
+ }
+
+ {
+ // Remove marks for files that are no longer inputs.
+ var i: usize = 0;
+ while (i < w.os.handle_table.entries.len) {
+ const dir = w.os.handle_table.keys()[i];
+ {
+ var step_set_i: usize = 0;
+ while (step_set_i < dir.reaction_set.entries.len) {
+ const step_set = &dir.reaction_set.values()[step_set_i];
+ var dirent_i: usize = 0;
+ while (dirent_i < step_set.entries.len) {
+ const generations = step_set.values();
+ if (generations[dirent_i] == w.generation) {
+ dirent_i += 1;
+ continue;
+ }
+ step_set.swapRemoveAt(dirent_i);
+ }
+ if (step_set.entries.len > 0) {
+ step_set_i += 1;
+ continue;
+ }
+ dir.reaction_set.swapRemoveAt(step_set_i);
+ }
+ if (dir.reaction_set.entries.len > 0) {
+ i += 1;
+ continue;
+ }
+ }
+
+ w.dir_table.swapRemoveAt(i);
+ w.os.handle_table.swapRemoveAt(i);
+ dir.deinit(gpa, w);
+ }
+ w.generation +%= 1;
+ }
+ w.dir_count = w.dir_table.count();
+ }
+
+ fn wait(w: *Watch, timeout: Timeout) !WaitResult {
+ const maker = w.maker;
+ const io = maker.graph.io;
+
+ for (0..2) |attempt| {
+ while (w.os.ready_dirs.popFirst()) |ready_node| {
+ const dir: *Directory = @fieldParentPtr("ready_node", ready_node);
+ assert(dir.state == .ready);
+ dir.state = .idle;
+ switch (dir.iosb.u.Status) {
+ .SUCCESS => return if (try markDirtySteps(w, dir)) .dirty else .clean,
+ .PENDING => unreachable,
+ .CANCELLED => {},
+ else => |status| return windows.unexpectedStatus(status),
+ }
+ try dir.startListening(w);
+ }
+ try io.checkCancel();
+ if (attempt == 1) return .timeout;
+ const delay_interval: windows.LARGE_INTEGER = switch (timeout) {
+ .none => std.math.minInt(windows.LARGE_INTEGER),
+ .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100),
+ };
+ _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval);
+ } else unreachable;
+ }
+ },
+ .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct {
+ const posix = std.posix;
+
+ kq_fd: i32,
+ /// Indexes correspond 1:1 with `dir_table`.
+ handles: std.MultiArrayList(struct {
+ rs: ReactionSet,
+ /// If the corresponding dir_table Path has sub_path == "", then it
+ /// suffices as the open directory handle, and this value will be
+ /// -1. Otherwise, it needs to be opened in update(), and will be
+ /// stored here.
+ dir_fd: i32,
+ }),
+
+ const dir_open_flags: posix.O = f: {
+ var f: posix.O = .{
+ .ACCMODE = .RDONLY,
+ .NOFOLLOW = false,
+ .DIRECTORY = true,
+ .CLOEXEC = true,
+ };
+ if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true;
+ if (@hasField(posix.O, "PATH")) f.PATH = true;
+ break :f f;
+ };
+
+ const EV = std.c.EV;
+ const NOTE = std.c.NOTE;
+
+ fn init(maker: *Maker) !Watch {
+ return .{
+ .dir_table = .{},
+ .dir_count = 0,
+ .os = .{
+ .kq_fd = try Io.Kqueue.createFileDescriptor(),
+ .handles = .empty,
+ },
+ .generation = 0,
+ .maker = maker,
+ };
+ }
+
+ fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
+ const maker = w.maker;
+ const gpa = maker.gpa;
+ const handles = &w.os.handles;
+ for (steps) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
+ const reaction_set = rs: {
+ const gop = try w.dir_table.getOrPut(gpa, path);
+ if (!gop.found_existing) {
+ const skip_open_dir = path.sub_path.len == 0;
+ const dir_fd = if (skip_open_dir)
+ path.root_dir.handle.handle
+ else
+ posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| {
+ fatal("failed to open directory {f}: {t}", .{ path, err });
+ };
+ // Empirically the dir has to stay open or else no events are triggered.
+ errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd);
+ const changes = [1]posix.Kevent{.{
+ .ident = @bitCast(@as(isize, dir_fd)),
+ .filter = std.c.EVFILT.VNODE,
+ .flags = EV.ADD | EV.ENABLE | EV.CLEAR,
+ .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
+ .data = 0,
+ .udata = gop.index,
+ }};
+ _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null);
+ assert(handles.len == gop.index);
+ try handles.append(gpa, .{
+ .rs = .{},
+ .dir_fd = if (skip_open_dir) -1 else dir_fd,
+ });
+ }
+
+ break :rs &handles.items(.rs)[gop.index];
+ };
+ for (files.items) |basename| {
+ const gop = try reaction_set.getOrPut(gpa, basename);
+ if (!gop.found_existing) gop.value_ptr.* = .{};
+ try gop.value_ptr.put(gpa, step_index, w.generation);
+ }
+ }
+ }
+
+ {
+ // Remove marks for files that are no longer inputs.
+ var i: usize = 0;
+ while (i < handles.len) {
+ {
+ const reaction_set = &handles.items(.rs)[i];
+ var step_set_i: usize = 0;
+ while (step_set_i < reaction_set.entries.len) {
+ const step_set = &reaction_set.values()[step_set_i];
+ var dirent_i: usize = 0;
+ while (dirent_i < step_set.entries.len) {
+ const generations = step_set.values();
+ if (generations[dirent_i] == w.generation) {
+ dirent_i += 1;
+ continue;
+ }
+ step_set.swapRemoveAt(dirent_i);
+ }
+ if (step_set.entries.len > 0) {
+ step_set_i += 1;
+ continue;
+ }
+ reaction_set.swapRemoveAt(step_set_i);
+ }
+ if (reaction_set.entries.len > 0) {
+ i += 1;
+ continue;
+ }
+ }
+
+ // If the sub_path == "" then this patch has already the
+ // dir fd that we need to use as the ident to remove the
+ // event. If it was opened above with openat() then we need
+ // to access that data via the dir_fd field.
+ const path = w.dir_table.keys()[i];
+ const dir_fd = if (path.sub_path.len == 0)
+ path.root_dir.handle.handle
+ else
+ handles.items(.dir_fd)[i];
+ assert(dir_fd != -1);
+
+ // The changelist also needs to update the udata field of the last
+ // event, since we are doing a swap remove, and we store the dir_table
+ // index in the udata field.
+ const last_dir_fd = fd: {
+ const last_path = w.dir_table.keys()[handles.len - 1];
+ const last_dir_fd = if (last_path.sub_path.len == 0)
+ last_path.root_dir.handle.handle
+ else
+ handles.items(.dir_fd)[handles.len - 1];
+ assert(last_dir_fd != -1);
+ break :fd last_dir_fd;
+ };
+ const changes = [_]posix.Kevent{
+ .{
+ .ident = @bitCast(@as(isize, dir_fd)),
+ .filter = std.c.EVFILT.VNODE,
+ .flags = EV.DELETE,
+ .fflags = 0,
+ .data = 0,
+ .udata = i,
+ },
+ .{
+ .ident = @bitCast(@as(isize, last_dir_fd)),
+ .filter = std.c.EVFILT.VNODE,
+ .flags = EV.ADD,
+ .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
+ .data = 0,
+ .udata = i,
+ },
+ };
+ const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes;
+ _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null);
+ if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd);
+
+ w.dir_table.swapRemoveAt(i);
+ handles.swapRemove(i);
+ }
+ w.generation +%= 1;
+ }
+ w.dir_count = w.dir_table.count();
+ }
+
+ fn wait(w: *Watch, timeout: Timeout) !WaitResult {
+ const maker = w.maker;
+ var timespec_buffer: posix.timespec = undefined;
+ var event_buffer: [100]posix.Kevent = undefined;
+ var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(×pec_buffer));
+ if (n == 0) return .timeout;
+ const reaction_sets = w.os.handles.items(.rs);
+ var any_dirty = markDirtySteps(maker, reaction_sets, event_buffer[0..n], false);
+ timespec_buffer = .{ .sec = 0, .nsec = 0 };
+ while (n == event_buffer.len) {
+ n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, ×pec_buffer);
+ if (n == 0) break;
+ any_dirty = markDirtySteps(maker, reaction_sets, event_buffer[0..n], any_dirty);
+ }
+ return if (any_dirty) .dirty else .clean;
+ }
+
+ fn markDirtySteps(
+ maker: *Maker,
+ reaction_sets: []ReactionSet,
+ events: []const std.c.Kevent,
+ start_any_dirty: bool,
+ ) bool {
+ var any_dirty = start_any_dirty;
+ for (events) |event| {
+ const index: usize = @intCast(event.udata);
+ const reaction_set = &reaction_sets[index];
+ // If we knew the basename of the changed file, here we would
+ // mark only the step set dirty, and possibly the glob set:
+ //if (reaction_set.getPtr(".")) |glob_set|
+ // any_dirty = markStepSetDirty(maker, glob_set, any_dirty);
+ //if (reaction_set.getPtr(file_name)) |step_set|
+ // any_dirty = markStepSetDirty(maker, step_set, any_dirty);
+ // However we don't know the file name so just mark all the
+ // sets dirty for this directory.
+ for (reaction_set.values()) |*step_set| {
+ any_dirty = markStepSetDirty(maker, step_set, any_dirty);
+ }
+ }
+ return any_dirty;
+ }
+ },
+ .macos => struct {
+ fse: FsEvents,
+
+ fn init(maker: *Maker) !Watch {
+ return .{
+ .os = .{ .fse = try .init(maker.graph.cache.cwd) },
+ .dir_count = 0,
+ .dir_table = undefined,
+ .generation = undefined,
+ .maker = maker,
+ };
+ }
+ fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
+ try w.os.fse.setPaths(w.maker, steps);
+ w.dir_count = w.os.fse.watch_roots.len;
+ }
+ fn wait(w: *Watch, timeout: Timeout) !WaitResult {
+ return w.os.fse.wait(w.maker, switch (timeout) {
+ .none => null,
+ .ms => |ms| @as(u64, ms) * std.time.ns_per_ms,
+ });
+ }
+ },
+ else => void,
+};
+
+pub fn init(maker: *Maker) !Watch {
+ return Os.init(maker);
+}
+
+pub const Match = struct {
+ /// Relative to the watched directory, the file path that triggers this
+ /// match.
+ basename: []const u8,
+ /// The step to re-run when file corresponding to `basename` is changed.
+ step_index: Configuration.Step.Index,
+
+ pub const Context = struct {
+ pub fn hash(self: Context, a: Match) u32 {
+ _ = self;
+ var hasher = Hash.init(@intFromEnum(a.step_index));
+ hasher.update(a.basename);
+ return @truncate(hasher.final());
+ }
+ pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool {
+ _ = self;
+ _ = b_index;
+ return a.step_index == b.step_index and std.mem.eql(u8, a.basename, b.basename);
+ }
+ };
+};
+
+fn markAllFilesDirty(w: *Watch) void {
+ const maker = w.maker;
+
+ for (switch (builtin.os.tag) {
+ .windows => w.os.handle_table.keys(),
+ else => w.os.handle_table.values(),
+ }) |item| {
+ const reaction_set = switch (builtin.os.tag) {
+ .linux, .windows => item.reaction_set,
+ else => item,
+ };
+ for (reaction_set.values()) |step_set| {
+ for (step_set.keys()) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ _ = maker.invalidateResult(step);
+ }
+ }
+ }
+}
+
+fn markStepSetDirty(maker: *Maker, step_set: *StepSet, any_dirty: bool) bool {
+ var this_any_dirty = false;
+ for (step_set.keys()) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ if (maker.invalidateResult(step)) this_any_dirty = true;
+ }
+ return any_dirty or this_any_dirty;
+}
+
+pub fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
+ return Os.update(w, steps);
+}
+
+pub const Timeout = union(enum) {
+ none,
+ ms: u16,
+
+ pub fn to_i32_ms(t: Timeout) i32 {
+ return switch (t) {
+ .none => -1,
+ .ms => |ms| ms,
+ };
+ }
+
+ pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec {
+ return switch (t) {
+ .none => null,
+ .ms => |ms_u16| {
+ const ms: isize = ms_u16;
+ buf.* = .{
+ .sec = @divTrunc(ms, std.time.ms_per_s),
+ .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms,
+ };
+ return buf;
+ },
+ };
+ }
+};
+
+pub const WaitResult = enum {
+ timeout,
+ /// File system watching triggered on files that were marked as inputs to at least one Step.
+ /// Relevant steps have been marked dirty.
+ dirty,
+ /// File system watching triggered but none of the events were relevant to
+ /// what we are listening to. There is nothing to do.
+ clean,
+};
+
+pub fn wait(w: *Watch, timeout: Timeout) !WaitResult {
+ return Os.wait(w, timeout);
+}
diff --git a/lib/compiler/Maker/Watch/FsEvents.zig b/lib/compiler/Maker/Watch/FsEvents.zig
@@ -0,0 +1,488 @@
+//! An implementation of file-system watching based on the `FSEventStream` API in macOS.
+//! While macOS supports kqueue, it does not allow detecting changes to files without
+//! placing watches on each individual file, meaning FD limits are reached incredibly
+//! quickly. The File System Events API works differently: it implements *recursive*
+//! directory watches, managed by a system service. Rather than being in libc, the API is
+//! exposed by the CoreServices framework. To avoid a compile dependency on the framework
+//! bundle, we dynamically load CoreServices with `std.DynLib`.
+//!
+//! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been
+//! made to keep that specialization to a minimum. Other use cases could be served with
+//! relatively minimal modifications to the `watch_paths` field and its usages (in
+//! particular the `setPaths` function). We avoid using the global GCD dispatch queue in
+//! favour of creating our own and synchronizing with an explicit semaphore, meaning this
+//! logic is thread-safe and does not affect process-global state.
+//!
+//! In theory, this API is quite good at avoiding filesystem race conditions. In practice,
+//! the logic that would avoid them is currently disabled, because the build system kind
+//! of relies on them at the time of writing to avoid redundant work -- see the comment at
+//! the top of `wait` for details.
+const FsEvents = @This();
+
+const enable_debug_logs = false;
+
+core_services: std.DynLib,
+resolved_symbols: ResolvedSymbols,
+
+paths_arena: std.heap.ArenaAllocator.State,
+/// The roots of the recursive watches. FSEvents has relatively small limits on the number
+/// of watched paths, so this slice must not be too long. The paths themselves are allocated
+/// into `paths_arena`, but this slice is allocated into the GPA.
+watch_roots: [][:0]const u8,
+/// All of the paths being watched. Value is the set of steps which depend on the file/directory.
+/// Keys and values are in `paths_arena`, but this map is allocated into the GPA.
+watch_paths: std.array_hash_map.String([]const std.Build.Configuration.Step.Index),
+
+/// The semaphore we use to block the thread calling `wait` until the callback determines a relevant
+/// event has occurred. This is retained across `wait` calls for simplicity and efficiency.
+waiting_semaphore: dispatch.semaphore_t,
+/// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the
+/// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained
+/// across `wait` calls for simplicity and efficiency.
+dispatch_queue: dispatch.queue_t,
+/// In theory, this field avoids race conditions. In practice, it is essentially unused at the time
+/// of writing. See the comment at the start of `wait` for details.
+since_event: FSEventStreamEventId,
+
+cwd_path: []const u8,
+
+/// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols
+/// is not present, `init` will close the framework and return an error.
+const ResolvedSymbols = struct {
+ FSEventStreamCreate: *const fn (
+ allocator: CFAllocatorRef,
+ callback: FSEventStreamCallback,
+ ctx: ?*const FSEventStreamContext,
+ paths_to_watch: CFArrayRef,
+ since_when: FSEventStreamEventId,
+ latency: CFTimeInterval,
+ flags: FSEventStreamCreateFlags,
+ ) callconv(.c) FSEventStreamRef,
+ FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void,
+ FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool,
+ FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void,
+ FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void,
+ FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void,
+ FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId,
+ FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId,
+ CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void,
+ CFArrayCreate: *const fn (
+ allocator: CFAllocatorRef,
+ values: [*]const usize,
+ num_values: CFIndex,
+ call_backs: ?*const CFArrayCallBacks,
+ ) callconv(.c) CFArrayRef,
+ CFStringCreateWithCString: *const fn (
+ alloc: CFAllocatorRef,
+ c_str: [*:0]const u8,
+ encoding: CFStringEncoding,
+ ) callconv(.c) CFStringRef,
+ CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef,
+ kCFAllocatorUseContext: *const CFAllocatorRef,
+};
+
+pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents {
+ var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch
+ return error.OpenFrameworkFailed;
+ errdefer core_services.close();
+
+ var resolved_symbols: ResolvedSymbols = undefined;
+ inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| {
+ @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol;
+ }
+
+ return .{
+ .core_services = core_services,
+ .resolved_symbols = resolved_symbols,
+ .paths_arena = .{},
+ .watch_roots = &.{},
+ .watch_paths = .empty,
+ .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources,
+ .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources,
+ // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order
+ // to notice any changes which happened during said work.
+ .since_event = resolved_symbols.FSEventsGetCurrentEventId(),
+ .cwd_path = cwd_path,
+ };
+}
+
+pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void {
+ fse.waiting_semaphore.as_object().release();
+ fse.dispatch_queue.as_object().release();
+ fse.core_services.close(io);
+
+ gpa.free(fse.watch_roots);
+ fse.watch_paths.deinit(gpa);
+ {
+ var paths_arena = fse.paths_arena.promote(gpa);
+ paths_arena.deinit();
+ }
+}
+
+pub fn setPaths(fse: *FsEvents, maker: *Maker, steps: []const std.Build.Configuration.Step.Index) !void {
+ const gpa = maker.gpa;
+
+ var paths_arena_instance = fse.paths_arena.promote(gpa);
+ defer fse.paths_arena = paths_arena_instance.state;
+ const paths_arena = paths_arena_instance.allocator();
+
+ var need_dirs: std.array_hash_map.String(void) = .empty;
+ defer need_dirs.deinit(gpa);
+
+ fse.watch_paths.clearRetainingCapacity();
+
+ // We take `step_index` by pointer for a slight memory optimization in a moment.
+ for (steps) |*step_index| {
+ const step = maker.stepByIndex(step_index.*);
+ for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
+ const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{
+ fse.cwd_path, path.root_dir.path orelse ".", path.sub_path,
+ });
+ try need_dirs.put(gpa, resolved_dir, {});
+ for (files.items) |file_name| {
+ const watch_path = if (std.mem.eql(u8, file_name, "."))
+ resolved_dir
+ else
+ try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name });
+ const gop = try fse.watch_paths.getOrPut(gpa, watch_path);
+ if (gop.found_existing) {
+ const old_steps = gop.value_ptr.*;
+ const new_steps = try paths_arena.alloc(std.Build.Configuration.Step.Index, old_steps.len + 1);
+ @memcpy(new_steps[0..old_steps.len], old_steps);
+ new_steps[old_steps.len] = step_index.*;
+ gop.value_ptr.* = new_steps;
+ } else {
+ // This is why we captured `step` by pointer! We can avoid allocating a slice of one
+ // step in the arena in the common case where a file is referenced by only one step.
+ gop.value_ptr.* = step_index[0..1];
+ }
+ }
+ }
+ }
+
+ {
+ // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar").
+ // To eliminate these, we'll re-add directories in order of path length with a redundancy check.
+ const old_dirs = try gpa.dupe([]const u8, need_dirs.keys());
+ defer gpa.free(old_dirs);
+ std.mem.sort([]const u8, old_dirs, {}, struct {
+ fn lessThan(ctx: void, a: []const u8, b: []const u8) bool {
+ ctx;
+ return std.mem.lessThan(u8, a, b);
+ }
+ }.lessThan);
+ need_dirs.clearRetainingCapacity();
+ for (old_dirs) |dir_path| {
+ var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
+ while (it.next()) |component| {
+ if (need_dirs.contains(component.path)) {
+ // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added
+ break;
+ }
+ } else {
+ need_dirs.putAssumeCapacityNoClobber(dir_path, {});
+ }
+ }
+ }
+
+ // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very
+ // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/`
+ // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit
+ // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be
+ // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below*
+ // that known limit.
+ if (need_dirs.count() > 2048) {
+ // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P
+ if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{});
+ fse.watch_roots = try gpa.realloc(fse.watch_roots, 1);
+ fse.watch_roots[0] = "/";
+ } else {
+ fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count());
+ for (fse.watch_roots, need_dirs.keys()) |*out, in| {
+ out.* = try paths_arena.dupeSentinel(u8, in, 0);
+ }
+ }
+ if (enable_debug_logs) {
+ watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len });
+ for (fse.watch_roots) |dir_path| {
+ watch_log.debug("- '{s}'", .{dir_path});
+ }
+ }
+}
+
+pub fn wait(fse: *FsEvents, maker: *Maker, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!Watch.WaitResult {
+ if (fse.watch_roots.len == 0) @panic("nothing to watch");
+ const gpa = maker.gpa;
+
+ const rs = fse.resolved_symbols;
+
+ // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds
+ // to occur, because one step modifies a file which is an input to another step. The solution
+ // to this problem will probably be either:
+ //
+ // a) Don't include the output of one step as a watch input of another; only mark external
+ // files as watch inputs. Or...
+ //
+ // b) Note the current event ID when a step begins, and disregard events preceding that ID
+ // when considering whether to dirty that step in `eventCallback`.
+ //
+ // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does
+ // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those
+ // too at the time of writing, so this is kind of expected.
+ fse.since_event = .since_now;
+
+ const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{
+ .version = 0,
+ .info = @constCast(&gpa),
+ .retain = null,
+ .release = null,
+ .copy_description = null,
+ .allocate = &cf_alloc_callbacks.allocate,
+ .reallocate = &cf_alloc_callbacks.reallocate,
+ .deallocate = &cf_alloc_callbacks.deallocate,
+ .preferred_size = null,
+ }) orelse return error.OutOfMemory;
+ defer rs.CFRelease(cf_allocator);
+
+ const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len);
+ @memset(cf_paths, null);
+ defer {
+ for (cf_paths) |o| if (o) |p| rs.CFRelease(p);
+ gpa.free(cf_paths);
+ }
+ for (fse.watch_roots, cf_paths) |raw_path, *cf_path| {
+ cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8);
+ }
+ const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null);
+ defer rs.CFRelease(cf_paths_array);
+
+ const callback_ctx: EventCallbackCtx = .{
+ .fse = fse,
+ .maker = maker,
+ };
+ const event_stream = rs.FSEventStreamCreate(
+ null,
+ &eventCallback,
+ &.{
+ .version = 0,
+ .info = @constCast(&callback_ctx),
+ .retain = null,
+ .release = null,
+ .copy_description = null,
+ },
+ cf_paths_array,
+ fse.since_event,
+ 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events
+ .{ .watch_root = true, .file_events = true },
+ );
+ defer rs.FSEventStreamRelease(event_stream);
+ rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue);
+ defer rs.FSEventStreamInvalidate(event_stream);
+ if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed;
+ defer rs.FSEventStreamStop(event_stream);
+ const result = fse.waiting_semaphore.wait(timeout: {
+ const ns = timeout_ns orelse break :timeout .FOREVER;
+ break :timeout .time(.NOW, @intCast(ns));
+ });
+ return switch (result) {
+ 0 => .dirty,
+ else => .timeout,
+ };
+}
+
+const cf_alloc_callbacks = struct {
+ const log = std.log.scoped(.cf_alloc);
+ fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
+ if (enable_debug_logs) log.debug("allocate {d}", .{size});
+ _ = hint;
+ const gpa: *const Allocator = @ptrCast(@alignCast(info));
+ const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null;
+ const metadata: *usize = @ptrCast(mem);
+ metadata.* = @intCast(size);
+ return mem[@sizeOf(usize)..].ptr;
+ }
+ fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
+ if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size });
+ _ = hint;
+ if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL
+ const gpa: *const Allocator = @ptrCast(@alignCast(info));
+ const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
+ const old_size = @as(*const usize, @ptrCast(old_base)).*;
+ const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
+ const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null;
+ const metadata: *usize = @ptrCast(new_mem);
+ metadata.* = @intCast(new_size);
+ return new_mem[@sizeOf(usize)..].ptr;
+ }
+ fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void {
+ if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr});
+ const gpa: *const Allocator = @ptrCast(@alignCast(info));
+ const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
+ const old_size = @as(*const usize, @ptrCast(old_base)).*;
+ const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
+ gpa.free(old_mem);
+ }
+};
+
+const EventCallbackCtx = struct {
+ fse: *FsEvents,
+ maker: *Maker,
+};
+
+fn eventCallback(
+ stream: ConstFSEventStreamRef,
+ client_callback_info: ?*anyopaque,
+ num_events: usize,
+ events_paths_ptr: *anyopaque,
+ events_flags_ptr: [*]const FSEventStreamEventFlags,
+ events_ids_ptr: [*]const FSEventStreamEventId,
+) callconv(.c) void {
+ const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info));
+ const maker = ctx.maker;
+ const fse = ctx.fse;
+ const rs = fse.resolved_symbols;
+ const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr));
+ const events_paths = events_paths_ptr_casted[0..num_events];
+ const events_ids = events_ids_ptr[0..num_events];
+ const events_flags = events_flags_ptr[0..num_events];
+ var any_dirty = false;
+ for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| {
+ _ = event_id;
+ if (event_flags.history_done) continue; // sentinel
+ const event_path = std.mem.span(event_path_nts);
+ switch (event_flags.must_scan_sub_dirs) {
+ false => {
+ if (fse.watch_paths.get(event_path)) |steps| {
+ assert(steps.len > 0);
+ if (invalidateSteps(maker, steps)) any_dirty = true;
+ }
+ if (std.fs.path.dirname(event_path)) |event_dirname| {
+ // Modifying '/foo/bar' triggers the watch on '/foo'.
+ if (fse.watch_paths.get(event_dirname)) |steps| {
+ assert(steps.len > 0);
+ if (invalidateSteps(maker, steps)) any_dirty = true;
+ }
+ }
+ },
+ true => {
+ // This is unlikely, but can occasionally happen when bottlenecked: events have been
+ // coalesced into one. We want to see if any of these events are actually relevant
+ // to us. The only way we can reasonably do that in this rare edge case is iterate
+ // the watch paths and see if any is under this directory. That's acceptable because
+ // we would otherwise kick off a rebuild which would be clearing those paths anyway.
+ const changed_path = std.fs.path.dirname(event_path) orelse event_path;
+ for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| {
+ if (dirStartsWith(watching_path, changed_path)) {
+ if (invalidateSteps(maker, steps)) any_dirty = true;
+ }
+ }
+ },
+ }
+ }
+ if (any_dirty) {
+ fse.since_event = rs.FSEventStreamGetLatestEventId(stream);
+ _ = fse.waiting_semaphore.signal();
+ }
+}
+fn dirStartsWith(path: []const u8, prefix: []const u8) bool {
+ if (std.mem.eql(u8, path, prefix)) return true;
+ if (!std.mem.startsWith(u8, path, prefix)) return false;
+ if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar`
+ return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar`
+}
+
+fn invalidateSteps(maker: *Maker, steps: []const std.Build.Configuration.Step.Index) bool {
+ var any_dirty = false;
+ for (steps) |step_index| {
+ const step = maker.stepByIndex(step_index);
+ if (maker.invalidateResult(step)) any_dirty = true;
+ }
+ return any_dirty;
+}
+
+const CFAllocatorRef = ?*const opaque {};
+const CFArrayRef = *const opaque {};
+const CFStringRef = *const opaque {};
+const CFTimeInterval = f64;
+const CFIndex = i32;
+const CFOptionFlags = enum(u32) { _ };
+const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque;
+const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void;
+const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef;
+const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
+const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
+const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void;
+const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex;
+const CFAllocatorContext = extern struct {
+ version: CFIndex,
+ info: ?*anyopaque,
+ retain: ?CFAllocatorRetainCallBack,
+ release: ?CFAllocatorReleaseCallBack,
+ copy_description: ?CFAllocatorCopyDescriptionCallBack,
+ allocate: CFAllocatorAllocateCallBack,
+ reallocate: ?CFAllocatorReallocateCallBack,
+ deallocate: ?CFAllocatorDeallocateCallBack,
+ preferred_size: ?CFAllocatorPreferredSizeCallBack,
+};
+const CFArrayCallBacks = opaque {};
+const CFStringEncoding = enum(u32) {
+ invalid_id = std.math.maxInt(u32),
+ mac_roman = 0,
+ windows_latin_1 = 0x500,
+ iso_latin_1 = 0x201,
+ next_step_latin = 0xB01,
+ ascii = 0x600,
+ unicode = 0x100,
+ utf8 = 0x8000100,
+ non_lossy_ascii = 0xBFF,
+};
+
+const FSEventStreamRef = *opaque {};
+const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child;
+const FSEventStreamCallback = *const fn (
+ stream: ConstFSEventStreamRef,
+ client_callback_info: ?*anyopaque,
+ num_events: usize,
+ event_paths: *anyopaque,
+ event_flags: [*]const FSEventStreamEventFlags,
+ event_ids: [*]const FSEventStreamEventId,
+) callconv(.c) void;
+const FSEventStreamContext = extern struct {
+ version: CFIndex,
+ info: ?*anyopaque,
+ retain: ?CFAllocatorRetainCallBack,
+ release: ?CFAllocatorReleaseCallBack,
+ copy_description: ?CFAllocatorCopyDescriptionCallBack,
+};
+const FSEventStreamEventId = enum(u64) {
+ since_now = std.math.maxInt(u64),
+ _,
+};
+const FSEventStreamCreateFlags = packed struct(u32) {
+ use_cf_types: bool = false,
+ no_defer: bool = false,
+ watch_root: bool = false,
+ ignore_self: bool = false,
+ file_events: bool = false,
+ _: u27 = 0,
+};
+const FSEventStreamEventFlags = packed struct(u32) {
+ must_scan_sub_dirs: bool,
+ user_dropped: bool,
+ kernel_dropped: bool,
+ event_ids_wrapped: bool,
+ history_done: bool,
+ root_changed: bool,
+ mount: bool,
+ unmount: bool,
+ _: u24 = 0,
+};
+
+const dispatch = std.c.dispatch;
+const std = @import("std");
+const Io = std.Io;
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const watch_log = std.log.scoped(.watch);
+const Maker = @import("../../Maker.zig");
+const Watch = @import("../Watch.zig");
diff --git a/lib/compiler/Maker/WebServer.zig b/lib/compiler/Maker/WebServer.zig
@@ -0,0 +1,953 @@
+const WebServer = @This();
+
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Cache = std.Build.Cache;
+const Configuration = std.Build.Configuration;
+const Io = std.Io;
+const abi = std.Build.abi;
+const assert = std.debug.assert;
+const http = std.http;
+const log = std.log.scoped(.web_server);
+const mem = std.mem;
+const net = std.Io.net;
+
+const Maker = @import("../Maker.zig");
+const Fuzz = @import("Fuzz.zig");
+const Graph = @import("Graph.zig");
+const Step = @import("Step.zig");
+
+maker: *Maker,
+listen_address: net.IpAddress,
+root_prog_node: std.Progress.Node,
+
+tcp_server: ?net.Server,
+serve_task: ?Io.Future(Io.Cancelable!void),
+
+/// Uses `Io.Clock.awake`.
+base_timestamp: Io.Timestamp,
+/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
+step_names_trailing: []u8,
+
+/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
+/// Accessed atomically.
+step_status_bits: []u8,
+
+fuzz: ?Fuzz,
+time_report_mutex: Io.Mutex,
+time_report_msgs: [][]u8,
+time_report_update_times: []i64,
+
+build_status: std.atomic.Value(abi.BuildStatus),
+/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
+/// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so
+/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it
+/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For
+/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes,
+/// because this value changes quickly so this would result in constantly spamming all clients with
+/// an unreasonable number of packets.
+update_id: std.atomic.Value(u32),
+
+runner_request_mutex: Io.Mutex,
+runner_request_ready_cond: Io.Condition,
+runner_request_empty_cond: Io.Condition,
+runner_request: ?RunnerRequest,
+
+/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
+/// on a fixed interval of this many milliseconds.
+const default_update_interval_ms = 500;
+
+pub const base_clock: Io.Clock = .awake;
+
+/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
+pub fn notifyUpdate(ws: *WebServer) void {
+ const io = ws.maker.graph.io;
+ _ = ws.update_id.rmw(.Add, 1, .release);
+ io.futexWake(u32, &ws.update_id.raw, 16);
+}
+
+pub const Options = struct {
+ maker: *Maker,
+ root_prog_node: std.Progress.Node,
+ listen_address: net.IpAddress,
+ base_timestamp: Io.Clock.Timestamp,
+};
+pub fn init(opts: Options) WebServer {
+ // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent`
+ // instead of threads, so that the web server can function in single-threaded builds.
+ comptime assert(!builtin.single_threaded);
+ assert(opts.base_timestamp.clock == base_clock);
+
+ const maker = opts.maker;
+ const all_steps = maker.step_stack.keys();
+ const c = &maker.scanned_config.configuration;
+ const gpa = maker.gpa;
+ const graph = maker.graph;
+
+ const step_names_trailing = gpa.alloc(u8, len: {
+ var name_bytes: usize = 0;
+ for (all_steps) |step_index| name_bytes += step_index.ptr(c).name.slice(c).len;
+ break :len name_bytes + all_steps.len * 4;
+ }) catch @panic("out of memory");
+ {
+ const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
+ var idx: usize = all_steps.len * 4;
+ for (all_steps, step_name_lens) |step_index, *name_len| {
+ const step_name = step_index.ptr(c).name.slice(c);
+ name_len.* = @intCast(step_name.len);
+ @memcpy(step_names_trailing[idx..][0..step_name.len], step_name);
+ idx += step_name.len;
+ }
+ assert(idx == step_names_trailing.len);
+ }
+
+ const step_status_bits = gpa.alloc(
+ u8,
+ std.math.divCeil(usize, all_steps.len, 4) catch unreachable,
+ ) catch @panic("out of memory");
+ @memset(step_status_bits, 0);
+
+ const time_reports_len: usize = if (graph.time_report) all_steps.len else 0;
+ const time_report_msgs = gpa.alloc([]u8, time_reports_len) catch @panic("out of memory");
+ const time_report_update_times = gpa.alloc(i64, time_reports_len) catch @panic("out of memory");
+ @memset(time_report_msgs, &.{});
+ @memset(time_report_update_times, std.math.minInt(i64));
+
+ return .{
+ .maker = maker,
+ .listen_address = opts.listen_address,
+ .root_prog_node = opts.root_prog_node,
+
+ .tcp_server = null,
+ .serve_task = null,
+
+ .base_timestamp = opts.base_timestamp.raw,
+ .step_names_trailing = step_names_trailing,
+
+ .step_status_bits = step_status_bits,
+
+ .fuzz = null,
+ .time_report_mutex = .init,
+ .time_report_msgs = time_report_msgs,
+ .time_report_update_times = time_report_update_times,
+
+ .build_status = .init(.idle),
+ .update_id = .init(0),
+
+ .runner_request_mutex = .init,
+ .runner_request_ready_cond = .init,
+ .runner_request_empty_cond = .init,
+ .runner_request = null,
+ };
+}
+pub fn deinit(ws: *WebServer) void {
+ const maker = ws.maker;
+ const gpa = maker.gpa;
+ const io = maker.graph.io;
+
+ gpa.free(ws.step_names_trailing);
+ gpa.free(ws.step_status_bits);
+
+ if (ws.fuzz) |*f| f.deinit();
+ for (ws.time_report_msgs) |msg| gpa.free(msg);
+ gpa.free(ws.time_report_msgs);
+ gpa.free(ws.time_report_update_times);
+
+ if (ws.serve_task) |t| {
+ if (ws.tcp_server) |*s| s.stream.close(io);
+ t.await();
+ }
+ if (ws.tcp_server) |*s| s.deinit();
+
+ gpa.free(ws.step_names_trailing);
+}
+pub fn start(ws: *WebServer) error{AlreadyReported}!void {
+ assert(ws.tcp_server == null);
+ assert(ws.serve_task == null);
+ const maker = ws.maker;
+ const io = maker.graph.io;
+
+ ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
+ log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err });
+ return error.AlreadyReported;
+ };
+ ws.serve_task = io.concurrent(serve, .{ws}) catch |err| {
+ log.err("unable to spawn web server thread: {t}", .{err});
+ ws.tcp_server.?.deinit(io);
+ ws.tcp_server = null;
+ return error.AlreadyReported;
+ };
+
+ log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address});
+ if (ws.listen_address.getPort() == 0) {
+ log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address});
+ }
+}
+fn serve(ws: *WebServer) Io.Cancelable!void {
+ const maker = ws.maker;
+ const io = maker.graph.io;
+
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+
+ while (true) {
+ var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) {
+ error.Canceled => |e| return e,
+ else => |e| {
+ log.err("failed to accept connection: {t}", .{e});
+ return;
+ },
+ };
+ group.concurrent(io, accept, .{ ws, stream }) catch |err| {
+ log.err("unable to spawn connection thread: {t}", .{err});
+ stream.close(io);
+ continue;
+ };
+ }
+}
+
+pub fn startBuild(ws: *WebServer) void {
+ if (ws.fuzz) |*fuzz| {
+ fuzz.deinit();
+ ws.fuzz = null;
+ }
+ for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
+ ws.build_status.store(.running, .monotonic);
+ ws.notifyUpdate();
+}
+
+pub fn updateStepStatus(
+ ws: *WebServer,
+ step_index: Configuration.Step.Index,
+ new_status: abi.StepUpdate.Status,
+) void {
+ const maker = ws.maker;
+ const all_steps = maker.step_stack.keys();
+ const step_idx: u32 = for (all_steps, 0..) |s, i| {
+ if (s == step_index) break @intCast(i);
+ } else unreachable;
+ const ptr = &ws.step_status_bits[step_idx / 4];
+ const bit_offset: u3 = @intCast((step_idx % 4) * 2);
+ const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
+ const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset;
+ _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
+ ws.notifyUpdate();
+}
+
+pub fn finishBuild(ws: *WebServer, opts: struct {
+ fuzz: bool,
+}) void {
+ const maker = ws.maker;
+ const all_steps = maker.step_stack.keys();
+
+ if (opts.fuzz) {
+ switch (builtin.os.tag) {
+ // Current implementation depends on two things that need to be ported to Windows:
+ // * Memory-mapping to share data between the fuzzer and build runner.
+ // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
+ // many addresses to source locations).
+ .windows => std.process.fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
+ else => {},
+ }
+ if (@bitSizeOf(usize) != 64) {
+ // Current implementation depends on posix.mmap()'s second
+ // parameter, `length: usize`, being compatible with file system's
+ // u64 return value. This is not the case on 32-bit platforms.
+ // Affects or affected by issues #5185, #22523, and #22464.
+ std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
+ }
+
+ assert(ws.fuzz == null);
+
+ ws.build_status.store(.fuzz_init, .monotonic);
+ ws.notifyUpdate();
+
+ ws.fuzz = Fuzz.init(maker, all_steps, ws.root_prog_node, .{ .forever = .{ .ws = ws } }) catch |err|
+ std.process.fatal("failed to start fuzzer: {t}", .{err});
+ ws.fuzz.?.start();
+ }
+
+ ws.build_status.store(if (maker.watch) .watching else .idle, .monotonic);
+ ws.notifyUpdate();
+}
+
+pub fn now(ws: *const WebServer) i64 {
+ const maker = ws.maker;
+ const io = maker.graph.io;
+ const ts = base_clock.now(io);
+ return @intCast(ws.base_timestamp.durationTo(ts).toNanoseconds());
+}
+
+fn accept(ws: *WebServer, stream: net.Stream) void {
+ const maker = ws.maker;
+ const io = maker.graph.io;
+
+ defer {
+ // `net.Stream.close` wants to helpfully overwrite `stream` with
+ // `undefined`, but it cannot do so since it is an immutable parameter.
+ var copy = stream;
+ copy.close(io);
+ }
+ var send_buffer: [4096]u8 = undefined;
+ var recv_buffer: [4096]u8 = undefined;
+ var connection_reader = stream.reader(io, &recv_buffer);
+ var connection_writer = stream.writer(io, &send_buffer);
+ var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface);
+
+ while (true) {
+ var request = server.receiveHead() catch |err| switch (err) {
+ error.HttpConnectionClosing => return,
+ else => return log.err("failed to receive http request: {t}", .{err}),
+ };
+ switch (request.upgradeRequested()) {
+ .websocket => |opt_key| {
+ const key = opt_key orelse return log.err("missing websocket key", .{});
+ var web_socket = request.respondWebSocket(.{ .key = key }) catch {
+ return log.err("failed to respond web socket: {t}", .{connection_writer.err.?});
+ };
+ ws.serveWebSocket(&web_socket) catch |err| {
+ log.err("failed to serve websocket: {t}", .{err});
+ return;
+ };
+ comptime unreachable;
+ },
+ .other => |name| return log.err("unknown upgrade request: {s}", .{name}),
+ .none => {
+ ws.serveRequest(&request) catch |err| switch (err) {
+ error.AlreadyReported => return,
+ else => {
+ log.err("failed to serve '{s}': {t}", .{ request.head.target, err });
+ return;
+ },
+ };
+ },
+ }
+ }
+}
+
+fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
+ const maker = ws.maker;
+ const gpa = maker.gpa;
+ const graph = maker.graph;
+ const io = graph.io;
+ const all_steps = maker.step_stack.keys();
+
+ var prev_build_status = ws.build_status.load(.monotonic);
+
+ const prev_step_status_bits = try gpa.alloc(u8, ws.step_status_bits.len);
+ defer gpa.free(prev_step_status_bits);
+ for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| {
+ copy.* = @atomicLoad(u8, shared, .monotonic);
+ }
+
+ var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock });
+ defer recv_thread.cancel(io);
+
+ {
+ const hello_header: abi.Hello = .{
+ .status = prev_build_status,
+ .flags = .{
+ .time_report = graph.time_report,
+ },
+ .timestamp = ws.now(),
+ .steps_len = @intCast(all_steps.len),
+ };
+ var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits };
+ try sock.writeMessageVec(&bufs, .binary);
+ }
+
+ var prev_fuzz: Fuzz.Previous = .init;
+ var prev_time: i64 = std.math.minInt(i64);
+ while (true) {
+ const start_time = ws.now();
+ const start_update_id = ws.update_id.load(.acquire);
+
+ if (ws.fuzz) |*fuzz| {
+ try fuzz.sendUpdate(sock, &prev_fuzz);
+ }
+
+ {
+ try ws.time_report_mutex.lock(io);
+ defer ws.time_report_mutex.unlock(io);
+ for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| {
+ if (update_time <= prev_time) continue;
+ // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so
+ // that we don't hold up the build system on the client accepting this packet.
+ const owned_msg = try gpa.dupe(u8, msg);
+ defer gpa.free(owned_msg);
+ // Temporarily unlock, then re-lock after the message is sent.
+ ws.time_report_mutex.unlock(io);
+ defer ws.time_report_mutex.lockUncancelable(io);
+ try sock.writeMessage(owned_msg, .binary);
+ }
+ }
+
+ {
+ const build_status = ws.build_status.load(.monotonic);
+ if (build_status != prev_build_status) {
+ prev_build_status = build_status;
+ const msg: abi.StatusUpdate = .{ .new = build_status };
+ try sock.writeMessage(@ptrCast(&msg), .binary);
+ }
+ }
+
+ for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
+ const cur_byte = @atomicLoad(u8, shared, .monotonic);
+ if (prev_byte.* == cur_byte) continue;
+ const cur: [4]abi.StepUpdate.Status = .{
+ @enumFromInt(@as(u2, @truncate(cur_byte >> 0))),
+ @enumFromInt(@as(u2, @truncate(cur_byte >> 2))),
+ @enumFromInt(@as(u2, @truncate(cur_byte >> 4))),
+ @enumFromInt(@as(u2, @truncate(cur_byte >> 6))),
+ };
+ const prev: [4]abi.StepUpdate.Status = .{
+ @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))),
+ @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))),
+ @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))),
+ @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))),
+ };
+ for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| {
+ const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } };
+ if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary);
+ }
+ prev_byte.* = cur_byte;
+ }
+
+ prev_time = start_time;
+
+ const old_cp = io.swapCancelProtection(.blocked);
+ defer _ = io.swapCancelProtection(old_cp);
+ io.futexWaitTimeout(
+ u32,
+ &ws.update_id.raw,
+ start_update_id,
+ .{ .duration = .{
+ .clock = .awake,
+ .raw = .fromMilliseconds(default_update_interval_ms),
+ } },
+ ) catch |err| switch (err) {
+ error.Canceled => unreachable,
+ };
+ }
+}
+fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
+ const maker = ws.maker;
+ const io = maker.graph.io;
+
+ while (true) {
+ const msg = sock.readSmallMessage() catch return;
+ if (msg.opcode != .binary) continue;
+ if (msg.data.len == 0) continue;
+ const tag: abi.ToServerTag = @enumFromInt(msg.data[0]);
+ switch (tag) {
+ _ => continue,
+ .rebuild => while (true) {
+ ws.runner_request_mutex.lock(io) catch |err| switch (err) {
+ error.Canceled => return,
+ };
+ defer ws.runner_request_mutex.unlock(io);
+ if (ws.runner_request == null) {
+ ws.runner_request = .rebuild;
+ ws.runner_request_ready_cond.signal(io);
+ break;
+ }
+ ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return;
+ },
+ }
+ }
+}
+
+fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void {
+ // Strip an optional leading '/debug' component from the request.
+ const target: []const u8, const debug: bool = target: {
+ if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true };
+ if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true };
+ if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true };
+ break :target .{ req.head.target, false };
+ };
+
+ if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html");
+ if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript");
+ if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css");
+ if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css");
+ if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast);
+
+ if (ws.fuzz) |*fuzz| {
+ if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req);
+ }
+
+ try req.respond("not found", .{
+ .status = .not_found,
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = "text/plain" },
+ },
+ });
+}
+
+fn serveLibFile(
+ ws: *WebServer,
+ request: *http.Server.Request,
+ sub_path: []const u8,
+ content_type: []const u8,
+) !void {
+ const maker = ws.maker;
+ const graph = maker.graph;
+
+ return serveFile(ws, request, .{
+ .root_dir = graph.zig_lib_directory,
+ .sub_path = sub_path,
+ }, content_type);
+}
+fn serveClientWasm(
+ ws: *WebServer,
+ req: *http.Server.Request,
+ optimize_mode: std.builtin.OptimizeMode,
+) !void {
+ const gpa = ws.maker.gpa;
+
+ var arena_state: std.heap.ArenaAllocator = .init(gpa);
+ defer arena_state.deinit();
+ const arena = arena_state.allocator();
+
+ // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page.
+ const bin_path = try buildClientWasm(ws, arena, optimize_mode);
+ return serveFile(ws, req, bin_path, "application/wasm");
+}
+
+pub fn serveFile(
+ ws: *WebServer,
+ request: *http.Server.Request,
+ path: Cache.Path,
+ content_type: []const u8,
+) !void {
+ const maker = ws.maker;
+ const gpa = ws.maker.gpa;
+ const io = maker.graph.io;
+
+ // The desired API is actually sendfile, which will require enhancing http.Server.
+ // We load the file with every request so that the user can make changes to the file
+ // and refresh the HTML page without restarting this server.
+ const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| {
+ log.err("failed to read '{f}': {t}", .{ path, err });
+ return error.AlreadyReported;
+ };
+ defer gpa.free(file_contents);
+ try request.respond(file_contents, .{
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = content_type },
+ cache_control_header,
+ },
+ });
+}
+pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
+ const maker = ws.maker;
+ const graph = maker.graph;
+ const io = graph.io;
+
+ var send_buffer: [0x4000]u8 = undefined;
+ var response = try request.respondStreaming(&send_buffer, .{
+ .respond_options = .{
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = "application/x-tar" },
+ cache_control_header,
+ },
+ },
+ });
+
+ var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
+
+ for (paths) |path| {
+ var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| {
+ log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
+ continue;
+ };
+ defer file.close(io);
+ const stat = try file.stat(io);
+ var read_buffer: [1024]u8 = undefined;
+ var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size);
+
+ archiver.prefix = path.root_dir.path orelse graph.cache.cwd;
+ try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds()));
+ }
+
+ // intentionally not calling `archiver.finishPedantically`
+ try response.end();
+}
+
+fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path {
+ const root_name = "build-web";
+ const arch_os_abi = "wasm32-freestanding";
+ const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
+
+ const maker = ws.maker;
+ const gpa = maker.gpa;
+ const graph = maker.graph;
+ const io = graph.io;
+
+ const main_src_path: Cache.Path = .{
+ .root_dir = graph.zig_lib_directory,
+ .sub_path = "build-web/main.zig",
+ };
+ const walk_src_path: Cache.Path = .{
+ .root_dir = graph.zig_lib_directory,
+ .sub_path = "docs/wasm/Walk.zig",
+ };
+ const html_render_src_path: Cache.Path = .{
+ .root_dir = graph.zig_lib_directory,
+ .sub_path = "docs/wasm/html_render.zig",
+ };
+
+ var argv: std.ArrayList([]const u8) = .empty;
+
+ try argv.appendSlice(arena, &.{
+ graph.zig_exe, "build-exe", //
+ "-fno-entry", //
+ "-O", @tagName(optimize), //
+ "-target", arch_os_abi, //
+ "-mcpu", cpu_features, //
+ "--cache-dir", graph.global_cache_root.path orelse ".", //
+ "--global-cache-dir", graph.global_cache_root.path orelse ".", //
+ "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
+ "--name", root_name, //
+ "-rdynamic", //
+ "-fsingle-threaded", //
+ "--dep", "Walk", //
+ "--dep", "html_render", //
+ try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), //
+ try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), //
+ "--dep", "Walk", //
+ try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), //
+ "--listen=-",
+ });
+
+ var child = try std.process.spawn(io, .{
+ .argv = argv.items,
+ .environ_map = &graph.environ_map,
+ .stdin = .pipe,
+ .stdout = .pipe,
+ .stderr = .pipe,
+ });
+ defer child.kill(io);
+
+ var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited });
+ defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
+
+ var stdout_buffer: [512]u8 = undefined;
+ var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
+ const stdout = &stdout_reader.interface;
+
+ {
+ var w = child.stdin.?.writer(io, &.{});
+ w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ }
+
+ const Header = std.zig.Server.Message.Header;
+
+ var result: ?Cache.Path = null;
+ var result_error_bundle = std.zig.ErrorBundle.empty;
+ var body_buffer: std.ArrayList(u8) = .empty;
+ defer body_buffer.deinit(gpa);
+
+ while (true) {
+ const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
+ error.ReadFailed => |e| return e,
+ error.EndOfStream => break,
+ };
+ body_buffer.clearRetainingCapacity();
+ try stdout.appendExact(gpa, &body_buffer, header.bytes_len);
+ const body = body_buffer.items;
+
+ switch (header.tag) {
+ .zig_version => {
+ if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
+ return error.ZigProtocolVersionMismatch;
+ }
+ },
+ .error_bundle => {
+ result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
+ },
+ .emit_digest => {
+ const EmitDigest = std.zig.Server.Message.EmitDigest;
+ const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
+ if (!ebp_hdr.flags.cache_hit) {
+ log.info("source changes detected; rebuilt wasm component", .{});
+ }
+ const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
+ result = .{
+ .root_dir = graph.global_cache_root,
+ .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
+ };
+ },
+ else => {}, // ignore other messages
+ }
+ }
+
+ const stderr_contents = try stderr_task.await(io);
+ if (stderr_contents.len > 0) {
+ std.debug.print("{s}", .{stderr_contents});
+ }
+
+ // Send EOF to stdin.
+ child.stdin.?.close(io);
+ child.stdin = null;
+
+ switch (try child.wait(io)) {
+ .exited => |code| {
+ if (code != 0) {
+ log.err(
+ "the following command exited with error code {d}:\n{s}",
+ .{ code, try std.zig.allocPrintCmd(arena, argv.items, .{}) },
+ );
+ return error.WasmCompilationFailed;
+ }
+ },
+ .signal => |sig| {
+ log.err(
+ "the following command terminated with signal {t}:\n{s}",
+ .{ sig, try std.zig.allocPrintCmd(arena, argv.items, .{}) },
+ );
+ return error.WasmCompilationFailed;
+ },
+ .stopped => |sig| {
+ log.err(
+ "the following command stopped unexpectedly with signal {t}:\n{s}",
+ .{ sig, try std.zig.allocPrintCmd(arena, argv.items, .{}) },
+ );
+ return error.WasmCompilationFailed;
+ },
+ .unknown => {
+ log.err(
+ "the following command terminated unexpectedly:\n{s}",
+ .{try std.zig.allocPrintCmd(arena, argv.items, .{})},
+ );
+ return error.WasmCompilationFailed;
+ },
+ }
+
+ if (result_error_bundle.errorMessageCount() > 0) {
+ try result_error_bundle.renderToStderr(io, .{}, .auto);
+ log.err("the following command failed with {d} compilation errors:\n{s}", .{
+ result_error_bundle.errorMessageCount(),
+ try std.zig.allocPrintCmd(arena, argv.items, .{}),
+ });
+ return error.WasmCompilationFailed;
+ }
+
+ const base_path = result orelse {
+ log.err("child process failed to report result\n{s}", .{
+ try std.zig.allocPrintCmd(arena, argv.items, .{}),
+ });
+ return error.WasmCompilationFailed;
+ };
+ const target = std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
+ .arch_os_abi = arch_os_abi,
+ .cpu_features = cpu_features,
+ }) catch unreachable) catch unreachable;
+ const bin_name = try std.zig.binNameAlloc(arena, .{
+ .root_name = root_name,
+ .cpu_arch = target.cpu.arch,
+ .os_tag = target.os.tag,
+ .ofmt = target.ofmt,
+ .abi = target.abi,
+ .output_mode = .Exe,
+ });
+ return base_path.join(arena, bin_name);
+}
+
+fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
+ var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
+ return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
+ error.ReadFailed => return file_reader.err.?,
+ else => |e| return e,
+ };
+}
+
+pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
+ compile_step: Configuration.Step.Index,
+
+ use_llvm: bool,
+ stats: abi.time_report.CompileResult.Stats,
+ ns_total: u64,
+
+ llvm_pass_timings_len: u32,
+ files_len: u32,
+ decls_len: u32,
+
+ /// The trailing data of `abi.time_report.CompileResult`, except the step name.
+ trailing: []const u8,
+}) void {
+ const maker = ws.maker;
+ const gpa = maker.gpa;
+ const io = maker.graph.io;
+ const all_steps = maker.step_stack.keys();
+
+ const step_idx: u32 = for (all_steps, 0..) |s, i| {
+ if (s == opts.compile_step) break @intCast(i);
+ } else unreachable;
+
+ const old_buf = old: {
+ ws.time_report_mutex.lock(io) catch return;
+ defer ws.time_report_mutex.unlock(io);
+ const old = ws.time_report_msgs[step_idx];
+ ws.time_report_msgs[step_idx] = &.{};
+ break :old old;
+ };
+ const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
+
+ const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]);
+ out_header.* = .{
+ .step_idx = step_idx,
+ .flags = .{
+ .use_llvm = opts.use_llvm,
+ },
+ .stats = opts.stats,
+ .ns_total = opts.ns_total,
+ .llvm_pass_timings_len = opts.llvm_pass_timings_len,
+ .files_len = opts.files_len,
+ .decls_len = opts.decls_len,
+ };
+ @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
+
+ {
+ ws.time_report_mutex.lock(io) catch return;
+ defer ws.time_report_mutex.unlock(io);
+ assert(ws.time_report_msgs[step_idx].len == 0);
+ ws.time_report_msgs[step_idx] = buf;
+ ws.time_report_update_times[step_idx] = ws.now();
+ }
+ ws.notifyUpdate();
+}
+
+pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.Index, duration: Io.Duration) void {
+ const maker = ws.maker;
+ const gpa = maker.gpa;
+ const io = maker.graph.io;
+ const all_steps = maker.step_stack.keys();
+
+ const step_idx: u32 = for (all_steps, 0..) |s, i| {
+ if (s == step_index) break @intCast(i);
+ } else unreachable;
+
+ const old_buf = old: {
+ ws.time_report_mutex.lock(io) catch return;
+ defer ws.time_report_mutex.unlock(io);
+ const old = ws.time_report_msgs[step_idx];
+ ws.time_report_msgs[step_idx] = &.{};
+ break :old old;
+ };
+ const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
+ const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf);
+ out.* = .{
+ .step_idx = step_idx,
+ .ns_total = @intCast(duration.toNanoseconds()),
+ };
+ {
+ ws.time_report_mutex.lock(io) catch return;
+ defer ws.time_report_mutex.unlock(io);
+ assert(ws.time_report_msgs[step_idx].len == 0);
+ ws.time_report_msgs[step_idx] = buf;
+ ws.time_report_update_times[step_idx] = ws.now();
+ }
+ ws.notifyUpdate();
+}
+
+pub fn updateTimeReportRunTest(
+ ws: *WebServer,
+ run_step_index: Configuration.Step.Index,
+ tests: *const Step.Run.CachedTestMetadata,
+ ns_per_test: []const u64,
+) void {
+ const maker = ws.maker;
+ const gpa = maker.gpa;
+ const io = maker.graph.io;
+ const all_steps = maker.step_stack.keys();
+
+ const step_idx: u32 = for (all_steps, 0..) |s, i| {
+ if (s == run_step_index) break @intCast(i);
+ } else unreachable;
+
+ assert(tests.names.len == ns_per_test.len);
+ const tests_len: u32 = @intCast(tests.names.len);
+
+ const new_len: u64 = len: {
+ var names_len: u64 = 0;
+ for (0..tests_len) |i| {
+ names_len += tests.testName(@intCast(i)).len + 1;
+ }
+ break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
+ };
+ const old_buf = old: {
+ ws.time_report_mutex.lock(io) catch return;
+ defer ws.time_report_mutex.unlock(io);
+ const old = ws.time_report_msgs[step_idx];
+ ws.time_report_msgs[step_idx] = &.{};
+ break :old old;
+ };
+ const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
+
+ const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]);
+ out_header.* = .{
+ .step_idx = step_idx,
+ .tests_len = tests_len,
+ };
+ var offset: usize = @sizeOf(abi.time_report.RunTestResult);
+ const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]);
+ @memcpy(ns_per_test_out, ns_per_test);
+ offset += tests_len * 8;
+ for (0..tests_len) |i| {
+ const name = tests.testName(@intCast(i));
+ @memcpy(buf[offset..][0..name.len], name);
+ buf[offset..][name.len] = 0;
+ offset += name.len + 1;
+ }
+ assert(offset == buf.len);
+
+ {
+ ws.time_report_mutex.lock(io) catch return;
+ defer ws.time_report_mutex.unlock(io);
+ assert(ws.time_report_msgs[step_idx].len == 0);
+ ws.time_report_msgs[step_idx] = buf;
+ ws.time_report_update_times[step_idx] = ws.now();
+ }
+ ws.notifyUpdate();
+}
+
+const RunnerRequest = union(enum) {
+ rebuild,
+};
+pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
+ const io = ws.maker.graph.io;
+ ws.runner_request_mutex.lock(io) catch return;
+ defer ws.runner_request_mutex.unlock(io);
+ if (ws.runner_request) |req| {
+ ws.runner_request = null;
+ ws.runner_request_empty_cond.signal();
+ return req;
+ }
+ return null;
+}
+pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
+ const io = ws.maker.graph.io;
+ try ws.runner_request_mutex.lock(io);
+ defer ws.runner_request_mutex.unlock(io);
+ while (true) {
+ if (ws.runner_request) |req| {
+ ws.runner_request = null;
+ ws.runner_request_empty_cond.signal(io);
+ return req;
+ }
+ try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex);
+ }
+}
+
+const cache_control_header: http.Header = .{
+ .name = "Cache-Control",
+ .value = "max-age=0, must-revalidate",
+};
diff --git a/lib/compiler/aro/aro/Driver.zig b/lib/compiler/aro/aro/Driver.zig
@@ -1041,9 +1041,10 @@ fn parseTarget(d: *Driver, arch_os_abi: []const u8, opt_cpu_features: ?[]const u
} else if (mem.eql(u8, cpu_name, "baseline")) {
query.cpu_model = .baseline;
} else {
- query.cpu_model = .{ .explicit = arch.parseCpuModel(cpu_name) catch |er| switch (er) {
- error.UnknownCpuModel => return d.fatal("unknown CPU model: '{s}'", .{cpu_name}),
- } };
+ query.cpu_model = .{
+ .explicit = arch.parseCpuModel(cpu_name) orelse
+ return d.fatal("unknown CPU model: '{s}'", .{cpu_name}),
+ };
}
if (opt_sub_arch) |sub_arch| {
diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig
@@ -1,1857 +0,0 @@
-const runner = @This();
-const builtin = @import("builtin");
-
-const std = @import("std");
-const Io = std.Io;
-const assert = std.debug.assert;
-const fmt = std.fmt;
-const mem = std.mem;
-const process = std.process;
-const File = std.Io.File;
-const Step = std.Build.Step;
-const Watch = std.Build.Watch;
-const WebServer = std.Build.WebServer;
-const Allocator = std.mem.Allocator;
-const fatal = std.process.fatal;
-const Writer = std.Io.Writer;
-
-pub const root = @import("@build");
-pub const dependencies = @import("@dependencies");
-
-pub const std_options: std.Options = .{
- .side_channels_mitigations = .none,
- .http_disable_tls = true,
-};
-
-pub fn main(init: process.Init.Minimal) !void {
- // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not
- // always the case. So, we do need a true gpa for some things.
- var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{});
- defer _ = safe_gpa_state.deinit();
- const gpa = safe_gpa_state.allocator();
-
- var threaded: std.Io.Threaded = .init(gpa, .{
- .environ = init.environ,
- .argv0 = .init(init.args),
- });
- defer threaded.deinit();
- const io = threaded.io();
-
- // ...but we'll back our arena by `std.heap.page_allocator` for efficiency.
- var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
- defer arena_instance.deinit();
- const arena = arena_instance.allocator();
-
- const args = try init.args.toSlice(arena);
-
- // skip my own exe name
- var arg_idx: usize = 1;
-
- const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{});
- const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{});
- const build_root = nextArg(args, &arg_idx) orelse fatal("missing build root directory path", .{});
- const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{});
- const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{});
-
- const cwd: Io.Dir = .cwd();
-
- const zig_lib_directory: std.Build.Cache.Directory = .{
- .path = zig_lib_dir,
- .handle = try cwd.openDir(io, zig_lib_dir, .{}),
- };
-
- const build_root_directory: std.Build.Cache.Directory = .{
- .path = build_root,
- .handle = try cwd.openDir(io, build_root, .{}),
- };
-
- const local_cache_directory: std.Build.Cache.Directory = .{
- .path = cache_root,
- .handle = try cwd.createDirPathOpen(io, cache_root, .{}),
- };
-
- const global_cache_directory: std.Build.Cache.Directory = .{
- .path = global_cache_root,
- .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}),
- };
-
- var graph: std.Build.Graph = .{
- .io = io,
- .arena = arena,
- .cache = .{
- .io = io,
- .gpa = gpa,
- .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}),
- .cwd = try process.currentPathAlloc(io, arena),
- },
- .zig_exe = zig_exe,
- .environ_map = try init.environ.createMap(arena),
- .global_cache_root = global_cache_directory,
- .zig_lib_directory = zig_lib_directory,
- .host = .{
- .query = .{},
- .result = try std.zig.system.resolveTargetQuery(io, .{}),
- },
- .time_report = false,
- };
-
- graph.cache.addPrefix(.{ .path = null, .handle = cwd });
- graph.cache.addPrefix(build_root_directory);
- graph.cache.addPrefix(local_cache_directory);
- graph.cache.addPrefix(global_cache_directory);
- graph.cache.hash.addBytes(builtin.zig_version_string);
-
- const builder = try std.Build.create(
- &graph,
- build_root_directory,
- local_cache_directory,
- dependencies.root_deps,
- );
-
- var targets = std.array_list.Managed([]const u8).init(arena);
- var debug_log_scopes = std.array_list.Managed([]const u8).init(arena);
-
- var install_prefix: ?[]const u8 = null;
- var dir_list = std.Build.DirList{};
- var error_style: ErrorStyle = .verbose;
- var multiline_errors: MultilineErrors = .indent;
- var summary: ?Summary = null;
- var max_rss: u64 = 0;
- var skip_oom_steps = false;
- var test_timeout_ns: ?u64 = null;
- var color: Color = .auto;
- var help_menu = false;
- var steps_menu = false;
- var output_tmp_nonce: ?[16]u8 = null;
- var watch = false;
- var fuzz: ?std.Build.Fuzz.Mode = null;
- var debounce_interval_ms: u16 = 50;
- var webui_listen: ?Io.net.IpAddress = null;
-
- if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| {
- if (std.meta.stringToEnum(ErrorStyle, str)) |style| {
- error_style = style;
- }
- }
-
- if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| {
- if (std.meta.stringToEnum(MultilineErrors, str)) |style| {
- multiline_errors = style;
- }
- }
-
- while (nextArg(args, &arg_idx)) |arg| {
- if (mem.startsWith(u8, arg, "-Z")) {
- if (arg.len != 18) fatalWithHint("bad argument: '{s}'", .{arg});
- output_tmp_nonce = arg[2..18].*;
- } else if (mem.startsWith(u8, arg, "-D")) {
- const option_contents = arg[2..];
- if (option_contents.len == 0)
- fatalWithHint("expected option name after '-D'", .{});
- if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| {
- const option_name = option_contents[0..name_end];
- const option_value = option_contents[name_end + 1 ..];
- if (try builder.addUserInputOption(option_name, option_value))
- fatal(" access the help menu with 'zig build -h'", .{});
- } else {
- if (try builder.addUserInputFlag(option_contents))
- fatal(" access the help menu with 'zig build -h'", .{});
- }
- } else if (mem.startsWith(u8, arg, "-")) {
- if (mem.eql(u8, arg, "--verbose")) {
- builder.verbose = true;
- } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
- help_menu = true;
- } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
- install_prefix = nextArgOrFatal(args, &arg_idx);
- } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
- steps_menu = true;
- } else if (mem.startsWith(u8, arg, "-fsys=")) {
- const name = arg["-fsys=".len..];
- graph.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM");
- } else if (mem.startsWith(u8, arg, "-fno-sys=")) {
- const name = arg["-fno-sys=".len..];
- graph.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM");
- } else if (mem.eql(u8, arg, "--release")) {
- builder.release_mode = .any;
- } else if (mem.startsWith(u8, arg, "--release=")) {
- const text = arg["--release=".len..];
- builder.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, text) orelse {
- fatalWithHint("expected [off|any|fast|safe|small] in '{s}', found '{s}'", .{
- arg, text,
- });
- };
- } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
- dir_list.lib_dir = nextArgOrFatal(args, &arg_idx);
- } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
- dir_list.exe_dir = nextArgOrFatal(args, &arg_idx);
- } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
- dir_list.include_dir = nextArgOrFatal(args, &arg_idx);
- } else if (mem.eql(u8, arg, "--sysroot")) {
- builder.sysroot = nextArgOrFatal(args, &arg_idx);
- } else if (mem.eql(u8, arg, "--maxrss")) {
- const max_rss_text = nextArgOrFatal(args, &arg_idx);
- max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| {
- std.debug.print("invalid byte size: '{s}': {s}\n", .{
- max_rss_text, @errorName(err),
- });
- process.exit(1);
- };
- } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
- skip_oom_steps = true;
- } else if (mem.eql(u8, arg, "--test-timeout")) {
- const units: []const struct { []const u8, u64 } = &.{
- .{ "ns", 1 },
- .{ "nanosecond", 1 },
- .{ "us", std.time.ns_per_us },
- .{ "microsecond", std.time.ns_per_us },
- .{ "ms", std.time.ns_per_ms },
- .{ "millisecond", std.time.ns_per_ms },
- .{ "s", std.time.ns_per_s },
- .{ "second", std.time.ns_per_s },
- .{ "m", std.time.ns_per_min },
- .{ "minute", std.time.ns_per_min },
- .{ "h", std.time.ns_per_hour },
- .{ "hour", std.time.ns_per_hour },
- };
- const timeout_str = nextArgOrFatal(args, &arg_idx);
- const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal(
- "invalid timeout '{s}': expected unit (ns, us, ms, s, m, h)",
- .{timeout_str},
- );
- const num_str = timeout_str[0 .. num_end_idx + 1];
- const unit_str = timeout_str[num_end_idx + 1 ..];
- const unit_factor: f64 = for (units) |unit_and_factor| {
- if (std.mem.eql(u8, unit_str, unit_and_factor[0])) {
- break @floatFromInt(unit_and_factor[1]);
- }
- } else fatal(
- "invalid timeout '{s}': invalid unit '{s}' (expected ns, us, ms, s, m, h)",
- .{ timeout_str, unit_str },
- );
- const num_parsed = std.fmt.parseFloat(f64, num_str) catch |err| fatal(
- "invalid timeout '{s}': invalid number '{s}' ({t})",
- .{ timeout_str, num_str, err },
- );
- test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed);
- } else if (mem.eql(u8, arg, "--search-prefix")) {
- const search_prefix = nextArgOrFatal(args, &arg_idx);
- builder.addSearchPrefix(search_prefix);
- } else if (mem.eql(u8, arg, "--libc")) {
- builder.libc_file = nextArgOrFatal(args, &arg_idx);
- } else if (mem.eql(u8, arg, "--color")) {
- const next_arg = nextArg(args, &arg_idx) orelse
- fatalWithHint("expected [auto|on|off] after '{s}'", .{arg});
- color = std.meta.stringToEnum(Color, next_arg) orelse {
- fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{
- arg, next_arg,
- });
- };
- } else if (mem.eql(u8, arg, "--error-style")) {
- const next_arg = nextArg(args, &arg_idx) orelse
- fatalWithHint("expected style after '{s}'", .{arg});
- error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse {
- fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg });
- };
- } else if (mem.eql(u8, arg, "--multiline-errors")) {
- const next_arg = nextArg(args, &arg_idx) orelse
- fatalWithHint("expected style after '{s}'", .{arg});
- multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse {
- fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg });
- };
- } else if (mem.eql(u8, arg, "--summary")) {
- const next_arg = nextArg(args, &arg_idx) orelse
- fatalWithHint("expected [all|new|failures|line|none] after '{s}'", .{arg});
- summary = std.meta.stringToEnum(Summary, next_arg) orelse {
- fatalWithHint("expected [all|new|failures|line|none] after '{s}', found '{s}'", .{
- arg, next_arg,
- });
- };
- } else if (mem.eql(u8, arg, "--seed")) {
- const next_arg = nextArg(args, &arg_idx) orelse
- fatalWithHint("expected u32 after '{s}'", .{arg});
- graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
- fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {s}\n", .{
- next_arg, @errorName(err),
- });
- };
- } else if (mem.eql(u8, arg, "--build-id")) {
- builder.build_id = .fast;
- } else if (mem.startsWith(u8, arg, "--build-id=")) {
- const style = arg["--build-id=".len..];
- builder.build_id = std.zig.BuildId.parse(style) catch |err| {
- fatal("unable to parse --build-id style '{s}': {s}", .{
- style, @errorName(err),
- });
- };
- } else if (mem.eql(u8, arg, "--debounce")) {
- const next_arg = nextArg(args, &arg_idx) orelse
- fatalWithHint("expected u16 after '{s}'", .{arg});
- debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
- fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {t}\n", .{
- next_arg, err,
- });
- };
- } else if (mem.eql(u8, arg, "--webui")) {
- if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
- } else if (mem.startsWith(u8, arg, "--webui=")) {
- const addr_str = arg["--webui=".len..];
- if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{});
- webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| {
- fatal("invalid web UI address '{s}': {s}", .{ addr_str, @errorName(err) });
- };
- } else if (mem.eql(u8, arg, "--debug-log")) {
- const next_arg = nextArgOrFatal(args, &arg_idx);
- try debug_log_scopes.append(next_arg);
- } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
- builder.debug_pkg_config = true;
- } else if (mem.eql(u8, arg, "--debug-rt")) {
- graph.debug_compiler_runtime_libs = .Debug;
- } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| {
- graph.debug_compiler_runtime_libs =
- std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse
- fatal("unrecognized optimization mode: '{s}'", .{rest});
- } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
- builder.debug_compile_errors = true;
- } else if (mem.eql(u8, arg, "--debug-incremental")) {
- builder.debug_incremental = true;
- } else if (mem.eql(u8, arg, "--system")) {
- // The usage text shows another argument after this parameter
- // but it is handled by the parent process. The build runner
- // only sees this flag.
- graph.system_package_mode = true;
- } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) {
- // --glibc-runtimes was the old name of the flag; kept for compatibility for now.
- builder.libc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
- } else if (mem.eql(u8, arg, "--verbose-link")) {
- builder.verbose_link = true;
- } else if (mem.eql(u8, arg, "--verbose-air")) {
- builder.verbose_air = true;
- } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
- builder.verbose_llvm_ir = "-";
- } else if (mem.startsWith(u8, arg, "--verbose-llvm-ir=")) {
- builder.verbose_llvm_ir = arg["--verbose-llvm-ir=".len..];
- } else if (mem.startsWith(u8, arg, "--verbose-llvm-bc=")) {
- builder.verbose_llvm_bc = arg["--verbose-llvm-bc=".len..];
- } else if (mem.eql(u8, arg, "--verbose-cc")) {
- builder.verbose_cc = true;
- } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
- builder.verbose_llvm_cpu_features = true;
- } else if (mem.eql(u8, arg, "--watch")) {
- watch = true;
- } else if (mem.eql(u8, arg, "--time-report")) {
- graph.time_report = true;
- if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
- } else if (mem.eql(u8, arg, "--fuzz")) {
- fuzz = .{ .forever = undefined };
- if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
- } else if (mem.startsWith(u8, arg, "--fuzz=")) {
- const value = arg["--fuzz=".len..];
- if (value.len == 0) fatal("missing argument to --fuzz", .{});
-
- const unit: u8 = value[value.len - 1];
- const digits = switch (unit) {
- '0'...'9' => value,
- 'K', 'M', 'G' => value[0 .. value.len - 1],
- else => fatal(
- "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
- .{},
- ),
- };
-
- const amount = std.fmt.parseInt(u64, digits, 10) catch {
- fatal(
- "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
- .{},
- );
- };
-
- const normalized_amount = std.math.mul(u64, amount, switch (unit) {
- else => unreachable,
- '0'...'9' => 1,
- 'K' => 1000,
- 'M' => 1_000_000,
- 'G' => 1_000_000_000,
- }) catch fatal("fuzzing limit amount overflows u64", .{});
-
- fuzz = .{
- .limit = .{
- .amount = normalized_amount,
- },
- };
- } else if (mem.eql(u8, arg, "-fincremental")) {
- graph.incremental = true;
- } else if (mem.eql(u8, arg, "-fno-incremental")) {
- graph.incremental = false;
- } else if (mem.eql(u8, arg, "-fwine")) {
- builder.enable_wine = true;
- } else if (mem.eql(u8, arg, "-fno-wine")) {
- builder.enable_wine = false;
- } else if (mem.eql(u8, arg, "-fqemu")) {
- builder.enable_qemu = true;
- } else if (mem.eql(u8, arg, "-fno-qemu")) {
- builder.enable_qemu = false;
- } else if (mem.eql(u8, arg, "-fwasmtime")) {
- builder.enable_wasmtime = true;
- } else if (mem.eql(u8, arg, "-fno-wasmtime")) {
- builder.enable_wasmtime = false;
- } else if (mem.eql(u8, arg, "-frosetta")) {
- builder.enable_rosetta = true;
- } else if (mem.eql(u8, arg, "-fno-rosetta")) {
- builder.enable_rosetta = false;
- } else if (mem.eql(u8, arg, "-fdarling")) {
- builder.enable_darling = true;
- } else if (mem.eql(u8, arg, "-fno-darling")) {
- builder.enable_darling = false;
- } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
- graph.allow_so_scripts = true;
- } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
- graph.allow_so_scripts = false;
- } else if (mem.eql(u8, arg, "-freference-trace")) {
- builder.reference_trace = 256;
- } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
- const num = arg["-freference-trace=".len..];
- builder.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
- std.debug.print("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
- process.exit(1);
- };
- } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
- builder.reference_trace = null;
- } else if (mem.cutPrefix(u8, arg, "-j")) |text| {
- const n = std.fmt.parseUnsigned(u32, text, 10) catch |err|
- fatal("unable to parse jobs count '{s}': {t}", .{ text, err });
- if (n < 1) fatal("number of jobs must be at least 1", .{});
- threaded.setAsyncLimit(.limited(n));
- graph.max_jobs = n;
- } else if (mem.eql(u8, arg, "--")) {
- builder.args = argsRest(args, arg_idx);
- break;
- } else {
- fatalWithHint("unrecognized argument: '{s}'", .{arg});
- }
- } else {
- try targets.append(arg);
- }
- }
-
- const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map);
- const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
-
- graph.stderr_mode = switch (color) {
- .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE),
- .on => .escape_codes,
- .off => .no_color,
- };
-
- if (webui_listen != null) {
- if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{});
- if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{});
- }
-
- const main_progress_node = std.Progress.start(io, .{
- .disable_printing = (color == .off),
- });
- defer main_progress_node.end();
-
- builder.debug_log_scopes = debug_log_scopes.items;
- builder.resolveInstallPrefix(install_prefix, dir_list);
- {
- var prog_node = main_progress_node.start("Configure", 0);
- defer prog_node.end();
- try builder.runBuild(root);
- createModuleDependencies(builder) catch @panic("OOM");
- }
-
- if (graph.needed_lazy_dependencies.entries.len != 0) {
- var buffer: std.ArrayList(u8) = .empty;
- for (graph.needed_lazy_dependencies.keys()) |k| {
- try buffer.appendSlice(arena, k);
- try buffer.append(arena, '\n');
- }
- const s = std.fs.path.sep_str;
- const tmp_sub_path = "tmp" ++ s ++ (output_tmp_nonce orelse fatal("missing -Z arg", .{}));
- local_cache_directory.handle.writeFile(io, .{
- .sub_path = tmp_sub_path,
- .data = buffer.items,
- .flags = .{ .exclusive = true },
- }) catch |err| {
- fatal("unable to write configuration results to '{f}{s}': {s}", .{
- local_cache_directory, tmp_sub_path, @errorName(err),
- });
- };
- process.exit(3); // Indicate configure phase failed with meaningful stdout.
- }
-
- if (builder.validateUserInputDidItFail()) {
- fatal(" access the help menu with 'zig build -h'", .{});
- }
-
- validateSystemLibraryOptions(builder);
-
- if (help_menu) {
- var w = initStdoutWriter(io);
- printUsage(builder, w) catch return stdout_writer_allocation.err.?;
- w.flush() catch return stdout_writer_allocation.err.?;
- return;
- }
-
- if (steps_menu) {
- var w = initStdoutWriter(io);
- printSteps(builder, w) catch return stdout_writer_allocation.err.?;
- w.flush() catch return stdout_writer_allocation.err.?;
- return;
- }
-
- var run: Run = .{
- .gpa = gpa,
-
- .available_rss = max_rss,
- .max_rss_is_default = false,
- .max_rss_mutex = .init,
- .skip_oom_steps = skip_oom_steps,
- .unit_test_timeout_ns = test_timeout_ns,
-
- .watch = watch,
- .web_server = undefined, // set after `prepare`
- .memory_blocked_steps = .empty,
- .step_stack = .empty,
-
- .error_style = error_style,
- .multiline_errors = multiline_errors,
- .summary = summary orelse if (watch or webui_listen != null) .line else .failures,
- };
- defer {
- run.memory_blocked_steps.deinit(gpa);
- run.step_stack.deinit(gpa);
- }
-
- if (run.available_rss == 0) {
- run.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
- run.max_rss_is_default = true;
- }
-
- prepare(arena, builder, targets.items, &run, graph.random_seed) catch |err| switch (err) {
- error.DependencyLoopDetected, error.InsufficientMemory => {
- // Perhaps in the future there could be an Advanced Options flag
- // such as --debug-build-runner-leaks which would make this code
- // return instead of calling exit.
- _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
- process.exit(1);
- },
- else => |e| return e,
- };
-
- var w: Watch = w: {
- if (!watch) break :w undefined;
- if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag});
- break :w try .init(graph.cache.cwd);
- };
-
- const now = Io.Clock.Timestamp.now(io, .awake);
-
- run.web_server = if (webui_listen) |listen_address| ws: {
- if (builtin.single_threaded) unreachable; // `fatal` above
- break :ws .init(.{
- .gpa = gpa,
- .graph = &graph,
- .all_steps = run.step_stack.keys(),
- .root_prog_node = main_progress_node,
- .watch = watch,
- .listen_address = listen_address,
- .base_timestamp = now,
- });
- } else null;
-
- if (run.web_server) |*ws| {
- ws.start() catch |err| fatal("failed to start web server: {t}", .{err});
- }
-
- rebuild: while (true) : (if (run.error_style.clearOnUpdate()) {
- const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
- defer io.unlockStderr();
- try stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H");
- }) {
- if (run.web_server) |*ws| ws.startBuild();
-
- try runStepNames(
- builder,
- targets.items,
- main_progress_node,
- &run,
- fuzz,
- );
-
- if (run.web_server) |*web_server| {
- if (fuzz) |mode| if (mode != .forever) fatal(
- "error: limited fuzzing is not implemented yet for --webui",
- .{},
- );
-
- web_server.finishBuild(.{ .fuzz = fuzz != null });
- }
-
- if (run.web_server) |*ws| {
- assert(!watch); // fatal error after CLI parsing
- while (true) switch (try ws.wait()) {
- .rebuild => {
- for (run.step_stack.keys()) |step| {
- step.state = .precheck_done;
- step.pending_deps = @intCast(step.dependencies.items.len);
- step.reset(gpa);
- }
- continue :rebuild;
- },
- };
- }
-
- // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
- if (!Watch.have_impl) unreachable;
-
- try w.update(gpa, run.step_stack.keys());
-
- // Wait until a file system notification arrives. Read all such events
- // until the buffer is empty. Then wait for a debounce interval, resetting
- // if any more events come in. After the debounce interval has passed,
- // trigger a rebuild on all steps with modified inputs, as well as their
- // recursive dependants.
- var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
- const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
- w.dir_count, countSubProcesses(run.step_stack.keys()),
- }) catch &caption_buf;
- var debouncing_node = main_progress_node.start(caption, 0);
- var in_debounce = false;
- while (true) switch (try w.wait(gpa, io, if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) {
- .timeout => {
- assert(in_debounce);
- debouncing_node.end();
- markFailedStepsDirty(gpa, run.step_stack.keys());
- continue :rebuild;
- },
- .dirty => if (!in_debounce) {
- in_debounce = true;
- debouncing_node.end();
- debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
- },
- .clean => {},
- };
- }
-}
-
-fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
- for (all_steps) |step| switch (step.state) {
- .dependency_failure, .failure, .skipped => _ = step.invalidateResult(gpa),
- else => continue,
- };
- // Now that all dirty steps have been found, the remaining steps that
- // succeeded from last run shall be marked "cached".
- for (all_steps) |step| switch (step.state) {
- .success => step.result_cached = true,
- else => continue,
- };
-}
-
-fn countSubProcesses(all_steps: []const *Step) usize {
- var count: usize = 0;
- for (all_steps) |s| {
- count += @intFromBool(s.getZigProcess() != null);
- }
- return count;
-}
-
-const Run = struct {
- gpa: Allocator,
-
- available_rss: usize,
- max_rss_is_default: bool,
- max_rss_mutex: Io.Mutex,
- skip_oom_steps: bool,
- unit_test_timeout_ns: ?u64,
- watch: bool,
- web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn,
- /// Allocated into `gpa`.
- memory_blocked_steps: std.ArrayList(*Step),
- /// Allocated into `gpa`.
- step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
-
- error_style: ErrorStyle,
- multiline_errors: MultilineErrors,
- summary: Summary,
-};
-
-fn prepare(
- arena: Allocator,
- b: *std.Build,
- step_names: []const []const u8,
- run: *Run,
- seed: u32,
-) !void {
- const gpa = run.gpa;
- const step_stack = &run.step_stack;
-
- if (step_names.len == 0) {
- try step_stack.put(gpa, b.default_step, {});
- } else {
- try step_stack.ensureUnusedCapacity(gpa, step_names.len);
- for (0..step_names.len) |i| {
- const step_name = step_names[step_names.len - i - 1];
- const s = b.top_level_steps.get(step_name) orelse {
- std.log.info("access the help menu with \"zig build -h\"", .{});
- fatal("no step named '{s}'", .{step_name});
- };
- step_stack.putAssumeCapacity(&s.step, {});
- }
- }
-
- const starting_steps = try arena.dupe(*Step, step_stack.keys());
-
- var rng = std.Random.DefaultPrng.init(seed);
- const rand = rng.random();
- rand.shuffle(*Step, starting_steps);
-
- for (starting_steps) |s| {
- try constructGraphAndCheckForDependencyLoop(gpa, b, s, &run.step_stack, rand);
- }
-
- {
- // Check that we have enough memory to complete the build.
- var any_problems = false;
- var max_needed: usize = 0;
- for (step_stack.keys()) |s| {
- if (s.max_rss == 0) continue;
- max_needed = @max(max_needed, s.max_rss);
- if (s.max_rss > run.available_rss) {
- if (run.skip_oom_steps) {
- s.state = .skipped_oom;
- for (s.dependants.items) |dependant| {
- dependant.pending_deps -= 1;
- }
- } else {
- std.log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{
- s.owner.dep_prefix, s.name, s.max_rss, run.available_rss,
- });
- any_problems = true;
- }
- }
- }
- if (any_problems) {
- if (run.max_rss_is_default) {
- std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{
- max_needed,
- });
- }
- return error.InsufficientMemory;
- }
- }
-}
-
-fn runStepNames(
- b: *std.Build,
- step_names: []const []const u8,
- parent_prog_node: std.Progress.Node,
- run: *Run,
- fuzz: ?std.Build.Fuzz.Mode,
-) !void {
- const gpa = run.gpa;
- const graph = b.graph;
- const io = graph.io;
- const step_stack = &run.step_stack;
-
- {
- // Collect the initial set of tasks (those with no outstanding dependencies) into a buffer,
- // then spawn them. The buffer is so that we don't race with `makeStep` and end up thinking
- // a step is initial when it actually became ready due to an earlier initial step.
- var initial_set: std.ArrayList(*Step) = .empty;
- defer initial_set.deinit(gpa);
- try initial_set.ensureUnusedCapacity(gpa, step_stack.count());
- for (step_stack.keys()) |s| {
- if (s.state == .precheck_done and s.pending_deps == 0) {
- initial_set.appendAssumeCapacity(s);
- }
- }
-
- const step_prog = parent_prog_node.start("steps", step_stack.count());
- defer step_prog.end();
-
- var group: Io.Group = .init;
- defer group.cancel(io);
- // Start working on all of the initial steps...
- for (initial_set.items) |s| try stepReady(&group, b, s, step_prog, run);
- // ...and `makeStep` will trigger every other step when their last dependency finishes.
- try group.await(io);
- }
-
- assert(run.memory_blocked_steps.items.len == 0);
-
- var test_pass_count: usize = 0;
- var test_skip_count: usize = 0;
- var test_fail_count: usize = 0;
- var test_crash_count: usize = 0;
- var test_timeout_count: usize = 0;
-
- var test_count: usize = 0;
-
- var success_count: usize = 0;
- var skipped_count: usize = 0;
- var failure_count: usize = 0;
- var pending_count: usize = 0;
- var total_compile_errors: usize = 0;
-
- var cleanup_task = io.async(cleanTmpFiles, .{ io, step_stack.keys() });
- defer cleanup_task.await(io);
-
- for (step_stack.keys()) |s| {
- test_pass_count += s.test_results.passCount();
- test_skip_count += s.test_results.skip_count;
- test_fail_count += s.test_results.fail_count;
- test_crash_count += s.test_results.crash_count;
- test_timeout_count += s.test_results.timeout_count;
-
- test_count += s.test_results.test_count;
-
- switch (s.state) {
- .precheck_unstarted => unreachable,
- .precheck_started => unreachable,
- .precheck_done => unreachable,
- .dependency_failure => pending_count += 1,
- .success => success_count += 1,
- .skipped, .skipped_oom => skipped_count += 1,
- .failure => {
- failure_count += 1;
- const compile_errors_len = s.result_error_bundle.errorMessageCount();
- if (compile_errors_len > 0) {
- total_compile_errors += compile_errors_len;
- }
- },
- }
- }
-
- if (fuzz) |mode| blk: {
- switch (builtin.os.tag) {
- // Current implementation depends on two things that need to be ported to Windows:
- // * Memory-mapping to share data between the fuzzer and build runner.
- // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
- // many addresses to source locations).
- .windows => fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
- else => {},
- }
- if (@bitSizeOf(usize) != 64) {
- // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
- // being compatible with file system's u64 return value. This is not the case
- // on 32-bit platforms.
- // Affects or affected by issues #5185, #22523, and #22464.
- fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
- }
-
- switch (mode) {
- .forever => break :blk,
- .limit => {},
- }
-
- assert(mode == .limit);
- var f = std.Build.Fuzz.init(
- gpa,
- io,
- step_stack.keys(),
- parent_prog_node,
- mode,
- ) catch |err| fatal("failed to start fuzzer: {t}", .{err});
- defer f.deinit();
-
- f.start();
- try f.waitAndPrintReport();
- }
-
- // Every test has a state
- assert(test_pass_count + test_skip_count + test_fail_count + test_crash_count + test_timeout_count == test_count);
-
- if (failure_count == 0) {
- std.Progress.setStatus(.success);
- } else {
- std.Progress.setStatus(.failure);
- }
-
- summary: {
- switch (run.summary) {
- .all, .new, .line => {},
- .failures => if (failure_count == 0) break :summary,
- .none => break :summary,
- }
-
- const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
- defer io.unlockStderr();
- const t = stderr.terminal();
- const w = &stderr.file_writer.interface;
-
- const total_count = success_count + failure_count + pending_count + skipped_count;
- t.setColor(.cyan) catch {};
- t.setColor(.bold) catch {};
- w.writeAll("Build Summary: ") catch {};
- t.setColor(.reset) catch {};
- w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
- {
- t.setColor(.dim) catch {};
- var first = true;
- if (skipped_count > 0) {
- w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {};
- first = false;
- }
- if (failure_count > 0) {
- w.print("{s}{d} failed", .{ if (first) " (" else ", ", failure_count }) catch {};
- first = false;
- }
- if (!first) w.writeByte(')') catch {};
- t.setColor(.reset) catch {};
- }
-
- if (test_count > 0) {
- w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {};
- t.setColor(.dim) catch {};
- var first = true;
- if (test_skip_count > 0) {
- w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {};
- first = false;
- }
- if (test_fail_count > 0) {
- w.print("{s}{d} failed", .{ if (first) " (" else ", ", test_fail_count }) catch {};
- first = false;
- }
- if (test_crash_count > 0) {
- w.print("{s}{d} crashed", .{ if (first) " (" else ", ", test_crash_count }) catch {};
- first = false;
- }
- if (test_timeout_count > 0) {
- w.print("{s}{d} timed out", .{ if (first) " (" else ", ", test_timeout_count }) catch {};
- first = false;
- }
- if (!first) w.writeByte(')') catch {};
- t.setColor(.reset) catch {};
- }
-
- w.writeAll("\n") catch {};
-
- if (run.summary == .line) break :summary;
-
- // Print a fancy tree with build results.
- var step_stack_copy = try step_stack.clone(gpa);
- defer step_stack_copy.deinit(gpa);
-
- var print_node: PrintNode = .{ .parent = null };
- if (step_names.len == 0) {
- print_node.last = true;
- printTreeStep(b, b.default_step, run, t, &print_node, &step_stack_copy) catch {};
- } else {
- const last_index = if (run.summary == .all) b.top_level_steps.count() else blk: {
- var i: usize = step_names.len;
- while (i > 0) {
- i -= 1;
- const step = b.top_level_steps.get(step_names[i]).?.step;
- const found = switch (run.summary) {
- .all, .line, .none => unreachable,
- .failures => step.state != .success,
- .new => !step.result_cached,
- };
- if (found) break :blk i;
- }
- break :blk b.top_level_steps.count();
- };
- for (step_names, 0..) |step_name, i| {
- const tls = b.top_level_steps.get(step_name).?;
- print_node.last = i + 1 == last_index;
- printTreeStep(b, &tls.step, run, t, &print_node, &step_stack_copy) catch {};
- }
- }
- w.writeByte('\n') catch {};
- }
-
- if (run.watch or run.web_server != null) return;
-
- // Perhaps in the future there could be an Advanced Options flag such as
- // --debug-build-runner-leaks which would make this code return instead of
- // calling exit.
-
- const code: u8 = code: {
- if (failure_count == 0) break :code 0; // success
- if (run.error_style.verboseContext()) break :code 1; // failure; print build command
- break :code 2; // failure; do not print build command
- };
- _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
- process.exit(code);
-}
-
-const PrintNode = struct {
- parent: ?*PrintNode,
- last: bool = false,
-};
-
-fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void {
- const parent = node.parent orelse return;
- const writer = stderr.writer;
- if (parent.parent == null) return;
- try printPrefix(parent, stderr);
- if (parent.last) {
- try writer.writeAll(" ");
- } else {
- try writer.writeAll(switch (stderr.mode) {
- .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │
- else => "| ",
- });
- }
-}
-
-fn printChildNodePrefix(stderr: Io.Terminal) !void {
- try stderr.writer.writeAll(switch (stderr.mode) {
- .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─
- else => "+- ",
- });
-}
-
-fn printStepStatus(s: *Step, stderr: Io.Terminal, run: *const Run) !void {
- const writer = stderr.writer;
- switch (s.state) {
- .precheck_unstarted => unreachable,
- .precheck_started => unreachable,
- .precheck_done => unreachable,
-
- .dependency_failure => {
- try stderr.setColor(.dim);
- try writer.writeAll(" transitive failure\n");
- try stderr.setColor(.reset);
- },
-
- .success => {
- try stderr.setColor(.green);
- if (s.result_cached) {
- try writer.writeAll(" cached");
- } else if (s.test_results.test_count > 0) {
- const pass_count = s.test_results.passCount();
- assert(s.test_results.test_count == pass_count + s.test_results.skip_count);
- try writer.print(" {d} pass", .{pass_count});
- if (s.test_results.skip_count > 0) {
- try stderr.setColor(.reset);
- try writer.writeAll(", ");
- try stderr.setColor(.yellow);
- try writer.print("{d} skip", .{s.test_results.skip_count});
- }
- try stderr.setColor(.reset);
- try writer.print(" ({d} total)", .{s.test_results.test_count});
- } else {
- try writer.writeAll(" success");
- }
- try stderr.setColor(.reset);
- if (s.result_duration_ns) |ns| {
- try stderr.setColor(.dim);
- if (ns >= std.time.ns_per_min) {
- try writer.print(" {d}m", .{ns / std.time.ns_per_min});
- } else if (ns >= std.time.ns_per_s) {
- try writer.print(" {d}s", .{ns / std.time.ns_per_s});
- } else if (ns >= std.time.ns_per_ms) {
- try writer.print(" {d}ms", .{ns / std.time.ns_per_ms});
- } else if (ns >= std.time.ns_per_us) {
- try writer.print(" {d}us", .{ns / std.time.ns_per_us});
- } else {
- try writer.print(" {d}ns", .{ns});
- }
- try stderr.setColor(.reset);
- }
- if (s.result_peak_rss != 0) {
- const rss = s.result_peak_rss;
- try stderr.setColor(.dim);
- if (rss >= 1000_000_000) {
- try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000});
- } else if (rss >= 1000_000) {
- try writer.print(" MaxRSS:{d}M", .{rss / 1000_000});
- } else if (rss >= 1000) {
- try writer.print(" MaxRSS:{d}K", .{rss / 1000});
- } else {
- try writer.print(" MaxRSS:{d}B", .{rss});
- }
- try stderr.setColor(.reset);
- }
- try writer.writeAll("\n");
- },
- .skipped => {
- try stderr.setColor(.yellow);
- try writer.writeAll(" skipped\n");
- try stderr.setColor(.reset);
- },
- .skipped_oom => {
- try stderr.setColor(.yellow);
- try writer.writeAll(" skipped (not enough memory)");
- try stderr.setColor(.dim);
- try writer.print(" upper bound of {d} exceeded runner limit ({d})\n", .{ s.max_rss, run.available_rss });
- try stderr.setColor(.reset);
- },
- .failure => {
- try printStepFailure(s, stderr, false);
- try stderr.setColor(.reset);
- },
- }
-}
-
-fn printStepFailure(s: *Step, stderr: Io.Terminal, dim: bool) !void {
- const w = stderr.writer;
- if (s.result_error_bundle.errorMessageCount() > 0) {
- try stderr.setColor(.red);
- try w.print(" {d} errors\n", .{
- s.result_error_bundle.errorMessageCount(),
- });
- } else if (!s.test_results.isSuccess()) {
- // These first values include all of the test "statuses". Every test is either passsed,
- // skipped, failed, crashed, or timed out.
- try stderr.setColor(.green);
- try w.print(" {d} pass", .{s.test_results.passCount()});
- try stderr.setColor(.reset);
- if (dim) try stderr.setColor(.dim);
- if (s.test_results.skip_count > 0) {
- try w.writeAll(", ");
- try stderr.setColor(.yellow);
- try w.print("{d} skip", .{s.test_results.skip_count});
- try stderr.setColor(.reset);
- if (dim) try stderr.setColor(.dim);
- }
- if (s.test_results.fail_count > 0) {
- try w.writeAll(", ");
- try stderr.setColor(.red);
- try w.print("{d} fail", .{s.test_results.fail_count});
- try stderr.setColor(.reset);
- if (dim) try stderr.setColor(.dim);
- }
- if (s.test_results.crash_count > 0) {
- try w.writeAll(", ");
- try stderr.setColor(.red);
- try w.print("{d} crash", .{s.test_results.crash_count});
- try stderr.setColor(.reset);
- if (dim) try stderr.setColor(.dim);
- }
- if (s.test_results.timeout_count > 0) {
- try w.writeAll(", ");
- try stderr.setColor(.red);
- try w.print("{d} timeout", .{s.test_results.timeout_count});
- try stderr.setColor(.reset);
- if (dim) try stderr.setColor(.dim);
- }
- try w.print(" ({d} total)", .{s.test_results.test_count});
-
- // Memory leaks are intentionally written after the total, because is isn't a test *status*,
- // but just a flag that any tests -- even passed ones -- can have. We also use a different
- // separator, so it looks like:
- // 2 pass, 1 skip, 2 fail (5 total); 2 leaks
- if (s.test_results.leak_count > 0) {
- try w.writeAll("; ");
- try stderr.setColor(.red);
- try w.print("{d} leaks", .{s.test_results.leak_count});
- try stderr.setColor(.reset);
- if (dim) try stderr.setColor(.dim);
- }
-
- // It's usually not helpful to know how many error logs there were because they tend to
- // just come with other errors (e.g. crashes and leaks print stack traces, and clean
- // failures print error traces). So only mention them if they're the only thing causing
- // the failure.
- const show_err_logs: bool = show: {
- var alt_results = s.test_results;
- alt_results.log_err_count = 0;
- break :show alt_results.isSuccess();
- };
- if (show_err_logs) {
- try w.writeAll("; ");
- try stderr.setColor(.red);
- try w.print("{d} error logs", .{s.test_results.log_err_count});
- try stderr.setColor(.reset);
- if (dim) try stderr.setColor(.dim);
- }
-
- try w.writeAll("\n");
- } else if (s.result_error_msgs.items.len > 0) {
- try stderr.setColor(.red);
- try w.writeAll(" failure\n");
- } else {
- assert(s.result_stderr.len > 0);
- try stderr.setColor(.red);
- try w.writeAll(" w\n");
- }
-}
-
-fn printTreeStep(
- b: *std.Build,
- s: *Step,
- run: *const Run,
- stderr: Io.Terminal,
- parent_node: *PrintNode,
- step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
-) !void {
- const writer = stderr.writer;
- const first = step_stack.swapRemove(s);
- const summary = run.summary;
- const skip = switch (summary) {
- .none, .line => unreachable,
- .all => false,
- .new => s.result_cached,
- .failures => s.state == .success,
- };
- if (skip) return;
- try printPrefix(parent_node, stderr);
-
- if (parent_node.parent != null) {
- if (parent_node.last) {
- try printChildNodePrefix(stderr);
- } else {
- try writer.writeAll(switch (stderr.mode) {
- .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─
- else => "+- ",
- });
- }
- }
-
- if (!first) try stderr.setColor(.dim);
-
- // dep_prefix omitted here because it is redundant with the tree.
- try writer.writeAll(s.name);
-
- if (first) {
- try printStepStatus(s, stderr, run);
-
- const last_index = if (summary == .all) s.dependencies.items.len -| 1 else blk: {
- var i: usize = s.dependencies.items.len;
- while (i > 0) {
- i -= 1;
-
- const step = s.dependencies.items[i];
- const found = switch (summary) {
- .all, .line, .none => unreachable,
- .failures => step.state != .success,
- .new => !step.result_cached,
- };
- if (found) break :blk i;
- }
- break :blk s.dependencies.items.len -| 1;
- };
- for (s.dependencies.items, 0..) |dep, i| {
- var print_node: PrintNode = .{
- .parent = parent_node,
- .last = i == last_index,
- };
- try printTreeStep(b, dep, run, stderr, &print_node, step_stack);
- }
- } else {
- if (s.dependencies.items.len == 0) {
- try writer.writeAll(" (reused)\n");
- } else {
- try writer.print(" (+{d} more reused dependencies)\n", .{
- s.dependencies.items.len,
- });
- }
- try stderr.setColor(.reset);
- }
-}
-
-/// Traverse the dependency graph depth-first and make it undirected by having
-/// steps know their dependants (they only know dependencies at start).
-/// Along the way, check that there is no dependency loop, and record the steps
-/// in traversal order in `step_stack`.
-/// Each step has its dependencies traversed in random order, this accomplishes
-/// two things:
-/// - `step_stack` will be in randomized-depth-first order, so the build runner
-/// spawns initial steps in a random order
-/// - each step's `dependants` list is also filled in a random order, so that
-/// when it finishes executing in `makeStep`, it spawns next steps to run in
-/// random order
-fn constructGraphAndCheckForDependencyLoop(
- gpa: Allocator,
- b: *std.Build,
- s: *Step,
- step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
- rand: std.Random,
-) !void {
- switch (s.state) {
- .precheck_started => {
- std.debug.print("dependency loop detected:\n {s}\n", .{s.name});
- return error.DependencyLoopDetected;
- },
- .precheck_unstarted => {
- s.state = .precheck_started;
-
- try step_stack.ensureUnusedCapacity(gpa, s.dependencies.items.len);
-
- // We dupe to avoid shuffling the steps in the summary, it depends
- // on s.dependencies' order.
- const deps = gpa.dupe(*Step, s.dependencies.items) catch @panic("OOM");
- defer gpa.free(deps);
-
- rand.shuffle(*Step, deps);
-
- for (deps) |dep| {
- try step_stack.put(gpa, dep, {});
- try dep.dependants.append(b.allocator, s);
- constructGraphAndCheckForDependencyLoop(gpa, b, dep, step_stack, rand) catch |err| {
- if (err == error.DependencyLoopDetected) {
- std.debug.print(" {s}\n", .{s.name});
- }
- return err;
- };
- }
-
- s.state = .precheck_done;
- s.pending_deps = @intCast(s.dependencies.items.len);
- },
- .precheck_done => {},
-
- // These don't happen until we actually run the step graph.
- .dependency_failure => unreachable,
- .success => unreachable,
- .failure => unreachable,
- .skipped => unreachable,
- .skipped_oom => unreachable,
- }
-}
-
-/// Runs the "make" function of the single step `s`, updates its state, and then spawns newly-ready
-/// dependant steps in `group`. If `s` makes an RSS claim (i.e. `s.max_rss != 0`), the caller must
-/// have already subtracted this value from `run.available_rss`. This function will release the RSS
-/// claim (i.e. add `s.max_rss` back into `run.available_rss`) and queue any viable memory-blocked
-/// steps after "make" completes for `s`.
-fn makeStep(
- group: *Io.Group,
- b: *std.Build,
- s: *Step,
- root_prog_node: std.Progress.Node,
- run: *Run,
-) Io.Cancelable!void {
- const graph = b.graph;
- const io = graph.io;
- const gpa = run.gpa;
-
- {
- const step_prog_node = root_prog_node.start(s.name, 0);
- defer step_prog_node.end();
-
- if (run.web_server) |*ws| ws.updateStepStatus(s, .wip);
-
- const new_state: Step.State = for (s.dependencies.items) |dep| {
- switch (@atomicLoad(Step.State, &dep.state, .monotonic)) {
- .precheck_unstarted => unreachable,
- .precheck_started => unreachable,
- .precheck_done => unreachable,
-
- .failure,
- .dependency_failure,
- .skipped_oom,
- => break .dependency_failure,
-
- .success, .skipped => {},
- }
- } else if (s.make(.{
- .progress_node = step_prog_node,
- .watch = run.watch,
- .web_server = if (run.web_server) |*ws| ws else null,
- .unit_test_timeout_ns = run.unit_test_timeout_ns,
- .gpa = gpa,
- })) state: {
- break :state .success;
- } else |err| switch (err) {
- error.MakeFailed => .failure,
- error.MakeSkipped => .skipped,
- };
-
- @atomicStore(Step.State, &s.state, new_state, .monotonic);
-
- switch (new_state) {
- .precheck_unstarted => unreachable,
- .precheck_started => unreachable,
- .precheck_done => unreachable,
-
- .failure,
- .dependency_failure,
- .skipped_oom,
- => {
- if (run.web_server) |*ws| ws.updateStepStatus(s, .failure);
- std.Progress.setStatus(.failure_working);
- },
-
- .success,
- .skipped,
- => {
- if (run.web_server) |*ws| ws.updateStepStatus(s, .success);
- },
- }
- }
-
- // No matter the result, we want to display error/warning messages.
- if (s.result_error_bundle.errorMessageCount() > 0 or
- s.result_error_msgs.items.len > 0 or
- s.result_stderr.len > 0)
- {
- const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
- defer io.unlockStderr();
- printErrorMessages(gpa, s, .{}, stderr.terminal(), run.error_style, run.multiline_errors) catch {};
- }
-
- if (s.max_rss != 0) {
- var dispatch_set: std.ArrayList(*Step) = .empty;
- defer dispatch_set.deinit(gpa);
-
- // Release our RSS claim and kick off some blocked steps if possible. We use `dispatch_set`
- // as a staging buffer to avoid recursing into `makeStep` while `run.max_rss_mutex` is held.
- {
- try run.max_rss_mutex.lock(io);
- defer run.max_rss_mutex.unlock(io);
- run.available_rss += s.max_rss;
- dispatch_set.ensureUnusedCapacity(gpa, run.memory_blocked_steps.items.len) catch @panic("OOM");
- while (run.memory_blocked_steps.getLast()) |candidate| {
- if (run.available_rss < candidate.max_rss) break;
- assert(run.memory_blocked_steps.pop() == candidate);
- dispatch_set.appendAssumeCapacity(candidate);
- }
- }
- for (dispatch_set.items) |candidate| {
- group.async(io, makeStep, .{ group, b, candidate, root_prog_node, run });
- }
- }
-
- for (s.dependants.items) |dependant| {
- // `.acq_rel` synchronizes with itself to ensure all dependencies' final states are visible when this hits 0.
- if (@atomicRmw(u32, &dependant.pending_deps, .Sub, 1, .acq_rel) == 1) {
- try stepReady(group, b, dependant, root_prog_node, run);
- }
- }
-}
-
-fn stepReady(
- group: *Io.Group,
- b: *std.Build,
- s: *Step,
- root_prog_node: std.Progress.Node,
- run: *Run,
-) !void {
- const io = b.graph.io;
- if (s.max_rss != 0) {
- try run.max_rss_mutex.lock(io);
- defer run.max_rss_mutex.unlock(io);
- if (run.available_rss < s.max_rss) {
- // Running this step right now could possibly exceed the allotted RSS.
- run.memory_blocked_steps.append(run.gpa, s) catch @panic("OOM");
- return;
- }
- run.available_rss -= s.max_rss;
- }
- group.async(io, makeStep, .{ group, b, s, root_prog_node, run });
-}
-
-pub fn printErrorMessages(
- gpa: Allocator,
- failing_step: *Step,
- options: std.zig.ErrorBundle.RenderOptions,
- stderr: Io.Terminal,
- error_style: ErrorStyle,
- multiline_errors: MultilineErrors,
-) !void {
- const writer = stderr.writer;
- if (error_style.verboseContext()) {
- // Provide context for where these error messages are coming from by
- // printing the corresponding Step subtree.
- var step_stack: std.ArrayList(*Step) = .empty;
- defer step_stack.deinit(gpa);
- try step_stack.append(gpa, failing_step);
- while (step_stack.items[step_stack.items.len - 1].dependants.items.len != 0) {
- try step_stack.append(gpa, step_stack.items[step_stack.items.len - 1].dependants.items[0]);
- }
-
- // Now, `step_stack` has the subtree that we want to print, in reverse order.
- try stderr.setColor(.dim);
- var indent: usize = 0;
- while (step_stack.pop()) |s| : (indent += 1) {
- if (indent > 0) {
- try writer.splatByteAll(' ', (indent - 1) * 3);
- try printChildNodePrefix(stderr);
- }
-
- try writer.writeAll(s.name);
-
- if (s == failing_step) {
- try printStepFailure(s, stderr, true);
- } else {
- try writer.writeAll("\n");
- }
- }
- try stderr.setColor(.reset);
- } else {
- // Just print the failing step itself.
- try stderr.setColor(.dim);
- try writer.writeAll(failing_step.name);
- try printStepFailure(failing_step, stderr, true);
- try stderr.setColor(.reset);
- }
-
- if (failing_step.result_stderr.len > 0) {
- try writer.writeAll(failing_step.result_stderr);
- if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) {
- try writer.writeAll("\n");
- }
- }
-
- try failing_step.result_error_bundle.renderToTerminal(options, stderr);
-
- for (failing_step.result_error_msgs.items) |msg| {
- try stderr.setColor(.red);
- try writer.writeAll("error:");
- try stderr.setColor(.reset);
- if (std.mem.indexOfScalar(u8, msg, '\n') == null) {
- try writer.print(" {s}\n", .{msg});
- } else switch (multiline_errors) {
- .indent => {
- var it = std.mem.splitScalar(u8, msg, '\n');
- try writer.print(" {s}\n", .{it.first()});
- while (it.next()) |line| {
- try writer.print(" {s}\n", .{line});
- }
- },
- .newline => try writer.print("\n{s}\n", .{msg}),
- .none => try writer.print(" {s}\n", .{msg}),
- }
- }
-
- if (error_style.verboseContext()) {
- if (failing_step.result_failed_command) |cmd_str| {
- try stderr.setColor(.red);
- try writer.writeAll("failed command: ");
- try stderr.setColor(.reset);
- try writer.writeAll(cmd_str);
- try writer.writeByte('\n');
- }
- }
-
- try writer.writeByte('\n');
-}
-
-fn printSteps(builder: *std.Build, w: *Writer) !void {
- const arena = builder.graph.arena;
- for (builder.top_level_steps.values()) |top_level_step| {
- const name = if (&top_level_step.step == builder.default_step)
- try fmt.allocPrint(arena, "{s} (default)", .{top_level_step.step.name})
- else
- top_level_step.step.name;
- try w.print(" {s:<28} {s}\n", .{ name, top_level_step.description });
- }
-}
-
-fn printUsage(b: *std.Build, w: *Writer) !void {
- const arena = b.graph.arena;
-
- try w.print(
- \\Usage: {s} build [steps] [options]
- \\
- \\Steps:
- \\
- , .{b.graph.zig_exe});
- try printSteps(b, w);
- try w.writeAll(
- \\
- \\Project-Specific Options:
- \\
- );
-
- if (b.available_options_list.items.len == 0) {
- try w.print(" (none)\n", .{});
- } else {
- for (b.available_options_list.items) |option| {
- const name = try fmt.allocPrint(arena, " -D{s}=[{t}]", .{ option.name, option.type_id });
- try w.print("{s:<30} {s}\n", .{ name, option.description });
- if (option.enum_options) |enum_options| {
- const padding: [33]u8 = @splat(' ');
- try w.writeAll(padding ++ "Supported Values:\n");
- for (enum_options) |enum_option| {
- try w.print(padding ++ " {s}\n", .{enum_option});
- }
- }
- }
- }
-
- try w.writeAll(
- \\
- \\System Integration Options:
- \\ --search-prefix [path] Add a path to look for binaries, libraries, headers
- \\ --sysroot [path] Set the system root directory (usually /)
- \\ --libc [file] Provide a file which specifies libc paths
- \\
- \\ --system [pkgdir] Disable package fetching; enable all integrations
- \\ -fsys=[name] Enable a system integration
- \\ -fno-sys=[name] Disable a system integration
- \\
- \\ -fdarling, -fno-darling Integration with system-installed Darling to
- \\ execute macOS programs on Linux hosts
- \\ (default: no)
- \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute
- \\ foreign-architecture programs on Linux hosts
- \\ (default: no)
- \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc
- \\ (e.g. glibc or musl) built for multiple foreign
- \\ architectures, allowing execution of non-native
- \\ programs that link with libc.
- \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
- \\ ARM64 macOS hosts. (default: no)
- \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
- \\ execute WASI binaries. (default: no)
- \\ -fwine, -fno-wine Integration with system-installed Wine to execute
- \\ Windows programs on Linux hosts. (default: no)
- \\
- \\ Available System Integrations: Enabled:
- \\
- );
- if (b.graph.system_library_options.entries.len == 0) {
- try w.writeAll(" (none) -\n");
- } else {
- for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
- const status = switch (v) {
- .declared_enabled => "yes",
- .declared_disabled => "no",
- .user_enabled, .user_disabled => unreachable, // already emitted error
- };
- try w.print(" {s:<43} {s}\n", .{ k, status });
- }
- }
-
- try w.writeAll(
- \\
- \\General Options:
- \\ -h, --help Print this help and exit
- \\ -l, --list-steps Print available steps
- \\
- \\ -p, --prefix [path] Where to install files (default: zig-out)
- \\ --prefix-lib-dir [path] Where to install libraries
- \\ --prefix-exe-dir [path] Where to install executables
- \\ --prefix-include-dir [path] Where to install C header files
- \\ --release[=mode] Request release mode, optionally specifying a
- \\ preferred optimization mode: fast, safe, small
- \\
- \\ --verbose Print commands before executing them
- \\ --color [auto|off|on] Enable or disable colored error messages
- \\ --error-style [style] Control how build errors are printed
- \\ verbose (Default) Report errors with full context
- \\ minimal Report errors after summary, excluding context like command lines
- \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update
- \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update
- \\ --multiline-errors [style] Control how multi-line error messages are printed
- \\ indent (Default) Indent non-initial lines to align with initial line
- \\ newline Include a leading newline so that the error message is on its own lines
- \\ none Print as usual so the first line is misaligned
- \\ --summary [mode] Control the printing of the build summary
- \\ all Print the build summary in its entirety
- \\ new Omit cached steps
- \\ failures (Default if short-lived) Only print failed steps
- \\ line (Default if long-lived) Only print the single-line summary
- \\ none Do not print the build summary
- \\ -j<N> Limit concurrent jobs (default is to use all CPU cores)
- \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
- \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
- \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded.
- \\ The timeout must include a unit: ns, us, ms, s, m, h
- \\ --watch Continuously rebuild when source files are modified
- \\ --debounce <ms> Delay before rebuilding after changed file detected
- \\ --webui[=ip] Enable the web interface on the given IP address
- \\ --fuzz[=limit] Continuously search for unit test failures with an optional
- \\ limit to the max number of iterations. The argument supports
- \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
- \\ '--webui' when no limit is specified.
- \\ --time-report Force full rebuild and provide detailed information on
- \\ compilation time of Zig source code (implies '--webui')
- \\ -fincremental Enable incremental compilation
- \\ -fno-incremental Disable incremental compilation
- \\
- \\Package Management Options:
- \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit
- \\ needed (Default) Lazy dependencies are fetched as needed
- \\ all Lazy dependencies are always fetched
- \\ --fork=[path] Override one or more projects from dependency tree
- \\
- \\Advanced Options:
- \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error
- \\ -fno-reference-trace Disable reference trace
- \\ -fallow-so-scripts Allows .so files to be GNU ld scripts
- \\ -fno-allow-so-scripts (default) .so files must be ELF files
- \\ --build-file [file] Override path to build.zig
- \\ --cache-dir [path] Override path to local Zig cache directory
- \\ --global-cache-dir [path] Override path to global Zig cache directory
- \\ --zig-lib-dir [arg] Override path to Zig lib directory
- \\ --build-runner [file] Override path to build runner
- \\ --seed [integer] For shuffling dependency traversal order (default: random)
- \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries
- \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM)
- \\ sha1, tree 20-byte cryptographic hash (ELF, WASM)
- \\ md5 16-byte cryptographic hash (ELF)
- \\ uuid 16-byte random UUID (ELF, WASM)
- \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM)
- \\ none (default) No build ID
- \\ --debug-log [scope] Enable debugging the compiler
- \\ --debug-pkg-config Fail if unknown pkg-config flags encountered
- \\ --debug-rt Debug compiler runtime libraries
- \\ --verbose-link Enable compiler debug output for linking
- \\ --verbose-air Enable compiler debug output for Zig AIR
- \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
- \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
- \\ --verbose-cimport Enable compiler debug output for C imports
- \\ --verbose-cc Enable compiler debug output for C compilation
- \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
- \\
- );
-}
-
-fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
- if (idx.* >= args.len) return null;
- defer idx.* += 1;
- return args[idx.*];
-}
-
-fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
- return nextArg(args, idx) orelse {
- std.debug.print("expected argument after '{s}'\n access the help menu with 'zig build -h'\n", .{args[idx.* - 1]});
- process.exit(1);
- };
-}
-
-fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
- if (idx >= args.len) return null;
- return args[idx..];
-}
-
-const Color = std.zig.Color;
-const ErrorStyle = enum {
- verbose,
- minimal,
- verbose_clear,
- minimal_clear,
- fn verboseContext(s: ErrorStyle) bool {
- return switch (s) {
- .verbose, .verbose_clear => true,
- .minimal, .minimal_clear => false,
- };
- }
- fn clearOnUpdate(s: ErrorStyle) bool {
- return switch (s) {
- .verbose, .minimal => false,
- .verbose_clear, .minimal_clear => true,
- };
- }
-};
-const MultilineErrors = enum { indent, newline, none };
-const Summary = enum { all, new, failures, line, none };
-
-fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
- std.debug.print(f ++ "\n access the help menu with 'zig build -h'\n", args);
- process.exit(1);
-}
-
-fn validateSystemLibraryOptions(b: *std.Build) void {
- var bad = false;
- for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
- switch (v) {
- .user_disabled, .user_enabled => {
- // The user tried to enable or disable a system library integration, but
- // the build script did not recognize that option.
- std.debug.print("system library name not recognized by build script: '{s}'\n", .{k});
- bad = true;
- },
- .declared_disabled, .declared_enabled => {},
- }
- }
- if (bad) {
- std.debug.print(" access the help menu with 'zig build -h'\n", .{});
- process.exit(1);
- }
-}
-
-/// Starting from all top-level steps in `b`, traverses the entire step graph
-/// and adds all step dependencies implied by module graphs.
-fn createModuleDependencies(b: *std.Build) Allocator.Error!void {
- const arena = b.graph.arena;
-
- var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty;
- var next_step_idx: usize = 0;
-
- try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count());
- for (b.top_level_steps.values()) |tls| {
- all_steps.putAssumeCapacityNoClobber(&tls.step, {});
- }
-
- while (next_step_idx < all_steps.count()) {
- const step = all_steps.keys()[next_step_idx];
- next_step_idx += 1;
-
- // Set up any implied dependencies for this step. It's important that we do this first, so
- // that the loop below discovers steps implied by the module graph.
- try createModuleDependenciesForStep(step);
-
- try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len);
- for (step.dependencies.items) |other_step| {
- all_steps.putAssumeCapacity(other_step, {});
- }
- }
-}
-
-/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
-/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
-fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
- const root_module = if (step.cast(Step.Compile)) |cs| root: {
- break :root cs.root_module;
- } else return; // not a compile step so no module dependencies
-
- // Starting from `root_module`, discover all modules in this graph.
- const modules = root_module.getGraph().modules;
-
- // For each of those modules, set up the implied step dependencies.
- for (modules) |mod| {
- if (mod.root_source_file) |lp| lp.addStepDependencies(step);
- for (mod.include_dirs.items) |include_dir| switch (include_dir) {
- .path,
- .path_system,
- .path_after,
- .framework_path,
- .framework_path_system,
- .embed_path,
- => |lp| lp.addStepDependencies(step),
-
- .other_step => |other| {
- other.getEmittedIncludeTree().addStepDependencies(step);
- step.dependOn(&other.step);
- },
-
- .config_header_step => |other| step.dependOn(&other.step),
- };
- for (mod.lib_paths.items) |lp| lp.addStepDependencies(step);
- for (mod.rpaths.items) |rpath| switch (rpath) {
- .lazy_path => |lp| lp.addStepDependencies(step),
- .special => {},
- };
- for (mod.link_objects.items) |link_object| switch (link_object) {
- .static_path,
- .assembly_file,
- => |lp| lp.addStepDependencies(step),
- .other_step => |other| step.dependOn(&other.step),
- .system_lib => {},
- .c_source_file => |source| source.file.addStepDependencies(step),
- .c_source_files => |source_files| source_files.root.addStepDependencies(step),
- .win32_resource_file => |rc_source| {
- rc_source.file.addStepDependencies(step);
- for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
- },
- };
- }
-}
-
-var stdio_buffer_allocation: [256]u8 = undefined;
-var stdout_writer_allocation: Io.File.Writer = undefined;
-
-fn initStdoutWriter(io: Io) *Writer {
- stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation);
- return &stdout_writer_allocation.interface;
-}
-
-fn cleanTmpFiles(io: Io, steps: []const *Step) void {
- for (steps) |step| {
- const wf = step.cast(std.Build.Step.WriteFile) orelse continue;
- if (wf.mode != .tmp) continue;
- const path = wf.generated_directory.path orelse continue;
- Io.Dir.cwd().deleteTree(io, path) catch |err| {
- std.log.warn("failed to delete {s}: {t}", .{ path, err });
- };
- }
-}
diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig
@@ -0,0 +1,1402 @@
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const Color = std.zig.Color;
+const Configuration = std.Build.Configuration;
+const File = std.Io.File;
+const Io = std.Io;
+const Step = std.Build.Step;
+const Writer = std.Io.Writer;
+const assert = std.debug.assert;
+const fatal = std.process.fatal;
+const fmt = std.fmt;
+const log = std.log;
+const mem = std.mem;
+const process = std.process;
+
+pub const root = @import("@build");
+pub const dependencies = @import("@dependencies");
+
+pub const std_options: std.Options = .{
+ .side_channels_mitigations = .none,
+ .http_disable_tls = true,
+};
+
+pub fn main(init: process.Init.Minimal) !void {
+ var arena_allocator: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
+ defer arena_allocator.deinit();
+ const arena = arena_allocator.allocator();
+
+ // The configurer is always short-lived because all it does is serialize
+ // the configuration, which is picked up by a separate maker process.
+ var threaded: std.Io.Threaded = .init(arena, .{
+ .environ = init.environ,
+ .argv0 = .init(init.args),
+ });
+ defer threaded.deinit();
+ const io = threaded.io();
+
+ const args = try init.args.toSlice(arena);
+
+ var arg_i: usize = 1; // Skip own executable name.
+
+ const zig_exe = expectArgOrFatal(args, &arg_i, "--zig");
+ const build_root_sub_path = expectArgOrFatal(args, &arg_i, "--build-root");
+
+ var graph: std.Build.Graph = .{
+ .io = io,
+ .arena = arena,
+ .environ_map = try init.environ.createMap(arena),
+ // TODO get this from parent process instead
+ .host = .{
+ .query = .{},
+ .result = try std.zig.system.resolveTargetQuery(io, .{}),
+ },
+ .generated_files = .empty,
+ .zig_exe = zig_exe,
+
+ // Created before running the user's configure script so that some things
+ // can be added during script execution such as strings.
+ //
+ // Use of arena here is load-bearing because `std.Build.dupe` is
+ // implemented by string internment, and then returning the interned
+ // slice. When the string bytes array is reallocated, that reference
+ // must stay alive.
+ .wip_configuration = .init(arena),
+ };
+ assert(try graph.wip_configuration.addString("") == .empty);
+ assert(try graph.wip_configuration.addString("root") == .root);
+
+ const cwd: Io.Dir = .cwd();
+
+ const build_root: std.Build.Cache.Path = .{
+ .root_dir = .{
+ .handle = try cwd.openDir(io, build_root_sub_path, .{}),
+ .path = build_root_sub_path,
+ },
+ };
+
+ const builder = try std.Build.create(&graph, build_root, dependencies.root_deps);
+
+ var color: Color = .auto;
+
+ while (nextArg(args, &arg_i)) |arg| {
+ if (mem.cutPrefix(u8, arg, "-D")) |option_contents| {
+ if (option_contents.len == 0)
+ fatalWithHint("expected option name after '-D'", .{});
+ if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| {
+ const option_name = option_contents[0..name_end];
+ const option_value = option_contents[name_end + 1 ..];
+ if (try builder.addUserInputOption(option_name, option_value))
+ fatal(" access the help menu with 'zig build -h'", .{});
+ } else {
+ if (try builder.addUserInputFlag(option_contents))
+ fatal(" access the help menu with 'zig build -h'", .{});
+ }
+ } else if (mem.cutPrefix(u8, arg, "-fsys=")) |name| {
+ try graph.system_integration_options.put(arena, name, .user_enabled);
+ } else if (mem.cutPrefix(u8, arg, "-fno-sys=")) |name| {
+ try graph.system_integration_options.put(arena, name, .user_disabled);
+ } else if (mem.eql(u8, arg, "--release")) {
+ graph.release_mode = .any;
+ } else if (mem.cutPrefix(u8, arg, "--release=")) |rest| {
+ graph.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, rest) orelse {
+ fatalWithHint("expected --release=[off|any|fast|safe|small]; found: {s}", .{arg});
+ };
+ } else if (mem.cutPrefix(u8, arg, "--color=")) |rest| {
+ color = std.meta.stringToEnum(Color, rest) orelse
+ fatalWithHint("expected --color=[auto|on|off]; found: {s}", .{arg});
+ } else if (mem.eql(u8, arg, "--system")) {
+ // The usage text shows another argument after this parameter
+ // but it is handled by the parent process. The build runner
+ // only sees this flag.
+ graph.system_package_mode = true;
+ } else if (mem.eql(u8, arg, "--verbose")) {
+ graph.verbose = true;
+ } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
+ graph.cache_poison = std.meta.stringToEnum(std.Build.Graph.CachePoison, rest) orelse
+ fatalWithHint("expected --cache-poison=[pure|poisoned|disallowed|ignored]; found: {s}", .{arg});
+ } else if (mem.eql(u8, arg, "--search-prefix")) {
+ try graph.search_prefixes.append(arena, nextArgOrFatal(args, &arg_i));
+ } else {
+ fatalWithHint("unrecognized argument: {s}", .{arg});
+ }
+ }
+
+ const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map);
+ const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
+
+ graph.stderr_mode = switch (color) {
+ .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE),
+ .on => .escape_codes,
+ .off => .no_color,
+ };
+
+ try builder.runBuild(root);
+
+ if (builder.validateUserInputDidItFail()) {
+ fatal(" access the help menu with 'zig build -h'", .{});
+ }
+
+ try serializePackageOptions(builder, &graph.wip_configuration);
+ try serializeSystemIntegrationOptions(&graph, &graph.wip_configuration);
+
+ var stdout_buffer: [1024]u8 = undefined;
+ var file_writer = Io.File.stdout().writerStreaming(io, &stdout_buffer);
+ serialize(builder, &graph.wip_configuration, &file_writer.interface) catch |err| switch (err) {
+ error.WriteFailed => fatal("failed to write configuration output: {t}", .{file_writer.err.?}),
+ error.OutOfMemory => |e| return e,
+ };
+ file_writer.flush() catch |err| fatal("failed to write configuration output: {t}", .{err});
+
+ // This executable is short-lived and run in Debug mode, so we'd rather
+ // have `zig build` run faster than catch resource leaks in the user's
+ // build.zig script (or, frankly, this configure runner), therefore we call
+ // exit directly here rather than cleanExit.
+ process.exit(0);
+}
+
+const Serialize = struct {
+ arena: Allocator,
+ wc: *Configuration.Wip,
+ module_map: std.AutoArrayHashMapUnmanaged(*std.Build.Module, Configuration.Module.Index) = .empty,
+ package_map: std.AutoArrayHashMapUnmanaged(*std.Build, Configuration.Package.Index) = .empty,
+ /// Index corresponds to `Configuration.steps` index.
+ step_map: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty,
+
+ fn builderToPackage(s: *Serialize, b: *std.Build) !Configuration.Package.Index {
+ if (b.pkg_hash.len == 0) return .root;
+ const arena = s.arena;
+ const wc = s.wc;
+ const gop = try s.package_map.getOrPut(arena, b);
+ if (!gop.found_existing) {
+ gop.value_ptr.* = try wc.addExtra(Configuration.Package, .{
+ .hash = try wc.addString(b.pkg_hash),
+ .dep_prefix = try wc.addString(b.dep_prefix),
+ .root_path = try wc.addString(try b.root.toString(arena)),
+ });
+ }
+ return gop.value_ptr.*;
+ }
+
+ fn addOptionalLazyPathEnum(s: *Serialize, lp: ?std.Build.LazyPath) !Configuration.LazyPath.OptionalIndex {
+ const wc = s.wc;
+ return @enumFromInt(switch (lp orelse return .none) {
+ .src_path => |src_path| i: {
+ const sub_path = try wc.addString(src_path.sub_path);
+ break :i try wc.addExtraErased(Configuration.LazyPath.SourcePath, .{
+ .owner = try s.builderToPackage(src_path.owner),
+ .sub_path = sub_path,
+ });
+ },
+ .generated => |generated| i: {
+ const sub_path = try wc.addString(generated.sub_path);
+ break :i try wc.addExtraErased(Configuration.LazyPath.Generated, .{
+ .flags = .{ .up = @intCast(generated.up) },
+ .index = generated.index,
+ .sub_path = sub_path,
+ });
+ },
+ .cwd_relative => |cwd_relative_sub_path| i: {
+ const sub_path = try wc.addString(cwd_relative_sub_path);
+ break :i try wc.addExtraErased(Configuration.LazyPath.Relative, .{
+ .flags = .{ .base = .cwd },
+ .sub_path = sub_path,
+ });
+ },
+ .relative => |relative| i: {
+ break :i try wc.addExtraErased(Configuration.LazyPath.Relative, .{
+ .flags = .{ .base = relative.base },
+ .sub_path = try wc.addString(relative.sub_path),
+ });
+ },
+ .dependency => |dependency| i: {
+ const sub_path = try wc.addString(dependency.sub_path);
+ break :i try wc.addExtraErased(Configuration.LazyPath.SourcePath, .{
+ .owner = try s.builderToPackage(dependency.dependency.builder),
+ .sub_path = sub_path,
+ });
+ },
+ });
+ }
+
+ fn addOptionalLazyPath(s: *Serialize, lp: ?std.Build.LazyPath) !?Configuration.LazyPath.Index {
+ return (try addOptionalLazyPathEnum(s, lp)).unwrap();
+ }
+
+ fn addLazyPath(s: *Serialize, lp: std.Build.LazyPath) !Configuration.LazyPath.Index {
+ return @enumFromInt(@intFromEnum(try addOptionalLazyPathEnum(s, lp)));
+ }
+
+ fn addOptionalSemVer(s: *Serialize, sem_ver: ?std.SemanticVersion) !?Configuration.String {
+ return if (sem_ver) |sv| try s.wc.addSemVer(sv) else null;
+ }
+
+ fn addOptionalString(s: *Serialize, opt_slice: ?[]const u8) !?Configuration.String {
+ return if (opt_slice) |slice| try s.wc.addString(slice) else null;
+ }
+
+ fn addSystemLib(s: *Serialize, sl: *const std.Build.Module.SystemLib) !Configuration.SystemLib.Index {
+ const wc = s.wc;
+ return try wc.addDeduped(Configuration.SystemLib, .{
+ .flags = .{
+ .needed = sl.needed,
+ .weak = sl.weak,
+ .use_pkg_config = sl.use_pkg_config,
+ .preferred_link_mode = sl.preferred_link_mode,
+ .search_strategy = sl.search_strategy,
+ },
+ .name = try wc.addString(sl.name),
+ });
+ }
+
+ fn addCSourceFile(s: *Serialize, csf: *const std.Build.Module.CSourceFile) !Configuration.CSourceFile.Index {
+ const wc = s.wc;
+ const args = try initStringList(s, csf.flags);
+ return try wc.addExtra(Configuration.CSourceFile, .{
+ .flags = .{
+ .args_len = @intCast(args.len),
+ .lang = .init(csf.language),
+ },
+ .file = try addLazyPath(s, csf.file),
+ .args = .{ .slice = args },
+ });
+ }
+
+ fn addCSourceFiles(s: *Serialize, csf: *const std.Build.Module.CSourceFiles) !Configuration.CSourceFiles.Index {
+ const wc = s.wc;
+ const sub_paths = try initStringList(s, csf.files);
+ const args = try initStringList(s, csf.flags);
+ return try wc.addExtra(Configuration.CSourceFiles, .{
+ .flags = .{
+ .args_len = @intCast(args.len),
+ .lang = .init(csf.language),
+ },
+ .root = try addLazyPath(s, csf.root),
+ .sub_paths = .{ .slice = sub_paths },
+ .args = .{ .slice = args },
+ });
+ }
+
+ fn addRcSourceFile(s: *Serialize, rsf: *const std.Build.Module.RcSourceFile) !Configuration.RcSourceFile.Index {
+ const wc = s.wc;
+ const include_paths = try initLazyPathList(s, rsf.include_paths);
+ const args = try initStringList(s, rsf.flags);
+ return try wc.addExtra(Configuration.RcSourceFile, .{
+ .flags = .{
+ .args_len = @intCast(args.len),
+ .include_paths = include_paths.len != 0,
+ },
+ .file = try addLazyPath(s, rsf.file),
+ .include_paths = .{ .slice = include_paths },
+ .args = .{ .slice = args },
+ });
+ }
+
+ fn addEnvironMap(s: *Serialize, opt_map: ?*std.process.Environ.Map) !?Configuration.EnvironMap.Index {
+ const wc = s.wc;
+ const map = opt_map orelse return null;
+ return try wc.addDeduped(Configuration.EnvironMap, .{
+ .keys = try wc.addStringList(map.array_hash_map.keys()),
+ .values = try wc.addStringList(map.array_hash_map.values()),
+ });
+ }
+
+ fn initArgsList(s: *Serialize, args: []const Step.Run.Arg) ![]const Configuration.Step.Run.Arg.Index {
+ const wc = s.wc;
+ const result = try s.arena.alloc(Configuration.Step.Run.Arg.Index, args.len);
+ for (result, args) |*dest, src| {
+ dest.* = try wc.addExtra(Configuration.Step.Run.Arg, switch (src) {
+ .artifact => |a| .{
+ .flags = .{
+ .tag = .artifact,
+ .prefix = a.prefix.len != 0,
+ .suffix = false,
+ .basename = false,
+ .path = false,
+ .producer = true,
+ .generated = false,
+ .dep_file = false,
+ },
+ .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null },
+ .suffix = .{ .value = null },
+ .basename = .{ .value = null },
+ .path = .{ .value = null },
+ .producer = .{ .value = stepIndex(s, &a.artifact.step) },
+ .generated = .{ .value = null },
+ },
+ .lazy_path => |a| .{
+ .flags = .{
+ .tag = .path_file,
+ .prefix = a.prefix.len != 0,
+ .suffix = false,
+ .basename = false,
+ .path = true,
+ .producer = false,
+ .generated = false,
+ .dep_file = false,
+ },
+ .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null },
+ .suffix = .{ .value = null },
+ .basename = .{ .value = null },
+ .path = .{ .value = try addLazyPath(s, a.lazy_path) },
+ .producer = .{ .value = null },
+ .generated = .{ .value = null },
+ },
+ .decorated_directory => |a| .{
+ .flags = .{
+ .tag = .path_directory,
+ .prefix = a.prefix.len != 0,
+ .suffix = a.suffix.len != 0,
+ .basename = false,
+ .path = true,
+ .producer = false,
+ .generated = false,
+ .dep_file = false,
+ },
+ .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null },
+ .suffix = .{ .value = if (a.suffix.len != 0) try wc.addString(a.suffix) else null },
+ .basename = .{ .value = null },
+ .path = .{ .value = try addLazyPath(s, a.lazy_path) },
+ .producer = .{ .value = null },
+ .generated = .{ .value = null },
+ },
+ .file_content => |a| .{
+ .flags = .{
+ .tag = .file_content,
+ .prefix = a.prefix.len != 0,
+ .suffix = false,
+ .basename = false,
+ .path = true,
+ .producer = false,
+ .generated = false,
+ .dep_file = false,
+ },
+ .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null },
+ .suffix = .{ .value = null },
+ .basename = .{ .value = null },
+ .path = .{ .value = try addLazyPath(s, a.lazy_path) },
+ .producer = .{ .value = null },
+ .generated = .{ .value = null },
+ },
+ .bytes => |a| .{
+ .flags = .{
+ .tag = .string,
+ .prefix = true,
+ .suffix = false,
+ .basename = false,
+ .path = false,
+ .producer = false,
+ .generated = false,
+ .dep_file = false,
+ },
+ .prefix = .{ .value = try wc.addString(a) },
+ .suffix = .{ .value = null },
+ .basename = .{ .value = null },
+ .path = .{ .value = null },
+ .producer = .{ .value = null },
+ .generated = .{ .value = null },
+ },
+ .output_file, .output_file_dep => |a, tag| .{
+ .flags = .{
+ .tag = .output_file,
+ .prefix = a.prefix.len != 0,
+ .suffix = false,
+ .basename = a.basename.len != 0,
+ .path = false,
+ .producer = false,
+ .generated = true,
+ .dep_file = tag == .output_file_dep,
+ },
+ .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null },
+ .suffix = .{ .value = null },
+ .basename = .{ .value = if (a.basename.len != 0) try wc.addString(a.basename) else null },
+ .path = .{ .value = null },
+ .producer = .{ .value = null },
+ .generated = .{ .value = a.generated_file },
+ },
+ .output_directory => |a| .{
+ .flags = .{
+ .tag = .output_directory,
+ .prefix = a.prefix.len != 0,
+ .suffix = false,
+ .basename = a.basename.len != 0,
+ .path = false,
+ .producer = false,
+ .generated = true,
+ .dep_file = false,
+ },
+ .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null },
+ .suffix = .{ .value = null },
+ .basename = .{ .value = if (a.basename.len != 0) try wc.addString(a.basename) else null },
+ .path = .{ .value = null },
+ .producer = .{ .value = null },
+ .generated = .{ .value = a.generated_file },
+ },
+ .passthru => .{
+ .flags = .{
+ .tag = .passthru,
+ .prefix = false,
+ .suffix = false,
+ .basename = false,
+ .path = false,
+ .producer = false,
+ .generated = false,
+ .dep_file = false,
+ },
+ .prefix = .{ .value = null },
+ .suffix = .{ .value = null },
+ .basename = .{ .value = null },
+ .path = .{ .value = null },
+ .producer = .{ .value = null },
+ .generated = .{ .value = null },
+ },
+ });
+ }
+ return result;
+ }
+
+ fn initIncludeDirList(
+ s: *Serialize,
+ list: []const std.Build.Module.IncludeDir,
+ ) ![]const Configuration.Module.IncludeDir {
+ const result = try s.arena.alloc(Configuration.Module.IncludeDir, list.len);
+ for (result, list) |*dest, src| dest.* = switch (src) {
+ .path => |lp| .{ .path = try addLazyPath(s, lp) },
+ .path_system => |lp| .{ .path_system = try addLazyPath(s, lp) },
+ .path_after => |lp| .{ .path_after = try addLazyPath(s, lp) },
+ .framework_path => |lp| .{ .framework_path = try addLazyPath(s, lp) },
+ .framework_path_system => |lp| .{ .framework_path_system = try addLazyPath(s, lp) },
+ .embed_path => |lp| .{ .embed_path = try addLazyPath(s, lp) },
+ .other_step => |cs| .{ .path = try addLazyPath(s, cs.installed_headers_include_tree.?.getDirectory()) },
+ .config_header_step => |chs| .{ .config_header_step = stepIndex(s, &chs.step) },
+ };
+ return result;
+ }
+
+ fn initLazyPathList(s: *Serialize, list: []const std.Build.LazyPath) ![]const Configuration.LazyPath.Index {
+ const result = try s.arena.alloc(Configuration.LazyPath.Index, list.len);
+ for (result, list) |*dest, src| dest.* = try addLazyPath(s, src);
+ return result;
+ }
+
+ fn initStringList(s: *Serialize, list: []const []const u8) ![]const Configuration.String {
+ const wc = s.wc;
+ const result = try s.arena.alloc(Configuration.String, list.len);
+ for (result, list) |*dest, src| dest.* = try wc.addString(src);
+ return result;
+ }
+
+ fn initCopyList(s: *Serialize, list: []const Step.WriteFile.Copy) ![]const Configuration.Step.WriteFile.Copy {
+ const result = try s.arena.alloc(Configuration.Step.WriteFile.Copy, list.len);
+ for (result, list) |*dest, src| dest.* = .{
+ .sub_path = src.sub_path,
+ .src_file = try s.addLazyPath(src.src_file),
+ };
+ return result;
+ }
+
+ fn initOptionalStringList(s: *Serialize, list: []const ?[]const u8) ![]const Configuration.OptionalString {
+ const wc = s.wc;
+ const result = try s.arena.alloc(Configuration.OptionalString, list.len);
+ for (result, list) |*dest, src| dest.* = try wc.addOptionalString(src);
+ return result;
+ }
+
+ fn addModule(s: *Serialize, m: *std.Build.Module) !Configuration.Module.Index {
+ if (s.module_map.get(m)) |index| return index;
+
+ const wc = s.wc;
+ const arena = s.arena;
+
+ const rpaths = try arena.alloc(Configuration.Module.RPath, m.rpaths.items.len);
+ for (rpaths, m.rpaths.items) |*dest, src| dest.* = switch (src) {
+ .lazy_path => |lp| .{ .lazy_path = try addLazyPath(s, lp) },
+ .special => |slice| .{ .special = try wc.addString(slice) },
+ };
+
+ const link_objects = try arena.alloc(Configuration.Module.LinkObject, m.link_objects.items.len);
+ for (link_objects, m.link_objects.items) |*dest, *src| dest.* = switch (src.*) {
+ .static_path => |lp| .{ .static_path = try addLazyPath(s, lp) },
+ .other_step => |cs| .{ .other_step = stepIndex(s, &cs.step) },
+ .system_lib => |*sl| .{ .system_lib = try addSystemLib(s, sl) },
+ .assembly_file => |lp| .{ .assembly_file = try addLazyPath(s, lp) },
+ .c_source_file => |csf| .{ .c_source_file = try addCSourceFile(s, csf) },
+ .c_source_files => |csf| .{ .c_source_files = try addCSourceFiles(s, csf) },
+ .win32_resource_file => |wrf| .{ .win32_resource_file = try addRcSourceFile(s, wrf) },
+ };
+
+ const frameworks = try arena.alloc(Configuration.Module.Framework, m.frameworks.entries.len);
+ for (frameworks, m.frameworks.keys(), m.frameworks.values()) |*dest, name, options| dest.* = .{
+ .flags = .{
+ .needed = options.needed,
+ .weak = options.weak,
+ },
+ .name = try wc.addString(name),
+ };
+
+ const lib_paths = try initLazyPathList(s, m.lib_paths.items);
+ const c_macros = try initStringList(s, m.c_macros.items);
+ const export_symbol_names = try initStringList(s, m.export_symbol_names);
+
+ const module_index: Configuration.Module.Index = try wc.addExtra(Configuration.Module, .{
+ .flags = .{
+ .optimize = .init(m.optimize),
+ .strip = .init(m.strip),
+ .unwind_tables = .init(m.unwind_tables),
+ .dwarf_format = .init(m.dwarf_format),
+ .single_threaded = .init(m.single_threaded),
+ .stack_protector = .init(m.stack_protector),
+ .stack_check = .init(m.stack_check),
+ .sanitize_c = .init(m.sanitize_c),
+ .sanitize_thread = .init(m.sanitize_thread),
+ .fuzz = .init(m.fuzz),
+ .code_model = m.code_model,
+ .c_macros = c_macros.len != 0,
+ .include_dirs = m.include_dirs.items.len != 0,
+ .lib_paths = lib_paths.len != 0,
+ .rpaths = rpaths.len != 0,
+ .frameworks = frameworks.len != 0,
+ .link_objects = link_objects.len != 0,
+ .export_symbol_names = export_symbol_names.len != 0,
+ },
+ .flags2 = .{
+ .valgrind = .init(m.valgrind),
+ .pic = .init(m.pic),
+ .red_zone = .init(m.red_zone),
+ .omit_frame_pointer = .init(m.omit_frame_pointer),
+ .error_tracing = .init(m.error_tracing),
+ .link_libc = .init(m.link_libc),
+ .link_libcpp = .init(m.link_libcpp),
+ .no_builtin = .init(m.no_builtin),
+ },
+ .owner = try s.builderToPackage(m.owner),
+ .root_source_file = try s.addOptionalLazyPathEnum(m.root_source_file),
+ .import_table = .invalid,
+ .resolved_target = try addOptionalResolvedTarget(wc, m.resolved_target),
+ .c_macros = .{ .slice = c_macros },
+ .lib_paths = .{ .slice = lib_paths },
+ .export_symbol_names = .{ .slice = export_symbol_names },
+ .include_dirs = .init(try s.initIncludeDirList(m.include_dirs.items)),
+ .rpaths = .init(rpaths),
+ .link_objects = .init(link_objects),
+ .frameworks = .{ .slice = frameworks },
+ });
+
+ // The import table is the only place that modules can form dependency
+ // loops. Therefore, we populate the module indexes only after adding
+ // the module to module_map.
+ try s.module_map.putNoClobber(arena, m, module_index);
+
+ var imports = try std.MultiArrayList(Configuration.ImportTable.Import).initCapacity(arena, m.import_table.entries.len);
+ imports.len = m.import_table.entries.len;
+ for (
+ imports.items(.name),
+ imports.items(.module),
+ m.import_table.keys(),
+ m.import_table.values(),
+ ) |*dest_name, *dest_module, src_name, src_module| {
+ dest_name.* = try wc.addString(src_name);
+ dest_module.* = try addModule(s, src_module);
+ }
+
+ comptime assert(std.mem.eql(u8, @typeInfo(Configuration.Module).@"struct".fields[2].name, "import_table"));
+ comptime assert(@typeInfo(Configuration.Module).@"struct".fields[2].type == Configuration.ImportTable.Index);
+ assert(wc.extra.items[@intFromEnum(module_index) + 2] == @intFromEnum(Configuration.ImportTable.Index.invalid));
+ const import_table_index = try wc.addDeduped(Configuration.ImportTable, .{
+ .imports = .{ .mal = imports },
+ });
+ wc.extra.items[@intFromEnum(module_index) + 2] = @intFromEnum(import_table_index);
+
+ return module_index;
+ }
+
+ fn stepIndex(s: *const Serialize, step: *Step) Configuration.Step.Index {
+ return @enumFromInt(s.step_map.getIndex(step).?);
+ }
+};
+
+fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
+ const graph = b.graph;
+ const arena = graph.arena;
+ const gpa = wc.gpa;
+
+ var s: Serialize = .{ .wc = wc, .arena = arena };
+
+ // Starting from all top-level steps in `b`, traverse the entire step graph
+ // and add all step dependencies implied by module graphs.
+ const top_level_steps = b.top_level_steps.values();
+ try s.step_map.ensureUnusedCapacity(arena, top_level_steps.len);
+ for (top_level_steps) |tls| {
+ s.step_map.putAssumeCapacityNoClobber(&tls.step, {});
+ }
+ {
+ while (wc.steps.items.len < s.step_map.count()) {
+ const step = s.step_map.keys()[wc.steps.items.len];
+
+ // Set up any implied dependencies for this step. It's important that we do this first, so
+ // that the loop below discovers steps implied by the module graph.
+ try createModuleDependenciesForStep(step);
+
+ try s.step_map.ensureUnusedCapacity(arena, step.dependencies.items.len);
+ for (step.dependencies.items) |other_step| {
+ s.step_map.putAssumeCapacity(other_step, {});
+ }
+
+ // Add and then de-duplicate dependencies.
+ const dep_steps = try arena.alloc(Configuration.Step.Index, step.dependencies.items.len);
+ for (dep_steps, step.dependencies.items) |*dest, src|
+ dest.* = @enumFromInt(s.step_map.getIndex(src).?);
+
+ const deps: Configuration.Deps.Index = try wc.addDeduped(Configuration.Deps, .{
+ .steps = .{ .slice = dep_steps },
+ });
+
+ try wc.steps.ensureTotalCapacity(gpa, s.step_map.entries.capacity);
+ wc.steps.appendAssumeCapacity(.{
+ .name = try wc.addString(step.name),
+ .owner = try s.builderToPackage(step.owner),
+ .deps = deps,
+ .max_rss = .fromBytes(step.max_rss),
+ .extended = @enumFromInt(switch (step.tag) {
+ .top_level => e: {
+ const top_level: *Step.TopLevel = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.TopLevel, .{
+ .description = try wc.addString(top_level.description),
+ });
+ },
+ .compile => e: {
+ const c: *Step.Compile = @fieldParentPtr("step", step);
+ const exec_cmd_args: []const ?[]const u8 = c.exec_cmd_args orelse &.{};
+ const installed_headers: []u32 = try arena.alloc(u32, c.installed_headers.items.len);
+ for (installed_headers, c.installed_headers.items) |*dst, src| switch (src) {
+ .file => |file| {
+ dst.* = try wc.addExtraErased(Configuration.Step.Compile.InstalledHeader.File, .{
+ .source = try s.addLazyPath(file.source),
+ .dest_sub_path = try wc.addString(file.dest_rel_path),
+ });
+ },
+ .directory => |directory| {
+ const include_extensions = directory.options.include_extensions orelse &.{};
+ dst.* = try wc.addExtraErased(Configuration.Step.Compile.InstalledHeader.Directory, .{
+ .flags = .{
+ .include_extensions = include_extensions.len != 0,
+ .exclude_extensions = directory.options.exclude_extensions.len != 0,
+ },
+ .source = try s.addLazyPath(directory.source),
+ .dest_sub_path = try wc.addString(directory.dest_rel_path),
+ .exclude_extensions = .{ .slice = try s.initStringList(directory.options.exclude_extensions) },
+ .include_extensions = .{ .slice = try s.initStringList(include_extensions) },
+ });
+ },
+ };
+
+ break :e try wc.addExtraErased(Configuration.Step.Compile, .{
+ .flags = .{
+ .filters_len = c.filters.len != 0,
+ .exec_cmd_args_len = exec_cmd_args.len != 0,
+ .installed_headers_len = installed_headers.len != 0,
+ .force_undefined_symbols_len = c.force_undefined_symbols.entries.len != 0,
+
+ .verbose_link = c.verbose_link,
+ .verbose_cc = c.verbose_cc,
+ .rdynamic = c.rdynamic,
+ .import_memory = c.import_memory,
+ .export_memory = c.export_memory,
+ .import_symbols = c.import_symbols,
+ .import_table = c.import_table,
+ .export_table = c.export_table,
+ .shared_memory = c.shared_memory,
+ .link_eh_frame_hdr = c.link_eh_frame_hdr,
+ .link_emit_relocs = c.link_emit_relocs,
+ .link_function_sections = c.link_function_sections,
+ .link_data_sections = c.link_data_sections,
+ .linker_dynamicbase = c.linker_dynamicbase,
+ .link_z_notext = c.link_z_notext,
+ .link_z_relro = c.link_z_relro,
+ .link_z_lazy = c.link_z_lazy,
+ .link_z_defs = c.link_z_defs,
+ .headerpad_max_install_names = c.headerpad_max_install_names,
+ .dead_strip_dylibs = c.dead_strip_dylibs,
+ .force_load_objc = c.force_load_objc,
+ .discard_local_symbols = c.discard_local_symbols,
+ .mingw_unicode_entry_point = c.mingw_unicode_entry_point,
+ },
+ .flags2 = .{
+ .pie = .init(c.pie),
+ .formatted_panics = .init(c.formatted_panics),
+ .bundle_compiler_rt = .init(c.bundle_compiler_rt),
+ .bundle_ubsan_rt = .init(c.bundle_ubsan_rt),
+ .each_lib_rpath = .init(c.each_lib_rpath),
+ .link_gc_sections = .init(c.link_gc_sections),
+ .linker_allow_shlib_undefined = .init(c.linker_allow_shlib_undefined),
+ .linker_allow_undefined_version = .init(c.linker_allow_undefined_version),
+ .linker_enable_new_dtags = .init(c.linker_enable_new_dtags),
+ .dll_export_fns = .init(c.dll_export_fns),
+ .use_llvm = .init(c.use_llvm),
+ .use_lld = .init(c.use_lld),
+ .use_new_linker = .init(c.use_new_linker),
+ .allow_so_scripts = .init(c.allow_so_scripts),
+ .sanitize_coverage_trace_pc_guard = .init(c.sanitize_coverage_trace_pc_guard),
+ .linkage = .init(c.linkage),
+ },
+ .flags3 = .{
+ .is_linking_libc = c.is_linking_libc,
+ .is_linking_libcpp = c.is_linking_libcpp,
+ .version = c.version != null,
+ .compress_debug_sections = c.compress_debug_sections,
+ .initial_memory = c.initial_memory != null,
+ .max_memory = c.max_memory != null,
+ .kind = c.kind,
+ .global_base = c.global_base != null,
+ .test_runner = if (c.test_runner) |tr| switch (tr.mode) {
+ .simple => .simple,
+ .server => .server,
+ } else .default,
+ .wasi_exec_model = .init(c.wasi_exec_model),
+ .win32_manifest = c.win32_manifest != null,
+ .win32_module_definition = c.win32_module_definition != null,
+ .zig_lib_dir = c.zig_lib_dir != null,
+ .rc_includes = c.rc_includes,
+ .image_base = c.image_base != null,
+ .build_id = .init(c.build_id),
+ .entry = switch (c.entry) {
+ .default => .default,
+ .disabled => .disabled,
+ .enabled => .enabled,
+ .symbol_name => .symbol_name,
+ },
+ .lto = .init(c.lto),
+ .subsystem = .init(c.subsystem),
+ },
+ .flags4 = .{
+ .libc_file = c.libc_file != null,
+ .link_z_common_page_size = c.link_z_common_page_size != null,
+ .link_z_max_page_size = c.link_z_max_page_size != null,
+ .pagezero_size = c.pagezero_size != null,
+ .stack_size = c.stack_size != null,
+ .headerpad_size = c.headerpad_size != null,
+ .error_limit = c.error_limit != null,
+ .install_name = c.install_name != null,
+ .entitlements = c.entitlements != null,
+ .expect_errors = if (c.expect_errors) |x| switch (x) {
+ .contains => .contains,
+ .exact => .exact,
+ .starts_with => .starts_with,
+ .stderr_contains => .stderr_contains,
+ } else .none,
+ .linker_script = c.linker_script != null,
+ .version_script = c.version_script != null,
+ .emit_directory = c.emit_directory != .none,
+ .generated_docs = c.generated_docs != .none,
+ .generated_asm = c.generated_asm != .none,
+ .generated_bin = c.generated_bin != .none,
+ .generated_pdb = c.generated_pdb != .none,
+ .generated_implib = c.generated_implib != .none,
+ .generated_llvm_bc = c.generated_llvm_bc != .none,
+ .generated_llvm_ir = c.generated_llvm_ir != .none,
+ .generated_h = c.generated_h != .none,
+ },
+ .root_module = try s.addModule(c.root_module),
+ .root_name = try wc.addString(c.name),
+ .linker_script = .{ .value = try s.addOptionalLazyPath(c.linker_script) },
+ .version_script = .{ .value = try s.addOptionalLazyPath(c.version_script) },
+ .zig_lib_dir = .{ .value = try s.addOptionalLazyPath(c.zig_lib_dir) },
+ .libc_file = .{ .value = try s.addOptionalLazyPath(c.libc_file) },
+ .win32_manifest = .{ .value = try s.addOptionalLazyPath(c.win32_manifest) },
+ .win32_module_definition = .{ .value = try s.addOptionalLazyPath(c.win32_module_definition) },
+ .entitlements = .{ .value = try s.addOptionalLazyPath(c.entitlements) },
+ .version = .{ .value = try s.addOptionalSemVer(c.version) },
+ .install_name = .{ .value = try s.addOptionalString(c.install_name) },
+ .initial_memory = .{ .value = c.initial_memory },
+ .max_memory = .{ .value = c.max_memory },
+ .global_base = .{ .value = c.global_base },
+ .image_base = .{ .value = c.image_base },
+ .link_z_common_page_size = .{ .value = c.link_z_common_page_size },
+ .link_z_max_page_size = .{ .value = c.link_z_max_page_size },
+ .pagezero_size = .{ .value = c.pagezero_size },
+ .stack_size = .{ .value = c.stack_size },
+ .headerpad_size = .{ .value = c.headerpad_size },
+ .error_limit = .{ .value = c.error_limit },
+ .entry = .{ .value = switch (c.entry) {
+ .symbol_name => |name| try wc.addString(name),
+ .default, .disabled, .enabled => null,
+ } },
+ .build_id = .{ .value = if (c.build_id) |id| switch (id) {
+ .hexstring => |*hexstring| try wc.addString(hexstring.toSlice()),
+ .none, .fast, .uuid, .sha1, .md5 => null,
+ } else null },
+ .filters = .{ .slice = try s.initStringList(c.filters) },
+ .exec_cmd_args = .{ .slice = try s.initOptionalStringList(exec_cmd_args) },
+ .installed_headers = .initErased(installed_headers),
+ .force_undefined_symbols = .{ .slice = try s.initStringList(c.force_undefined_symbols.keys()) },
+ .expect_errors = .{ .u = if (c.expect_errors) |x| switch (x) {
+ .contains => |slice| .{ .contains = try wc.addString(slice) },
+ .exact => |exact| .{ .exact = .{ .slice = try s.initStringList(exact) } },
+ .starts_with => |slice| .{ .starts_with = try wc.addString(slice) },
+ .stderr_contains => |slice| .{ .stderr_contains = try wc.addString(slice) },
+ } else .none },
+ .test_runner = .{ .u = if (c.test_runner) |tr| switch (tr.mode) {
+ .simple => .{ .simple = try s.addLazyPath(tr.path) },
+ .server => .{ .server = try s.addLazyPath(tr.path) },
+ } else .default },
+
+ .emit_directory = .{ .value = c.emit_directory.unwrap() },
+ .generated_docs = .{ .value = c.generated_docs.unwrap() },
+ .generated_asm = .{ .value = c.generated_asm.unwrap() },
+ .generated_bin = .{ .value = c.generated_bin.unwrap() },
+ .generated_pdb = .{ .value = c.generated_pdb.unwrap() },
+ .generated_implib = .{ .value = c.generated_implib.unwrap() },
+ .generated_llvm_bc = .{ .value = c.generated_llvm_bc.unwrap() },
+ .generated_llvm_ir = .{ .value = c.generated_llvm_ir.unwrap() },
+ .generated_h = .{ .value = c.generated_h.unwrap() },
+ });
+ },
+ .install_artifact => e: {
+ const ia: *Step.InstallArtifact = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.InstallArtifact, .{
+ .flags = .{
+ .dylib_symlinks = ia.dylib_symlinks,
+ .bin_dir = ia.dest_dir != null,
+ .implib_dir = ia.implib_dir != null,
+ .pdb_dir = ia.pdb_dir != null,
+ .h_dir = ia.h_dir != null,
+ .bin_sub_path = ia.dest_sub_path != null,
+ },
+ .bin_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.dest_dir) },
+ .implib_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.implib_dir) },
+ .pdb_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.pdb_dir) },
+ .h_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.h_dir) },
+ .bin_sub_path = .{ .value = try s.addOptionalString(ia.dest_sub_path) },
+ });
+ },
+ .install_file => e: {
+ const sif: *Step.InstallFile = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.InstallFile, .{
+ .source = try s.addLazyPath(sif.source),
+ .dest_dir = try addInstallDir(wc, sif.dir),
+ .dest_sub_path = try wc.addString(sif.dest_rel_path),
+ });
+ },
+ .install_dir => e: {
+ const sid: *Step.InstallDir = @fieldParentPtr("step", step);
+ const dest_sub_path: ?[]const u8 = if (sid.options.install_subdir.len != 0)
+ sid.options.install_subdir
+ else
+ null;
+ const include_extensions = sid.options.include_extensions orelse &.{};
+ break :e try wc.addExtraErased(Configuration.Step.InstallDir, .{
+ .flags = .{
+ .dest_sub_path = dest_sub_path != null,
+ .exclude_extensions = sid.options.exclude_extensions.len != 0,
+ .include_extensions = include_extensions.len != 0,
+ .include_extensions_active = sid.options.include_extensions != null,
+ .blank_extensions = sid.options.blank_extensions.len != 0,
+ },
+ .source_dir = try s.addLazyPath(sid.options.source_dir),
+ .dest_dir = try addInstallDir(wc, sid.options.install_dir),
+ .dest_sub_path = .{ .value = try s.addOptionalString(dest_sub_path) },
+ .exclude_extensions = .{ .slice = try s.initStringList(sid.options.exclude_extensions) },
+ .include_extensions = .{ .slice = try s.initStringList(include_extensions) },
+ .blank_extensions = .{ .slice = try s.initStringList(sid.options.blank_extensions) },
+ });
+ },
+ .fail => e: {
+ const sf: *Step.Fail = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.Fail, .{
+ .msg = sf.error_msg,
+ });
+ },
+ .find_program => e: {
+ const fp: *Step.FindProgram = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.FindProgram, .{
+ .names = fp.names,
+ .found_path = fp.found_path,
+ });
+ },
+ .fmt => e: {
+ const sf: *Step.Fmt = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.Fmt, .{
+ .flags = .{
+ .paths = sf.paths.len != 0,
+ .exclude_paths = sf.exclude_paths.len != 0,
+ .check = sf.check,
+ },
+ .paths = .{ .slice = try s.initLazyPathList(sf.paths) },
+ .exclude_paths = .{ .slice = try s.initLazyPathList(sf.exclude_paths) },
+ });
+ },
+ .translate_c => e: {
+ const tc: *Step.TranslateC = @fieldParentPtr("step", step);
+
+ const system_libs = try arena.alloc(Configuration.SystemLib.Index, tc.system_libs.items.len);
+ for (system_libs, tc.system_libs.items) |*dest, *src| dest.* = try s.addSystemLib(src);
+
+ break :e try wc.addExtraErased(Configuration.Step.TranslateC, .{
+ .flags = .{
+ .include_dirs = tc.include_dirs.items.len != 0,
+ .system_libs = system_libs.len != 0,
+ .c_macros = tc.c_macros.items.len != 0,
+ .link_libc = tc.link_libc,
+ .optimize = .init(tc.optimize),
+ },
+ .src_path = try s.addLazyPath(tc.source),
+ .output_file = tc.output_file,
+ .include_dirs = .init(try s.initIncludeDirList(tc.include_dirs.items)),
+ .system_libs = .{ .slice = system_libs },
+ .c_macros = .{ .slice = tc.c_macros.items },
+ .target = try addOptionalResolvedTarget(wc, tc.target),
+ });
+ },
+ .write_file => e: {
+ const wf: *Step.WriteFile = @fieldParentPtr("step", step);
+
+ const directories = try arena.alloc(
+ Configuration.Step.WriteFile.Directory,
+ wf.directories.items.len,
+ );
+ for (directories, wf.directories.items) |*dest, src| dest.* = .{
+ .sub_path = src.sub_path,
+ .src_path = try s.addLazyPath(src.src_path),
+ .exclude_extensions = src.exclude_extensions,
+ .include_extensions = src.include_extensions,
+ };
+
+ break :e try wc.addExtraErased(Configuration.Step.WriteFile, .{
+ .flags = .{
+ .embeds = wf.embeds.items.len != 0,
+ .copies = wf.copies.items.len != 0,
+ .directories = directories.len != 0,
+ .mode = switch (wf.mode) {
+ .whole_cached => .whole_cached,
+ .tmp => .tmp,
+ .mutate => .mutate,
+ },
+ },
+ .generated_directory = wf.generated_directory,
+ .embeds = .{ .slice = wf.embeds.items },
+ .copies = .{ .slice = try s.initCopyList(wf.copies.items) },
+ .directories = .{ .slice = directories },
+ .mutate_path = .{ .value = switch (wf.mode) {
+ .mutate => |lp| try s.addLazyPath(lp),
+ .whole_cached, .tmp => null,
+ } },
+ });
+ },
+ .update_source_files => e: {
+ const usf: *Step.UpdateSourceFiles = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.UpdateSourceFiles, .{
+ .flags = .{
+ .embeds = usf.embeds.items.len != 0,
+ .copies = usf.copies.items.len != 0,
+ },
+ .embeds = .{ .slice = usf.embeds.items },
+ .copies = .{ .slice = try s.initCopyList(usf.copies.items) },
+ });
+ },
+ .run => e: {
+ const run: *Step.Run = @fieldParentPtr("step", step);
+ var expect_stderr_exact: ?Configuration.Bytes = null;
+ var expect_stdout_exact: ?Configuration.Bytes = null;
+ var expect_stderr_match: std.ArrayList(Configuration.Bytes) = .empty;
+ var expect_stdout_match: std.ArrayList(Configuration.Bytes) = .empty;
+ var expect_term: ?struct {
+ status: Configuration.Step.Run.ExpectTermStatus,
+ value: u32,
+ } = null;
+ switch (run.stdio) {
+ .check => |checks| for (checks.items) |check| switch (check) {
+ .expect_stderr_exact => |bytes| expect_stderr_exact = try wc.addBytes(bytes),
+ .expect_stdout_exact => |bytes| expect_stdout_exact = try wc.addBytes(bytes),
+ .expect_stderr_match => |bytes| {
+ try expect_stderr_match.append(arena, try wc.addBytes(bytes));
+ },
+ .expect_stdout_match => |bytes| {
+ try expect_stdout_match.append(arena, try wc.addBytes(bytes));
+ },
+ .expect_term => |t| expect_term = switch (t) {
+ .exited => |x| .{ .status = .exited, .value = x },
+ .signal => |x| .{ .status = .signal, .value = @intFromEnum(x) },
+ .stopped => |x| .{ .status = .stopped, .value = @intFromEnum(x) },
+ .unknown => |x| .{ .status = .unknown, .value = x },
+ },
+ },
+ else => {},
+ }
+
+ break :e try wc.addExtraErased(Configuration.Step.Run, .{
+ .flags = .{
+ .disable_zig_progress = run.disable_zig_progress,
+ .skip_foreign_checks = run.skip_foreign_checks,
+ .failing_to_execute_foreign_is_an_error = run.failing_to_execute_foreign_is_an_error,
+ .has_side_effects = run.has_side_effects,
+ .test_runner_mode = run.test_runner_mode,
+ .color = run.color,
+ .stdio = switch (run.stdio) {
+ .infer_from_args => .infer_from_args,
+ .inherit => .inherit,
+ .check => .check,
+ .zig_test => .zig_test,
+ },
+ .stdin = switch (run.stdin) {
+ .none => .none,
+ .bytes => .bytes,
+ .lazy_path => .lazy_path,
+ },
+ .stdout_trim_whitespace = if (run.captured_stdout) |cs| cs.trim_whitespace else .none,
+ .stderr_trim_whitespace = if (run.captured_stderr) |cs| cs.trim_whitespace else .none,
+ .stdio_limit = run.stdio_limit != .unlimited,
+ .producer = run.producer != null,
+ .cwd = run.cwd != null,
+ .captured_stdout = run.captured_stdout != null,
+ .captured_stderr = run.captured_stderr != null,
+ .environ_map = run.environ_map != null,
+ },
+ .flags2 = .{
+ .expect_stderr_exact = expect_stderr_exact != null,
+ .expect_stdout_exact = expect_stdout_exact != null,
+ .expect_stderr_match = expect_stderr_match.items.len != 0,
+ .expect_stdout_match = expect_stdout_match.items.len != 0,
+ .expect_term = expect_term != null,
+ .expect_term_status = if (expect_term) |t| t.status else .exited,
+ },
+ .file_inputs = .{ .slice = try s.initLazyPathList(run.file_inputs.items) },
+ .args = .{ .slice = try s.initArgsList(run.argv.items) },
+ .cwd = .{ .value = try s.addOptionalLazyPath(run.cwd) },
+ .captured_stdout = .{ .value = if (run.captured_stdout) |cs| .{
+ .basename = try wc.addString(cs.output.basename),
+ .generated_file = cs.output.generated_file,
+ } else null },
+ .captured_stderr = .{ .value = if (run.captured_stderr) |cs| .{
+ .basename = try wc.addString(cs.output.basename),
+ .generated_file = cs.output.generated_file,
+ } else null },
+ .environ_map = .{ .value = try s.addEnvironMap(run.environ_map) },
+ .expect_term_value = .{ .value = if (expect_term) |t| t.value else null },
+ .stdio_limit = .{ .value = run.stdio_limit.toInt() },
+ .producer = .{ .value = if (run.producer) |cs| s.stepIndex(&cs.step) else null },
+ .expect_stderr_exact = .{ .value = if (expect_stderr_exact) |bytes| bytes else null },
+ .expect_stdout_exact = .{ .value = if (expect_stdout_exact) |bytes| bytes else null },
+ .expect_stderr_match = .{ .slice = expect_stderr_match.items },
+ .expect_stdout_match = .{ .slice = expect_stdout_match.items },
+ .stdin = .{ .u = switch (run.stdin) {
+ .none => .none,
+ .bytes => |bytes| .{ .bytes = try wc.addBytes(bytes) },
+ .lazy_path => |lp| .{ .lazy_path = try s.addLazyPath(lp) },
+ } },
+ });
+ },
+ .check_file => e: {
+ const cf: *Step.CheckFile = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.CheckFile, .{
+ .flags = .{
+ .expected_exact = cf.expected_exact != null,
+ .expected_matches = cf.expected_matches.len != 0,
+ .max_bytes = cf.max_bytes != null,
+ },
+ .file = try s.addLazyPath(cf.file),
+ .expected_exact = .{ .value = cf.expected_exact },
+ .expected_matches = .{ .slice = cf.expected_matches },
+ .max_bytes = .{ .value = cf.max_bytes },
+ });
+ },
+ .config_header => e: {
+ const ch: *Step.ConfigHeader = @fieldParentPtr("step", step);
+ const lazy_path: ?std.Build.LazyPath = ch.style.getPath();
+ const pairs = try arena.alloc(Configuration.Step.ConfigHeader.Value.Pair, ch.values.count());
+ for (pairs, ch.values.keys(), ch.values.values()) |*pair, key, value| pair.* = .{
+ .key = try wc.addString(key),
+ .index = switch (value) {
+ .undef => .undef,
+ .defined => .defined,
+ .boolean => |x| switch (x) {
+ false => .bool_false,
+ true => .bool_true,
+ },
+ .int => |x| switch (x) {
+ 0 => .int_0,
+ 1 => .int_1,
+ else => try wc.addExtra(Configuration.Step.ConfigHeader.Value, .initSigned(x)),
+ },
+ .ident => |x| try wc.addExtra(Configuration.Step.ConfigHeader.Value, .{
+ .flags = .{
+ .tag = .ident,
+ .small = 0,
+ },
+ .i64 = .{ .value = null },
+ .u64 = .{ .value = null },
+ .ident = .{ .value = try wc.addString(x) },
+ .string = .{ .value = null },
+ }),
+ .string => |x| try wc.addExtra(Configuration.Step.ConfigHeader.Value, .{
+ .flags = .{
+ .tag = .string,
+ .small = 0,
+ },
+ .i64 = .{ .value = null },
+ .u64 = .{ .value = null },
+ .ident = .{ .value = null },
+ .string = .{ .value = try wc.addString(x) },
+ }),
+ },
+ };
+ break :e try wc.addExtraErased(Configuration.Step.ConfigHeader, .{
+ .flags = .{
+ .template_file = lazy_path != null,
+ .style = .init(ch.style),
+ .input_size_limit = ch.input_size_limit != null,
+ .include_guard = ch.include_guard != .none,
+ },
+ .template_file = .{ .value = try s.addOptionalLazyPath(lazy_path) },
+ .generated_dir = ch.generated_dir,
+ .input_size_limit = .{ .value = ch.input_size_limit },
+ .include_path = try wc.addString(ch.include_path),
+ .include_guard = .{ .value = ch.include_guard.unwrap() },
+ .values = .{ .slice = pairs },
+ });
+ },
+ .obj_copy => e: {
+ const oc: *Step.ObjCopy = @fieldParentPtr("step", step);
+
+ const debug_basename: ?Configuration.String = if (oc.debug_file) |df|
+ df.basename.unwrap()
+ else
+ null;
+
+ const debug_file: ?Configuration.GeneratedFileIndex = if (oc.debug_file) |df|
+ df.output_file
+ else
+ null;
+
+ const add_sections = try arena.alloc(
+ Configuration.Step.ObjCopy.AddSection,
+ oc.add_sections.items.len,
+ );
+ for (add_sections, oc.add_sections.items) |*dest, src| dest.* = .{
+ .section_name = src.section_name,
+ .file_path = try s.addLazyPath(src.file_path),
+ };
+
+ break :e try wc.addExtraErased(Configuration.Step.ObjCopy, .{
+ .flags = .{
+ .basename = oc.basename != .none,
+ .debug_file = debug_file != null,
+ .debug_basename = debug_basename != null,
+ .format = .init(oc.format),
+ .strip = oc.strip,
+ .compress_debug = oc.compress_debug,
+ .only_section = oc.only_section != .none,
+ .pad_to = oc.pad_to != null,
+ .add_section = add_sections.len != 0,
+ .update_section = oc.update_sections.items.len != 0,
+ },
+ .input_file = try s.addLazyPath(oc.input_file),
+ .output_file = oc.output_file,
+ .basename = .{ .value = oc.basename.unwrap() },
+ .debug_file = .{ .value = debug_file },
+ .debug_basename = .{ .value = debug_basename },
+ .only_section = .{ .value = oc.only_section.unwrap() },
+ .pad_to = .{ .value = oc.pad_to },
+ .add_section = .{ .slice = add_sections },
+ .update_section = .{ .slice = oc.update_sections.items },
+ });
+ },
+ .options => e: {
+ const so: *Step.Options = @fieldParentPtr("step", step);
+
+ const args = try arena.alloc(Configuration.Step.Options.Arg, so.args.items.len);
+ for (args, so.args.items) |*dest, src| dest.* = .{
+ .name = src.name,
+ .path = try s.addLazyPath(src.path),
+ };
+
+ break :e try wc.addExtraErased(Configuration.Step.Options, .{
+ .flags = .{
+ .args = so.args.items.len != 0,
+ },
+ .generated_file = so.generated_file,
+ .contents = try wc.addBytes(so.contents.items),
+ .args = .{ .slice = args },
+ });
+ },
+ }),
+ });
+ }
+ }
+
+ try wc.unlazy_deps.ensureUnusedCapacity(gpa, graph.needed_lazy_dependencies.keys().len);
+ for (graph.needed_lazy_dependencies.keys()) |k| {
+ wc.unlazy_deps.appendAssumeCapacity(try wc.addString(k));
+ }
+
+ try wc.write(writer, .{
+ .default_step = s.stepIndex(b.default_step),
+ .generated_files_len = @intCast(graph.generated_files.items.len),
+ .poisoned = switch (graph.cache_poison) {
+ .pure, .disallowed, .ignored => false,
+ .poisoned => true,
+ },
+ });
+}
+
+fn addOptionalResolvedTarget(
+ wc: *Configuration.Wip,
+ optional_resolved_target: ?std.Build.ResolvedTarget,
+) !Configuration.ResolvedTarget.OptionalIndex {
+ const resolved_target = optional_resolved_target orelse return .none;
+ return .init(try wc.addDeduped(Configuration.ResolvedTarget, .{
+ .query = try wc.addTargetQuery(&resolved_target.query),
+ .result = try wc.addTarget(resolved_target.result),
+ }));
+}
+
+fn addInstallDir(wc: *Configuration.Wip, install_dir: ?std.Build.InstallDir) !Configuration.InstallDestDir {
+ switch (install_dir orelse return .none) {
+ .prefix => return .prefix,
+ .lib => return .lib,
+ .bin => return .bin,
+ .header => return .header,
+ .custom => |sub_path| return .initCustom(try wc.addString(sub_path)),
+ }
+}
+
+fn addInstallDirDefaultNull(wc: *Configuration.Wip, install_dir: ?std.Build.InstallDir) !?Configuration.InstallDestDir {
+ return try addInstallDir(wc, install_dir orelse return null);
+}
+
+/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
+/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
+fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
+ const root_module = if (step.cast(Step.Compile)) |cs| root: {
+ break :root cs.root_module;
+ } else return; // not a compile step so no module dependencies
+
+ // Starting from `root_module`, discover all modules in this graph.
+ const modules = root_module.getGraph().modules;
+
+ // For each of those modules, set up the implied step dependencies.
+ for (modules) |mod| {
+ if (mod.root_source_file) |lp| lp.addStepDependencies(step);
+ for (mod.include_dirs.items) |include_dir| switch (include_dir) {
+ .path,
+ .path_system,
+ .path_after,
+ .framework_path,
+ .framework_path_system,
+ .embed_path,
+ => |lp| lp.addStepDependencies(step),
+
+ .other_step => |other| {
+ other.getEmittedIncludeTree().addStepDependencies(step);
+ step.dependOn(&other.step);
+ },
+
+ .config_header_step => |other| step.dependOn(&other.step),
+ };
+ for (mod.lib_paths.items) |lp| lp.addStepDependencies(step);
+ for (mod.rpaths.items) |rpath| switch (rpath) {
+ .lazy_path => |lp| lp.addStepDependencies(step),
+ .special => {},
+ };
+ for (mod.link_objects.items) |link_object| switch (link_object) {
+ .static_path,
+ .assembly_file,
+ => |lp| lp.addStepDependencies(step),
+ .other_step => |other| step.dependOn(&other.step),
+ .system_lib => {},
+ .c_source_file => |source| source.file.addStepDependencies(step),
+ .c_source_files => |source_files| source_files.root.addStepDependencies(step),
+ .win32_resource_file => |rc_source| {
+ rc_source.file.addStepDependencies(step);
+ for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
+ },
+ };
+ }
+}
+
+fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
+ if (idx.* >= args.len) return null;
+ defer idx.* += 1;
+ return args[idx.*];
+}
+
+fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
+ return nextArg(args, idx) orelse {
+ fatalWithHint("expected argument after {q}", .{args[idx.* - 1]});
+ };
+}
+
+fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {
+ const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});
+ if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg });
+ const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first});
+ return arg;
+}
+
+const ErrorStyle = enum {
+ verbose,
+ minimal,
+ verbose_clear,
+ minimal_clear,
+ fn verboseContext(s: ErrorStyle) bool {
+ return switch (s) {
+ .verbose, .verbose_clear => true,
+ .minimal, .minimal_clear => false,
+ };
+ }
+ fn clearOnUpdate(s: ErrorStyle) bool {
+ return switch (s) {
+ .verbose, .minimal => false,
+ .verbose_clear, .minimal_clear => true,
+ };
+ }
+};
+const MultilineErrors = enum { indent, newline, none };
+const Summary = enum { all, new, failures, line, none };
+
+fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
+ log.info("to access the help menu: zig build -h", .{});
+ fatal(f, args);
+}
+
+fn serializeSystemIntegrationOptions(graph: *std.Build.Graph, wc: *Configuration.Wip) Allocator.Error!void {
+ const gpa = wc.gpa;
+
+ var bad = false;
+ try wc.system_integrations.ensureTotalCapacityPrecise(gpa, graph.system_integration_options.entries.len);
+ for (graph.system_integration_options.keys(), graph.system_integration_options.values()) |k, v| {
+ wc.system_integrations.appendAssumeCapacity(.{
+ .name = try wc.addString(k),
+ .status = switch (v) {
+ .user_disabled, .user_enabled => x: {
+ // The user tried to enable or disable a system library integration, but
+ // the configure script did not recognize that option.
+ log.err("system integration name not recognized by configure script: {s}", .{k});
+ bad = true;
+ break :x .disabled;
+ },
+ .declared_disabled => .disabled,
+ .declared_enabled => .enabled,
+ },
+ });
+ }
+ if (bad) {
+ log.info("help menu contains available options: zig build -h", .{});
+ process.exit(1);
+ }
+}
+
+fn serializePackageOptions(b: *std.Build, wc: *Configuration.Wip) Allocator.Error!void {
+ const gpa = wc.gpa;
+
+ try wc.available_options.ensureTotalCapacityPrecise(gpa, b.available_options_map.count());
+ for (b.available_options_map.keys(), b.available_options_map.values()) |name, *opt| {
+ wc.available_options.appendAssumeCapacity(.{
+ .name = try wc.addString(name),
+ .description = try wc.addString(opt.description),
+ .type = opt.type_id,
+ .enum_options = if (opt.enum_options) |enum_vals| .init(try wc.addStringList(enum_vals)) else .none,
+ });
+ }
+}
diff --git a/lib/compiler/std-docs.zig b/lib/compiler/std-docs.zig
@@ -271,12 +271,16 @@ fn serveWasm(
// Do the compilation every request, so that the user can edit the files
// and see the changes without restarting the server.
const wasm_base_path = try buildWasmBinary(arena, context, optimize_mode);
+ const target = std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
+ .arch_os_abi = autodoc_arch_os_abi,
+ .cpu_features = autodoc_cpu_features,
+ }) catch unreachable) catch unreachable;
const bin_name = try std.zig.binNameAlloc(arena, .{
.root_name = autodoc_root_name,
- .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
- .arch_os_abi = autodoc_arch_os_abi,
- .cpu_features = autodoc_cpu_features,
- }) catch unreachable) catch unreachable),
+ .cpu_arch = target.cpu.arch,
+ .os_tag = target.os.tag,
+ .ofmt = target.ofmt,
+ .abi = target.abi,
.output_mode = .Exe,
});
// std.http.Server does not have a sendfile API yet.
@@ -406,51 +410,26 @@ fn buildWasmBinary(
child.stdin.?.close(io);
child.stdin = null;
- switch (try child.wait(io)) {
- .exited => |code| {
- if (code != 0) {
- std.log.err(
- "the following command exited with error code {d}:\n{s}",
- .{ code, try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
- );
- return error.WasmCompilationFailed;
- }
- },
- .signal => |sig| {
- std.log.err(
- "the following command terminated with signal {t}:\n{s}",
- .{ sig, try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
- );
- return error.WasmCompilationFailed;
- },
- .stopped => |sig| {
- std.log.err(
- "the following command stopped unexpectedly with signal {t}:\n{s}",
- .{ sig, try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
- );
- return error.WasmCompilationFailed;
- },
- .unknown => {
- std.log.err(
- "the following command terminated unexpectedly:\n{s}",
- .{try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items)},
- );
- return error.WasmCompilationFailed;
- },
+ const term = try child.wait(io);
+ if (!term.success()) {
+ std.log.err("the following command {f}:\n{s}", .{
+ term, try std.zig.allocPrintCmd(arena, argv.items, .{}),
+ });
+ return error.WasmCompilationFailed;
}
if (result_error_bundle.errorMessageCount() > 0) {
try result_error_bundle.renderToStderr(io, .{}, .auto);
std.log.err("the following command failed with {d} compilation errors:\n{s}", .{
result_error_bundle.errorMessageCount(),
- try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
+ try std.zig.allocPrintCmd(arena, argv.items, .{}),
});
return error.WasmCompilationFailed;
}
return result orelse {
std.log.err("child process failed to report result\n{s}", .{
- try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
+ try std.zig.allocPrintCmd(arena, argv.items, .{}),
});
return error.WasmCompilationFailed;
};
diff --git a/lib/init/build.zig b/lib/init/build.zig
@@ -111,9 +111,7 @@ pub fn build(b: *std.Build) void {
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
- if (b.args) |args| {
- run_cmd.addArgs(args);
- }
+ run_cmd.addPassthruArgs();
// Creates an executable that will run `test` blocks from the provided module.
// Here `mod` needs to define a target, which is why earlier we made sure to
diff --git a/lib/std/Build.zig b/lib/std/Build.zig
@@ -1,4 +1,5 @@
const Build = @This();
+
const builtin = @import("builtin");
const std = @import("std.zig");
@@ -19,48 +20,23 @@ const ArrayList = std.ArrayList;
pub const Cache = @import("Build/Cache.zig");
pub const Step = @import("Build/Step.zig");
pub const Module = @import("Build/Module.zig");
-pub const Watch = @import("Build/Watch.zig");
-pub const Fuzz = @import("Build/Fuzz.zig");
-pub const WebServer = @import("Build/WebServer.zig");
pub const abi = @import("Build/abi.zig");
+/// The serialized output of configure phase ingested by make phase.
+pub const Configuration = @import("Build/Configuration.zig");
/// Shared state among all Build instances.
graph: *Graph,
-install_tls: TopLevelStep,
-uninstall_tls: TopLevelStep,
+install_tls: Step.TopLevel,
+uninstall_tls: Step.TopLevel,
allocator: Allocator,
user_input_options: UserInputOptionsMap,
-available_options_map: AvailableOptionsMap,
-available_options_list: std.array_list.Managed(AvailableOption),
-verbose: bool,
-verbose_link: bool,
-verbose_cc: bool,
-verbose_air: bool,
-verbose_llvm_ir: ?[]const u8,
-verbose_llvm_bc: ?[]const u8,
-verbose_llvm_cpu_features: bool,
-reference_trace: ?u32 = null,
+available_options_map: std.array_hash_map.String(AvailableOption) = .empty,
invalid_user_input: bool,
default_step: *Step,
-top_level_steps: std.StringArrayHashMapUnmanaged(*TopLevelStep),
-install_prefix: []const u8,
-dest_dir: ?[]const u8,
-lib_dir: []const u8,
-exe_dir: []const u8,
-h_dir: []const u8,
-install_path: []const u8,
-sysroot: ?[]const u8 = null,
-search_prefixes: ArrayList([]const u8),
-libc_file: ?[]const u8 = null,
+top_level_steps: std.StringArrayHashMapUnmanaged(*Step.TopLevel),
/// Path to the directory containing build.zig.
-build_root: Cache.Directory,
-cache_root: Cache.Directory,
-pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null,
-args: ?[]const []const u8 = null,
+root: Cache.Path,
debug_log_scopes: []const []const u8 = &.{},
-debug_compile_errors: bool = false,
-debug_incremental: bool = false,
-debug_pkg_config: bool = false,
/// Number of stack frames captured when a `StackTrace` is recorded for debug purposes,
/// in particular at `Step` creation.
/// Set to 0 to disable stack collection.
@@ -76,12 +52,6 @@ enable_rosetta: bool = false,
enable_wasmtime: bool = false,
/// Use system Wine installation to run cross compiled Windows build artifacts.
enable_wine: bool = false,
-/// After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md,
-/// this will be the directory $glibc-build-dir/install/glibcs
-/// Given the example of the aarch64 target, this is the directory
-/// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`.
-/// Also works for dynamic musl.
-libc_runtimes_dir: ?[]const u8 = null,
dep_prefix: []const u8 = "",
@@ -94,10 +64,6 @@ pkg_hash: []const u8,
/// A mapping from dependency names to package hashes.
available_deps: AvailableDeps,
-release_mode: ReleaseMode,
-
-build_id: ?std.zig.BuildId = null,
-
pub const ReleaseMode = enum {
off,
any,
@@ -112,33 +78,145 @@ pub const Graph = struct {
io: Io,
/// Process lifetime.
arena: Allocator,
- system_library_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .empty,
+ system_integration_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .empty,
system_package_mode: bool = false,
- debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null,
- cache: Cache,
- zig_exe: [:0]const u8,
+ zig_exe: []const u8,
environ_map: process.Environ.Map,
- global_cache_root: Cache.Directory,
- zig_lib_directory: Cache.Directory,
needed_lazy_dependencies: std.StringArrayHashMapUnmanaged(void) = .empty,
/// Information about the native target. Computed before build() is invoked.
host: ResolvedTarget,
- incremental: ?bool = null,
- random_seed: u32 = 0,
dependency_cache: InitializedDepMap = .empty,
allow_so_scripts: ?bool = null,
- /// Steps should use `io` to limit the number of jobs, however in the case of
- /// a single step spawning a fixed number of processes this can be used.
- max_jobs: ?u32 = null,
- time_report: bool,
+ time_report: bool = false,
+ verbose: bool = false,
/// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also
/// respects the '--color' flag.
stderr_mode: ?Io.Terminal.Mode = null,
+ release_mode: ReleaseMode = .off,
+
+ /// Indexes correspond to `Configuration.GeneratedFileIndex`.
+ generated_files: std.ArrayList(*Step),
+ wip_configuration: Configuration.Wip,
+
+ cache_poison: CachePoison = .pure,
+ /// Observing this data causes cache poisoning. See `CachePoison`.
+ search_prefixes: std.ArrayList([]const u8) = .empty,
+
+ /// If the cache is poisoned means that the **configure logic** had side
+ /// effects, or otherwise did something that could not be tracked by the
+ /// cache system.
+ ///
+ /// This is not to be confused with whether individual steps may have side
+ /// effects when being evaluated; it has to do with the logic inside build.zig
+ /// itself. For example, a `Run` step that prints "hello world" has side
+ /// effects *at make time* and therefore does not warrant setting this flag,
+ /// while checking for the existence of `scdoc` *at configure time* in order to
+ /// choose the default value for a configuration option does.
+ ///
+ /// Keeping the cache pure will make `zig build` faster, bypassing the
+ /// configurer process when identical configuration would be generated.
+ ///
+ /// When the cache is poisoned, the maker process will delete the build
+ /// configuration file upon ingesting it since it cannot be reused.
+ pub const CachePoison = enum {
+ pure,
+ poisoned,
+ /// Indicates the user would like to see a stack trace if the cache
+ /// would become poisoned.
+ disallowed,
+ /// Indicates the user would like to ignore the cache being poisoned
+ /// and cache anyway, opting into cache hits on stale configuration.
+ ignored,
+ };
+
+ pub fn addGeneratedFile(graph: *Graph, owner: *Step) Configuration.GeneratedFileIndex {
+ graph.generated_files.append(graph.arena, owner) catch @panic("OOM");
+ return @enumFromInt(graph.generated_files.items.len - 1);
+ }
+
+ pub fn dupeString(graph: *const Graph, bytes: []const u8) []const u8 {
+ return graph.arena.dupe(u8, bytes) catch @panic("OOM");
+ }
+
+ pub fn dupePath(graph: *const Graph, bytes: []const u8) []const u8 {
+ return dupePathInner(graph.arena, bytes);
+ }
+
+ fn dupePathInner(arena: Allocator, bytes: []const u8) []const u8 {
+ if (builtin.os.tag != .windows) return arena.dupe(u8, bytes) catch @panic("OOM");
+ const the_copy = arena.dupe(u8, bytes) catch @panic("OOM");
+ mem.replaceScalar(u8, the_copy, '/', '\\');
+ return the_copy;
+ }
+
+ pub fn dupeStrings(graph: *const Graph, strings: []const []const u8) []const []const u8 {
+ const array = graph.alloc([]const u8, strings.len);
+ for (array, strings) |*dest, source| dest.* = dupeString(graph, source);
+ return array;
+ }
+
+ /// An absolute path or a path relative to the current working directory of
+ /// the build runner process.
+ ///
+ /// Use of this function indicates a dependency on the host system.
+ pub fn cwdRelativePath(graph: *Graph, sub_path: []const u8) LazyPath {
+ return @This().path(graph, .cwd, sub_path);
+ }
+
+ /// A path whose components and contents are known at some point during
+ /// `Step` resolution, relative to the provided base directory.
+ pub fn path(graph: *Graph, base: Configuration.Path.Base, sub_path: []const u8) LazyPath {
+ return .{ .relative = .{
+ .base = base,
+ .sub_path = @This().dupePath(graph, sub_path),
+ } };
+ }
+
+ /// Allocates using the global process arena, failing the build on
+ /// allocation failure.
+ pub fn alloc(graph: *const Graph, comptime T: type, n: usize) []T {
+ return graph.arena.allocAdvancedWithRetAddr(T, null, n, @returnAddress()) catch @panic("OOM");
+ }
+
+ /// Allocates using the global process arena, failing the build on
+ /// allocation failure.
+ pub fn create(graph: *const Graph, comptime T: type) *T {
+ return @ptrCast(graph.arena.allocBytesAligned(.of(T), @sizeOf(T), @returnAddress()) catch @panic("OOM"));
+ }
+
+ pub fn addBytesList(graph: *Graph, bytes_list: []const []const u8) []const Configuration.Bytes {
+ const result = graph.alloc(Configuration.Bytes, bytes_list.len);
+ for (result, bytes_list) |*d, s| d.* = addBytes(graph, s);
+ return result;
+ }
+
+ pub fn addBytes(graph: *Graph, bytes: []const u8) Configuration.Bytes {
+ const wc = &graph.wip_configuration;
+ return wc.addBytes(bytes) catch @panic("OOM");
+ }
+
+ pub fn addString(graph: *Graph, bytes: []const u8) Configuration.String {
+ const wc = &graph.wip_configuration;
+ return wc.addString(bytes) catch @panic("OOM");
+ }
+
+ /// Indicates that the **configure logic** had side effects, or otherwise
+ /// did something that could not be tracked by the cache system.
+ ///
+ /// See `CachePoison` documentation for more details.
+ pub fn poisonCache(graph: *Graph) void {
+ switch (graph.cache_poison) {
+ .pure => graph.cache_poison = .poisoned,
+ .poisoned => return,
+ .disallowed => @panic("cache poisoned"),
+ .ignored => log.warn("ignoring cache poisoning", .{}),
+ }
+ }
};
const AvailableDeps = []const struct { []const u8, []const u8 };
-const SystemLibraryMode = enum {
+pub const SystemLibraryMode = enum {
/// User asked for the library to be disabled.
/// The build runner has not confirmed whether the setting is recognized yet.
user_disabled,
@@ -187,31 +265,11 @@ const InitializedDepContext = struct {
}
};
-pub const RunError = error{
- ReadFailure,
- ExitCodeFailure,
- ProcessTerminated,
- ExecNotSupported,
-} || std.process.SpawnError;
-
-pub const PkgConfigError = error{
- PkgConfigCrashed,
- PkgConfigFailed,
- PkgConfigNotInstalled,
- PkgConfigInvalidOutput,
-};
-
-pub const PkgConfigPkg = struct {
- name: []const u8,
- desc: []const u8,
-};
-
const UserInputOptionsMap = StringHashMap(UserInputOption);
-const AvailableOptionsMap = StringHashMap(AvailableOption);
const AvailableOption = struct {
name: []const u8,
- type_id: TypeId,
+ type_id: Configuration.AvailableOption.Type,
description: []const u8,
/// If the `type_id` is `enum` or `enum_list` this provides the list of enum options
enum_options: ?[]const []const u8,
@@ -232,36 +290,9 @@ const UserValue = union(enum) {
lazy_path_list: std.array_list.Managed(LazyPath),
};
-const TypeId = enum {
- bool,
- int,
- float,
- @"enum",
- enum_list,
- string,
- list,
- build_id,
- lazy_path,
- lazy_path_list,
-};
-
-const TopLevelStep = struct {
- pub const base_id: Step.Id = .top_level;
-
- step: Step,
- description: []const u8,
-};
-
-pub const DirList = struct {
- lib_dir: ?[]const u8 = null,
- exe_dir: ?[]const u8 = null,
- include_dir: ?[]const u8 = null,
-};
-
pub fn create(
graph: *Graph,
- build_root: Cache.Directory,
- cache_root: Cache.Directory,
+ root: Cache.Path,
available_deps: AvailableDeps,
) error{OutOfMemory}!*Build {
const arena = graph.arena;
@@ -269,31 +300,15 @@ pub fn create(
const b = try arena.create(Build);
b.* = .{
.graph = graph,
- .build_root = build_root,
- .cache_root = cache_root,
- .verbose = false,
- .verbose_link = false,
- .verbose_cc = false,
- .verbose_air = false,
- .verbose_llvm_ir = null,
- .verbose_llvm_bc = null,
- .verbose_llvm_cpu_features = false,
+ .root = root,
.invalid_user_input = false,
.allocator = arena,
.user_input_options = UserInputOptionsMap.init(arena),
- .available_options_map = AvailableOptionsMap.init(arena),
- .available_options_list = std.array_list.Managed(AvailableOption).init(arena),
.top_level_steps = .{},
.default_step = undefined,
- .search_prefixes = .empty,
- .install_prefix = undefined,
- .lib_dir = undefined,
- .exe_dir = undefined,
- .h_dir = undefined,
- .dest_dir = graph.environ_map.get("DESTDIR"),
.install_tls = .{
.step = .init(.{
- .id = TopLevelStep.base_id,
+ .tag = .top_level,
.name = "install",
.owner = b,
}),
@@ -301,21 +316,17 @@ pub fn create(
},
.uninstall_tls = .{
.step = .init(.{
- .id = TopLevelStep.base_id,
+ .tag = .top_level,
.name = "uninstall",
.owner = b,
- .makeFn = makeUninstall,
}),
.description = "Remove build artifacts from prefix path",
},
- .install_path = undefined,
- .args = null,
.modules = .empty,
.named_writefiles = .empty,
.named_lazy_paths = .empty,
.pkg_hash = "",
.available_deps = available_deps,
- .release_mode = .off,
};
try b.top_level_steps.put(arena, b.install_tls.step.name, &b.install_tls);
try b.top_level_steps.put(arena, b.uninstall_tls.step.name, &b.uninstall_tls);
@@ -326,32 +337,20 @@ pub fn create(
fn createChild(
parent: *Build,
dep_name: []const u8,
- build_root: Cache.Directory,
- pkg_hash: []const u8,
- pkg_deps: AvailableDeps,
- user_input_options: UserInputOptionsMap,
-) error{OutOfMemory}!*Build {
- const child = try createChildOnly(parent, dep_name, build_root, pkg_hash, pkg_deps, user_input_options);
- try determineAndApplyInstallPrefix(child);
- return child;
-}
-
-fn createChildOnly(
- parent: *Build,
- dep_name: []const u8,
- build_root: Cache.Directory,
+ root: Cache.Path,
pkg_hash: []const u8,
pkg_deps: AvailableDeps,
user_input_options: UserInputOptionsMap,
) error{OutOfMemory}!*Build {
- const allocator = parent.allocator;
- const child = try allocator.create(Build);
+ const arena = parent.graph.arena;
+ const child = try arena.create(Build);
child.* = .{
.graph = parent.graph,
- .allocator = allocator,
+ .root = root,
+ .allocator = arena,
.install_tls = .{
.step = .init(.{
- .id = TopLevelStep.base_id,
+ .tag = .top_level,
.name = "install",
.owner = child,
}),
@@ -359,58 +358,31 @@ fn createChildOnly(
},
.uninstall_tls = .{
.step = .init(.{
- .id = TopLevelStep.base_id,
+ .tag = .top_level,
.name = "uninstall",
.owner = child,
- .makeFn = makeUninstall,
}),
.description = "Remove build artifacts from prefix path",
},
.user_input_options = user_input_options,
- .available_options_map = AvailableOptionsMap.init(allocator),
- .available_options_list = std.array_list.Managed(AvailableOption).init(allocator),
- .verbose = parent.verbose,
- .verbose_link = parent.verbose_link,
- .verbose_cc = parent.verbose_cc,
- .verbose_air = parent.verbose_air,
- .verbose_llvm_ir = parent.verbose_llvm_ir,
- .verbose_llvm_bc = parent.verbose_llvm_bc,
- .verbose_llvm_cpu_features = parent.verbose_llvm_cpu_features,
- .reference_trace = parent.reference_trace,
.invalid_user_input = false,
.default_step = undefined,
.top_level_steps = .{},
- .install_prefix = undefined,
- .dest_dir = parent.dest_dir,
- .lib_dir = parent.lib_dir,
- .exe_dir = parent.exe_dir,
- .h_dir = parent.h_dir,
- .install_path = parent.install_path,
- .sysroot = parent.sysroot,
- .search_prefixes = parent.search_prefixes,
- .libc_file = parent.libc_file,
- .build_root = build_root,
- .cache_root = parent.cache_root,
.debug_log_scopes = parent.debug_log_scopes,
- .debug_compile_errors = parent.debug_compile_errors,
- .debug_incremental = parent.debug_incremental,
- .debug_pkg_config = parent.debug_pkg_config,
.enable_darling = parent.enable_darling,
.enable_qemu = parent.enable_qemu,
.enable_rosetta = parent.enable_rosetta,
.enable_wasmtime = parent.enable_wasmtime,
.enable_wine = parent.enable_wine,
- .libc_runtimes_dir = parent.libc_runtimes_dir,
.dep_prefix = parent.fmt("{s}{s}.", .{ parent.dep_prefix, dep_name }),
.modules = .empty,
.named_writefiles = .empty,
.named_lazy_paths = .empty,
.pkg_hash = pkg_hash,
.available_deps = pkg_deps,
- .release_mode = parent.release_mode,
};
- try child.top_level_steps.put(allocator, child.install_tls.step.name, &child.install_tls);
- try child.top_level_steps.put(allocator, child.uninstall_tls.step.name, &child.uninstall_tls);
+ try child.top_level_steps.put(arena, child.install_tls.step.name, &child.install_tls);
+ try child.top_level_steps.put(arena, child.uninstall_tls.step.name, &child.uninstall_tls);
child.default_step = &child.install_tls.step;
return child;
}
@@ -624,13 +596,17 @@ const OrderedUserValue = union(enum) {
hasher.update(sp.sub_path);
},
.generated => |gen| {
- hasher.update(gen.file.step.owner.pkg_hash);
- hasher.update(std.mem.asBytes(&gen.up));
+ hasher.update(@ptrCast(&gen.index));
+ hasher.update(@ptrCast(&gen.up));
hasher.update(gen.sub_path);
},
.cwd_relative => |rel_path| {
hasher.update(rel_path);
},
+ .relative => |r| {
+ hasher.update(@ptrCast(&r.base));
+ hasher.update(@ptrCast(&r.sub_path));
+ },
.dependency => |dep| {
hasher.update(dep.dependency.builder.pkg_hash);
hasher.update(dep.sub_path);
@@ -702,59 +678,6 @@ fn hashUserInputOptionsMap(allocator: Allocator, user_input_options: UserInputOp
user_option.hash(hasher);
}
-fn determineAndApplyInstallPrefix(b: *Build) error{OutOfMemory}!void {
- // Create an installation directory local to this package. This will be used when
- // dependant packages require a standard prefix, such as include directories for C headers.
- var hash = b.graph.cache.hash;
- // Random bytes to make unique. Refresh this with new random bytes when
- // implementation is modified in a non-backwards-compatible way.
- hash.add(@as(u32, 0xd8cb0055));
- hash.addBytes(b.dep_prefix);
-
- var wyhash = std.hash.Wyhash.init(0);
- hashUserInputOptionsMap(b.allocator, b.user_input_options, &wyhash);
- hash.add(wyhash.final());
-
- const digest = hash.final();
- const install_prefix = try b.cache_root.join(b.allocator, &.{ "i", &digest });
- b.resolveInstallPrefix(install_prefix, .{});
-}
-
-/// This function is intended to be called by lib/build_runner.zig, not a build.zig file.
-pub fn resolveInstallPrefix(b: *Build, install_prefix: ?[]const u8, dir_list: DirList) void {
- if (b.dest_dir) |dest_dir| {
- b.install_prefix = install_prefix orelse "/usr";
- b.install_path = b.pathJoin(&.{ dest_dir, b.install_prefix });
- } else {
- b.install_prefix = install_prefix orelse
- (b.build_root.join(b.allocator, &.{"zig-out"}) catch @panic("unhandled error"));
- b.install_path = b.install_prefix;
- }
-
- var lib_list = [_][]const u8{ b.install_path, "lib" };
- var exe_list = [_][]const u8{ b.install_path, "bin" };
- var h_list = [_][]const u8{ b.install_path, "include" };
-
- if (dir_list.lib_dir) |dir| {
- if (fs.path.isAbsolute(dir)) lib_list[0] = b.dest_dir orelse "";
- lib_list[1] = dir;
- }
-
- if (dir_list.exe_dir) |dir| {
- if (fs.path.isAbsolute(dir)) exe_list[0] = b.dest_dir orelse "";
- exe_list[1] = dir;
- }
-
- if (dir_list.include_dir) |dir| {
- if (fs.path.isAbsolute(dir)) h_list[0] = b.dest_dir orelse "";
- h_list[1] = dir;
- }
-
- b.lib_dir = b.pathJoin(&lib_list);
- b.exe_dir = b.pathJoin(&exe_list);
- b.h_dir = b.pathJoin(&h_list);
-}
-
/// Create a set of key-value pairs that can be converted into a Zig source
/// file and then inserted into a Zig compilation's module table for importing.
/// In other words, this provides a way to expose build.zig values to Zig
@@ -904,8 +827,10 @@ pub const AssemblyOptions = struct {
/// it available to other packages which depend on this one.
/// `createModule` can be used instead to create a private module.
pub fn addModule(b: *Build, name: []const u8, options: Module.CreateOptions) *Module {
+ const graph = b.graph;
+ const arena = graph.arena;
const module = Module.create(b, options);
- b.modules.put(b.graph.arena, b.dupe(name), module) catch @panic("OOM");
+ b.modules.put(arena, graph.dupeString(name), module) catch @panic("OOM");
return module;
}
@@ -916,11 +841,23 @@ pub fn createModule(b: *Build, options: Module.CreateOptions) *Module {
return Module.create(b, options);
}
-/// Initializes a `Step.Run` with argv, which must at least have the path to the
-/// executable. More command line arguments can be added with `addArg`,
-/// `addArgs`, and `addArtifactArg`.
-/// Be careful using this function, as it introduces a system dependency.
-/// To run an executable built with zig build, see `Step.Compile.run`.
+/// Creates a step that executes a process on the host system.
+///
+/// `argv` is one or more command line arguments passed to the executed
+/// process. The first element is the name of the executable to run. More
+/// command line arguments can be added with methods of `Step.Run`, such as:
+/// * `Step.Run.addArgs`
+/// * `Step.Run.addArtifactArg`
+/// * `Step.Run.addFileArg`
+/// * `Step.Run.addOutputFileArg`
+///
+/// This function introduces a system dependency, compromising reproducibility
+/// and making it more difficult to set up one's computer in order to build the
+/// project from source.
+///
+/// See also:
+/// * `addRunArtifact`
+/// * `addRunFile`
pub fn addSystemCommand(b: *Build, argv: []const []const u8) *Step.Run {
assert(argv.len >= 1);
const run_step = Step.Run.create(b, b.fmt("run {s}", .{argv[0]}));
@@ -930,16 +867,22 @@ pub fn addSystemCommand(b: *Build, argv: []const []const u8) *Step.Run {
/// Creates a `Step.Run` with an executable built with `addExecutable`.
/// Add command line arguments with methods of `Step.Run`.
+///
+/// It doesn't have to target the host. In some cases cross-compiled binaries
+/// can even be executed.
+///
+/// This is declarative; it constructs a build step that may or may not be run
+/// depending on the options provided by the user to the build command.
+///
+/// See also:
+/// * `addSystemCommand`
+/// * `addRunFile`
pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run {
- // It doesn't have to be native. We catch that if you actually try to run it.
- // Consider that this is declarative; the run step may not be run unless a user
- // option is supplied.
-
// Avoid the common case of the step name looking like "run test test".
const step_name = if (exe.kind.isTest() and mem.eql(u8, exe.name, "test"))
- b.fmt("run {s}", .{@tagName(exe.kind)})
+ b.fmt("run {t}", .{exe.kind})
else
- b.fmt("run {s} {s}", .{ @tagName(exe.kind), exe.name });
+ b.fmt("run {t} {s}", .{ exe.kind, exe.name });
const run_step = Step.Run.create(b, step_name);
run_step.producer = exe;
@@ -994,6 +937,19 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run {
return run_step;
}
+/// Creates a step that executes the provided file.
+///
+/// Add more command line arguments via methods of `Step.Run`.
+///
+/// See also:
+/// * `addSystemCommand`
+/// * `addRunArtifact`
+pub fn addRunFile(b: *Build, executable: LazyPath) *Step.Run {
+ const run_step = Step.Run.create(b, b.fmt("run {f}", .{executable.fmt(b.graph)}));
+ run_step.addFileArg(executable);
+ return run_step;
+}
+
/// Using the `values` provided, produces a C header file, possibly based on a
/// template input file (e.g. config.h.in).
/// When an input template file is provided, this function will fail the build
@@ -1013,36 +969,18 @@ pub fn addConfigHeader(
return config_header_step;
}
-/// Allocator.dupe without the need to handle out of memory.
-pub fn dupe(b: *Build, bytes: []const u8) []u8 {
- return dupeInner(b.allocator, bytes);
-}
-
-pub fn dupeInner(allocator: std.mem.Allocator, bytes: []const u8) []u8 {
- return allocator.dupe(u8, bytes) catch @panic("OOM");
+pub fn dupe(b: *Build, bytes: []const u8) []const u8 {
+ return b.graph.dupeString(bytes);
}
-/// Duplicates an array of strings without the need to handle out of memory.
-pub fn dupeStrings(b: *Build, strings: []const []const u8) [][]u8 {
- const array = b.allocator.alloc([]u8, strings.len) catch @panic("OOM");
- for (array, strings) |*dest, source| dest.* = b.dupe(source);
- return array;
+/// Deprecated, call `Graph.dupeStrings` instead.
+pub fn dupeStrings(b: *Build, strings: []const []const u8) []const []const u8 {
+ return b.graph.dupeStrings(strings);
}
-/// Duplicates a path and converts all slashes to the OS's canonical path separator.
-pub fn dupePath(b: *Build, bytes: []const u8) []u8 {
- return dupePathInner(b.allocator, bytes);
-}
-
-fn dupePathInner(allocator: std.mem.Allocator, bytes: []const u8) []u8 {
- const the_copy = dupeInner(allocator, bytes);
- for (the_copy) |*byte| {
- switch (byte.*) {
- '/', '\\' => byte.* = fs.path.sep,
- else => {},
- }
- }
- return the_copy;
+/// Deprecated, call `Graph.dupePath` instead.
+pub fn dupePath(b: *Build, bytes: []const u8) []const u8 {
+ return b.graph.dupePath(bytes);
}
pub fn addWriteFile(b: *Build, file_path: []const u8, data: []const u8) *Step.WriteFile {
@@ -1052,13 +990,15 @@ pub fn addWriteFile(b: *Build, file_path: []const u8, data: []const u8) *Step.Wr
}
pub fn addNamedWriteFiles(b: *Build, name: []const u8) *Step.WriteFile {
+ const graph = b.graph;
const wf = Step.WriteFile.create(b);
- b.named_writefiles.put(b.graph.arena, b.dupe(name), wf) catch @panic("OOM");
+ b.named_writefiles.put(graph.arena, graph.dupeString(name), wf) catch @panic("OOM");
return wf;
}
pub fn addNamedLazyPath(b: *Build, name: []const u8, lp: LazyPath) void {
- b.named_lazy_paths.put(b.graph.arena, b.dupe(name), lp.dupe(b)) catch @panic("OOM");
+ const graph = b.graph;
+ b.named_lazy_paths.put(graph.arena, graph.dupeString(name), lp.dupe(graph)) catch @panic("OOM");
}
/// Creates a step for mutating files inside a temporary directory created lazily
@@ -1097,6 +1037,16 @@ pub fn addWriteFiles(b: *Build) *Step.WriteFile {
return Step.WriteFile.create(b);
}
+/// Creates a step for writing data to paths relative to the build root,
+/// mutating the project's source files.
+///
+/// This build step was designed not to be used during the normal build
+/// process, but rather as a utility run by a developer with intention to
+/// update source files, which will then be committed to version control.
+///
+/// Example use cases:
+/// * precompiling assets which are tracked by version control
+/// * snapshot testing
pub fn addUpdateSourceFiles(b: *Build) *Step.UpdateSourceFiles {
return Step.UpdateSourceFiles.create(b);
}
@@ -1121,28 +1071,21 @@ pub fn getUninstallStep(b: *Build) *Step {
return &b.uninstall_tls.step;
}
-fn makeUninstall(uninstall_step: *Step, options: Step.MakeOptions) anyerror!void {
- _ = options;
- const uninstall_tls: *TopLevelStep = @fieldParentPtr("step", uninstall_step);
- const b: *Build = @fieldParentPtr("uninstall_tls", uninstall_tls);
-
- _ = b;
- @panic("TODO implement https://github.com/ziglang/zig/issues/14943");
-}
-
/// Creates a configuration option to be passed to the build.zig script.
/// When a user directly runs `zig build`, they can set these options with `-D` arguments.
/// When a project depends on a Zig package as a dependency, it programmatically sets
/// these options when calling the dependency's build.zig script as a function.
/// `null` is returned when an option is left to default.
pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw: []const u8) ?T {
- const name = b.dupe(name_raw);
- const description = b.dupe(description_raw);
+ const graph = b.graph;
+ const arena = graph.arena;
+ const name = graph.dupeString(name_raw);
+ const description = graph.dupeString(description_raw);
const type_id = comptime typeToEnum(T);
const enum_options = if (type_id == .@"enum" or type_id == .enum_list) blk: {
const EnumType = if (type_id == .enum_list) @typeInfo(T).pointer.child else T;
const fields = comptime std.meta.fields(EnumType);
- var options = std.array_list.Managed([]const u8).initCapacity(b.allocator, fields.len) catch @panic("OOM");
+ var options = std.array_list.Managed([]const u8).initCapacity(arena, fields.len) catch @panic("OOM");
inline for (fields) |field| {
options.appendAssumeCapacity(field.name);
@@ -1156,10 +1099,9 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
.description = description,
.enum_options = enum_options,
};
- if ((b.available_options_map.fetchPut(name, available_option) catch @panic("OOM")) != null) {
- panic("Option '{s}' declared twice", .{name});
+ if ((b.available_options_map.fetchPut(arena, name, available_option) catch @panic("OOM")) != null) {
+ panic("option '{s}' declared twice", .{name});
}
- b.available_options_list.append(available_option) catch @panic("OOM");
const option_ptr = b.user_input_options.getPtr(name) orelse return null;
option_ptr.used = true;
@@ -1172,36 +1114,32 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
} else if (mem.eql(u8, s, "false")) {
return false;
} else {
- log.err("Expected -D{s} to be a boolean, but received '{s}'", .{ name, s });
+ log.err("expected -D{s} to be a boolean; received: {s}", .{ name, s });
b.markInvalidUserInput();
return null;
}
},
.list, .map, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be a boolean, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be a boolean; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
},
.int => switch (option_ptr.value) {
.flag, .list, .map, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be an integer, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be an integer; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
.scalar => |s| {
const n = std.fmt.parseInt(T, s, 10) catch |err| switch (err) {
error.Overflow => {
- log.err("-D{s} value {s} cannot fit into type {s}.", .{ name, s, @typeName(T) });
+ log.err("-D{s} value {s} cannot fit into type {s}", .{ name, s, @typeName(T) });
b.markInvalidUserInput();
return null;
},
else => {
- log.err("Expected -D{s} to be an integer of type {s}.", .{ name, @typeName(T) });
+ log.err("expected -D{s} to be an integer of type {s}", .{ name, @typeName(T) });
b.markInvalidUserInput();
return null;
},
@@ -1211,15 +1149,13 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
},
.float => switch (option_ptr.value) {
.flag, .map, .list, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be a float, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be a float; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
.scalar => |s| {
const n = std.fmt.parseFloat(T, s) catch {
- log.err("Expected -D{s} to be a float of type {s}.", .{ name, @typeName(T) });
+ log.err("expected -D{s} to be a float of type {s}", .{ name, @typeName(T) });
b.markInvalidUserInput();
return null;
};
@@ -1228,9 +1164,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
},
.@"enum" => switch (option_ptr.value) {
.flag, .map, .list, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be an enum, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be an enum; received: {t}.", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
@@ -1238,7 +1172,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
if (std.meta.stringToEnum(T, s)) |enum_lit| {
return enum_lit;
} else {
- log.err("Expected -D{s} to be of type {s}.", .{ name, @typeName(T) });
+ log.err("expected -D{s} to be of type {s}", .{ name, @typeName(T) });
b.markInvalidUserInput();
return null;
}
@@ -1246,9 +1180,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
},
.string => switch (option_ptr.value) {
.flag, .list, .map, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be a string, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be a string; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
@@ -1256,9 +1188,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
},
.build_id => switch (option_ptr.value) {
.flag, .map, .list, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be an enum, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be an enum; received: {t}.", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
@@ -1266,7 +1196,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
if (std.zig.BuildId.parse(s)) |build_id| {
return build_id;
} else |err| {
- log.err("unable to parse option '-D{s}': {s}", .{ name, @errorName(err) });
+ log.err("failed to parse option -D{s}: {t}", .{ name, err });
b.markInvalidUserInput();
return null;
}
@@ -1274,42 +1204,38 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
},
.list => switch (option_ptr.value) {
.flag, .map, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be a list, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be a list; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
.scalar => |s| {
- return b.allocator.dupe([]const u8, &[_][]const u8{s}) catch @panic("OOM");
+ return arena.dupe([]const u8, &[_][]const u8{s}) catch @panic("OOM");
},
.list => |lst| return lst.items,
},
.enum_list => switch (option_ptr.value) {
.flag, .map, .lazy_path, .lazy_path_list => {
- log.err("Expected -D{s} to be a list, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be a list; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
.scalar => |s| {
const Child = @typeInfo(T).pointer.child;
const value = std.meta.stringToEnum(Child, s) orelse {
- log.err("Expected -D{s} to be of type {s}.", .{ name, @typeName(Child) });
+ log.err("expected -D{s} to be of type {s}", .{ name, @typeName(Child) });
b.markInvalidUserInput();
return null;
};
- return b.allocator.dupe(Child, &[_]Child{value}) catch @panic("OOM");
+ return arena.dupe(Child, &[_]Child{value}) catch @panic("OOM");
},
.list => |lst| {
const Child = @typeInfo(T).pointer.child;
- const new_list = b.allocator.alloc(Child, lst.items.len) catch @panic("OOM");
+ const new_list = graph.alloc(Child, lst.items.len);
for (new_list, lst.items) |*new_item, str| {
new_item.* = std.meta.stringToEnum(Child, str) orelse {
- log.err("Expected -D{s} to be of type {s}.", .{ name, @typeName(Child) });
+ log.err("expected -D{s} to be of type {s}", .{ name, @typeName(Child) });
b.markInvalidUserInput();
- b.allocator.free(new_list);
+ arena.free(new_list);
return null;
};
}
@@ -1320,18 +1246,16 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
.scalar => |s| return .{ .cwd_relative = s },
.lazy_path => |lp| return lp,
.flag, .map, .list, .lazy_path_list => {
- log.err("Expected -D{s} to be a path, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be a path; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
},
.lazy_path_list => switch (option_ptr.value) {
- .scalar => |s| return b.allocator.dupe(LazyPath, &[_]LazyPath{.{ .cwd_relative = s }}) catch @panic("OOM"),
- .lazy_path => |lp| return b.allocator.dupe(LazyPath, &[_]LazyPath{lp}) catch @panic("OOM"),
+ .scalar => |s| return arena.dupe(LazyPath, &[_]LazyPath{.{ .cwd_relative = s }}) catch @panic("OOM"),
+ .lazy_path => |lp| return arena.dupe(LazyPath, &[_]LazyPath{lp}) catch @panic("OOM"),
.list => |lst| {
- const new_list = b.allocator.alloc(LazyPath, lst.items.len) catch @panic("OOM");
+ const new_list = graph.alloc(LazyPath, lst.items.len);
for (new_list, lst.items) |*new_item, str| {
new_item.* = .{ .cwd_relative = str };
}
@@ -1339,9 +1263,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
},
.lazy_path_list => |lp_list| return lp_list.items,
.flag, .map => {
- log.err("Expected -D{s} to be a path, but received a {s}.", .{
- name, @tagName(option_ptr.value),
- });
+ log.err("expected -D{s} to be a path; received: {t}", .{ name, option_ptr.value });
b.markInvalidUserInput();
return null;
},
@@ -1350,17 +1272,19 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
}
pub fn step(b: *Build, name: []const u8, description: []const u8) *Step {
- const step_info = b.allocator.create(TopLevelStep) catch @panic("OOM");
+ const graph = b.graph;
+ const arena = graph.arena;
+ const step_info = arena.create(Step.TopLevel) catch @panic("OOM");
step_info.* = .{
.step = .init(.{
- .id = TopLevelStep.base_id,
+ .tag = .top_level,
.name = name,
.owner = b,
}),
- .description = b.dupe(description),
+ .description = graph.dupeString(description),
};
- const gop = b.top_level_steps.getOrPut(b.allocator, name) catch @panic("OOM");
- if (gop.found_existing) std.debug.panic("A top-level step with name \"{s}\" already exists", .{name});
+ const gop = b.top_level_steps.getOrPut(arena, name) catch @panic("OOM");
+ if (gop.found_existing) panic("A top-level step with name \"{s}\" already exists", .{name});
gop.key_ptr.* = step_info.step.name;
gop.value_ptr.* = step_info;
@@ -1373,8 +1297,10 @@ pub const StandardOptimizeOptionOptions = struct {
};
pub fn standardOptimizeOption(b: *Build, options: StandardOptimizeOptionOptions) std.builtin.OptimizeMode {
+ const graph = b.graph;
+
if (options.preferred_optimize_mode) |mode| {
- if (b.option(bool, "release", "optimize for end users") orelse (b.release_mode != .off)) {
+ if (b.option(bool, "release", "optimize for end users") orelse (graph.release_mode != .off)) {
return mode;
} else {
return .Debug;
@@ -1389,7 +1315,7 @@ pub fn standardOptimizeOption(b: *Build, options: StandardOptimizeOptionOptions)
return mode;
}
- return switch (b.release_mode) {
+ return switch (graph.release_mode) {
.off => .Debug,
.any => {
std.debug.print("the project does not declare a preferred optimization mode. choose: --release=fast, --release=safe, or --release=small\n", .{});
@@ -1424,8 +1350,8 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile
opts_copy.diagnostics = &diags;
return std.Target.Query.parse(opts_copy) catch |err| switch (err) {
error.UnknownCpuModel => {
- std.debug.print("unknown CPU: '{s}'\navailable CPUs for architecture '{s}':\n", .{
- diags.cpu_name.?, @tagName(diags.arch.?),
+ std.debug.print("unknown CPU: '{s}'\navailable CPUs for architecture '{t}':\n", .{
+ diags.cpu_name.?, diags.arch.?,
});
for (diags.arch.?.allCpuModels()) |cpu| {
std.debug.print(" {s}\n", .{cpu.name});
@@ -1435,11 +1361,10 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile
error.UnknownCpuFeature => {
std.debug.print(
\\unknown CPU feature: '{s}'
- \\available CPU features for architecture '{s}':
+ \\available CPU features for architecture '{t}':
\\
, .{
- diags.unknown_feature_name.?,
- @tagName(diags.arch.?),
+ diags.unknown_feature_name.?, diags.arch.?,
});
for (diags.arch.?.allFeaturesList()) |feature| {
std.debug.print(" {s}: {s}\n", .{ feature.name, feature.description });
@@ -1468,6 +1393,9 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile
/// Exposes standard `zig build` options for choosing a target.
pub fn standardTargetOptionsQueryOnly(b: *Build, args: StandardTargetOptionsArgs) Target.Query {
+ const graph = b.graph;
+ const arena = graph.arena;
+
const maybe_triple = b.option(
[]const u8,
"target",
@@ -1516,20 +1444,22 @@ pub fn standardTargetOptionsQueryOnly(b: *Build, args: StandardTargetOptionsArgs
for (whitelist) |q| {
log.info("allowed target: -Dtarget={s} -Dcpu={s}", .{
- q.zigTriple(b.allocator) catch @panic("OOM"),
- q.serializeCpuAlloc(b.allocator) catch @panic("OOM"),
+ q.zigTriple(arena) catch @panic("OOM"),
+ q.serializeCpuAlloc(arena) catch @panic("OOM"),
});
}
log.err("chosen target '{s}' does not match one of the allowed targets", .{
- selected_target.zigTriple(b.allocator) catch @panic("OOM"),
+ selected_target.zigTriple(arena) catch @panic("OOM"),
});
b.markInvalidUserInput();
return args.default_target;
}
pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8) error{OutOfMemory}!bool {
- const name = b.dupe(name_raw);
- const value = b.dupe(value_raw);
+ const graph = b.graph;
+ const arena = graph.arena;
+ const name = graph.dupeString(name_raw);
+ const value = graph.dupeString(value_raw);
const gop = try b.user_input_options.getOrPut(name);
if (!gop.found_existing) {
gop.value_ptr.* = UserInputOption{
@@ -1544,7 +1474,7 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8
switch (gop.value_ptr.value) {
.scalar => |s| {
// turn it into a list
- var list = std.array_list.Managed([]const u8).init(b.allocator);
+ var list = std.array_list.Managed([]const u8).init(arena);
try list.append(s);
try list.append(value);
try b.user_input_options.put(name, .{
@@ -1572,7 +1502,9 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8
return true;
},
.lazy_path, .lazy_path_list => {
- log.warn("the lazy path value type isn't added from the CLI, but somehow '{s}' is a .{f}", .{ name, std.zig.fmtId(@tagName(gop.value_ptr.value)) });
+ log.warn("the lazy path value type isn't added from the CLI, but somehow '{s}' is a .{f}", .{
+ name, std.zig.fmtId(@tagName(gop.value_ptr.value)),
+ });
return true;
},
}
@@ -1580,7 +1512,8 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8
}
pub fn addUserInputFlag(b: *Build, name_raw: []const u8) error{OutOfMemory}!bool {
- const name = b.dupe(name_raw);
+ const graph = b.graph;
+ const name = graph.dupeString(name_raw);
const gop = try b.user_input_options.getOrPut(name);
if (!gop.found_existing) {
gop.value_ptr.* = .{
@@ -1602,7 +1535,7 @@ pub fn addUserInputFlag(b: *Build, name_raw: []const u8) error{OutOfMemory}!bool
return true;
},
.lazy_path => |lp| {
- log.err("Flag '-D{s}' conflicts with option '-D{s}={s}'.", .{ name, name, lp.getDisplayName() });
+ log.err("Flag '-D{s}' conflicts with option '-D{s}={f}'.", .{ name, name, lp });
return true;
},
@@ -1611,7 +1544,7 @@ pub fn addUserInputFlag(b: *Build, name_raw: []const u8) error{OutOfMemory}!bool
return false;
}
-fn typeToEnum(comptime T: type) TypeId {
+fn typeToEnum(comptime T: type) Configuration.AvailableOption.Type {
return switch (T) {
std.zig.BuildId => .build_id,
LazyPath => .lazy_path,
@@ -1732,26 +1665,10 @@ pub fn addCheckFile(
return Step.CheckFile.create(b, file_source, options);
}
-pub fn truncateFile(b: *Build, dest_path: []const u8) (Io.Dir.CreateDirError || Io.Dir.StatFileError)!void {
- const io = b.graph.io;
- if (b.verbose) log.info("truncate {s}", .{dest_path});
- const cwd = Io.Dir.cwd();
- var src_file = cwd.createFile(io, dest_path, .{}) catch |err| switch (err) {
- error.FileNotFound => blk: {
- if (fs.path.dirname(dest_path)) |dirname| {
- try cwd.createDirPath(io, dirname);
- }
- break :blk try cwd.createFile(io, dest_path, .{});
- },
- else => |e| return e,
- };
- src_file.close(io);
-}
-
/// References a file or directory relative to the source root.
pub fn path(b: *Build, sub_path: []const u8) LazyPath {
if (fs.path.isAbsolute(sub_path)) {
- std.debug.panic("sub_path is expected to be relative to the build root, but was this absolute path: '{s}'. It is best avoid absolute paths, but if you must, it is supported by LazyPath.cwd_relative", .{
+ panic("sub_path is expected to be relative to the build root, but was this absolute path: '{s}'. Absolute paths can cause problems but can be created via Graph.cwdRelativePath", .{
sub_path,
});
}
@@ -1761,27 +1678,106 @@ pub fn path(b: *Build, sub_path: []const u8) LazyPath {
} };
}
-/// This is low-level implementation details of the build system, not meant to
-/// be called by users' build scripts. Even in the build system itself it is a
-/// code smell to call this function.
-pub fn pathFromRoot(b: *Build, sub_path: []const u8) []u8 {
- return b.pathResolve(&.{ b.build_root.path orelse ".", sub_path });
-}
-
-fn pathFromCwd(b: *Build, sub_path: []const u8) []u8 {
- return b.pathResolve(&.{ b.graph.cache.cwd, sub_path });
+/// Creates a list of files and/or directories relative to the source root.
+pub fn pathList(b: *Build, sub_paths: []const []const u8) []const LazyPath {
+ const graph = b.graph;
+ const result = graph.alloc(LazyPath, sub_paths.len);
+ for (result, sub_paths) |*d, s| d.* = path(b, s);
+ return result;
}
pub fn pathJoin(b: *Build, paths: []const []const u8) []u8 {
- return fs.path.join(b.allocator, paths) catch @panic("OOM");
+ const graph = b.graph;
+ const arena = graph.arena;
+ return fs.path.join(arena, paths) catch @panic("OOM");
}
pub fn pathResolve(b: *Build, paths: []const []const u8) []u8 {
- return fs.path.resolve(b.allocator, paths) catch @panic("OOM");
+ const graph = b.graph;
+ const arena = graph.arena;
+ return fs.path.resolve(arena, paths) catch @panic("OOM");
}
pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
- return std.fmt.allocPrint(b.allocator, format, args) catch @panic("OOM");
+ const graph = b.graph;
+ const arena = graph.arena;
+ return std.fmt.allocPrint(arena, format, args) catch @panic("OOM");
+}
+
+/// Creates an anonymous `Step` that searches for an executable on the host that
+/// has more than one possible name.
+///
+/// Returns the `LazyPath` of the found executable. The search only takes place
+/// if the `LazyPath` will be used by a depending `Step`.
+///
+/// This API is useful in the following cases:
+/// * The binary is not named the same across all systems (for example "python"
+/// vs "python3").
+/// * The binary may be produced by building from source rather than being
+/// globally installed and will therefore be possibly found in one of the
+/// search prefix paths.
+///
+/// Names are searched in order, observing search prefixes first and then PATH
+/// environment variable.
+///
+/// Windows file name extensions are searched automatically, respecting the
+/// PATHEXT environment variable, so they need not be included in this list.
+/// However, even on Windows, the names will be checked without appending
+/// extensions first, so that can be used as a priority system.
+///
+/// See also:
+/// * `findProgram`
+pub fn findProgramLazy(b: *Build, options: Step.FindProgram.Options) LazyPath {
+ return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } };
+}
+
+pub const FindProgramOptions = Step.FindProgram.Options;
+
+/// Immediately (in the configure phase), searches for an executable on the host
+/// that has more than one possible name.
+///
+/// Calling this function poisons the configuration cache, so it is only
+/// appropriate when the existence of the program or its output needs to be
+/// observed by configuration logic. For more information, see
+/// `Graph.CachePoison` documentation.
+///
+/// Names are searched in order, observing search prefixes first and then PATH
+/// environment variable.
+///
+/// Windows file name extensions are searched automatically, respecting the
+/// PATHEXT environment variable, so they need not be included in this list.
+/// However, even on Windows, the names will be checked without appending
+/// extensions first, so that can be used as a priority system.
+///
+/// See also:
+/// * `findProgramLazy`
+pub fn findProgram(b: *Build, options: FindProgramOptions) ?[]const u8 {
+ const graph = b.graph;
+
+ // Because it observes search prefixes and contents of directories in PATH.
+ graph.poisonCache();
+
+ for (options.names) |name| {
+ if (Io.Dir.path.isAbsolute(name)) {
+ if (tryFindProgram(b, name)) |found| return found;
+ }
+ for (graph.search_prefixes.items) |search_prefix| {
+ const full_path = b.pathJoin(&.{ search_prefix, "bin", name });
+ if (tryFindProgram(b, full_path)) |found| return found;
+ }
+ }
+
+ if (b.graph.environ_map.get("PATH")) |PATH| {
+ for (options.names) |name| {
+ var it = mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter);
+ while (it.next()) |p| {
+ const full_path = b.pathJoin(&.{ p, name });
+ if (tryFindProgram(b, full_path)) |found| return found;
+ }
+ }
+ }
+
+ return null;
}
fn supportedWindowsProgramExtension(ext: []const u8) bool {
@@ -1792,14 +1788,17 @@ fn supportedWindowsProgramExtension(ext: []const u8) bool {
}
fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 {
- const io = b.graph.io;
- const arena = b.allocator;
+ const graph = b.graph;
+ const io = graph.io;
+ const arena = graph.arena;
- if (b.build_root.handle.realPathFileAlloc(io, full_path, arena)) |p| {
- return p;
+ if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| {
+ return full_path;
} else |err| switch (err) {
- error.OutOfMemory => @panic("OOM"),
- else => {},
+ error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
+ if (graph.verbose) log.info("searched: {t} {s}", .{ e, full_path });
+ },
+ else => |e| return panic("failed accessing {s}: {t}", .{ full_path, e }),
}
if (builtin.os.tag == .windows) {
@@ -1809,14 +1808,16 @@ fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 {
while (it.next()) |ext| {
if (!supportedWindowsProgramExtension(ext)) continue;
- return b.build_root.handle.realPathFileAlloc(
- io,
- b.fmt("{s}{s}", .{ full_path, ext }),
- arena,
- ) catch |err| switch (err) {
- error.OutOfMemory => @panic("OOM"),
- else => continue,
- };
+ const extended_path = try mem.concat(arena, &.{ full_path, ext });
+
+ if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| {
+ return extended_path;
+ } else |err| switch (err) {
+ error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
+ if (graph.verbose) log.info("searched: {t} {s}", .{ e, extended_path });
+ },
+ else => |e| return panic("failed accessing {s}: {t}", .{ extended_path, e }),
+ }
}
}
}
@@ -1824,114 +1825,158 @@ fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 {
return null;
}
-pub fn findProgram(b: *Build, names: []const []const u8, paths: []const []const u8) error{FileNotFound}![]const u8 {
- // TODO report error for ambiguous situations
- for (b.search_prefixes.items) |search_prefix| {
- for (names) |name| {
- if (fs.path.isAbsolute(name)) {
- return name;
- }
- return tryFindProgram(b, b.pathJoin(&.{ search_prefix, "bin", name })) orelse continue;
- }
- }
- if (b.graph.environ_map.get("PATH")) |PATH| {
- for (names) |name| {
- if (fs.path.isAbsolute(name)) {
- return name;
- }
- var it = mem.tokenizeScalar(u8, PATH, fs.path.delimiter);
- while (it.next()) |p| {
- return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue;
- }
- }
- }
- for (names) |name| {
- if (fs.path.isAbsolute(name)) {
- return name;
- }
- for (paths) |p| {
- return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue;
- }
- }
- return error.FileNotFound;
-}
-
+/// Deprecated; use `runFallible`.
pub fn runAllowFail(
b: *Build,
argv: []const []const u8,
- out_code: *u8,
- stderr_behavior: std.process.SpawnOptions.StdIo,
-) RunError![]u8 {
- assert(argv.len != 0);
+ exit_code: *u8,
+ stderr_behavior: process.SpawnOptions.StdIo,
+) anyerror![]u8 {
+ if (!process.can_spawn) return error.ExecNotSupported;
+ switch (runFallible(b, argv, .{
+ .stderr_behavior = stderr_behavior,
+ })) {
+ .success => |stdout| return stdout,
+ .spawn_failed => |err| return err,
+ .bad_exit_code => |code| {
+ exit_code.* = code;
+ return error.ExitCodeFailure;
+ },
+ .crashed => {
+ exit_code.* = 255;
+ return error.ProcessTerminated;
+ },
+ }
+}
+
+pub const RunOptions = struct {
+ stderr_behavior: process.SpawnOptions.StdIo = .inherit,
+ /// Fail the configuration if stdout is larger than this.
+ stdout_limit: Io.Limit = .limited(1_000_000),
+ /// Set to change the current working directory when spawning the child
+ /// process.
+ cwd: process.Child.Cwd = .inherit,
+ /// Replaces the child environment when provided. The PATH value from here
+ /// is not used to resolve `argv[0]`; that resolution always uses parent
+ /// environment.
+ environ_map: ?*const process.Environ.Map = null,
+ expand_arg0: process.ArgExpansion = .no_expand,
+};
+
+pub const RunResult = union(enum) {
+ /// Thild process exited with code 0, writing this stdout.
+ success: []u8,
+ /// The child process could not be created.
+ spawn_failed: process.SpawnError,
+ /// The child process indicated failure.
+ bad_exit_code: u8,
+ /// The child process terminated abnormally.
+ crashed,
+};
- if (!process.can_spawn)
- return error.ExecNotSupported;
+/// Executes the provided command immediately, allowing failure.
+///
+/// If the program exits successfully, stdout is returned. Otherwise, returns
+/// an indication of failure.
+///
+/// See also:
+/// * `run`.
+pub fn runFallible(b: *Build, argv: []const []const u8, options: RunOptions) RunResult {
+ assert(argv.len != 0);
const graph = b.graph;
const io = graph.io;
+ const arena = graph.arena;
+
+ const print_opts: std.zig.AllocPrintCmdOptions = .{
+ .cwd = switch (options.cwd) {
+ .inherit => null,
+ .path => |p| p,
+ .dir => null, // Unknown without changing function signature of runFallible.
+ },
+ .child_env = options.environ_map,
+ .parent_env = &graph.environ_map,
+ };
- const max_output_size = 400 * 1024;
- try Step.handleVerbose2(b, .inherit, &graph.environ_map, argv);
+ if (graph.verbose) {
+ const text = std.zig.allocPrintCmd(arena, argv, print_opts) catch @panic("OOM");
+ std.log.scoped(.verbose).info("{s}", .{text});
+ }
- var child = try std.process.spawn(io, .{
+ var child = process.spawn(io, .{
.argv = argv,
- .environ_map = &graph.environ_map,
.stdin = .ignore,
.stdout = .pipe,
- .stderr = stderr_behavior,
- });
+ .stderr = options.stderr_behavior,
+ .cwd = options.cwd,
+ .environ_map = &graph.environ_map,
+ .expand_arg0 = options.expand_arg0,
+ }) catch |err| return .{ .spawn_failed = err };
var stdout_reader = child.stdout.?.readerStreaming(io, &.{});
- const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch {
- return error.ReadFailure;
+ const stdout = stdout_reader.interface.allocRemaining(arena, options.stdout_limit) catch |err| switch (err) {
+ error.ReadFailed => panic("failed to read from child: {t}", .{stdout_reader.err.?}),
+ else => |e| panic("failed to read from child: {t}", .{e}),
};
- errdefer b.allocator.free(stdout);
-
- const term = try child.wait(io);
- switch (term) {
- .exited => |code| {
- if (code != 0) {
- out_code.* = @as(u8, @truncate(code));
- return error.ExitCodeFailure;
- }
- return stdout;
- },
- .signal, .stopped => |sig| {
- out_code.* = @as(u8, @truncate(@intFromEnum(sig)));
- return error.ProcessTerminated;
- },
- .unknown => |code| {
- out_code.* = @as(u8, @truncate(code));
- return error.ProcessTerminated;
+
+ const term = child.wait(io) catch @panic("unexpected");
+
+ return switch (term) {
+ .exited => |code| switch (code) {
+ 0 => .{ .success = stdout },
+ else => .{ .bad_exit_code = code },
},
- }
+ .signal, .stopped, .unknown => .crashed,
+ };
}
-/// This is a helper function to be called from build.zig scripts, *not* from
-/// inside step make() functions. If any errors occur, it fails the build with
-/// a helpful message.
+/// Executes the provided command immediately.
+///
+/// If the program exits successfully, stdout is returned. Otherwise, fails the
+/// build with a helpful message.
+///
+/// See also:
+/// * `runFallible`.
pub fn run(b: *Build, argv: []const []const u8) []u8 {
- var code: u8 = undefined;
- return b.runAllowFail(argv, &code, .inherit) catch |err| process.fatal(
- "the following command failed with {t}:\n{s}",
- .{ err, Step.allocPrintCmd(b.allocator, .inherit, null, argv) catch @panic("OOM") },
- );
+ const graph = b.graph;
+ const arena = graph.arena;
+ switch (b.runFallible(argv, .{
+ .stderr_behavior = .inherit,
+ })) {
+ .success => |stdout| return stdout,
+ .spawn_failed => |err| process.fatal("the following command failed with {t}:\n{s}", .{
+ err, std.zig.allocPrintCmd(arena, argv, .{}) catch @panic("OOM"),
+ }),
+ .bad_exit_code => |code| process.fatal("the following command exited with code {d}:\n{s}", .{
+ code, std.zig.allocPrintCmd(arena, argv, .{}) catch @panic("OOM"),
+ }),
+ .crashed => process.fatal("the following command crashed:\n{s}", .{
+ std.zig.allocPrintCmd(arena, argv, .{}) catch @panic("OOM"),
+ }),
+ }
}
+/// Adds additional paths, equivalent to the `--search-prefix` arguments
+/// provided by the user. Paths added with this function have lower precedence
+/// than the ones specified by the user on the command line.
+///
+/// It is generally best practice to avoid calling this function, instead
+/// relying on the user to provide these paths via the standard build system
+/// interface. However, when integrating with other build systems, the user may
+/// have already provided the information to the other build system, and thus
+/// it is desirable to use that same information without requiring the user to
+/// provide it again.
pub fn addSearchPrefix(b: *Build, search_prefix: []const u8) void {
- b.search_prefixes.append(b.allocator, b.dupePath(search_prefix)) catch @panic("OOM");
+ if (b.isRoot()) {
+ const graph = b.graph;
+ const wc = &graph.wip_configuration;
+ const string = wc.addString(search_prefix) catch @panic("OOM");
+ wc.search_prefixes.append(wc.gpa, string) catch @panic("OOM");
+ }
}
-pub fn getInstallPath(b: *Build, dir: InstallDir, dest_rel_path: []const u8) []const u8 {
- assert(!fs.path.isAbsolute(dest_rel_path)); // Install paths must be relative to the prefix
- const base_dir = switch (dir) {
- .prefix => b.install_path,
- .bin => b.exe_dir,
- .lib => b.lib_dir,
- .header => b.h_dir,
- .custom => |p| b.pathJoin(&.{ b.install_path, p }),
- };
- return b.pathResolve(&.{ base_dir, dest_rel_path });
+pub fn isRoot(b: *const Build) bool {
+ return b.pkg_hash.len == 0;
}
pub const Dependency = struct {
@@ -1987,14 +2032,15 @@ fn findPkgHashOrFatal(b: *Build, name: []const u8) []const u8 {
for (b.available_deps) |dep| {
if (mem.eql(u8, dep[0], name)) return dep[1];
}
-
- const full_path = b.pathFromRoot("build.zig.zon");
- std.debug.panic("no dependency named '{s}' in '{s}'. All packages used in build.zig must be declared in this file", .{ name, full_path });
+ std.log.info("all dependencies used by build.zig must be declared in corresponding build.zig.zon", .{});
+ if (b.pkg_hash.len == 0) panic("no dependency named {s}", .{name});
+ panic("no dependency named {s} in {s} ({s})", .{ name, b.dep_prefix, b.pkg_hash });
}
inline fn findImportPkgHashOrFatal(b: *Build, comptime asking_build_zig: type, comptime dep_name: []const u8) []const u8 {
const build_runner = @import("root");
const deps = build_runner.dependencies;
+ const arena = b.graph.arena;
const b_pkg_hash, const b_pkg_deps = comptime for (@typeInfo(deps.packages).@"struct".decls) |decl| {
const pkg_hash = decl.name;
@@ -2002,14 +2048,19 @@ inline fn findImportPkgHashOrFatal(b: *Build, comptime asking_build_zig: type, c
if (@hasDecl(pkg, "build_zig") and pkg.build_zig == asking_build_zig) break .{ pkg_hash, pkg.deps };
} else .{ "", deps.root_deps };
if (!std.mem.eql(u8, b_pkg_hash, b.pkg_hash)) {
- std.debug.panic("'{}' is not the struct that corresponds to '{s}'", .{ asking_build_zig, b.pathFromRoot("build.zig") });
+ const build_zig_path = b.root.join(arena, "build.zig") catch @panic("OOM");
+ panic("{} is not the struct that corresponds to {f}", .{
+ asking_build_zig, build_zig_path,
+ });
}
comptime for (b_pkg_deps) |dep| {
if (std.mem.eql(u8, dep[0], dep_name)) return dep[1];
};
- const full_path = b.pathFromRoot("build.zig.zon");
- std.debug.panic("no dependency named '{s}' in '{s}'. All packages used in build.zig must be declared in this file", .{ dep_name, full_path });
+ const full_path = b.root.join(arena, "build.zig.zon") catch @panic("OOM");
+ panic("no dependency named {s} in {f}. All packages used in build.zig must be declared in this file", .{
+ dep_name, full_path,
+ });
}
fn markNeededLazyDep(b: *Build, pkg_hash: []const u8) void {
@@ -2026,6 +2077,7 @@ fn markNeededLazyDep(b: *Build, pkg_hash: []const u8) void {
/// In other words, if this function returns `null` it means that the only
/// purpose of completing the configure phase is to find out all the other lazy
/// dependencies that are also required.
+///
/// It is allowed to use this function for non-lazy dependencies, in which case
/// it will never return `null`. This allows toggling laziness via
/// build.zig.zon without changing build.zig logic.
@@ -2058,7 +2110,7 @@ pub fn dependency(b: *Build, name: []const u8, args: anytype) *Dependency {
if (mem.eql(u8, decl.name, pkg_hash)) {
const pkg = @field(deps.packages, decl.name);
if (@hasDecl(pkg, "available")) {
- std.debug.panic("dependency '{s}{s}' is marked as lazy in build.zig.zon which means it must use the lazyDependency function instead", .{ b.dep_prefix, name });
+ panic("dependency '{s}{s}' is marked as lazy in build.zig.zon which means it must use the lazyDependency function instead", .{ b.dep_prefix, name });
}
return dependencyInner(b, name, pkg.build_root, if (@hasDecl(pkg, "build_zig")) pkg.build_zig else null, pkg_hash, pkg.deps, args);
}
@@ -2111,6 +2163,8 @@ pub fn dependencyFromBuildZig(
) *Dependency {
const build_runner = @import("root");
const deps = build_runner.dependencies;
+ const graph = b.graph;
+ const arena = graph.arena;
find_dep: {
const pkg, const pkg_hash = inline for (@typeInfo(deps.packages).@"struct".decls) |decl| {
@@ -2124,8 +2178,8 @@ pub fn dependencyFromBuildZig(
return dependencyInner(b, dep_name, pkg.build_root, pkg.build_zig, pkg_hash, pkg.deps, args);
}
- const full_path = b.pathFromRoot("build.zig.zon");
- std.debug.panic("'{}' is not a build.zig struct of a dependency in '{s}'", .{ build_zig, full_path });
+ const full_path = b.root.join(arena, "build.zig.zon") catch @panic("OOM");
+ panic("{} is not a build.zig struct of a dependency in {f}", .{ build_zig, full_path });
}
fn userValuesAreSame(lhs: UserValue, rhs: UserValue) bool {
@@ -2188,10 +2242,10 @@ fn userLazyPathsAreTheSame(lhs_lp: LazyPath, rhs_lp: LazyPath) bool {
if (lhs_sp.owner != rhs_sp.owner) return false;
if (std.mem.eql(u8, lhs_sp.sub_path, rhs_sp.sub_path)) return false;
},
- .generated => |lhs_gen| {
- const rhs_gen = rhs_lp.generated;
+ .generated => |*lhs_gen| {
+ const rhs_gen = &rhs_lp.generated;
- if (lhs_gen.file != rhs_gen.file) return false;
+ if (lhs_gen.index != rhs_gen.index) return false;
if (lhs_gen.up != rhs_gen.up) return false;
if (std.mem.eql(u8, lhs_gen.sub_path, rhs_gen.sub_path)) return false;
},
@@ -2200,6 +2254,7 @@ fn userLazyPathsAreTheSame(lhs_lp: LazyPath, rhs_lp: LazyPath) bool {
if (!std.mem.eql(u8, lhs_rel_path, rhs_rel_path)) return false;
},
+ .relative => |lhs| return lhs.eql(rhs_lp.relative),
.dependency => |lhs_dep| {
const rhs_dep = rhs_lp.dependency;
@@ -2219,25 +2274,25 @@ fn dependencyInner(
pkg_deps: AvailableDeps,
args: anytype,
) *Dependency {
- const io = b.graph.io;
- const user_input_options = userInputOptionsFromArgs(b.allocator, args);
- if (b.graph.dependency_cache.getContext(.{
+ const graph = b.graph;
+ const io = graph.io;
+ const arena = graph.arena;
+ const user_input_options = userInputOptionsFromArgs(arena, args);
+ if (graph.dependency_cache.getContext(.{
.build_root_string = build_root_string,
.user_input_options = user_input_options,
- }, .{ .allocator = b.graph.arena })) |dep|
- return dep;
-
- const build_root: std.Build.Cache.Directory = .{
- .path = build_root_string,
- .handle = Io.Dir.cwd().openDir(io, build_root_string, .{}) catch |err| {
- std.debug.print("unable to open '{s}': {s}\n", .{
- build_root_string, @errorName(err),
- });
- process.exit(1);
+ }, .{ .allocator = arena })) |dep| return dep;
+
+ const dep_root: Cache.Path = .{
+ .root_dir = .{
+ .path = build_root_string,
+ .handle = Io.Dir.cwd().openDir(io, build_root_string, .{}) catch |err|
+ process.fatal("unable to open {s}: {t}", .{ build_root_string, err }),
},
};
- const sub_builder = b.createChild(name, build_root, pkg_hash, pkg_deps, user_input_options) catch @panic("unhandled error");
+ const sub_builder = b.createChild(name, dep_root, pkg_hash, pkg_deps, user_input_options) catch
+ @panic("unhandled error");
if (build_zig) |bz| {
sub_builder.runBuild(bz) catch @panic("unhandled error");
@@ -2246,13 +2301,13 @@ fn dependencyInner(
}
}
- const dep = b.allocator.create(Dependency) catch @panic("OOM");
+ const dep = graph.create(Dependency);
dep.* = .{ .builder = sub_builder };
- b.graph.dependency_cache.putContext(b.graph.arena, .{
+ graph.dependency_cache.putContext(arena, .{
.build_root_string = build_root_string,
.user_input_options = user_input_options,
- }, dep, .{ .allocator = b.graph.arena }) catch @panic("OOM");
+ }, dep, .{ .allocator = arena }) catch @panic("OOM");
return dep;
}
@@ -2264,41 +2319,6 @@ pub fn runBuild(b: *Build, build_zig: anytype) anyerror!void {
}
}
-/// A file that is generated by a build step.
-/// This struct is an interface that is meant to be used with `@fieldParentPtr` to implement the actual path logic.
-pub const GeneratedFile = struct {
- /// The step that generates the file.
- step: *Step,
- /// The path to the generated file. Must be either absolute or relative to the build runner cwd.
- /// This value must be set in the `fn make()` of the `step` and must not be `null` afterwards.
- path: ?[]const u8 = null,
-
- /// Deprecated, see `getPath3`.
- pub fn getPath(gen: GeneratedFile) []const u8 {
- return gen.step.owner.pathFromCwd(gen.path orelse std.debug.panic(
- "getPath() was called on a GeneratedFile that wasn't built yet. Is there a missing Step dependency on step '{s}'?",
- .{gen.step.name},
- ));
- }
-
- /// Deprecated, see `getPath3`.
- pub fn getPath2(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) []const u8 {
- return getPath3(gen, src_builder, asking_step) catch |err| switch (err) {
- error.Canceled => std.process.exit(1),
- };
- }
-
- pub fn getPath3(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) Io.Cancelable![]const u8 {
- return gen.path orelse {
- const graph = gen.step.owner.graph;
- const io = graph.io;
- const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
- dumpBadGetPathHelp(gen.step, stderr.terminal(), src_builder, asking_step) catch {};
- @panic("misconfigured build script");
- };
- }
-};
-
// dirnameAllowEmpty is a variant of fs.path.dirname
// that allows "" to refer to the root for relative paths.
//
@@ -2338,7 +2358,7 @@ pub const LazyPath = union(enum) {
},
generated: struct {
- file: *const GeneratedFile,
+ index: Configuration.GeneratedFileIndex,
/// The number of parent directories to go up.
/// 0 means the generated file itself.
@@ -2350,14 +2370,7 @@ pub const LazyPath = union(enum) {
sub_path: []const u8 = "",
},
- /// An absolute path or a path relative to the current working directory of
- /// the build runner process.
- ///
- /// This is uncommon but used for system environment paths such as `--zig-lib-dir` which
- /// ignore the file system path of build.zig and instead are relative to the directory from
- /// which `zig build` was invoked.
- ///
- /// Use of this tag indicates a dependency on the host system.
+ /// Deprecated; call `Graph.cwdRelativePath` instead.
cwd_relative: []const u8,
dependency: struct {
@@ -2365,13 +2378,30 @@ pub const LazyPath = union(enum) {
sub_path: []const u8,
},
+ relative: struct {
+ base: Configuration.Path.Base,
+ sub_path: []const u8 = "",
+
+ pub fn eql(a: @This(), b: @This()) bool {
+ return a.base == b.base and mem.eql(u8, a.sub_path, b.sub_path);
+ }
+ },
+
+ /// Path to the Zig executable being used to execute "zig build".
+ pub const zig_exe: LazyPath = .{ .relative = .{ .base = .zig_exe } };
+ /// Path to the "lib/" directory from the Zig installation being used to
+ /// execute "zig build".
+ pub const zig_lib: LazyPath = .{ .relative = .{ .base = .zig_lib } };
+ /// Path to the project's local cache directory (usually called ".zig-cache").
+ pub const cache_root: LazyPath = .{ .relative = .{ .base = .local_cache } };
+
/// Returns a lazy path referring to the directory containing this path.
///
- /// The dirname is not allowed to escape the logical root for underlying path.
- /// For example, if the path is relative to the build root,
- /// the dirname is not allowed to traverse outside of the build root.
- /// Similarly, if the path is a generated file inside zig-cache,
- /// the dirname is not allowed to traverse outside of zig-cache.
+ /// The dirname is not allowed to escape the logical root for underlying
+ /// path. For example, if the path is relative to the build root, the
+ /// dirname is not allowed to traverse outside of the build root.
+ /// Similarly, if the path is a generated file inside zig-cache, the
+ /// dirname is not allowed to traverse outside of zig-cache.
pub fn dirname(lazy_path: LazyPath) LazyPath {
return switch (lazy_path) {
.src_path => |sp| .{ .src_path = .{
@@ -2382,11 +2412,11 @@ pub const LazyPath = union(enum) {
},
} },
.generated => |generated| .{ .generated = if (dirnameAllowEmpty(generated.sub_path)) |sub_dirname| .{
- .file = generated.file,
+ .index = generated.index,
.up = generated.up,
.sub_path = sub_dirname,
} else .{
- .file = generated.file,
+ .index = generated.index,
.up = generated.up + 1,
.sub_path = "",
} },
@@ -2413,6 +2443,13 @@ pub const LazyPath = union(enum) {
}
},
},
+ .relative => |r| .{ .relative = .{
+ .base = r.base,
+ .sub_path = dirnameAllowEmpty(r.sub_path) orelse {
+ dumpBadDirnameHelp(null, null, "dirname() attempted to traverse outside the base path\n", .{}) catch {};
+ @panic("misconfigured build script");
+ },
+ } },
.dependency => |dep| .{ .dependency = .{
.dependency = dep.dependency,
.sub_path = dirnameAllowEmpty(dep.sub_path) orelse {
@@ -2427,7 +2464,9 @@ pub const LazyPath = union(enum) {
}
pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {
- return lazy_path.join(b.allocator, sub_path) catch @panic("OOM");
+ const graph = b.graph;
+ const arena = graph.arena;
+ return lazy_path.join(arena, sub_path) catch @panic("OOM");
}
pub fn join(lazy_path: LazyPath, arena: Allocator, sub_path: []const u8) Allocator.Error!LazyPath {
@@ -2437,13 +2476,17 @@ pub const LazyPath = union(enum) {
.sub_path = try fs.path.resolve(arena, &.{ src.sub_path, sub_path }),
} },
.generated => |gen| .{ .generated = .{
- .file = gen.file,
+ .index = gen.index,
.up = gen.up,
.sub_path = try fs.path.resolve(arena, &.{ gen.sub_path, sub_path }),
} },
.cwd_relative => |cwd_relative| .{
.cwd_relative = try fs.path.resolve(arena, &.{ cwd_relative, sub_path }),
},
+ .relative => |r| .{ .relative = .{
+ .base = r.base,
+ .sub_path = try fs.path.resolve(arena, &.{ r.sub_path, sub_path }),
+ } },
.dependency => |dep| .{ .dependency = .{
.dependency = dep.dependency,
.sub_path = try fs.path.resolve(arena, &.{ dep.sub_path, sub_path }),
@@ -2451,148 +2494,59 @@ pub const LazyPath = union(enum) {
};
}
- /// Returns a string that can be shown to represent the file source.
- /// Either returns the path, `"generated"`, or `"dependency"`.
+ /// Deprecated, use `format` instead.
pub fn getDisplayName(lazy_path: LazyPath) []const u8 {
return switch (lazy_path) {
.src_path => |sp| sp.sub_path,
.cwd_relative => |p| p,
.generated => "generated",
.dependency => "dependency",
+ .relative => |r| @tagName(r.base),
};
}
- /// Adds dependencies this file source implies to the given step.
- pub fn addStepDependencies(lazy_path: LazyPath, other_step: *Step) void {
- switch (lazy_path) {
- .src_path, .cwd_relative, .dependency => {},
- .generated => |gen| other_step.dependOn(gen.file.step),
+ pub fn format(lp: LazyPath, w: *Io.Writer) Io.Writer.Error!void {
+ switch (lp) {
+ .src_path => |sp| try w.writeAll(sp.sub_path),
+ .cwd_relative => |p| try w.writeAll(p),
+ .generated => try w.writeAll("generated"),
+ .dependency => try w.writeAll("dependency"),
+ .relative => |r| try w.print("{t} {s}", .{ r.base, r.sub_path }),
}
}
- /// Deprecated, see `getPath4`.
- pub fn getPath(lazy_path: LazyPath, src_builder: *Build) []const u8 {
- return getPath2(lazy_path, src_builder, null);
- }
-
- /// Deprecated, see `getPath4`.
- pub fn getPath2(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) []const u8 {
- const p = getPath3(lazy_path, src_builder, asking_step);
- return src_builder.pathResolve(&.{ p.root_dir.path orelse ".", p.sub_path });
- }
-
- /// Deprecated, see `getPath4`.
- pub fn getPath3(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) Cache.Path {
- return getPath4(lazy_path, src_builder, asking_step) catch |err| switch (err) {
- error.Canceled => std.process.exit(1),
- };
- }
-
- /// Intended to be used during the make phase only.
- ///
- /// `asking_step` is only used for debugging purposes; it's the step being
- /// run that is asking for the path.
- pub fn getPath4(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) Io.Cancelable!Cache.Path {
+ /// Adds dependencies this file source implies to the given step.
+ pub fn addStepDependencies(lazy_path: LazyPath, other_step: *Step) void {
switch (lazy_path) {
- .src_path => |sp| return .{
- .root_dir = sp.owner.build_root,
- .sub_path = sp.sub_path,
- },
- .cwd_relative => |sub_path| return .{
- .root_dir = Cache.Directory.cwd(),
- .sub_path = sub_path,
- },
+ .src_path, .cwd_relative, .relative, .dependency => {},
.generated => |gen| {
- // TODO make gen.file.path not be absolute and use that as the
- // basis for not traversing up too many directories.
-
- const graph = src_builder.graph;
-
- var file_path: Cache.Path = .{
- .root_dir = Cache.Directory.cwd(),
- .sub_path = gen.file.path orelse {
- const io = graph.io;
- const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
- dumpBadGetPathHelp(gen.file.step, stderr.terminal(), src_builder, asking_step) catch {};
- io.unlockStderr();
- @panic("misconfigured build script");
- },
- };
-
- if (gen.up > 0) {
- const cache_root_path = src_builder.cache_root.path orelse
- (src_builder.cache_root.join(src_builder.allocator, &.{"."}) catch @panic("OOM"));
-
- for (0..gen.up) |_| {
- if (mem.eql(u8, file_path.sub_path, cache_root_path)) {
- // If we hit the cache root and there's still more to go,
- // the script attempted to go too far.
- dumpBadDirnameHelp(gen.file.step, asking_step,
- \\dirname() attempted to traverse outside the cache root.
- \\This is not allowed.
- \\
- , .{}) catch {};
- @panic("misconfigured build script");
- }
-
- // path is absolute.
- // dirname will return null only if we're at root.
- // Typically, we'll stop well before that at the cache root.
- file_path.sub_path = fs.path.dirname(file_path.sub_path) orelse {
- dumpBadDirnameHelp(gen.file.step, asking_step,
- \\dirname() reached root.
- \\No more directories left to go up.
- \\
- , .{}) catch {};
- @panic("misconfigured build script");
- };
- }
- }
-
- return file_path.join(src_builder.allocator, gen.sub_path) catch @panic("OOM");
- },
- .dependency => |dep| return .{
- .root_dir = dep.dependency.builder.build_root,
- .sub_path = dep.sub_path,
+ const graph = other_step.owner.graph;
+ const generated_owner_step = graph.generated_files.items[@intFromEnum(gen.index)];
+ other_step.dependOn(generated_owner_step);
},
}
}
- pub fn basename(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) []const u8 {
- return fs.path.basename(switch (lazy_path) {
- .src_path => |sp| sp.sub_path,
- .cwd_relative => |sub_path| sub_path,
- .generated => |gen| if (gen.sub_path.len > 0)
- gen.sub_path
- else
- gen.file.getPath2(src_builder, asking_step),
- .dependency => |dep| dep.sub_path,
- });
- }
-
/// Copies the internal strings.
///
- /// The `b` parameter is only used for its allocator. All *Build instances
- /// share the same allocator.
- pub fn dupe(lazy_path: LazyPath, b: *Build) LazyPath {
- return lazy_path.dupeInner(b.allocator);
+ /// The `graph` parameter is only used for the global arena allocator.
+ pub fn dupe(lazy_path: LazyPath, graph: *const Graph) LazyPath {
+ return dupeInner(lazy_path, graph.arena);
}
- fn dupeInner(lazy_path: LazyPath, allocator: std.mem.Allocator) LazyPath {
+ fn dupeInner(lazy_path: LazyPath, arena: Allocator) LazyPath {
return switch (lazy_path) {
- .src_path => |sp| .{ .src_path = .{
- .owner = sp.owner,
- .sub_path = sp.owner.dupePath(sp.sub_path),
- } },
- .cwd_relative => |p| .{ .cwd_relative = dupePathInner(allocator, p) },
+ .src_path => |sp| .{ .src_path = .{ .owner = sp.owner, .sub_path = sp.owner.dupePath(sp.sub_path) } },
+ .cwd_relative => |p| .{ .cwd_relative = Graph.dupePathInner(arena, p) },
+ .relative => |r| .{ .relative = r },
.generated => |gen| .{ .generated = .{
- .file = gen.file,
+ .index = gen.index,
.up = gen.up,
- .sub_path = dupePathInner(allocator, gen.sub_path),
+ .sub_path = Graph.dupePathInner(arena, gen.sub_path),
} },
.dependency => |dep| .{ .dependency = .{
.dependency = dep.dependency,
- .sub_path = dupePathInner(allocator, dep.sub_path),
+ .sub_path = Graph.dupePathInner(arena, dep.sub_path),
} },
};
}
@@ -2631,36 +2585,6 @@ fn dumpBadDirnameHelp(
stderr.setColor(.reset) catch {};
}
-/// In this function the stderr mutex has already been locked.
-pub fn dumpBadGetPathHelp(s: *Step, t: Io.Terminal, src_builder: *Build, asking_step: ?*Step) anyerror!void {
- const w = t.writer;
- try w.print(
- \\getPath() was called on a GeneratedFile that wasn't built yet.
- \\ source package path: {s}
- \\ Is there a missing Step dependency on step '{s}'?
- \\
- , .{
- src_builder.build_root.path orelse ".",
- s.name,
- });
-
- t.setColor(.red) catch {};
- try w.writeAll(" The step was created by this stack trace:\n");
- t.setColor(.reset) catch {};
-
- s.dump(t);
- if (asking_step) |as| {
- t.setColor(.red) catch {};
- try w.print(" The step '{s}' that is missing a dependency on the above step was created by this stack trace:\n", .{as.name});
- t.setColor(.reset) catch {};
-
- as.dump(t);
- }
- t.setColor(.red) catch {};
- try w.writeAll(" Proceeding to panic.\n");
- t.setColor(.reset) catch {};
-}
-
pub const InstallDir = union(enum) {
prefix: void,
lib: void,
@@ -2670,18 +2594,18 @@ pub const InstallDir = union(enum) {
custom: []const u8,
/// Duplicates the install directory including the path if set to custom.
- pub fn dupe(dir: InstallDir, builder: *Build) InstallDir {
+ pub fn dupe(dir: InstallDir, graph: *const Graph) InstallDir {
if (dir == .custom) {
- return .{ .custom = builder.dupe(dir.custom) };
+ return .{ .custom = graph.dupeString(dir.custom) };
} else {
return dir;
}
}
};
-/// Creates a path leading to a directory inside "tmp" subdirectory of
-/// `cache_root` which is created on demand and cleaned up by the build runner
-/// upon success.
+/// Creates a path leading to a directory inside "tmp" subdirectory of local
+/// cache which is created on demand and cleaned up by the build runner upon
+/// success.
pub fn tmpPath(b: *Build) LazyPath {
const wf = b.addTempFiles();
return wf.getDirectory();
@@ -2725,7 +2649,9 @@ pub fn systemIntegrationOption(
name: []const u8,
config: SystemIntegrationOptionConfig,
) bool {
- const gop = b.graph.system_library_options.getOrPut(b.allocator, name) catch @panic("OOM");
+ const graph = b.graph;
+ const arena = graph.arena;
+ const gop = graph.system_integration_options.getOrPut(arena, name) catch @panic("OOM");
if (gop.found_existing) switch (gop.value_ptr.*) {
.user_disabled => {
gop.value_ptr.* = .declared_disabled;
@@ -2738,8 +2664,8 @@ pub fn systemIntegrationOption(
.declared_disabled => return false,
.declared_enabled => return true,
} else {
- gop.key_ptr.* = b.dupe(name);
- if (config.default orelse b.graph.system_package_mode) {
+ gop.key_ptr.* = graph.dupeString(name);
+ if (config.default orelse graph.system_package_mode) {
gop.value_ptr.* = .declared_enabled;
return true;
} else {
diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig
@@ -189,12 +189,15 @@ pub const File = struct {
pub const HashHelper = struct {
hasher: Hasher = hasher_init,
- /// Record a slice of bytes as a dependency of the process being cached.
pub fn addBytes(hh: *HashHelper, bytes: []const u8) void {
hh.hasher.update(mem.asBytes(&bytes.len));
hh.hasher.update(bytes);
}
+ pub fn addBytesZ(hh: *HashHelper, bytes: [:0]const u8) void {
+ hh.hasher.update(mem.absorbSentinel(bytes));
+ }
+
pub fn addOptionalBytes(hh: *HashHelper, optional_bytes: ?[]const u8) void {
hh.add(optional_bytes != null);
hh.addBytes(optional_bytes orelse return);
@@ -1024,6 +1027,12 @@ pub const Manifest = struct {
try self.populateFileHash(gop.key_ptr);
}
+ pub fn addPathPost(man: *Manifest, path: Path) !void {
+ _ = man;
+ _ = path;
+ @panic("TODO");
+ }
+
/// Like `addFilePost` but when the file contents have already been loaded from disk.
pub fn addFilePostContents(
self: *Manifest,
diff --git a/lib/std/Build/Cache/Path.zig b/lib/std/Build/Cache/Path.zig
@@ -24,7 +24,7 @@ pub fn cwd() Path {
}
pub fn initCwd(sub_path: []const u8) Path {
- return .{ .root_dir = Cache.Directory.cwd(), .sub_path = sub_path };
+ return .{ .root_dir = .cwd(), .sub_path = sub_path };
}
pub fn join(p: Path, arena: Allocator, sub_path: []const u8) Allocator.Error!Path {
@@ -213,6 +213,13 @@ pub fn stem(p: Path) []const u8 {
return fs.path.stem(p.sub_path);
}
+pub fn dirname(p: Path) ?Path {
+ return .{
+ .root_dir = p.root_dir,
+ .sub_path = fs.path.dirname(p.subPathOpt() orelse return null) orelse "",
+ };
+}
+
pub fn basename(p: Path) []const u8 {
return fs.path.basename(p.sub_path);
}
diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig
@@ -0,0 +1,3436 @@
+const Configuration = @This();
+
+const std = @import("../std.zig");
+const Io = std.Io;
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const max_u32 = std.math.maxInt(u32);
+
+string_bytes: []u8,
+steps: []Step,
+path_deps_base: []Path.Base,
+path_deps_sub: []String,
+unlazy_deps: []String,
+system_integrations: []SystemIntegration,
+available_options: []AvailableOption,
+search_prefixes: []String,
+extra: []u32,
+default_step: Step.Index,
+generated_files_len: u32,
+poisoned: bool,
+
+/// The field order here matches `Configuration` which documents the order in
+/// the serialized format.
+pub const Header = extern struct {
+ string_bytes_len: u32,
+ steps_len: u32,
+ path_deps_len: u32,
+ unlazy_deps_len: u32,
+ system_integrations_len: u32,
+ available_options_len: u32,
+ search_prefixes_len: u32,
+ extra_len: u32,
+
+ default_step: Step.Index,
+ /// There is not actually any data stored for this - it just provides a way
+ /// for maker process to preallocate an array for these.
+ generated_files_len: u32,
+ flags: Flags,
+
+ pub const Flags = packed struct(u32) {
+ poisoned: bool,
+ _: u31 = 0,
+ };
+};
+
+pub const Wip = struct {
+ gpa: Allocator,
+ string_table: StringTable = .empty,
+ /// De-duplicates an array inside `extra`.
+ dedupe_table: DedupeTable = .empty,
+ targets_table: TargetsTable = .empty,
+
+ string_bytes: std.ArrayList(u8) = .empty,
+ unlazy_deps: std.ArrayList(String) = .empty,
+ system_integrations: std.ArrayList(SystemIntegration) = .empty,
+ available_options: std.ArrayList(AvailableOption) = .empty,
+ steps: std.ArrayList(Step) = .empty,
+ path_deps: std.MultiArrayList(Path) = .empty,
+ search_prefixes: std.ArrayList(String) = .empty,
+ extra: std.ArrayList(u32) = .empty,
+ next_generated_file_index: u32 = 0,
+ cache_poison: bool = false,
+
+ const DedupeTable = std.HashMapUnmanaged(ExtraSlice, void, ExtraSlice.Context, std.hash_map.default_max_load_percentage);
+ const TargetsTable = std.HashMapUnmanaged(TargetQuery.Index, void, TargetsTableContext, std.hash_map.default_max_load_percentage);
+
+ const ExtraSlice = struct {
+ index: u32,
+ len: u32,
+
+ const Context = struct {
+ extra: []const u32,
+
+ pub fn eql(ctx: @This(), a: ExtraSlice, b: ExtraSlice) bool {
+ const slice_a = ctx.extra[a.index..][0..a.len];
+ const slice_b = ctx.extra[b.index..][0..b.len];
+ return std.mem.eql(u32, slice_a, slice_b);
+ }
+
+ pub fn hash(ctx: @This(), key: ExtraSlice) u64 {
+ const slice = ctx.extra[key.index..][0..key.len];
+ return std.hash_map.hashString(@ptrCast(slice));
+ }
+ };
+ };
+
+ const TargetsTableContext = struct {
+ extra: []const u32,
+
+ pub fn eql(ctx: @This(), a: TargetQuery.Index, b: TargetQuery.Index) bool {
+ const slice_a = a.extraSlice(ctx.extra);
+ const slice_b = b.extraSlice(ctx.extra);
+ return std.mem.eql(u32, slice_a, slice_b);
+ }
+
+ pub fn hash(ctx: @This(), key: TargetQuery.Index) u64 {
+ const slice = key.extraSlice(ctx.extra);
+ return std.hash_map.hashString(@ptrCast(slice));
+ }
+ };
+
+ const StringTable = std.HashMapUnmanaged(String, void, StringTableContext, std.hash_map.default_max_load_percentage);
+ const StringTableContext = struct {
+ bytes: []const u8,
+
+ pub fn eql(_: @This(), a: String, b: String) bool {
+ return a == b;
+ }
+
+ pub fn hash(ctx: @This(), key: String) u64 {
+ return std.hash_map.hashString(std.mem.sliceTo(ctx.bytes[@intFromEnum(key)..], 0));
+ }
+ };
+
+ const StringTableIndexAdapter = struct {
+ bytes: []const u8,
+
+ pub fn eql(ctx: @This(), a: []const u8, b: String) bool {
+ return std.mem.eql(u8, a, std.mem.sliceTo(ctx.bytes[@intFromEnum(b)..], 0));
+ }
+
+ pub fn hash(_: @This(), adapted_key: []const u8) u64 {
+ assert(std.mem.indexOfScalar(u8, adapted_key, 0) == null);
+ return std.hash_map.hashString(adapted_key);
+ }
+ };
+
+ pub fn init(gpa: Allocator) Wip {
+ return .{ .gpa = gpa };
+ }
+
+ pub fn deinit(wip: *Wip) void {
+ const gpa = wip.gpa;
+ wip.string_bytes.deinit(gpa);
+ wip.unlazy_deps.deinit(gpa);
+ wip.system_integrations.deinit(gpa);
+ wip.available_options.deinit(gpa);
+ wip.steps.deinit(gpa);
+ wip.path_deps.deinit(gpa);
+ wip.search_prefixes.deinit(gpa);
+ wip.extra.deinit(gpa);
+ wip.* = undefined;
+ }
+
+ pub const Static = struct {
+ default_step: Step.Index,
+ generated_files_len: u32,
+ poisoned: bool,
+ };
+
+ pub fn write(wip: *Wip, w: *Io.Writer, static: Static) Io.Writer.Error!void {
+ const header: Header = .{
+ .string_bytes_len = @intCast(wip.string_bytes.items.len),
+ .steps_len = @intCast(wip.steps.items.len),
+ .path_deps_len = @intCast(wip.path_deps.len),
+ .unlazy_deps_len = @intCast(wip.unlazy_deps.items.len),
+ .system_integrations_len = @intCast(wip.system_integrations.items.len),
+ .available_options_len = @intCast(wip.available_options.items.len),
+ .search_prefixes_len = @intCast(wip.search_prefixes.items.len),
+ .extra_len = @intCast(wip.extra.items.len),
+
+ .default_step = static.default_step,
+ .generated_files_len = static.generated_files_len,
+ .flags = .{
+ .poisoned = static.poisoned,
+ },
+ };
+ var buffers = [_][]const u8{
+ @ptrCast(&header),
+ wip.string_bytes.items,
+ @ptrCast(wip.steps.items),
+ @ptrCast(wip.path_deps.items(.base)),
+ @ptrCast(wip.path_deps.items(.sub)),
+ @ptrCast(wip.unlazy_deps.items),
+ @ptrCast(wip.system_integrations.items),
+ @ptrCast(wip.available_options.items),
+ @ptrCast(wip.search_prefixes.items),
+ @ptrCast(wip.extra.items),
+ };
+ try w.writeVecAll(&buffers);
+ }
+
+ pub fn addString(wip: *Wip, bytes: []const u8) Allocator.Error!String {
+ const gpa = wip.gpa;
+ assert(std.mem.indexOfScalar(u8, bytes, 0) == null);
+ const gop = try wip.string_table.getOrPutContextAdapted(
+ gpa,
+ @as([]const u8, bytes),
+ @as(StringTableIndexAdapter, .{ .bytes = wip.string_bytes.items }),
+ @as(StringTableContext, .{ .bytes = wip.string_bytes.items }),
+ );
+ if (gop.found_existing) return gop.key_ptr.*;
+
+ try wip.string_bytes.ensureUnusedCapacity(gpa, bytes.len + 1);
+ const new_off: String = @enumFromInt(wip.string_bytes.items.len);
+
+ wip.string_bytes.appendSliceAssumeCapacity(bytes);
+ wip.string_bytes.appendAssumeCapacity(0);
+
+ gop.key_ptr.* = new_off;
+
+ return new_off;
+ }
+
+ pub fn addOptionalString(wip: *Wip, bytes: ?[]const u8) Allocator.Error!OptionalString {
+ return .init(try addString(wip, bytes orelse return .none));
+ }
+
+ pub fn addStringList(wip: *Wip, list: []const []const u8) Allocator.Error!StringList {
+ // Increase size of extra to support the list. Add the string list
+ // there. Then check for duplicate, reverting list if already found.
+ const gpa = wip.gpa;
+ const revert_index: u32 = @intCast(wip.extra.items.len);
+ const added = try wip.extra.addManyAsSlice(gpa, list.len + 1);
+ added[0] = @intCast(list.len);
+ for (added[1..], list) |*d, s| d.* = @intFromEnum(try addString(wip, s));
+ const gop = try wip.dedupe_table.getOrPutContext(gpa, .{
+ .index = revert_index,
+ .len = @intCast(added.len),
+ }, @as(ExtraSlice.Context, .{ .extra = wip.extra.items }));
+
+ if (gop.found_existing) {
+ wip.extra.items.len = revert_index;
+ return @enumFromInt(gop.key_ptr.index);
+ }
+
+ return @enumFromInt(revert_index);
+ }
+
+ pub fn addBytes(wip: *Wip, bytes: []const u8) Allocator.Error!Bytes {
+ try wip.string_bytes.appendSlice(wip.gpa, bytes);
+ return .{
+ .index = @intCast(wip.string_bytes.items.len - bytes.len),
+ .len = @intCast(bytes.len),
+ };
+ }
+
+ pub fn addSemVer(wip: *Wip, sv: std.SemanticVersion) Allocator.Error!String {
+ var buffer: [256]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buffer);
+ sv.format(&writer) catch return error.OutOfMemory;
+ return addString(wip, writer.buffered());
+ }
+
+ pub fn addTargetQuery(wip: *Wip, q: *const std.Target.Query) !TargetQuery.OptionalIndex {
+ if (q.isNative()) return .none;
+ const gpa = wip.gpa;
+ const cpu_name: ?String = switch (q.cpu_model) {
+ .native, .baseline, .determined_by_arch_os => null,
+ .explicit => |model| try wip.addString(model.name),
+ };
+ const os_version_min: TargetQuery.OsVersion = if (q.os_version_min) |ver| switch (ver) {
+ .none => .none,
+ .semver => |sem_ver| .{ .semver = try wip.addSemVer(sem_ver) },
+ .windows => |win_ver| .{ .windows = win_ver },
+ } else .default;
+ const os_version_max: TargetQuery.OsVersion = if (q.os_version_max) |ver| switch (ver) {
+ .none => .none,
+ .semver => |sem_ver| .{ .semver = try wip.addSemVer(sem_ver) },
+ .windows => |win_ver| .{ .windows = win_ver },
+ } else .default;
+ const glibc_version: ?String = if (q.glibc_version) |sem_ver| try wip.addSemVer(sem_ver) else null;
+ const dynamic_linker: ?String = if (q.dynamic_linker) |*dl|
+ if (dl.get()) |s| try wip.addString(s) else .empty
+ else
+ null;
+ const cpu_features_add_empty = q.cpu_features_add.isEmpty();
+ const cpu_features_sub_empty = q.cpu_features_sub.isEmpty();
+ const result_index: TargetQuery.Index = try wip.addExtra(TargetQuery, .{
+ .flags = .{
+ .cpu_arch = .init(q.cpu_arch),
+ .cpu_model = .init(q.cpu_model),
+ .cpu_features_add = !cpu_features_add_empty,
+ .cpu_features_sub = !cpu_features_sub_empty,
+ .os_tag = .init(q.os_tag),
+ .abi = .init(q.abi),
+ .object_format = .init(q.ofmt),
+ .os_version_min = os_version_min,
+ .os_version_max = os_version_max,
+ .glibc_version = glibc_version != null,
+ .android_api_level = q.android_api_level != null,
+ .dynamic_linker = dynamic_linker != null,
+ },
+ .cpu_features_add = .{ .value = if (cpu_features_add_empty) null else q.cpu_features_add },
+ .cpu_features_sub = .{ .value = if (cpu_features_sub_empty) null else q.cpu_features_sub },
+ .glibc_version = .{ .value = glibc_version },
+ .android_api_level = .{ .value = q.android_api_level },
+ .dynamic_linker = .{ .value = dynamic_linker },
+ .cpu_name = .{ .value = cpu_name },
+ .os_version_min = .{ .u = os_version_min },
+ .os_version_max = .{ .u = os_version_max },
+ });
+
+ // Deduplicate.
+ const gop = try wip.targets_table.getOrPutContext(gpa, result_index, @as(TargetsTableContext, .{
+ .extra = wip.extra.items,
+ }));
+ if (gop.found_existing) {
+ wip.extra.items.len = @intFromEnum(result_index);
+ return .init(gop.key_ptr.*);
+ } else {
+ return .init(result_index);
+ }
+ }
+
+ pub fn addTarget(wip: *Wip, t: std.Target) !TargetQuery.Index {
+ const gpa = wip.gpa;
+ const cpu_name: String = try wip.addString(t.cpu.model.name);
+
+ const os_version_min: TargetQuery.OsVersion, const os_version_max: TargetQuery.OsVersion, const glibc_version: ?String, const android_api_level: ?u32 = switch (t.os.versionRange()) {
+ .none => .{
+ .none,
+ .none,
+ null,
+ null,
+ },
+ .semver => |range| .{
+ .{ .semver = try wip.addSemVer(range.min) },
+ .{ .semver = try wip.addSemVer(range.max) },
+ null,
+ null,
+ },
+ .hurd => |hurd| .{
+ .{ .semver = try wip.addSemVer(hurd.range.min) },
+ .{ .semver = try wip.addSemVer(hurd.range.max) },
+ try wip.addSemVer(hurd.glibc),
+ null,
+ },
+ .linux => |linux| .{
+ .{ .semver = try wip.addSemVer(linux.range.min) },
+ .{ .semver = try wip.addSemVer(linux.range.max) },
+ try wip.addSemVer(linux.glibc),
+ linux.android,
+ },
+ .windows => |range| .{
+ .{ .windows = range.min },
+ .{ .windows = range.max },
+ null,
+ null,
+ },
+ };
+ const dynamic_linker: ?String = if (t.dynamic_linker.get()) |dl| try wip.addString(dl) else null;
+ const cpu_features_add_empty = t.cpu.features.isEmpty();
+ const result_index = try wip.addExtra(TargetQuery, .{
+ .flags = .{
+ .cpu_arch = .init(t.cpu.arch),
+ .cpu_model = .explicit,
+ .cpu_features_add = !cpu_features_add_empty,
+ .cpu_features_sub = false,
+ .os_tag = .init(t.os.tag),
+ .abi = .init(t.abi),
+ .object_format = .init(t.ofmt),
+ .os_version_min = os_version_min,
+ .os_version_max = os_version_max,
+ .glibc_version = glibc_version != null,
+ .android_api_level = android_api_level != null,
+ .dynamic_linker = dynamic_linker != null,
+ },
+ .cpu_features_add = .{ .value = if (cpu_features_add_empty) null else t.cpu.features },
+ .cpu_features_sub = .{ .value = null },
+ .glibc_version = .{ .value = glibc_version },
+ .android_api_level = .{ .value = android_api_level },
+ .dynamic_linker = .{ .value = dynamic_linker },
+ .cpu_name = .{ .value = cpu_name },
+ .os_version_min = .{ .u = os_version_min },
+ .os_version_max = .{ .u = os_version_max },
+ });
+
+ // Deduplicate.
+ const gop = try wip.targets_table.getOrPutContext(gpa, result_index, @as(TargetsTableContext, .{
+ .extra = wip.extra.items,
+ }));
+ if (gop.found_existing) {
+ wip.extra.items.len = @intFromEnum(result_index);
+ return gop.key_ptr.*;
+ } else {
+ return result_index;
+ }
+ }
+
+ pub fn addExtra(wip: *Wip, comptime T: type, v: T) Allocator.Error!T.Index {
+ const extra_len = Storage.extraLen(v);
+ try wip.extra.ensureUnusedCapacity(wip.gpa, extra_len);
+ return addExtraReserved(wip, T, v);
+ }
+
+ pub fn addExtraErased(wip: *Wip, comptime T: type, v: T) Allocator.Error!u32 {
+ const extra_len = Storage.extraLen(v);
+ try wip.extra.ensureUnusedCapacity(wip.gpa, extra_len);
+ return addExtraReservedErased(wip, T, v);
+ }
+
+ /// Same as `addExtra` but uses a hash map to possibly return an already
+ /// existing index instead of appending to `extra`.
+ pub fn addDeduped(wip: *Wip, comptime T: type, v: T) Allocator.Error!T.Index {
+ const gpa = wip.gpa;
+ const revert_index = wip.extra.items.len;
+ const upper_bound_len = Storage.extraLen(v);
+ try wip.extra.ensureUnusedCapacity(gpa, upper_bound_len);
+ try wip.dedupe_table.ensureUnusedCapacityContext(gpa, 1, @as(ExtraSlice.Context, .{
+ .extra = wip.extra.items,
+ }));
+ const new_index = addExtraReservedErased(wip, T, v);
+ const len: u32 = @intCast(wip.extra.items.len - new_index);
+ assert(len != 0);
+ const gop = wip.dedupe_table.getOrPutAssumeCapacityContext(.{
+ .index = new_index,
+ .len = len,
+ }, @as(ExtraSlice.Context, .{ .extra = wip.extra.items }));
+
+ if (gop.found_existing) {
+ wip.extra.items.len = revert_index;
+ return @enumFromInt(gop.key_ptr.index);
+ }
+
+ return @enumFromInt(new_index);
+ }
+
+ pub fn addExtraReserved(wip: *Wip, comptime T: type, v: T) T.Index {
+ return @enumFromInt(addExtraReservedErased(wip, T, v));
+ }
+
+ pub fn addExtraReservedErased(wip: *Wip, comptime T: type, v: T) u32 {
+ const result: u32 = @intCast(wip.extra.items.len);
+ wip.extra.items.len = Storage.setExtra(wip.extra.allocatedSlice(), result, v);
+ return result;
+ }
+
+ fn addExtraOptionalStringAssumeCapacity(wip: *Wip, optional_string: ?String) void {
+ const string = optional_string orelse return;
+ wip.extra.appendAssumeCapacity(@intFromEnum(string));
+ }
+
+ pub fn addGeneratedFile(wip: *Wip) GeneratedFileIndex {
+ defer wip.next_generated_file_index += 1;
+ return @enumFromInt(wip.next_generated_file_index);
+ }
+
+ /// Returned slice expires upon next append to the configuration.
+ pub fn stringSlice(wip: *const Wip, s: String) [:0]const u8 {
+ const start_slice = wip.string_bytes.items[@intFromEnum(s)..];
+ return start_slice[0..std.mem.indexOfScalar(u8, start_slice, 0).? :0];
+ }
+};
+
+pub const SystemIntegration = extern struct {
+ name: String,
+ status: Status,
+
+ pub const Status = enum(u32) {
+ disabled = 0,
+ enabled = 1,
+ };
+};
+
+pub const AvailableOption = extern struct {
+ name: String,
+ description: String,
+ type: Type,
+ /// If the `type_id` is `enum` or `enum_list` this provides the list of enum options
+ enum_options: OptionalStringList,
+
+ pub const Type = enum(u8) {
+ bool,
+ int,
+ float,
+ @"enum",
+ enum_list,
+ string,
+ list,
+ build_id,
+ lazy_path,
+ lazy_path_list,
+ };
+};
+
+pub const Step = extern struct {
+ name: String,
+ owner: Package.Index,
+ deps: Deps.Index,
+ max_rss: MaxRss,
+ extended: Storage.Extended(Flags, union(Tag) {
+ check_file: CheckFile,
+ compile: Compile,
+ config_header: ConfigHeader,
+ fail: Fail,
+ find_program: FindProgram,
+ fmt: Fmt,
+ install_artifact: InstallArtifact,
+ install_dir: InstallDir,
+ install_file: InstallFile,
+ obj_copy: ObjCopy,
+ options: Options,
+ run: Run,
+ top_level: TopLevel,
+ translate_c: TranslateC,
+ update_source_files: UpdateSourceFiles,
+ write_file: WriteFile,
+ }),
+
+ /// Points into `steps`.
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn ptr(i: Index, c: *const Configuration) *const Step {
+ return &c.steps[@intFromEnum(i)];
+ }
+ };
+
+ /// Shared by all steps.
+ pub const Flags = packed struct(u32) {
+ tag: Tag,
+ _: u27 = 0,
+ };
+
+ pub const Tag = enum(u5) {
+ check_file,
+ compile,
+ config_header,
+ fail,
+ find_program,
+ fmt,
+ install_artifact,
+ install_dir,
+ install_file,
+ obj_copy,
+ options,
+ run,
+ top_level,
+ translate_c,
+ update_source_files,
+ write_file,
+ };
+
+ pub const TopLevel = struct {
+ flags: @This().Flags = .{},
+ description: String,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .top_level,
+ _: u27 = 0,
+ };
+ };
+
+ /// The first dependency step index will be the compile step whose
+ /// artifacts are being installed with this step.
+ pub const InstallArtifact = struct {
+ flags: @This().Flags,
+ bin_dir: Storage.FlagOptional(.flags, .bin_dir, InstallDestDir),
+ implib_dir: Storage.FlagOptional(.flags, .implib_dir, InstallDestDir),
+ pdb_dir: Storage.FlagOptional(.flags, .pdb_dir, InstallDestDir),
+ h_dir: Storage.FlagOptional(.flags, .h_dir, InstallDestDir),
+ bin_sub_path: Storage.FlagOptional(.flags, .bin_sub_path, String),
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .install_artifact,
+ dylib_symlinks: bool,
+ bin_dir: bool,
+ implib_dir: bool,
+ pdb_dir: bool,
+ h_dir: bool,
+ bin_sub_path: bool,
+ _: u21 = 0,
+ };
+ };
+
+ pub const Run = struct {
+ flags: @This().Flags,
+ flags2: Flags2,
+ args: Storage.LengthPrefixedList(Arg.Index),
+ cwd: Storage.FlagOptional(.flags, .cwd, LazyPath.Index),
+ captured_stdout: Storage.FlagOptional(.flags, .captured_stdout, CapturedStream),
+ captured_stderr: Storage.FlagOptional(.flags, .captured_stderr, CapturedStream),
+ file_inputs: Storage.LengthPrefixedList(LazyPath.Index),
+ stdio_limit: Storage.FlagOptional(.flags, .stdio_limit, u64),
+ /// Always a compile step.
+ producer: Storage.FlagOptional(.flags, .producer, Step.Index),
+ /// First half is keys, second half is values.
+ environ_map: Storage.FlagOptional(.flags, .environ_map, EnvironMap.Index),
+ stdin: Storage.FlagUnion(.flags, .stdin, StdIn),
+ expect_stderr_exact: Storage.FlagOptional(.flags2, .expect_stderr_exact, Bytes),
+ expect_stdout_exact: Storage.FlagOptional(.flags2, .expect_stdout_exact, Bytes),
+ expect_stderr_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stderr_match, Bytes),
+ expect_stdout_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stdout_match, Bytes),
+ expect_term_value: Storage.FlagOptional(.flags2, .expect_term, u32),
+
+ pub const CapturedStream = extern struct {
+ generated_file: GeneratedFileIndex,
+ basename: String,
+ };
+
+ pub const Arg = struct {
+ flags: @This().Flags,
+ prefix: Storage.FlagOptional(.flags, .prefix, String),
+ suffix: Storage.FlagOptional(.flags, .suffix, String),
+ basename: Storage.FlagOptional(.flags, .basename, String),
+ path: Storage.FlagOptional(.flags, .path, LazyPath.Index),
+ /// Always a compile step.
+ producer: Storage.FlagOptional(.flags, .producer, Step.Index),
+ generated: Storage.FlagOptional(.flags, .generated, GeneratedFileIndex),
+
+ pub const Flags = packed struct(u32) {
+ tag: Arg.Tag,
+ prefix: bool,
+ suffix: bool,
+ basename: bool,
+ path: bool,
+ producer: bool,
+ generated: bool,
+ dep_file: bool,
+ _: u21 = 0,
+ };
+
+ pub const Tag = enum(u4) {
+ artifact,
+ /// `path` contains the file.
+ path_file,
+ path_directory,
+ /// `prefix` contains the string.
+ string,
+ file_content,
+ output_file,
+ output_directory,
+ passthru,
+ };
+
+ pub const Index = IndexType(@This());
+ };
+
+ pub const Color = enum(u4) {
+ /// `CLICOLOR_FORCE` is set, and `NO_COLOR` is unset.
+ enable,
+ /// `NO_COLOR` is set, and `CLICOLOR_FORCE` is unset.
+ disable,
+ /// If the build runner is using color, equivalent to `.enable`. Otherwise, equivalent to `.disable`.
+ inherit,
+ /// If stderr is captured or checked, equivalent to `.disable`. Otherwise, equivalent to `.inherit`.
+ auto,
+ /// The build runner does not modify the `CLICOLOR_FORCE` or `NO_COLOR` environment variables.
+ /// They are treated like normal variables, so can be controlled through `setEnvironmentVariable`.
+ manual,
+ };
+
+ pub const StdIn = union(@This().Tag) {
+ none: void,
+ bytes: Bytes,
+ lazy_path: LazyPath.Index,
+
+ pub const Tag = enum(u2) { none, bytes, lazy_path };
+ };
+ pub const TrimWhitespace = enum(u2) { none, all, leading, trailing };
+ pub const StdIo = enum(u2) { infer_from_args, inherit, check, zig_test };
+
+ pub const ExpectTermStatus = enum(u2) { exited, signal, stopped, unknown };
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .run,
+ disable_zig_progress: bool,
+ skip_foreign_checks: bool,
+ failing_to_execute_foreign_is_an_error: bool,
+ has_side_effects: bool,
+ test_runner_mode: bool,
+ color: Color,
+ stdin: StdIn.Tag,
+ stdio: StdIo,
+ stdout_trim_whitespace: TrimWhitespace,
+ stderr_trim_whitespace: TrimWhitespace,
+ stdio_limit: bool,
+ producer: bool,
+ cwd: bool,
+ captured_stdout: bool,
+ captured_stderr: bool,
+ environ_map: bool,
+ _: u4 = 0,
+ };
+
+ pub const Flags2 = packed struct(u32) {
+ expect_stderr_exact: bool,
+ expect_stdout_exact: bool,
+ expect_stderr_match: bool,
+ expect_stdout_match: bool,
+ expect_term: bool,
+ expect_term_status: ExpectTermStatus,
+ _: u25 = 0,
+ };
+ };
+
+ pub const Compile = struct {
+ flags: @This().Flags,
+ flags2: Flags2,
+ flags3: Flags3,
+ flags4: Flags4,
+
+ root_module: Module.Index,
+ root_name: String,
+
+ filters: Storage.FlagLengthPrefixedList(.flags, .filters_len, String),
+ exec_cmd_args: Storage.FlagLengthPrefixedList(.flags, .exec_cmd_args_len, OptionalString),
+ installed_headers: Storage.FlagLengthPrefixedList(.flags, .installed_headers_len, Storage.Extended(InstalledHeader.Flags, InstalledHeader)),
+ force_undefined_symbols: Storage.FlagLengthPrefixedList(.flags, .force_undefined_symbols_len, String),
+ expect_errors: Storage.FlagUnion(.flags4, .expect_errors, ExpectErrors),
+ linker_script: Storage.FlagOptional(.flags4, .linker_script, LazyPath.Index),
+ version_script: Storage.FlagOptional(.flags4, .version_script, LazyPath.Index),
+ zig_lib_dir: Storage.FlagOptional(.flags3, .zig_lib_dir, LazyPath.Index),
+ libc_file: Storage.FlagOptional(.flags4, .libc_file, LazyPath.Index),
+ win32_manifest: Storage.FlagOptional(.flags3, .win32_manifest, LazyPath.Index),
+ win32_module_definition: Storage.FlagOptional(.flags3, .win32_module_definition, LazyPath.Index),
+ entitlements: Storage.FlagOptional(.flags4, .entitlements, LazyPath.Index),
+ version: Storage.FlagOptional(.flags3, .version, String), // semantic version string
+ entry: Storage.EnumOptional(.flags3, .entry, .symbol_name, String),
+ install_name: Storage.FlagOptional(.flags4, .install_name, String),
+ initial_memory: Storage.FlagOptional(.flags3, .initial_memory, u64),
+ max_memory: Storage.FlagOptional(.flags3, .max_memory, u64),
+ global_base: Storage.FlagOptional(.flags3, .global_base, u64),
+ image_base: Storage.FlagOptional(.flags3, .image_base, u64),
+ link_z_common_page_size: Storage.FlagOptional(.flags4, .link_z_common_page_size, u64),
+ link_z_max_page_size: Storage.FlagOptional(.flags4, .link_z_max_page_size, u64),
+ pagezero_size: Storage.FlagOptional(.flags4, .pagezero_size, u64),
+ stack_size: Storage.FlagOptional(.flags4, .stack_size, u64),
+ headerpad_size: Storage.FlagOptional(.flags4, .headerpad_size, u32),
+ error_limit: Storage.FlagOptional(.flags4, .error_limit, u32),
+ build_id: Storage.EnumOptional(.flags3, .build_id, .hexstring, String),
+ test_runner: Storage.FlagUnion(.flags3, .test_runner, TestRunner),
+
+ emit_directory: Storage.FlagOptional(.flags4, .emit_directory, GeneratedFileIndex),
+ generated_docs: Storage.FlagOptional(.flags4, .generated_docs, GeneratedFileIndex),
+ generated_asm: Storage.FlagOptional(.flags4, .generated_asm, GeneratedFileIndex),
+ generated_bin: Storage.FlagOptional(.flags4, .generated_bin, GeneratedFileIndex),
+ generated_pdb: Storage.FlagOptional(.flags4, .generated_pdb, GeneratedFileIndex),
+ generated_implib: Storage.FlagOptional(.flags4, .generated_implib, GeneratedFileIndex),
+ generated_llvm_bc: Storage.FlagOptional(.flags4, .generated_llvm_bc, GeneratedFileIndex),
+ generated_llvm_ir: Storage.FlagOptional(.flags4, .generated_llvm_ir, GeneratedFileIndex),
+ generated_h: Storage.FlagOptional(.flags4, .generated_h, GeneratedFileIndex),
+
+ pub const InstalledHeader = union(@This().Tag) {
+ file: File,
+ directory: Directory,
+
+ pub const Flags = packed struct(u32) {
+ tag: InstalledHeader.Tag,
+ _: u24 = 0,
+ };
+
+ pub const Tag = enum(u8) {
+ file,
+ directory,
+ };
+
+ pub const File = struct {
+ flags: @This().Flags = .{},
+ source: LazyPath.Index,
+ dest_sub_path: String,
+
+ pub const Flags = packed struct(u32) {
+ tag: InstalledHeader.Tag = .file,
+ _: u24 = 0,
+ };
+ };
+
+ pub const Directory = struct {
+ flags: @This().Flags,
+ source: LazyPath.Index,
+ dest_sub_path: String,
+ exclude_extensions: Storage.FlagLengthPrefixedList(.flags, .exclude_extensions, String),
+ include_extensions: Storage.FlagLengthPrefixedList(.flags, .include_extensions, String),
+
+ pub const Flags = packed struct(u32) {
+ tag: InstalledHeader.Tag = .directory,
+ exclude_extensions: bool,
+ include_extensions: bool,
+ _: u22 = 0,
+ };
+ };
+ };
+ pub const ExpectErrors = union(@This().Tag) {
+ pub const Tag = enum(u3) { contains, exact, starts_with, stderr_contains, none };
+
+ contains: String,
+ exact: Storage.LengthPrefixedList(String),
+ starts_with: String,
+ stderr_contains: String,
+ none: void,
+ };
+ pub const TestRunner = union(@This().Tag) {
+ pub const Tag = enum(u2) { default, simple, server };
+
+ default: void,
+ simple: LazyPath.Index,
+ server: LazyPath.Index,
+ };
+ pub const Entry = enum(u2) { default, disabled, enabled, symbol_name };
+
+ pub const Lto = enum(u2) {
+ none,
+ full,
+ thin,
+ default,
+
+ pub fn init(lto: ?std.zig.LtoMode) Lto {
+ return switch (lto orelse return .default) {
+ .none => .none,
+ .full => .full,
+ .thin => .thin,
+ };
+ }
+ };
+
+ pub const BuildId = enum(u3) {
+ none,
+ fast,
+ uuid,
+ sha1,
+ md5,
+ hexstring,
+ default,
+
+ pub fn init(build_id: ?std.zig.BuildId) BuildId {
+ return switch (build_id orelse return .default) {
+ .none => .none,
+ .fast => .fast,
+ .uuid => .uuid,
+ .sha1 => .sha1,
+ .md5 => .md5,
+ .hexstring => .hexstring,
+ };
+ }
+
+ pub fn unwrap(this: @This(), hexstring: ?String, c: *const Configuration) ?std.zig.BuildId {
+ if (hexstring) |h| {
+ assert(this == .hexstring);
+ return .initHexString(h.slice(c));
+ }
+ return switch (this) {
+ .none => .none,
+ .fast => .fast,
+ .uuid => .uuid,
+ .sha1 => .sha1,
+ .md5 => .md5,
+ .hexstring => unreachable,
+ .default => null,
+ };
+ }
+ };
+ pub const WasiExecModel = enum(u2) {
+ default,
+ command,
+ reactor,
+
+ pub fn init(wasi_exec_model: ?std.builtin.WasiExecModel) WasiExecModel {
+ return switch (wasi_exec_model orelse return .default) {
+ .command => .command,
+ .reactor => .reactor,
+ };
+ }
+ };
+ pub const Linkage = enum(u2) {
+ static,
+ dynamic,
+ default,
+
+ pub fn init(link_mode: ?std.builtin.LinkMode) Linkage {
+ return switch (link_mode orelse return .default) {
+ .static => .static,
+ .dynamic => .dynamic,
+ };
+ }
+
+ pub fn unwrap(this: @This()) ?std.builtin.LinkMode {
+ return switch (this) {
+ .static => .static,
+ .dynamic => .dynamic,
+ .default => null,
+ };
+ }
+ };
+ pub const Kind = enum(u3) {
+ exe,
+ lib,
+ obj,
+ @"test",
+ test_obj,
+
+ pub fn isTest(kind: Kind) bool {
+ return switch (kind) {
+ .exe, .lib, .obj => false,
+ .@"test", .test_obj => true,
+ };
+ }
+
+ pub fn toOutputMode(kind: Kind) std.builtin.OutputMode {
+ return switch (kind) {
+ .exe, .@"test" => .Exe,
+ .lib => .Lib,
+ .obj, .test_obj => .Obj,
+ };
+ }
+ };
+ pub const Subsystem = enum(u4) {
+ console,
+ windows,
+ posix,
+ native,
+ efi_application,
+ efi_boot_service_driver,
+ efi_rom,
+ efi_runtime_driver,
+ default,
+
+ pub fn init(subsystem: ?std.zig.Subsystem) Subsystem {
+ return switch (subsystem orelse return .default) {
+ .console => .console,
+ .windows => .windows,
+ .posix => .posix,
+ .native => .native,
+ .efi_application => .efi_application,
+ .efi_boot_service_driver => .efi_boot_service_driver,
+ .efi_rom => .efi_rom,
+ .efi_runtime_driver => .efi_runtime_driver,
+ };
+ }
+ };
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .compile,
+
+ filters_len: bool,
+ exec_cmd_args_len: bool,
+ installed_headers_len: bool,
+ force_undefined_symbols_len: bool,
+
+ verbose_link: bool,
+ verbose_cc: bool,
+ rdynamic: bool,
+ import_memory: bool,
+ export_memory: bool,
+ import_symbols: bool,
+ import_table: bool,
+ export_table: bool,
+ shared_memory: bool,
+ link_eh_frame_hdr: bool,
+ link_emit_relocs: bool,
+ link_function_sections: bool,
+ link_data_sections: bool,
+ linker_dynamicbase: bool,
+ link_z_notext: bool,
+ link_z_relro: bool,
+ link_z_lazy: bool,
+ link_z_defs: bool,
+ headerpad_max_install_names: bool,
+ dead_strip_dylibs: bool,
+ force_load_objc: bool,
+ discard_local_symbols: bool,
+ mingw_unicode_entry_point: bool,
+ };
+
+ pub const Flags2 = packed struct(u32) {
+ pie: DefaultingBool,
+ formatted_panics: DefaultingBool,
+ bundle_compiler_rt: DefaultingBool,
+ bundle_ubsan_rt: DefaultingBool,
+ each_lib_rpath: DefaultingBool,
+ link_gc_sections: DefaultingBool,
+ linker_allow_shlib_undefined: DefaultingBool,
+ linker_allow_undefined_version: DefaultingBool,
+ linker_enable_new_dtags: DefaultingBool,
+ dll_export_fns: DefaultingBool,
+ use_llvm: DefaultingBool,
+ use_lld: DefaultingBool,
+ use_new_linker: DefaultingBool,
+ allow_so_scripts: DefaultingBool,
+ sanitize_coverage_trace_pc_guard: DefaultingBool,
+ linkage: Linkage,
+ };
+
+ pub const Flags3 = packed struct(u32) {
+ is_linking_libc: bool,
+ is_linking_libcpp: bool,
+ version: bool,
+ initial_memory: bool,
+ max_memory: bool,
+ kind: Kind,
+ compress_debug_sections: std.zig.CompressDebugSections,
+ global_base: bool,
+ test_runner: TestRunner.Tag,
+ wasi_exec_model: WasiExecModel,
+ win32_manifest: bool,
+ win32_module_definition: bool,
+ zig_lib_dir: bool,
+ rc_includes: std.zig.RcIncludes,
+ image_base: bool,
+ build_id: BuildId,
+ entry: Entry,
+ lto: Lto,
+ subsystem: Subsystem,
+ };
+
+ pub const Flags4 = packed struct(u32) {
+ libc_file: bool,
+ link_z_common_page_size: bool,
+ link_z_max_page_size: bool,
+ pagezero_size: bool,
+ stack_size: bool,
+ headerpad_size: bool,
+ error_limit: bool,
+ install_name: bool,
+ entitlements: bool,
+ expect_errors: ExpectErrors.Tag,
+ linker_script: bool,
+ version_script: bool,
+ emit_directory: bool,
+ generated_docs: bool,
+ generated_asm: bool,
+ generated_bin: bool,
+ generated_pdb: bool,
+ generated_implib: bool,
+ generated_llvm_bc: bool,
+ generated_llvm_ir: bool,
+ generated_h: bool,
+ _: u9 = 0,
+ };
+
+ pub fn isDynamicLibrary(compile: *const Compile) bool {
+ return compile.flags3.kind == .lib and compile.flags2.linkage == .dynamic;
+ }
+
+ pub fn isStaticLibrary(compile: *const Compile) bool {
+ return compile.flags3.kind == .lib and compile.flags2.linkage != .dynamic;
+ }
+
+ pub fn producesImplib(compile: *const Compile, c: *const Configuration) bool {
+ return isDll(compile, c);
+ }
+
+ pub fn isDll(compile: *const Compile, c: *const Configuration) bool {
+ return isDynamicLibrary(compile) and rootModuleTarget(compile, c).flags.os_tag == .windows;
+ }
+
+ pub fn rootModuleTarget(compile: *const Compile, c: *const Configuration) TargetQuery {
+ return compile.root_module.get(c).resolved_target.get(c).?.result.get(c);
+ }
+ };
+
+ pub const CheckFile = struct {
+ flags: @This().Flags,
+ file: LazyPath.Index,
+ expected_exact: Storage.FlagOptional(.flags, .expected_exact, Bytes),
+ expected_matches: Storage.FlagLengthPrefixedList(.flags, .expected_matches, Bytes),
+ max_bytes: Storage.FlagOptional(.flags, .max_bytes, u32),
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .check_file,
+ expected_exact: bool,
+ expected_matches: bool,
+ max_bytes: bool,
+ _: u24 = 0,
+ };
+ };
+
+ pub const ConfigHeader = struct {
+ flags: @This().Flags,
+ template_file: Storage.FlagOptional(.flags, .template_file, LazyPath.Index),
+ generated_dir: GeneratedFileIndex,
+ input_size_limit: Storage.FlagOptional(.flags, .input_size_limit, u64),
+ include_path: String,
+ include_guard: Storage.FlagOptional(.flags, .include_guard, String),
+ values: Storage.LengthPrefixedList(Value.Pair),
+
+ pub const Style = enum(u3) {
+ autoconf_undef,
+ autoconf_at,
+ cmake,
+ blank,
+ nasm,
+
+ pub fn init(s: std.Build.Step.ConfigHeader.Style) Style {
+ return switch (s) {
+ .autoconf_undef => .autoconf_undef,
+ .autoconf_at => .autoconf_at,
+ .cmake => .cmake,
+ .blank => .blank,
+ .nasm => .nasm,
+ };
+ }
+ };
+
+ pub const Value = struct {
+ flags: @This().Flags,
+ i64: Storage.EnumOptional(.flags, .tag, .i64, i64),
+ u64: Storage.EnumOptional(.flags, .tag, .u64, u64),
+ ident: Storage.EnumOptional(.flags, .tag, .ident, String),
+ string: Storage.EnumOptional(.flags, .tag, .string, String),
+
+ pub const Flags = packed struct(u32) {
+ tag: Value.Tag,
+ small: u29,
+ };
+
+ pub const Tag = enum(u3) {
+ ident,
+ string,
+ small_unsigned,
+ small_signed,
+ i64,
+ u64,
+ };
+
+ pub const Pair = extern struct {
+ key: String,
+ index: Value.Index,
+ };
+
+ pub const Index = enum(u32) {
+ int_0 = max_u32 - 5,
+ int_1 = max_u32 - 4,
+ bool_false = max_u32 - 3,
+ bool_true = max_u32 - 2,
+ undef = max_u32 - 1,
+ defined = max_u32,
+ _,
+
+ pub fn unpack(this: @This(), c: *const Configuration) Unpacked {
+ return switch (this) {
+ .int_0 => .{ .u64 = 0 },
+ .int_1 => .{ .u64 = 1 },
+ .bool_false => .{ .bool = false },
+ .bool_true => .{ .bool = true },
+ .undef => .undef,
+ .defined => .defined,
+ _ => {
+ const value = extraData(c, Value, @intFromEnum(this));
+ return switch (value.flags.tag) {
+ .ident => .{ .ident = value.ident.value.?.slice(c) },
+ .string => .{ .string = value.string.value.?.slice(c) },
+ .small_unsigned => .{ .u64 = value.flags.small },
+ .small_signed => .{ .i64 = @as(i29, @bitCast(value.flags.small)) },
+ .i64 => .{ .i64 = value.i64.value.? },
+ .u64 => .{ .u64 = value.u64.value.? },
+ };
+ },
+ };
+ }
+ };
+
+ pub const Unpacked = union(enum) {
+ bool: bool,
+ undef,
+ defined,
+ i64: i64,
+ u64: u64,
+ ident: []const u8,
+ string: []const u8,
+ };
+
+ pub fn initSigned(x: i64) @This() {
+ return switch (x) {
+ 0 => unreachable, // should have been an Index
+ 1 => unreachable, // should have been an Index
+ 2...std.math.maxInt(u29) => .{
+ .flags = .{
+ .tag = .small_unsigned,
+ .small = @intCast(x),
+ },
+ .i64 = .{ .value = null },
+ .u64 = .{ .value = null },
+ .ident = .{ .value = null },
+ .string = .{ .value = null },
+ },
+ std.math.minInt(i29)...-1 => .{
+ .flags = .{
+ .tag = .small_signed,
+ .small = @bitCast(@as(i29, @intCast(x))),
+ },
+ .i64 = .{ .value = null },
+ .u64 = .{ .value = null },
+ .ident = .{ .value = null },
+ .string = .{ .value = null },
+ },
+ else => .{
+ .flags = .{
+ .tag = .i64,
+ .small = 0,
+ },
+ .i64 = .{ .value = x },
+ .u64 = .{ .value = null },
+ .ident = .{ .value = null },
+ .string = .{ .value = null },
+ },
+ };
+ }
+ };
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .config_header,
+ template_file: bool,
+ style: Style,
+ input_size_limit: bool,
+ include_guard: bool,
+ _: u21 = 0,
+ };
+ };
+
+ pub const Fail = struct {
+ flags: @This().Flags = .{},
+ msg: String,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .fail,
+ _: u27 = 0,
+ };
+ };
+
+ pub const Fmt = struct {
+ flags: @This().Flags,
+ paths: Storage.FlagLengthPrefixedList(.flags, .paths, LazyPath.Index),
+ exclude_paths: Storage.FlagLengthPrefixedList(.flags, .exclude_paths, LazyPath.Index),
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .fmt,
+ paths: bool,
+ exclude_paths: bool,
+ check: bool,
+ _: u24 = 0,
+ };
+ };
+
+ pub const FindProgram = struct {
+ flags: @This().Flags = .{},
+ names: StringList,
+ found_path: GeneratedFileIndex,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .find_program,
+ _: u27 = 0,
+ };
+ };
+
+ pub const InstallDir = struct {
+ flags: @This().Flags,
+ source_dir: LazyPath.Index,
+ dest_dir: InstallDestDir,
+ dest_sub_path: Storage.FlagOptional(.flags, .dest_sub_path, String),
+ exclude_extensions: Storage.FlagLengthPrefixedList(.flags, .exclude_extensions, String),
+ include_extensions: Storage.FlagLengthPrefixedList(.flags, .include_extensions, String),
+ blank_extensions: Storage.FlagLengthPrefixedList(.flags, .blank_extensions, String),
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .install_dir,
+ dest_sub_path: bool,
+ exclude_extensions: bool,
+ include_extensions: bool,
+ include_extensions_active: bool,
+ blank_extensions: bool,
+ _: u22 = 0,
+ };
+ };
+
+ pub const InstallFile = struct {
+ flags: @This().Flags = .{},
+ source: LazyPath.Index,
+ dest_dir: InstallDestDir,
+ dest_sub_path: String,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .install_file,
+ _: u27 = 0,
+ };
+ };
+
+ pub const ObjCopy = struct {
+ flags: @This().Flags,
+ input_file: LazyPath.Index,
+ output_file: GeneratedFileIndex,
+ basename: Storage.FlagOptional(.flags, .basename, String),
+ debug_file: Storage.FlagOptional(.flags, .debug_file, GeneratedFileIndex),
+ debug_basename: Storage.FlagOptional(.flags, .debug_basename, String),
+ only_section: Storage.FlagOptional(.flags, .only_section, String),
+ pad_to: Storage.FlagOptional(.flags, .pad_to, u64),
+ add_section: Storage.FlagLengthPrefixedList(.flags, .add_section, AddSection),
+ update_section: Storage.FlagLengthPrefixedList(.flags, .update_section, UpdateSection),
+
+ pub const Format = enum(u2) {
+ binary,
+ hex,
+ elf,
+ default,
+
+ pub fn init(f: ?std.Build.Step.ObjCopy.Format) @This() {
+ return switch (f orelse return .default) {
+ .binary => .binary,
+ .hex => .hex,
+ .elf => .elf,
+ };
+ }
+ };
+
+ pub const Strip = enum(u2) {
+ none,
+ debug,
+ debug_and_symbols,
+ };
+
+ pub const AddSection = extern struct {
+ section_name: String,
+ file_path: LazyPath.Index,
+ };
+
+ pub const UpdateSection = extern struct {
+ section_name: String,
+ flags: @This().Flags,
+
+ pub const Flags = packed struct(u32) {
+ section_flags: SectionFlags,
+ alignment: Alignment,
+ _: u17 = 0,
+ };
+ };
+
+ pub const SectionFlags = packed struct(u9) {
+ /// add SHF_ALLOC
+ alloc: bool = false,
+ /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing
+ contents: bool = false,
+ /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing (same as contents)
+ load: bool = false,
+ /// readonly: clear default SHF_WRITE flag
+ readonly: bool = false,
+ /// add SHF_EXECINSTR
+ code: bool = false,
+ /// add SHF_EXCLUDE
+ exclude: bool = false,
+ /// add SHF_X86_64_LARGE. Fatal error if target is not x86_64
+ large: bool = false,
+ /// add SHF_MERGE
+ merge: bool = false,
+ /// add SHF_STRINGS
+ strings: bool = false,
+
+ pub const default: @This() = .{};
+ };
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .obj_copy,
+ basename: bool,
+ debug_file: bool,
+ debug_basename: bool,
+ format: Format,
+ strip: Strip,
+ compress_debug: bool,
+ only_section: bool,
+ pad_to: bool,
+ add_section: bool,
+ update_section: bool,
+ _: u15 = 0,
+ };
+ };
+
+ pub const Options = struct {
+ flags: @This().Flags,
+ generated_file: GeneratedFileIndex,
+ contents: Bytes,
+ args: Storage.FlagLengthPrefixedList(.flags, .args, Arg),
+
+ pub const Arg = extern struct {
+ name: String,
+ path: LazyPath.Index,
+ };
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .options,
+ args: bool,
+ _: u26 = 0,
+ };
+ };
+
+ pub const TranslateC = struct {
+ flags: @This().Flags,
+ src_path: LazyPath.Index,
+ output_file: GeneratedFileIndex,
+ include_dirs: Storage.UnionList(.flags, .include_dirs, Module.IncludeDir),
+ system_libs: Storage.FlagLengthPrefixedList(.flags, .system_libs, SystemLib.Index),
+ c_macros: Storage.FlagLengthPrefixedList(.flags, .c_macros, String),
+ target: ResolvedTarget.OptionalIndex,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .translate_c,
+ include_dirs: bool,
+ system_libs: bool,
+ c_macros: bool,
+ link_libc: bool,
+ optimize: Module.Optimize,
+ _: u20 = 0,
+ };
+ };
+
+ pub const UpdateSourceFiles = struct {
+ flags: @This().Flags,
+ embeds: Storage.FlagLengthPrefixedList(.flags, .embeds, Embed),
+ copies: Storage.FlagLengthPrefixedList(.flags, .copies, Copy),
+
+ pub const Embed = WriteFile.Embed;
+ pub const Copy = WriteFile.Copy;
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .update_source_files,
+ embeds: bool,
+ copies: bool,
+ _: u25 = 0,
+ };
+ };
+
+ pub const WriteFile = struct {
+ flags: @This().Flags,
+ generated_directory: GeneratedFileIndex,
+ embeds: Storage.FlagLengthPrefixedList(.flags, .embeds, Embed),
+ copies: Storage.FlagLengthPrefixedList(.flags, .copies, Copy),
+ directories: Storage.FlagLengthPrefixedList(.flags, .directories, Directory),
+ mutate_path: Storage.EnumOptional(.flags, .mode, .mutate, LazyPath.Index),
+
+ pub const Embed = extern struct {
+ sub_path: String,
+ contents: Bytes,
+ };
+
+ pub const Copy = extern struct {
+ sub_path: String,
+ src_file: LazyPath.Index,
+ };
+
+ pub const Directory = extern struct {
+ sub_path: String,
+ src_path: LazyPath.Index,
+ exclude_extensions: OptionalStringList,
+ include_extensions: OptionalStringList,
+ };
+
+ pub const Mode = enum(u2) {
+ whole_cached,
+ tmp,
+ mutate,
+ };
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .write_file,
+ embeds: bool,
+ copies: bool,
+ directories: bool,
+ mode: Mode,
+ _: u22 = 0,
+ };
+ };
+
+ pub fn flags(s: *const Step, c: *const Configuration) Flags {
+ return @bitCast(c.extra[@intFromEnum(s.extended)]);
+ }
+};
+
+pub const MaxRss = enum(u32) {
+ none = 0,
+ _,
+
+ pub fn toBytes(mr: MaxRss) usize {
+ const x: usize = @intFromEnum(mr);
+ return x << 8;
+ }
+
+ pub fn fromBytes(bytes: usize) MaxRss {
+ return @enumFromInt(bytes >> 8);
+ }
+};
+
+pub const LazyPath = union(@This().Tag) {
+ source_path: SourcePath,
+ relative: Relative,
+ generated: Generated,
+
+ pub const Tag = enum(u8) {
+ /// A source file path relative to build root.
+ source_path,
+ /// Relative to the directory indicated in flags.
+ relative,
+ /// Path is available only after it is populated by its owning step.
+ generated,
+ };
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag,
+ _: u24 = 0,
+ };
+
+ /// An index into `extra`.
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) LazyPath {
+ return extraData(c, LazyPath, @intFromEnum(this));
+ }
+ };
+
+ /// An index into `extra`, or `null`.
+ pub const OptionalIndex = enum(u32) {
+ none = max_u32,
+ _,
+
+ pub fn unwrap(this: @This()) ?Index {
+ return switch (this) {
+ .none => null,
+ else => @enumFromInt(@intFromEnum(this)),
+ };
+ }
+ };
+
+ pub const SourcePath = struct {
+ flags: @This().Flags = .{},
+ owner: Package.Index,
+ sub_path: String,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .source_path,
+ _: u24 = 0,
+ };
+ };
+
+ pub const Generated = struct {
+ flags: @This().Flags = .{},
+ index: GeneratedFileIndex,
+ /// Applied after `up`.
+ sub_path: String = .empty,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .generated,
+ /// The number of parent directories to go up.
+ /// 0 means the generated file itself.
+ /// 1 means the directory of the generated file.
+ /// 2 means the parent of that directory, and so on.
+ up: u24 = 0,
+ };
+ };
+
+ pub const Relative = struct {
+ flags: @This().Flags,
+ sub_path: String,
+
+ pub const Flags = packed struct(u32) {
+ tag: Tag = .relative,
+ base: Path.Base,
+ _: u16 = 0,
+ };
+ };
+};
+
+pub const GeneratedFileIndex = enum(u32) {
+ _,
+};
+
+pub const OptionalGeneratedFileIndex = enum(u32) {
+ none = max_u32,
+ _,
+
+ pub fn init(i: ?GeneratedFileIndex) OptionalGeneratedFileIndex {
+ return @enumFromInt(@intFromEnum(i orelse return .none));
+ }
+
+ pub fn unwrap(this: @This()) ?GeneratedFileIndex {
+ return switch (this) {
+ .none => null,
+ else => @enumFromInt(@intFromEnum(this)),
+ };
+ }
+};
+
+pub const Package = struct {
+ dep_prefix: String,
+ hash: String,
+ root_path: String,
+
+ pub const Index = enum(u32) {
+ root = max_u32,
+ _,
+
+ /// Returns `null` for root package.
+ pub fn get(i: @This(), c: *const Configuration) ?Package {
+ if (i == .root) return null;
+ return extraData(c, Package, @intFromEnum(i));
+ }
+
+ pub fn depPrefixSlice(i: @This(), c: *const Configuration) [:0]const u8 {
+ const package = get(i, c) orelse return "";
+ return package.dep_prefix.slice(c);
+ }
+ };
+};
+
+pub const Module = struct {
+ flags: Flags,
+ flags2: Flags2,
+ import_table: ImportTable.Index,
+ owner: Package.Index,
+ root_source_file: LazyPath.OptionalIndex,
+ resolved_target: ResolvedTarget.OptionalIndex,
+ c_macros: Storage.FlagLengthPrefixedList(.flags, .c_macros, String),
+ lib_paths: Storage.FlagLengthPrefixedList(.flags, .lib_paths, LazyPath.Index),
+ export_symbol_names: Storage.FlagLengthPrefixedList(.flags, .export_symbol_names, String),
+ include_dirs: Storage.UnionList(.flags, .include_dirs, IncludeDir),
+ rpaths: Storage.UnionList(.flags, .rpaths, RPath),
+ link_objects: Storage.UnionList(.flags, .link_objects, LinkObject),
+ frameworks: Storage.FlagLengthPrefixedList(.flags, .frameworks, Framework),
+
+ pub const Optimize = enum(u3) {
+ debug,
+ safe,
+ fast,
+ small,
+ default,
+
+ pub fn init(o: ?std.builtin.OptimizeMode) Optimize {
+ return switch (o orelse return .default) {
+ .Debug => .debug,
+ .ReleaseSafe => .safe,
+ .ReleaseFast => .fast,
+ .ReleaseSmall => .small,
+ };
+ }
+ };
+
+ pub const UnwindTables = enum(u2) {
+ none,
+ sync,
+ async,
+ default,
+
+ pub fn init(ut: ?std.builtin.UnwindTables) UnwindTables {
+ return switch (ut orelse return .default) {
+ .none => .none,
+ .sync => .sync,
+ .async => .async,
+ };
+ }
+ };
+
+ pub const SanitizeC = enum(u2) {
+ off,
+ trap,
+ full,
+ default,
+
+ pub fn init(sc: ?std.zig.SanitizeC) SanitizeC {
+ return switch (sc orelse return .default) {
+ .off => .off,
+ .trap => .trap,
+ .full => .full,
+ };
+ }
+ };
+
+ pub const DwarfFormat = enum(u2) {
+ @"32",
+ @"64",
+ default,
+
+ pub fn init(df: ?std.dwarf.Format) DwarfFormat {
+ return switch (df orelse return .default) {
+ .@"32" => .@"32",
+ .@"64" => .@"64",
+ };
+ }
+ };
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) Module {
+ return extraData(c, Module, @intFromEnum(this));
+ }
+ };
+
+ pub const Flags = packed struct(u32) {
+ optimize: Optimize,
+ strip: DefaultingBool,
+ unwind_tables: UnwindTables,
+ dwarf_format: DwarfFormat,
+ single_threaded: DefaultingBool,
+ stack_protector: DefaultingBool,
+ stack_check: DefaultingBool,
+ sanitize_c: SanitizeC,
+ sanitize_thread: DefaultingBool,
+ fuzz: DefaultingBool,
+ code_model: std.builtin.CodeModel,
+ c_macros: bool,
+ include_dirs: bool,
+ lib_paths: bool,
+ rpaths: bool,
+ frameworks: bool,
+ link_objects: bool,
+ export_symbol_names: bool,
+ };
+
+ pub const Flags2 = packed struct(u32) {
+ valgrind: DefaultingBool,
+ pic: DefaultingBool,
+ red_zone: DefaultingBool,
+ omit_frame_pointer: DefaultingBool,
+ error_tracing: DefaultingBool,
+ link_libc: DefaultingBool,
+ link_libcpp: DefaultingBool,
+ no_builtin: DefaultingBool,
+ _: u16 = 0,
+ };
+
+ pub const IncludeDir = union(enum(u3)) {
+ path: LazyPath.Index,
+ path_system: LazyPath.Index,
+ path_after: LazyPath.Index,
+ framework_path: LazyPath.Index,
+ framework_path_system: LazyPath.Index,
+ /// Always `Step.Tag.config_header`.
+ config_header_step: Step.Index,
+ embed_path: LazyPath.Index,
+ };
+
+ pub const RPath = union(enum(u1)) {
+ lazy_path: LazyPath.Index,
+ special: String,
+ };
+
+ pub const LinkObject = union(enum(u3)) {
+ static_path: LazyPath.Index,
+ /// Always `Step.Tag.compile`.
+ other_step: Step.Index,
+ system_lib: SystemLib.Index,
+ assembly_file: LazyPath.Index,
+ c_source_file: CSourceFile.Index,
+ c_source_files: CSourceFiles.Index,
+ win32_resource_file: RcSourceFile.Index,
+ };
+
+ pub const Framework = extern struct {
+ flags: @This().Flags,
+ name: String,
+
+ pub const Flags = packed struct(u32) {
+ needed: bool,
+ weak: bool,
+ _: u30 = 0,
+ };
+ };
+};
+
+pub const ImportTable = struct {
+ imports: Storage.MultiList(Import),
+
+ pub const Import = struct {
+ name: String,
+ module: Module.Index,
+ };
+
+ /// Points into `extra`.
+ pub const Index = enum(u32) {
+ invalid = max_u32,
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) ImportTable {
+ return switch (this) {
+ .invalid => unreachable,
+ _ => extraData(c, ImportTable, @intFromEnum(this)),
+ };
+ }
+ };
+};
+
+pub const Deps = struct {
+ steps: Storage.LengthPrefixedList(Step.Index),
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) Deps {
+ return extraData(c, Deps, @intFromEnum(this));
+ }
+
+ pub fn slice(this: @This(), c: *const Configuration) []const Step.Index {
+ return get(this, c).steps.slice;
+ }
+ };
+};
+
+pub const EnvironMap = struct {
+ keys: StringList,
+ values: StringList,
+
+ pub const Index = IndexType(@This());
+};
+
+/// Points into `extra`, where the first element is count of strings, following
+/// elements is `String` per count.
+///
+/// Stored identically to `Deps`.
+pub const StringList = enum(u32) {
+ _,
+
+ pub fn slice(this: @This(), c: *const Configuration) []const String {
+ const len = c.extra[@intFromEnum(this)];
+ return @ptrCast(c.extra[@intFromEnum(this) + 1 ..][0..len]);
+ }
+};
+
+pub const OptionalStringList = enum(u32) {
+ none = max_u32,
+ _,
+
+ pub fn init(opt_string_list: ?StringList) OptionalStringList {
+ const sl = opt_string_list orelse return .none;
+ const result: OptionalStringList = @enumFromInt(@intFromEnum(sl));
+ assert(result != .none);
+ return result;
+ }
+
+ pub fn unwrap(this: @This()) ?StringList {
+ if (this == .none) return null;
+ return @enumFromInt(@intFromEnum(this));
+ }
+
+ pub fn slice(this: @This(), c: *const Configuration) ?[]const String {
+ return (unwrap(this) orelse return null).slice(c);
+ }
+};
+
+pub const Path = extern struct {
+ base: Base,
+ sub: String,
+
+ pub const Base = enum(u8) {
+ cwd,
+ local_cache,
+ global_cache,
+ build_root,
+ zig_exe,
+ zig_lib,
+ install_prefix,
+ install_lib,
+ install_bin,
+ install_include,
+ };
+
+ pub fn toCachePath(path: Path, c: *const Configuration, arena: Allocator) std.Build.Cache.Path {
+ _ = c;
+ _ = arena;
+ _ = path;
+ @panic("TODO");
+ }
+};
+
+pub const InstallDestDir = enum(u32) {
+ none = max_u32 - 4,
+ prefix = max_u32 - 3,
+ lib = max_u32 - 2,
+ bin = max_u32 - 1,
+ header = max_u32,
+ /// A `String` path relative to the prefix.
+ _,
+
+ pub fn initCustom(sub_path: String) InstallDestDir {
+ assert(@intFromEnum(sub_path) < @intFromEnum(InstallDestDir.none));
+ return @enumFromInt(@intFromEnum(sub_path));
+ }
+
+ pub const Unpacked = union(enum) {
+ prefix,
+ lib,
+ bin,
+ header,
+ sub_path: String,
+ };
+
+ pub fn unpack(this: @This()) ?Unpacked {
+ return switch (this) {
+ .none => null,
+ .prefix => .prefix,
+ .lib => .lib,
+ .bin => .bin,
+ .header => .header,
+ _ => .{ .sub_path = @enumFromInt(@intFromEnum(this)) },
+ };
+ }
+};
+
+/// Points into `string_bytes`, null-terminated.
+pub const OptionalString = enum(u32) {
+ empty = 0,
+ /// The string "root".
+ root = 1,
+ none = max_u32,
+ _,
+
+ pub fn init(s: String) OptionalString {
+ const result: OptionalString = @enumFromInt(@intFromEnum(s));
+ assert(result != .none);
+ return result;
+ }
+
+ pub fn unwrap(this: @This()) ?String {
+ if (this == .none) return null;
+ return @enumFromInt(@intFromEnum(this));
+ }
+
+ pub fn slice(this: @This(), c: *const Configuration) ?[:0]const u8 {
+ return (unwrap(this) orelse return null).slice(c);
+ }
+};
+
+/// Points into `string_bytes`, null-terminated.
+pub const String = enum(u32) {
+ empty = 0,
+ /// The string "root".
+ root = 1,
+ _,
+
+ pub fn slice(index: String, c: *const Configuration) [:0]const u8 {
+ const start_slice = c.string_bytes[@intFromEnum(index)..];
+ return start_slice[0..std.mem.indexOfScalar(u8, start_slice, 0).? :0];
+ }
+};
+
+/// Arbitrary sequence of bytes that may contain null bytes.
+pub const Bytes = extern struct {
+ /// Points into `string_bytes`.
+ index: u32,
+ len: u32,
+
+ pub fn slice(bytes: Bytes, c: *const Configuration) []const u8 {
+ return c.string_bytes[bytes.index..][0..bytes.len];
+ }
+};
+
+/// Stored as a power-of-two, with one special value to indicate none.
+pub const Alignment = enum(u6) {
+ @"1" = 0,
+ @"2" = 1,
+ @"4" = 2,
+ @"8" = 3,
+ @"16" = 4,
+ @"32" = 5,
+ @"64" = 6,
+ none = std.math.maxInt(u6),
+ _,
+
+ pub fn init(optional_alignment: ?std.mem.Alignment) @This() {
+ const a = optional_alignment orelse return .none;
+ return @enumFromInt(@intFromEnum(a));
+ }
+
+ pub fn toBytes(a: @This()) ?u64 {
+ return switch (a) {
+ .none => null,
+ else => @as(u64, 1) << @intFromEnum(a),
+ };
+ }
+};
+
+pub const DefaultingBool = enum(u2) {
+ false,
+ true,
+ default,
+
+ pub fn init(b: ?bool) DefaultingBool {
+ return switch (b orelse return .default) {
+ false => .false,
+ true => .true,
+ };
+ }
+
+ pub fn toBool(db: DefaultingBool) ?bool {
+ return switch (db) {
+ .false => false,
+ .true => true,
+ .default => null,
+ };
+ }
+};
+
+pub const SystemLib = struct {
+ name: String,
+ flags: Flags,
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) SystemLib {
+ return extraData(c, SystemLib, @intFromEnum(this));
+ }
+ };
+
+ pub const UsePkgConfig = enum(u2) {
+ /// Don't use pkg-config, just pass -lfoo where foo is name.
+ no,
+ /// Try to get information on how to link the library from pkg-config.
+ /// If that fails, fall back to passing -lfoo where foo is name.
+ yes,
+ /// Try to get information on how to link the library from pkg-config.
+ /// If that fails, error out.
+ force,
+ };
+
+ pub const LinkMode = std.builtin.LinkMode;
+
+ pub const Flags = packed struct(u32) {
+ needed: bool,
+ weak: bool,
+ use_pkg_config: UsePkgConfig,
+ preferred_link_mode: LinkMode,
+ search_strategy: SearchStrategy,
+ _: u25 = 0,
+ };
+
+ pub const SearchStrategy = enum(u2) { paths_first, mode_first, no_fallback };
+};
+
+pub const CSourceFiles = struct {
+ flags: Flags,
+ root: LazyPath.Index,
+ args: Storage.FlagList(.flags, .args_len, String),
+ sub_paths: Storage.LengthPrefixedList(String),
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) CSourceFiles {
+ return extraData(c, CSourceFiles, @intFromEnum(this));
+ }
+ };
+
+ pub const Flags = packed struct(u32) {
+ /// C compiler CLI flags.
+ args_len: u29,
+ lang: OptionalCSourceLanguage,
+ };
+};
+
+pub const CSourceFile = struct {
+ flags: Flags,
+ file: LazyPath.Index,
+ args: Storage.FlagList(.flags, .args_len, String),
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) CSourceFile {
+ return extraData(c, CSourceFile, @intFromEnum(this));
+ }
+ };
+
+ pub const Flags = packed struct(u32) {
+ /// C compiler CLI flags.
+ args_len: u29,
+ lang: OptionalCSourceLanguage,
+ };
+};
+
+pub const RcSourceFile = struct {
+ flags: Flags,
+ file: LazyPath.Index,
+ args: Storage.FlagList(.flags, .args_len, String),
+ include_paths: Storage.FlagLengthPrefixedList(.flags, .include_paths, LazyPath.Index),
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) RcSourceFile {
+ return extraData(c, RcSourceFile, @intFromEnum(this));
+ }
+ };
+
+ pub const Flags = packed struct(u32) {
+ /// C compiler CLI flags.
+ args_len: u31,
+ include_paths: bool,
+ };
+};
+
+pub const OptionalCSourceLanguage = enum(u3) {
+ c,
+ cpp,
+ objective_c,
+ objective_cpp,
+ assembly,
+ assembly_with_preprocessor,
+ default,
+
+ pub fn init(x: ?std.Build.Module.CSourceLanguage) @This() {
+ return switch (x orelse return .default) {
+ .c => .c,
+ .cpp => .cpp,
+ .objective_c => .objective_c,
+ .objective_cpp => .objective_cpp,
+ .assembly => .assembly,
+ .assembly_with_preprocessor => .assembly_with_preprocessor,
+ };
+ }
+
+ pub fn get(this: @This()) ?std.Build.Module.CSourceLanguage {
+ return switch (this) {
+ .c => .c,
+ .cpp => .cpp,
+ .objective_c => .objective_c,
+ .objective_cpp => .objective_cpp,
+ .assembly => .assembly,
+ .assembly_with_preprocessor => .assembly_with_preprocessor,
+ .default => null,
+ };
+ }
+};
+
+pub const ResolvedTarget = struct {
+ /// none indicates host.
+ query: TargetQuery.OptionalIndex,
+ /// defaults will be resolved.
+ result: TargetQuery.Index,
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) ResolvedTarget {
+ return extraData(c, ResolvedTarget, @intFromEnum(this));
+ }
+ };
+
+ pub const OptionalIndex = enum(u32) {
+ none = max_u32,
+ _,
+
+ pub fn init(i: Index) OptionalIndex {
+ const result: OptionalIndex = @enumFromInt(@intFromEnum(i));
+ assert(result != .none);
+ return result;
+ }
+
+ pub fn unwrap(this: @This()) ?Index {
+ return switch (this) {
+ .none => null,
+ _ => @enumFromInt(@intFromEnum(this)),
+ };
+ }
+
+ pub fn get(this: @This(), c: *const Configuration) ?ResolvedTarget {
+ return (unwrap(this) orelse return null).get(c);
+ }
+ };
+};
+
+pub const TargetQuery = struct {
+ flags: Flags,
+
+ cpu_features_add: Storage.FlagOptional(.flags, .cpu_features_add, std.Target.Cpu.Feature.Set),
+ cpu_features_sub: Storage.FlagOptional(.flags, .cpu_features_sub, std.Target.Cpu.Feature.Set),
+ cpu_name: Storage.EnumOptional(.flags, .cpu_model, .explicit, String),
+ os_version_min: Storage.FlagUnion(.flags, .os_version_min, OsVersion),
+ os_version_max: Storage.FlagUnion(.flags, .os_version_max, OsVersion),
+ glibc_version: Storage.FlagOptional(.flags, .glibc_version, String),
+ android_api_level: Storage.FlagOptional(.flags, .android_api_level, u32),
+ dynamic_linker: Storage.FlagOptional(.flags, .dynamic_linker, String),
+
+ pub const Index = enum(u32) {
+ _,
+
+ pub fn extraSlice(i: Index, extra: []const u32) []const u32 {
+ return extra[@intFromEnum(i)..][0..length(i, extra)];
+ }
+
+ pub fn length(i: Index, extra: []const u32) usize {
+ return Storage.dataLength(extra, @intFromEnum(i), TargetQuery);
+ }
+
+ pub fn get(this: @This(), c: *const Configuration) TargetQuery {
+ return extraData(c, TargetQuery, @intFromEnum(this));
+ }
+ };
+
+ pub const OptionalIndex = enum(u32) {
+ none = max_u32,
+ _,
+
+ pub fn init(i: Index) OptionalIndex {
+ const result: OptionalIndex = @enumFromInt(@intFromEnum(i));
+ assert(result != .none);
+ return result;
+ }
+
+ pub fn unwrap(this: @This()) ?Index {
+ return switch (this) {
+ .none => null,
+ _ => @enumFromInt(@intFromEnum(this)),
+ };
+ }
+
+ pub fn get(this: @This(), c: *const Configuration) ?TargetQuery {
+ return (this.unwrap() orelse return null).get(c);
+ }
+ };
+
+ pub const CpuModel = enum(u2) {
+ native,
+ baseline,
+ determined_by_arch_os,
+ explicit,
+
+ pub fn init(x: std.Target.Query.CpuModel) @This() {
+ return switch (x) {
+ .native => .native,
+ .baseline => .baseline,
+ .determined_by_arch_os => .determined_by_arch_os,
+ .explicit => .explicit,
+ };
+ }
+ };
+ pub const OsVersion = union(@This().Tag) {
+ pub const Tag = enum(u2) { none, semver, windows, default };
+
+ none: void,
+ semver: String,
+ windows: std.Target.Os.WindowsVersion,
+ default: void,
+
+ pub fn init(x: ?std.Target.Query.OsVersion) @This() {
+ return switch (x orelse return .default) {
+ .none => .none,
+ .semver => .semver,
+ .windows => .windows,
+ };
+ }
+
+ pub fn unwrap(this: @This(), c: *const Configuration) ?std.Target.Query.OsVersion {
+ return switch (this) {
+ .none => .none,
+ .semver => |sv| .{ .semver = std.SemanticVersion.parse(sv.slice(c)) catch unreachable },
+ .windows => |wv| .{ .windows = wv },
+ .default => null,
+ };
+ }
+ };
+
+ pub const Abi = enum(u5) {
+ none,
+ gnu,
+ gnuabin32,
+ gnuabi64,
+ gnueabi,
+ gnueabihf,
+ gnuf32,
+ gnusf,
+ gnux32,
+ eabi,
+ eabihf,
+ ilp32,
+ android,
+ androideabi,
+ musl,
+ muslabin32,
+ muslabi64,
+ musleabi,
+ musleabihf,
+ muslf32,
+ muslsf,
+ muslx32,
+ msvc,
+ itanium,
+ simulator,
+ ohos,
+ ohoseabi,
+ call0,
+
+ default,
+
+ pub fn init(x: ?std.Target.Abi) @This() {
+ return switch (x orelse return .default) {
+ .none => .none,
+ .gnu => .gnu,
+ .gnuabin32 => .gnuabin32,
+ .gnuabi64 => .gnuabi64,
+ .gnueabi => .gnueabi,
+ .gnueabihf => .gnueabihf,
+ .gnuf32 => .gnuf32,
+ .gnusf => .gnusf,
+ .gnux32 => .gnux32,
+ .eabi => .eabi,
+ .eabihf => .eabihf,
+ .ilp32 => .ilp32,
+ .android => .android,
+ .androideabi => .androideabi,
+ .musl => .musl,
+ .muslabin32 => .muslabin32,
+ .muslabi64 => .muslabi64,
+ .musleabi => .musleabi,
+ .musleabihf => .musleabihf,
+ .muslf32 => .muslf32,
+ .muslsf => .muslsf,
+ .muslx32 => .muslx32,
+ .msvc => .msvc,
+ .itanium => .itanium,
+ .simulator => .simulator,
+ .ohos => .ohos,
+ .ohoseabi => .ohoseabi,
+ .call0 => .call0,
+ };
+ }
+
+ pub fn unwrap(this: @This()) ?std.Target.Abi {
+ return switch (this) {
+ .none => .none,
+ .gnu => .gnu,
+ .gnuabin32 => .gnuabin32,
+ .gnuabi64 => .gnuabi64,
+ .gnueabi => .gnueabi,
+ .gnueabihf => .gnueabihf,
+ .gnuf32 => .gnuf32,
+ .gnusf => .gnusf,
+ .gnux32 => .gnux32,
+ .eabi => .eabi,
+ .eabihf => .eabihf,
+ .ilp32 => .ilp32,
+ .android => .android,
+ .androideabi => .androideabi,
+ .musl => .musl,
+ .muslabin32 => .muslabin32,
+ .muslabi64 => .muslabi64,
+ .musleabi => .musleabi,
+ .musleabihf => .musleabihf,
+ .muslf32 => .muslf32,
+ .muslsf => .muslsf,
+ .muslx32 => .muslx32,
+ .msvc => .msvc,
+ .itanium => .itanium,
+ .simulator => .simulator,
+ .ohos => .ohos,
+ .ohoseabi => .ohoseabi,
+ .call0 => .call0,
+ .default => null,
+ };
+ }
+ };
+
+ pub const CpuArch = enum(u6) {
+ aarch64,
+ aarch64_be,
+ alpha,
+ amdgcn,
+ arc,
+ arceb,
+ arm,
+ armeb,
+ avr,
+ bpfeb,
+ bpfel,
+ csky,
+ ez80,
+ hexagon,
+ hppa,
+ hppa64,
+ kalimba,
+ kvx,
+ lanai,
+ loongarch32,
+ loongarch64,
+ m68k,
+ m88k,
+ microblaze,
+ microblazeel,
+ mips,
+ mipsel,
+ mips64,
+ mips64el,
+ msp430,
+ nvptx,
+ nvptx64,
+ or1k,
+ powerpc,
+ powerpcle,
+ powerpc64,
+ powerpc64le,
+ propeller,
+ riscv32,
+ riscv32be,
+ riscv64,
+ riscv64be,
+ s390x,
+ sh,
+ sheb,
+ sparc,
+ sparc64,
+ spirv32,
+ spirv64,
+ thumb,
+ thumbeb,
+ ve,
+ wasm32,
+ wasm64,
+ x86_16,
+ x86,
+ x86_64,
+ xcore,
+ xtensa,
+ xtensaeb,
+
+ default,
+
+ pub fn init(x: ?std.Target.Cpu.Arch) @This() {
+ return switch (x orelse return .default) {
+ .aarch64 => .aarch64,
+ .aarch64_be => .aarch64_be,
+ .alpha => .alpha,
+ .amdgcn => .amdgcn,
+ .arc => .arc,
+ .arceb => .arceb,
+ .arm => .arm,
+ .armeb => .armeb,
+ .avr => .avr,
+ .bpfeb => .bpfeb,
+ .bpfel => .bpfel,
+ .csky => .csky,
+ .ez80 => .ez80,
+ .hexagon => .hexagon,
+ .hppa => .hppa,
+ .hppa64 => .hppa64,
+ .kalimba => .kalimba,
+ .kvx => .kvx,
+ .lanai => .lanai,
+ .loongarch32 => .loongarch32,
+ .loongarch64 => .loongarch64,
+ .m68k => .m68k,
+ .m88k => .m88k,
+ .microblaze => .microblaze,
+ .microblazeel => .microblazeel,
+ .mips => .mips,
+ .mipsel => .mipsel,
+ .mips64 => .mips64,
+ .mips64el => .mips64el,
+ .msp430 => .msp430,
+ .nvptx => .nvptx,
+ .nvptx64 => .nvptx64,
+ .or1k => .or1k,
+ .powerpc => .powerpc,
+ .powerpcle => .powerpcle,
+ .powerpc64 => .powerpc64,
+ .powerpc64le => .powerpc64le,
+ .propeller => .propeller,
+ .riscv32 => .riscv32,
+ .riscv32be => .riscv32be,
+ .riscv64 => .riscv64,
+ .riscv64be => .riscv64be,
+ .s390x => .s390x,
+ .sh => .sh,
+ .sheb => .sheb,
+ .sparc => .sparc,
+ .sparc64 => .sparc64,
+ .spirv32 => .spirv32,
+ .spirv64 => .spirv64,
+ .thumb => .thumb,
+ .thumbeb => .thumbeb,
+ .ve => .ve,
+ .wasm32 => .wasm32,
+ .wasm64 => .wasm64,
+ .x86_16 => .x86_16,
+ .x86 => .x86,
+ .x86_64 => .x86_64,
+ .xcore => .xcore,
+ .xtensa => .xtensa,
+ .xtensaeb => .xtensaeb,
+ };
+ }
+
+ pub fn unwrap(this: @This()) ?std.Target.Cpu.Arch {
+ return switch (this) {
+ .aarch64 => .aarch64,
+ .aarch64_be => .aarch64_be,
+ .alpha => .alpha,
+ .amdgcn => .amdgcn,
+ .arc => .arc,
+ .arceb => .arceb,
+ .arm => .arm,
+ .armeb => .armeb,
+ .avr => .avr,
+ .bpfeb => .bpfeb,
+ .bpfel => .bpfel,
+ .csky => .csky,
+ .ez80 => .ez80,
+ .hexagon => .hexagon,
+ .hppa => .hppa,
+ .hppa64 => .hppa64,
+ .kalimba => .kalimba,
+ .kvx => .kvx,
+ .lanai => .lanai,
+ .loongarch32 => .loongarch32,
+ .loongarch64 => .loongarch64,
+ .m68k => .m68k,
+ .m88k => .m88k,
+ .microblaze => .microblaze,
+ .microblazeel => .microblazeel,
+ .mips => .mips,
+ .mipsel => .mipsel,
+ .mips64 => .mips64,
+ .mips64el => .mips64el,
+ .msp430 => .msp430,
+ .nvptx => .nvptx,
+ .nvptx64 => .nvptx64,
+ .or1k => .or1k,
+ .powerpc => .powerpc,
+ .powerpcle => .powerpcle,
+ .powerpc64 => .powerpc64,
+ .powerpc64le => .powerpc64le,
+ .propeller => .propeller,
+ .riscv32 => .riscv32,
+ .riscv32be => .riscv32be,
+ .riscv64 => .riscv64,
+ .riscv64be => .riscv64be,
+ .s390x => .s390x,
+ .sh => .sh,
+ .sheb => .sheb,
+ .sparc => .sparc,
+ .sparc64 => .sparc64,
+ .spirv32 => .spirv32,
+ .spirv64 => .spirv64,
+ .thumb => .thumb,
+ .thumbeb => .thumbeb,
+ .ve => .ve,
+ .wasm32 => .wasm32,
+ .wasm64 => .wasm64,
+ .x86_16 => .x86_16,
+ .x86 => .x86,
+ .x86_64 => .x86_64,
+ .xcore => .xcore,
+ .xtensa => .xtensa,
+ .xtensaeb => .xtensaeb,
+
+ .default => null,
+ };
+ }
+ };
+
+ pub const OsTag = enum(u6) {
+ freestanding,
+ other,
+ contiki,
+ fuchsia,
+ hermit,
+ managarm,
+ haiku,
+ hurd,
+ illumos,
+ linux,
+ plan9,
+ rtems,
+ serenity,
+ dragonfly,
+ freebsd,
+ netbsd,
+ openbsd,
+ driverkit,
+ ios,
+ maccatalyst,
+ macos,
+ tvos,
+ visionos,
+ watchos,
+ windows,
+ uefi,
+ @"3ds",
+ ps3,
+ ps4,
+ ps5,
+ psp,
+ vita,
+ emscripten,
+ wasi,
+ amdhsa,
+ amdpal,
+ cuda,
+ mesa3d,
+ nvcl,
+ opencl,
+ opengl,
+ vulkan,
+ tios,
+
+ default,
+
+ pub fn init(x: ?std.Target.Os.Tag) @This() {
+ return switch (x orelse return .default) {
+ .freestanding => .freestanding,
+ .other => .other,
+ .contiki => .contiki,
+ .fuchsia => .fuchsia,
+ .hermit => .hermit,
+ .managarm => .managarm,
+ .haiku => .haiku,
+ .hurd => .hurd,
+ .illumos => .illumos,
+ .linux => .linux,
+ .plan9 => .plan9,
+ .rtems => .rtems,
+ .serenity => .serenity,
+ .dragonfly => .dragonfly,
+ .freebsd => .freebsd,
+ .netbsd => .netbsd,
+ .openbsd => .openbsd,
+ .driverkit => .driverkit,
+ .ios => .ios,
+ .maccatalyst => .maccatalyst,
+ .macos => .macos,
+ .tvos => .tvos,
+ .visionos => .visionos,
+ .watchos => .watchos,
+ .windows => .windows,
+ .uefi => .uefi,
+ .@"3ds" => .@"3ds",
+ .ps3 => .ps3,
+ .ps4 => .ps4,
+ .ps5 => .ps5,
+ .psp => .psp,
+ .vita => .vita,
+ .emscripten => .emscripten,
+ .wasi => .wasi,
+ .amdhsa => .amdhsa,
+ .amdpal => .amdpal,
+ .cuda => .cuda,
+ .mesa3d => .mesa3d,
+ .nvcl => .nvcl,
+ .opencl => .opencl,
+ .opengl => .opengl,
+ .vulkan => .vulkan,
+ .tios => .tios,
+ };
+ }
+
+ pub fn unwrap(this: @This()) ?std.Target.Os.Tag {
+ return switch (this) {
+ .freestanding => .freestanding,
+ .other => .other,
+ .contiki => .contiki,
+ .fuchsia => .fuchsia,
+ .hermit => .hermit,
+ .managarm => .managarm,
+ .haiku => .haiku,
+ .hurd => .hurd,
+ .illumos => .illumos,
+ .linux => .linux,
+ .plan9 => .plan9,
+ .rtems => .rtems,
+ .serenity => .serenity,
+ .dragonfly => .dragonfly,
+ .freebsd => .freebsd,
+ .netbsd => .netbsd,
+ .openbsd => .openbsd,
+ .driverkit => .driverkit,
+ .ios => .ios,
+ .maccatalyst => .maccatalyst,
+ .macos => .macos,
+ .tvos => .tvos,
+ .visionos => .visionos,
+ .watchos => .watchos,
+ .windows => .windows,
+ .uefi => .uefi,
+ .@"3ds" => .@"3ds",
+ .ps3 => .ps3,
+ .ps4 => .ps4,
+ .ps5 => .ps5,
+ .psp => .psp,
+ .vita => .vita,
+ .emscripten => .emscripten,
+ .wasi => .wasi,
+ .amdhsa => .amdhsa,
+ .amdpal => .amdpal,
+ .cuda => .cuda,
+ .mesa3d => .mesa3d,
+ .nvcl => .nvcl,
+ .opencl => .opencl,
+ .opengl => .opengl,
+ .vulkan => .vulkan,
+ .tios => .tios,
+
+ .default => null,
+ };
+ }
+ };
+
+ pub const ObjectFormat = enum(u4) {
+ c,
+ coff,
+ elf,
+ hex,
+ macho,
+ plan9,
+ raw,
+ spirv,
+ wasm,
+
+ default,
+
+ pub fn init(x: ?std.Target.ObjectFormat) @This() {
+ return switch (x orelse return .default) {
+ .c => .c,
+ .coff => .coff,
+ .elf => .elf,
+ .hex => .hex,
+ .macho => .macho,
+ .plan9 => .plan9,
+ .raw => .raw,
+ .spirv => .spirv,
+ .wasm => .wasm,
+ };
+ }
+
+ pub fn unwrap(this: @This()) ?std.Target.ObjectFormat {
+ return switch (this) {
+ .c => .c,
+ .coff => .coff,
+ .elf => .elf,
+ .hex => .hex,
+ .macho => .macho,
+ .plan9 => .plan9,
+ .raw => .raw,
+ .spirv => .spirv,
+ .wasm => .wasm,
+
+ .default => null,
+ };
+ }
+ };
+
+ pub const Flags = packed struct(u32) {
+ cpu_arch: CpuArch,
+ cpu_model: CpuModel,
+ cpu_features_add: bool,
+ cpu_features_sub: bool,
+ os_tag: OsTag,
+ abi: Abi,
+ object_format: ObjectFormat,
+ os_version_min: OsVersion.Tag,
+ os_version_max: OsVersion.Tag,
+ glibc_version: bool,
+ android_api_level: bool,
+ dynamic_linker: bool,
+ };
+
+ pub fn unwrap(tq: *const TargetQuery, c: *const Configuration) std.Target.Query {
+ const cpu_arch = tq.flags.cpu_arch.unwrap();
+ return .{
+ .cpu_arch = cpu_arch,
+ .cpu_model = switch (tq.flags.cpu_model) {
+ .native => .native,
+ .baseline => .baseline,
+ .determined_by_arch_os => .determined_by_arch_os,
+ .explicit => .{ .explicit = cpu_arch.?.parseCpuModel(tq.cpu_name.value.?.slice(c)).? },
+ },
+ .cpu_features_add = tq.cpu_features_add.value orelse .empty,
+ .cpu_features_sub = tq.cpu_features_sub.value orelse .empty,
+ .os_tag = tq.flags.os_tag.unwrap(),
+ .os_version_min = tq.os_version_min.u.unwrap(c),
+ .os_version_max = tq.os_version_max.u.unwrap(c),
+ .glibc_version = if (tq.glibc_version.value) |s|
+ std.SemanticVersion.parse(s.slice(c)) catch unreachable
+ else
+ null,
+ .android_api_level = tq.android_api_level.value,
+ .abi = tq.flags.abi.unwrap(),
+ .dynamic_linker = if (tq.dynamic_linker.value) |s| .init(s.slice(c)) else null,
+ .ofmt = tq.flags.object_format.unwrap(),
+ };
+ }
+};
+
+pub const Storage = enum {
+ flag_optional,
+ enum_optional,
+ extended,
+ length_prefixed_list,
+ flag_length_prefixed_list,
+ union_list,
+ flag_union,
+ multi_list,
+ flag_list,
+
+ /// The presence of the field is determined by a boolean within a packed
+ /// struct.
+ pub fn FlagOptional(
+ comptime flags_arg: @EnumLiteral(),
+ comptime flag_arg: @EnumLiteral(),
+ comptime ValueArg: type,
+ ) type {
+ return struct {
+ value: ?Value,
+
+ pub const storage: Storage = .flag_optional;
+ pub const flags = flags_arg;
+ pub const flag = flag_arg;
+ pub const Value = ValueArg;
+ };
+ }
+
+ /// The type of the field is determined by an enum within a packed struct.
+ pub fn FlagUnion(
+ comptime flags_arg: @EnumLiteral(),
+ comptime flag_arg: @EnumLiteral(),
+ comptime UnionArg: type,
+ ) type {
+ return struct {
+ u: Union,
+
+ pub const storage: Storage = .flag_union;
+ pub const flags = flags_arg;
+ pub const flag = flag_arg;
+ pub const Union = UnionArg;
+
+ pub const Tag = @typeInfo(Union).@"union".tag_type.?;
+ };
+ }
+
+ /// The field is present if an enum tag from flags matches a specific value.
+ pub fn EnumOptional(
+ comptime flags_arg: @EnumLiteral(),
+ comptime flag_arg: @EnumLiteral(),
+ comptime tag_arg: @EnumLiteral(),
+ comptime ValueArg: type,
+ ) type {
+ return struct {
+ value: ?Value,
+
+ pub const storage: Storage = .enum_optional;
+ pub const flags = flags_arg;
+ pub const flag = flag_arg;
+ pub const tag = tag_arg;
+ pub const Value = ValueArg;
+ };
+ }
+
+ /// The field indexes into an auxilary buffer, with the first element being
+ /// a packed struct that contains the tag.
+ pub fn Extended(comptime BaseFlags: type, comptime U: type) type {
+ return enum(u32) {
+ _,
+
+ pub const storage: Storage = .extended;
+
+ pub fn tag(this: @This(), c: *const Configuration) @FieldType(BaseFlags, "tag") {
+ const base_flags: BaseFlags = @bitCast(c.extra[@intFromEnum(this)]);
+ return base_flags.tag;
+ }
+
+ pub fn cast(this: @This(), c: *const Configuration, comptime S: type) ?S {
+ const wanted_tag = @typeInfo(S.Flags).@"struct".fields[0].defaultValue().?;
+ const base_flags: BaseFlags = @bitCast(c.extra[@intFromEnum(this)]);
+ if (base_flags.tag != wanted_tag) return null;
+ var i: usize = @intFromEnum(this);
+ return data(c.extra, &i, S);
+ }
+
+ pub fn get(this: @This(), buffer: []const u32) U {
+ var i: usize = @intFromEnum(this);
+ const base_flags: BaseFlags = @bitCast(buffer[i]);
+ return switch (base_flags.tag) {
+ inline else => |t| @unionInit(U, @tagName(t), data(buffer, &i, @FieldType(U, @tagName(t)))),
+ };
+ }
+ };
+ }
+
+ /// A field in flags determines whether the length is zero or nonzero. If
+ /// the length is nonzero, then there is a length field followed by the
+ /// list. The elements need well-defined memory layout but can otherwise be
+ /// any multiple of u32 length. The length is the number of elements, not
+ /// the number of u32s.
+ pub fn FlagLengthPrefixedList(
+ comptime flags_arg: @EnumLiteral(),
+ comptime flag_arg: @EnumLiteral(),
+ comptime ElemArg: type,
+ ) type {
+ return struct {
+ slice: []const Elem,
+
+ pub const storage: Storage = .flag_length_prefixed_list;
+ pub const flags = flags_arg;
+ pub const flag = flag_arg;
+ pub const Elem = ElemArg;
+
+ pub fn initErased(s: []const u32) @This() {
+ return .{ .slice = @ptrCast(s) };
+ }
+ };
+ }
+
+ /// The field contains a u32 length followed by that many items. Each
+ /// element needs well-defined memory layout but can otherwise be any
+ /// multiple of u32 length. The length is number of elements, not the
+ /// number of u32s.
+ pub fn LengthPrefixedList(comptime ElemArg: type) type {
+ return struct {
+ slice: []const Elem,
+
+ pub const storage: Storage = .length_prefixed_list;
+ pub const Elem = ElemArg;
+
+ pub fn initErased(s: []const u32) @This() {
+ return .{ .slice = @ptrCast(s) };
+ }
+ };
+ }
+
+ /// The field is a list whose length is an integer inside flags.
+ pub fn FlagList(
+ comptime flags_arg: @EnumLiteral(),
+ comptime flag_arg: @EnumLiteral(),
+ comptime ElemArg: type,
+ ) type {
+ return struct {
+ slice: []const Elem,
+
+ pub const storage: Storage = .flag_list;
+ pub const flags = flags_arg;
+ pub const flag = flag_arg;
+ pub const Elem = ElemArg;
+
+ pub fn initErased(s: []const u32) @This() {
+ return .{ .slice = @ptrCast(s) };
+ }
+ };
+ }
+
+ /// The field contains a u32 length followed by that many items for the
+ /// first field, that many items for the second field, etc.
+ pub fn MultiList(comptime ElemArg: type) type {
+ return struct {
+ mal: std.MultiArrayList(Elem),
+
+ pub const storage: Storage = .multi_list;
+ pub const Elem = ElemArg;
+ };
+ }
+
+ /// `UnionArg` is a tagged union with a small integer for the enum tag.
+ ///
+ /// A field in flags determines whether the metadata is present.
+ ///
+ /// The metadata is bit-packed consecutive packed struct which is the
+ /// `UnionArg` enum tag combined with a "last" marker boolean field.
+ /// When "last" is true, the element is the last one, providing
+ /// the length of the list.
+ ///
+ /// Following is each element of the list; each bitcastable to u32.
+ pub fn UnionList(
+ comptime flags_arg: @EnumLiteral(),
+ comptime flag_arg: @EnumLiteral(),
+ comptime UnionArg: type,
+ ) type {
+ return struct {
+ /// When serializing it is UnionArg slice pointer.
+ /// When deserializing it is extra index of first UnionArg element.
+ data: ?*const anyopaque,
+ len: usize,
+
+ pub const storage: Storage = .union_list;
+ pub const flags = flags_arg;
+ pub const flag = flag_arg;
+ pub const Union = UnionArg;
+
+ pub const Tag = @typeInfo(Union).@"union".tag_type.?;
+ pub const MetaInt = @Int(.unsigned, @bitSizeOf(Tag) + 1);
+ pub const Meta = packed struct(MetaInt) {
+ tag: Tag,
+ last: bool,
+ };
+
+ /// Valid to call only when serializing.
+ pub fn init(s: []const Union) @This() {
+ return .{ .data = s.ptr, .len = s.len };
+ }
+
+ /// Valid to call only when deserializing.
+ pub fn slice(this: *const @This(), extra: []const u32) []const u32 {
+ return extra[@intFromPtr(this.data)..][0..this.len];
+ }
+
+ /// Valid to call only when deserializing.
+ pub fn get(this: *const @This(), extra: []const u32, i: usize) Union {
+ const elem = slice(this, extra)[i];
+ return switch (this.tag(extra, i)) {
+ inline else => |comptime_tag| @unionInit(Union, @tagName(comptime_tag), @enumFromInt(elem)),
+ };
+ }
+
+ /// Valid to call only when deserializing.
+ pub fn tag(this: *const @This(), extra: []const u32, i: usize) Tag {
+ const start = @intFromPtr(this.data);
+ const meta_start = start - (this.len * @bitSizeOf(Meta) + 31) / 32;
+ return loadBits(u32, extra[meta_start..], i * @bitSizeOf(Meta), Meta).tag;
+ }
+
+ fn extraLen(len: usize) usize {
+ return len + (len * @bitSizeOf(Meta) + 31) / 32;
+ }
+ };
+ }
+
+ pub fn dataLength(buffer: []const u32, i: usize, comptime S: type) usize {
+ var end = i;
+ _ = data(buffer, &end, S);
+ return end - i;
+ }
+
+ pub fn data(buffer: []const u32, i: *usize, comptime T: type) T {
+ switch (@typeInfo(T)) {
+ .@"struct" => |info| {
+ var result: T = undefined;
+ inline for (info.fields) |field| {
+ @field(result, field.name) = dataField(buffer, i, &result, field.type);
+ }
+ return result;
+ },
+ .@"union" => |info| {
+ const flags: T.Flags = @bitCast(buffer[i.*]);
+ return switch (flags.tag) {
+ inline else => |comptime_tag| @unionInit(
+ T,
+ @tagName(comptime_tag),
+ data(buffer, i, info.fields[@intFromEnum(comptime_tag)].type),
+ ),
+ };
+ },
+ else => comptime unreachable,
+ }
+ }
+
+ fn dataField(buffer: []const u32, i: *usize, container: anytype, comptime Field: type) Field {
+ switch (@typeInfo(Field)) {
+ .void => return {},
+ .int => |info| switch (info.bits) {
+ 32 => {
+ defer i.* += 1;
+ return buffer[i.*];
+ },
+ 64 => {
+ defer i.* += 2;
+ return @bitCast(buffer[i.*..][0..2].*);
+ },
+ else => comptime unreachable,
+ },
+ .@"enum" => {
+ defer i.* += 1;
+ return @enumFromInt(buffer[i.*]);
+ },
+ .@"struct" => |info| switch (info.layout) {
+ .@"packed" => switch (info.backing_integer.?) {
+ u32 => {
+ defer i.* += 1;
+ return @bitCast(buffer[i.*]);
+ },
+ u64 => {
+ defer i.* += 2;
+ return @bitCast(buffer[i.*..][0..2].*);
+ },
+ else => comptime unreachable,
+ },
+ .auto => switch (Field) {
+ std.Target.Cpu.Feature.Set => {
+ const u32_count = (Field.usize_count * @sizeOf(usize)) / @sizeOf(u32);
+ defer i.* += u32_count;
+ return .{ .ints = @as(
+ *align(@alignOf(u32)) const [Field.usize_count]usize,
+ @ptrCast(buffer[i.*..][0..u32_count]),
+ ).* };
+ },
+ else => switch (Field.storage) {
+ .flag_optional => {
+ const flags = @field(container, @tagName(Field.flags));
+ const flag = @field(flags, @tagName(Field.flag));
+ return .{
+ .value = if (flag) dataField(buffer, i, container, Field.Value) else null,
+ };
+ },
+ .flag_union => {
+ const flags = @field(container, @tagName(Field.flags));
+ const tag: Field.Tag = @field(flags, @tagName(Field.flag));
+ return .{
+ .u = switch (tag) {
+ inline else => |comptime_tag| @unionInit(
+ Field.Union,
+ @tagName(comptime_tag),
+ dataField(
+ buffer,
+ i,
+ container,
+ @typeInfo(Field.Union).@"union".fields[@intFromEnum(comptime_tag)].type,
+ ),
+ ),
+ },
+ };
+ },
+ .enum_optional => {
+ const flags = @field(container, @tagName(Field.flags));
+ const tag = @field(flags, @tagName(Field.flag));
+ const match = tag == Field.tag;
+ return .{
+ .value = if (match) dataField(buffer, i, container, Field.Value) else null,
+ };
+ },
+ .extended => @compileError("unimplemented"),
+ .length_prefixed_list => {
+ const n = @divExact(@sizeOf(Field.Elem), @sizeOf(u32));
+ const data_start = i.* + 1;
+ const buf_len = buffer[data_start - 1] * n;
+ defer i.* = data_start + buf_len;
+ return .{ .slice = @ptrCast(buffer[data_start..][0..buf_len]) };
+ },
+ .flag_length_prefixed_list => {
+ const flags = @field(container, @tagName(Field.flags));
+ const flag = @field(flags, @tagName(Field.flag));
+ if (!flag) return .{ .slice = &.{} };
+ const n = @divExact(@sizeOf(Field.Elem), @sizeOf(u32));
+ const data_start = i.* + 1;
+ const buf_len = buffer[data_start - 1] * n;
+ defer i.* = data_start + buf_len;
+ return .{ .slice = @ptrCast(buffer[data_start..][0..buf_len]) };
+ },
+ .flag_list => {
+ const flags = @field(container, @tagName(Field.flags));
+ const len: u32 = @field(flags, @tagName(Field.flag));
+ const data_start = i.*;
+ defer i.* = data_start + len;
+ return .{ .slice = @ptrCast(buffer[data_start..][0..len]) };
+ },
+ .multi_list => {
+ const data_start = i.* + 1;
+ const len = buffer[data_start - 1];
+ defer i.* = data_start + len * @typeInfo(Field.Elem).@"struct".fields.len;
+ return .{ .mal = .{
+ .bytes = @ptrCast(@constCast(buffer[data_start..][0..len])),
+ .len = len,
+ .capacity = len,
+ } };
+ },
+ .union_list => {
+ const flags = @field(container, @tagName(Field.flags));
+ const flag = @field(flags, @tagName(Field.flag));
+ if (!flag) return .{ .data = null, .len = 0 };
+ const meta_start = i.*;
+ const meta_buffer = buffer[meta_start..];
+ var len: u32 = 0;
+ var bit_offset: usize = 0;
+ while (true) : (bit_offset += @bitSizeOf(Field.Meta)) {
+ const meta = loadBits(u32, meta_buffer, bit_offset, Field.Meta);
+ len += 1;
+ if (meta.last) break;
+ }
+ const end = meta_start + Field.extraLen(len);
+ i.* = end;
+ return .{ .data = @ptrFromInt(end - len), .len = len };
+ },
+ },
+ },
+ .@"extern" => {
+ const n = @divExact(@sizeOf(Field), @sizeOf(u32));
+ defer i.* += n;
+ return @bitCast(buffer[i.*..][0..n].*);
+ },
+ },
+ else => comptime unreachable,
+ }
+ }
+
+ /// Returns new end index.
+ fn setExtra(buffer: []u32, index: usize, extra: anytype) usize {
+ const fields = @typeInfo(@TypeOf(extra)).@"struct".fields;
+ var i = index;
+ inline for (fields) |field| {
+ i += setExtraField(buffer, i, field.type, @field(extra, field.name));
+ }
+ return i;
+ }
+
+ fn extraFieldLen(field: anytype) usize {
+ const Field = @TypeOf(field);
+ return switch (@typeInfo(Field)) {
+ .void => 0,
+ .int => |info| switch (info.bits) {
+ 32 => 1,
+ 64 => 2,
+ else => comptime unreachable,
+ },
+ .@"enum" => 1,
+ .@"struct" => |info| switch (info.layout) {
+ .@"packed" => switch (info.backing_integer.?) {
+ u32 => 1,
+ u64 => 2,
+ else => comptime unreachable,
+ },
+ .auto => switch (Field.storage) {
+ .flag_optional, .enum_optional => (@sizeOf(Field.Value) + 3) / 4,
+ .extended => 1,
+ .length_prefixed_list,
+ .flag_length_prefixed_list,
+ .flag_list,
+ => 1 + @divExact(@sizeOf(Field.Elem), @sizeOf(u32)) * field.slice.len,
+ .multi_list => 1 + field.mal.len * @typeInfo(Field.Elem).@"struct".fields.len,
+ .union_list => Field.extraLen(field.len),
+ .flag_union => switch (field.u) {
+ inline else => |v| extraFieldLen(v),
+ },
+ },
+ .@"extern" => @divExact(@sizeOf(Field), @sizeOf(u32)),
+ },
+ else => @compileError("bad type: " ++ @typeName(Field)),
+ };
+ }
+
+ fn extraLen(extra: anytype) usize {
+ const fields = @typeInfo(@TypeOf(extra)).@"struct".fields;
+ var i: usize = 0;
+ inline for (fields) |field| {
+ i += Storage.extraFieldLen(@field(extra, field.name));
+ }
+ return i;
+ }
+
+ inline fn setExtraField(buffer: []u32, i: usize, comptime Field: type, value: anytype) usize {
+ switch (@typeInfo(Field)) {
+ .void => return 0,
+ .int => |info| switch (info.bits) {
+ 32 => {
+ buffer[i] = value;
+ return 1;
+ },
+ 64 => {
+ buffer[i..][0..2].* = @bitCast(value);
+ return 2;
+ },
+ else => comptime unreachable,
+ },
+ .@"enum" => {
+ buffer[i] = @intFromEnum(value);
+ return 1;
+ },
+ .@"struct" => |info| switch (info.layout) {
+ .@"packed" => switch (info.backing_integer.?) {
+ u32 => {
+ buffer[i] = @bitCast(value);
+ return 1;
+ },
+ u64 => {
+ buffer[i..][0..2].* = @bitCast(value);
+ return 2;
+ },
+ else => comptime unreachable,
+ },
+ .auto => switch (Field) {
+ std.Target.Cpu.Feature.Set => {
+ const casted: []const u32 = @ptrCast(&value.ints);
+ @memcpy(buffer[i..][0..casted.len], casted);
+ return casted.len;
+ },
+ else => switch (Field.storage) {
+ .flag_optional, .enum_optional => {
+ return if (value.value) |v| setExtraField(buffer, i, Field.Value, v) else 0;
+ },
+ .flag_union => return switch (value.u) {
+ inline else => |x| setExtraField(buffer, i, @TypeOf(x), x),
+ },
+ .extended => @compileError("unimplemented"),
+ .flag_length_prefixed_list => {
+ const len: u32 = @intCast(value.slice.len);
+ if (len == 0) return 0; // Flag bit hides the length prefix.
+ buffer[i] = len;
+ const buf_len = len * @divExact(@sizeOf(Field.Elem), @sizeOf(u32));
+ @memcpy(buffer[i + 1 ..][0..buf_len], @as([]const u32, @ptrCast(value.slice)));
+ return 1 + buf_len;
+ },
+ .length_prefixed_list => {
+ const len: u32 = @intCast(value.slice.len);
+ buffer[i] = len;
+ const buf_len = len * @divExact(@sizeOf(Field.Elem), @sizeOf(u32));
+ @memcpy(buffer[i + 1 ..][0..buf_len], @as([]const u32, @ptrCast(value.slice)));
+ return 1 + buf_len;
+ },
+ .flag_list => {
+ const len: u32 = @intCast(value.slice.len);
+ @memcpy(buffer[i..][0..len], @as([]const u32, @ptrCast(value.slice)));
+ return len;
+ },
+ .multi_list => {
+ const len: u32 = @intCast(value.mal.len);
+ buffer[i] = len;
+ const fields = @typeInfo(Field.Elem).@"struct".fields;
+ inline for (0..fields.len) |field_i| @memcpy(
+ buffer[i + 1 + field_i * len ..][0..len],
+ @as([]const u32, @ptrCast(value.mal.items(@enumFromInt(field_i)))),
+ );
+ return 1 + fields.len * len;
+ },
+ .union_list => {
+ if (value.len == 0) return 0;
+ const Tag = @typeInfo(Field.Union).@"union".tag_type.?;
+ const slice_ptr: [*]const Field.Union = @ptrCast(@alignCast(value.data));
+ const slice = slice_ptr[0..value.len];
+ const meta_buffer = buffer[i..][0 .. (slice.len * @bitSizeOf(Field.Meta) + 31) / 32];
+ for (slice[0 .. slice.len - 1], 0..) |elem, elem_index| {
+ const union_tag: Tag = elem;
+ storeBits(u32, meta_buffer, elem_index * @bitSizeOf(Field.Meta), @as(Field.Meta, .{
+ .tag = union_tag,
+ .last = false,
+ }));
+ } else {
+ const elem_index = slice.len - 1;
+ const elem = slice[elem_index];
+ const union_tag: Tag = elem;
+ storeBits(u32, meta_buffer, elem_index * @bitSizeOf(Field.Meta), @as(Field.Meta, .{
+ .tag = union_tag,
+ .last = true,
+ }));
+ }
+ var total: usize = meta_buffer.len;
+ for (i + meta_buffer.len.., slice) |elem_index, src| switch (src) {
+ inline else => |x| total += setExtraField(buffer, elem_index, @TypeOf(x), x),
+ };
+ return total;
+ },
+ },
+ },
+ .@"extern" => {
+ const n = @divExact(@sizeOf(Field), @sizeOf(u32));
+ buffer[i..][0..n].* = @bitCast(value);
+ return n;
+ },
+ },
+ else => @compileError("bad field type: " ++ @typeName(Field)),
+ }
+ }
+};
+
+fn IndexType(comptime T: type) type {
+ return enum(u32) {
+ _,
+
+ pub fn get(this: @This(), c: *const Configuration) T {
+ return extraData(c, T, @intFromEnum(this));
+ }
+ };
+}
+
+pub fn extraData(c: *const Configuration, comptime T: type, index: usize) T {
+ var i: usize = index;
+ return Storage.data(c.extra, &i, T);
+}
+
+pub const LoadFileError = Io.File.Reader.Error || Allocator.Error || error{EndOfStream};
+
+pub fn loadFile(arena: Allocator, io: Io, file: Io.File) LoadFileError!Configuration {
+ var buffer: [2000]u8 = undefined;
+ var fr = file.reader(io, &buffer);
+ return load(arena, &fr.interface) catch |err| switch (err) {
+ error.ReadFailed => return fr.err.?,
+ else => |e| return e,
+ };
+}
+
+pub const LoadError = Io.Reader.Error || Allocator.Error;
+
+pub fn load(arena: Allocator, reader: *Io.Reader) LoadError!Configuration {
+ const header = try reader.takeStruct(Header, .native);
+ const result: Configuration = .{
+ .string_bytes = try arena.alloc(u8, header.string_bytes_len),
+ .steps = try arena.alloc(Step, header.steps_len),
+ .path_deps_sub = try arena.alloc(String, header.path_deps_len),
+ .path_deps_base = try arena.alloc(Path.Base, header.path_deps_len),
+ .unlazy_deps = try arena.alloc(String, header.unlazy_deps_len),
+ .system_integrations = try arena.alloc(SystemIntegration, header.system_integrations_len),
+ .available_options = try arena.alloc(AvailableOption, header.available_options_len),
+ .search_prefixes = try arena.alloc(String, header.search_prefixes_len),
+ .extra = try arena.alloc(u32, header.extra_len),
+ .default_step = header.default_step,
+ .generated_files_len = header.generated_files_len,
+ .poisoned = header.flags.poisoned,
+ };
+ var vecs = [_][]u8{
+ result.string_bytes,
+ @ptrCast(result.steps),
+ @ptrCast(result.path_deps_base),
+ @ptrCast(result.path_deps_sub),
+ @ptrCast(result.unlazy_deps),
+ @ptrCast(result.system_integrations),
+ @ptrCast(result.available_options),
+ @ptrCast(result.search_prefixes),
+ @ptrCast(result.extra),
+ };
+ try reader.readVecAll(&vecs);
+ return result;
+}
+
+pub fn loadBits(comptime Int: type, buffer: []const Int, bit_offset: usize, comptime Result: type) Result {
+ const index = bit_offset / @bitSizeOf(Int);
+ const small_bit_offset = bit_offset % @bitSizeOf(Int);
+ const ResultInt = @Int(.unsigned, @bitSizeOf(Result));
+ const result: ResultInt = @truncate(buffer[index] >> @intCast(small_bit_offset));
+ const available_bits = @bitSizeOf(Int) - small_bit_offset;
+ if (available_bits >= @bitSizeOf(ResultInt)) return @bitCast(result);
+ const missing_bits = @bitSizeOf(ResultInt) - available_bits;
+ const upper: ResultInt = @truncate(buffer[index + 1] & ((@as(usize, 1) << @intCast(missing_bits)) - 1));
+ return @bitCast(result | (upper << @intCast(available_bits)));
+}
+
+pub fn storeBits(comptime Int: type, buffer: []Int, bit_offset: usize, value: anytype) void {
+ const Value = @TypeOf(value);
+ const ValueInt = @Int(.unsigned, @bitSizeOf(Value));
+ const value_int: ValueInt = @bitCast(value);
+ const index = bit_offset / @bitSizeOf(Int);
+ const small_bit_offset = bit_offset % @bitSizeOf(Int);
+ const available_bits = @bitSizeOf(Int) - small_bit_offset;
+ if (available_bits >= @bitSizeOf(ValueInt)) {
+ buffer[index] &= ~(((@as(Int, 1) << @intCast(@bitSizeOf(Value))) - 1) << @intCast(small_bit_offset));
+ buffer[index] |= @as(Int, value_int) << @intCast(small_bit_offset);
+ } else {
+ const DoubleInt = @Int(.unsigned, @bitSizeOf(Int) * 2);
+ const ptr: *align(@alignOf(Int)) DoubleInt = @ptrCast(buffer[index..][0..2]);
+ ptr.* &= ~(((@as(DoubleInt, 1) << @intCast(@bitSizeOf(Value))) - 1) << @intCast(small_bit_offset));
+ ptr.* |= @as(DoubleInt, value_int) << @intCast(small_bit_offset);
+ }
+}
+
+test "loadBits and storeBits" {
+ var buffer: [2]u32 = .{
+ 0b01111111000000001111111100000000,
+ 0b11111111000000001111111100000100,
+ };
+ try std.testing.expectEqual(0b100, loadBits(u32, &buffer, 6, u3));
+ try std.testing.expectEqual(0b100011, loadBits(u32, &buffer, 29, u6));
+
+ storeBits(u32, &buffer, 6, @as(u3, 0b010));
+ storeBits(u32, &buffer, 29, @as(u6, 0b010010));
+
+ try std.testing.expectEqual(0b010, loadBits(u32, &buffer, 6, u3));
+ try std.testing.expectEqual(0b010010, loadBits(u32, &buffer, 29, u6));
+}
diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig
@@ -1,597 +0,0 @@
-const std = @import("../std.zig");
-const Io = std.Io;
-const Build = std.Build;
-const Cache = Build.Cache;
-const Step = std.Build.Step;
-const assert = std.debug.assert;
-const fatal = std.process.fatal;
-const Allocator = std.mem.Allocator;
-const log = std.log;
-const Coverage = std.debug.Coverage;
-const abi = Build.abi.fuzz;
-
-const Fuzz = @This();
-const build_runner = @import("root");
-
-gpa: Allocator,
-io: Io,
-mode: Mode,
-
-/// Allocated into `gpa`.
-run_steps: []const *Step.Run,
-
-group: Io.Group,
-root_prog_node: std.Progress.Node,
-prog_node: std.Progress.Node,
-
-/// Protects `coverage_files`.
-coverage_mutex: Io.Mutex,
-coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
-
-queue_mutex: Io.Mutex,
-queue_cond: Io.Condition,
-msg_queue: std.ArrayList(Msg),
-
-pub const Mode = union(enum) {
- forever: struct { ws: *Build.WebServer },
- limit: Limited,
-
- pub const Limited = struct {
- amount: u64,
- };
-};
-
-const Msg = union(enum) {
- coverage: struct {
- id: u64,
- cumulative: struct {
- runs: u64,
- unique: u64,
- coverage: u64,
- },
- run: *Step.Run,
- },
- entry_point: struct {
- coverage_id: u64,
- addr: u64,
- },
-};
-
-const CoverageMap = struct {
- mapped_memory: []align(std.heap.page_size_min) const u8,
- coverage: Coverage,
- source_locations: []Coverage.SourceLocation,
- /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
- entry_points: std.ArrayList(u32),
- start_timestamp: i64,
- start_n_runs: u64,
-
- fn deinit(cm: *CoverageMap, gpa: Allocator) void {
- std.posix.munmap(cm.mapped_memory);
- cm.coverage.deinit(gpa);
- cm.* = undefined;
- }
-};
-
-pub fn init(
- gpa: Allocator,
- io: Io,
- all_steps: []const *Build.Step,
- root_prog_node: std.Progress.Node,
- mode: Mode,
-) error{ OutOfMemory, Canceled }!Fuzz {
- const run_steps: []const *Step.Run = steps: {
- var steps: std.ArrayList(*Step.Run) = .empty;
- defer steps.deinit(gpa);
- const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0);
- defer rebuild_node.end();
- var rebuild_group: Io.Group = .init;
- defer rebuild_group.cancel(io);
-
- for (all_steps) |step| {
- const run = step.cast(Step.Run) orelse continue;
- if (run.producer == null) continue;
- if (run.fuzz_tests.items.len == 0) continue;
- try steps.append(gpa, run);
- rebuild_group.async(io, rebuildTestsWorkerRun, .{ run, gpa, rebuild_node });
- }
-
- if (steps.items.len == 0) fatal("no fuzz tests found", .{});
- rebuild_node.setEstimatedTotalItems(steps.items.len);
- const run_steps = try gpa.dupe(*Step.Run, steps.items);
- try rebuild_group.await(io);
- break :steps run_steps;
- };
- errdefer gpa.free(run_steps);
-
- for (run_steps) |run| {
- assert(run.fuzz_tests.items.len > 0);
- if (run.rebuilt_executable == null)
- fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
- }
-
- return .{
- .gpa = gpa,
- .io = io,
- .mode = mode,
- .run_steps = run_steps,
- .group = .init,
- .root_prog_node = root_prog_node,
- .prog_node = .none,
- .coverage_files = .empty,
- .coverage_mutex = .init,
- .queue_mutex = .init,
- .queue_cond = .init,
- .msg_queue = .empty,
- };
-}
-
-pub fn start(fuzz: *Fuzz) void {
- const io = fuzz.io;
- fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0);
-
- if (fuzz.mode == .forever) {
- // For polling messages and sending updates to subscribers.
- fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err|
- fatal("unable to spawn coverage task: {t}", .{err});
- }
-
- for (fuzz.run_steps) |run| {
- assert(run.rebuilt_executable != null);
- fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run });
- }
-}
-
-pub fn deinit(fuzz: *Fuzz) void {
- const io = fuzz.io;
- fuzz.group.cancel(io);
- fuzz.prog_node.end();
- fuzz.gpa.free(fuzz.run_steps);
-}
-
-fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) void {
- rebuildTestsWorkerRunFallible(run, gpa, parent_prog_node) catch |err| {
- const compile = run.producer.?;
- log.err("step '{s}': failed to rebuild in fuzz mode: {t}", .{ compile.step.name, err });
- };
-}
-
-fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) !void {
- const graph = run.step.owner.graph;
- const io = graph.io;
- const compile = run.producer.?;
- const prog_node = parent_prog_node.start(compile.step.name, 0);
- defer prog_node.end();
-
- const result = compile.rebuildInFuzzMode(gpa, prog_node);
-
- const show_compile_errors = compile.step.result_error_bundle.errorMessageCount() > 0;
- const show_error_msgs = compile.step.result_error_msgs.items.len > 0;
- const show_stderr = compile.step.result_stderr.len > 0;
-
- if (show_error_msgs or show_compile_errors or show_stderr) {
- var buf: [256]u8 = undefined;
- const stderr = try io.lockStderr(&buf, graph.stderr_mode);
- defer io.unlockStderr();
- build_runner.printErrorMessages(gpa, &compile.step, .{}, stderr.terminal(), .verbose, .indent) catch {};
- }
-
- const rebuilt_bin_path = result catch |err| switch (err) {
- error.MakeFailed => return,
- else => |other| return other,
- };
- run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename);
-}
-
-fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run) void {
- const owner = run.step.owner;
- const gpa = owner.allocator;
- const graph = owner.graph;
- const io = graph.io;
-
- run.rerunInFuzzMode(fuzz, fuzz.prog_node) catch |err| switch (err) {
- error.MakeFailed => {
- var buf: [256]u8 = undefined;
- const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) {
- error.Canceled => return,
- };
- defer io.unlockStderr();
- build_runner.printErrorMessages(gpa, &run.step, .{}, stderr.terminal(), .verbose, .indent) catch {};
- return;
- },
- else => {
- log.err("step '{s}': failed to rerun in fuzz mode: {t}", .{ run.step.name, err });
- return;
- },
- };
-}
-
-pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void {
- assert(fuzz.mode == .forever);
-
- var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa);
- defer arena_state.deinit();
- const arena = arena_state.allocator();
-
- const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
- var dedup_table: DedupTable = .empty;
- defer dedup_table.deinit(fuzz.gpa);
-
- for (fuzz.run_steps) |run_step| {
- const compile_inputs = run_step.producer.?.step.inputs.table;
- for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| {
- try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len);
- for (file_list.items) |sub_path| {
- if (!std.mem.endsWith(u8, sub_path, ".zig")) continue;
- const joined_path = try dir_path.join(arena, sub_path);
- dedup_table.putAssumeCapacity(joined_path, {});
- }
- }
- }
-
- const deduped_paths = dedup_table.keys();
- const SortContext = struct {
- pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
- _ = this;
- return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
- .lt => true,
- .gt => false,
- .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
- };
- }
- };
- std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
- return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths);
-}
-
-pub const Previous = struct {
- unique_runs: usize,
- entry_points: usize,
- sent_source_index: bool,
- pub const init: Previous = .{
- .unique_runs = 0,
- .entry_points = 0,
- .sent_source_index = false,
- };
-};
-pub fn sendUpdate(
- fuzz: *Fuzz,
- socket: *std.http.Server.WebSocket,
- prev: *Previous,
-) !void {
- const io = fuzz.io;
-
- try fuzz.coverage_mutex.lock(io);
- defer fuzz.coverage_mutex.unlock(io);
-
- const coverage_maps = fuzz.coverage_files.values();
- if (coverage_maps.len == 0) return;
- // TODO: handle multiple fuzz steps in the WebSocket packets
- const coverage_map = &coverage_maps[0];
- const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
- // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the
- // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the
- // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass
- // this data straight to the socket with sendfile...
- const seen_pcs = cov_header.seenBits();
- const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
- const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
- {
- if (!prev.sent_source_index) {
- prev.sent_source_index = true;
- // We need to send initial context.
- const header: abi.SourceIndexHeader = .{
- .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
- .files_len = @intCast(coverage_map.coverage.files.entries.len),
- .source_locations_len = @intCast(coverage_map.source_locations.len),
- .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
- .start_timestamp = coverage_map.start_timestamp,
- .start_n_runs = coverage_map.start_n_runs,
- };
- var iovecs: [5][]const u8 = .{
- @ptrCast(&header),
- @ptrCast(coverage_map.coverage.directories.keys()),
- @ptrCast(coverage_map.coverage.files.keys()),
- @ptrCast(coverage_map.source_locations),
- coverage_map.coverage.string_bytes.items,
- };
- try socket.writeMessageVec(&iovecs, .binary);
- }
-
- const header: abi.CoverageUpdateHeader = .{
- .n_runs = n_runs,
- .unique_runs = unique_runs,
- };
- var iovecs: [2][]const u8 = .{
- @ptrCast(&header),
- @ptrCast(seen_pcs),
- };
- try socket.writeMessageVec(&iovecs, .binary);
-
- prev.unique_runs = unique_runs;
- }
-
- if (prev.entry_points != coverage_map.entry_points.items.len) {
- const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len));
- var iovecs: [2][]const u8 = .{
- @ptrCast(&header),
- @ptrCast(coverage_map.entry_points.items),
- };
- try socket.writeMessageVec(&iovecs, .binary);
-
- prev.entry_points = coverage_map.entry_points.items.len;
- }
-}
-
-fn coverageRun(fuzz: *Fuzz) void {
- coverageRunCancelable(fuzz) catch |err| switch (err) {
- error.Canceled => return,
- };
-}
-
-fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void {
- const io = fuzz.io;
-
- try fuzz.queue_mutex.lock(io);
- defer fuzz.queue_mutex.unlock(io);
-
- while (true) {
- try fuzz.queue_cond.wait(io, &fuzz.queue_mutex);
- for (fuzz.msg_queue.items) |msg| switch (msg) {
- .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) {
- error.AlreadyReported => continue,
- error.Canceled => return,
- else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
- },
- .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
- error.AlreadyReported => continue,
- error.Canceled => return,
- else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
- },
- };
- fuzz.msg_queue.clearRetainingCapacity();
- }
-}
-fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void {
- assert(fuzz.mode == .forever);
- const ws = fuzz.mode.forever.ws;
- const gpa = fuzz.gpa;
- const io = fuzz.io;
-
- try fuzz.coverage_mutex.lock(io);
- defer fuzz.coverage_mutex.unlock(io);
-
- const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id);
- if (gop.found_existing) {
- // We are fuzzing the same executable with multiple threads.
- // Perhaps the same unit test; perhaps a different one. In any
- // case, since the coverage file is the same, we only have to
- // notice changes to that one file in order to learn coverage for
- // this particular executable.
- return;
- }
- errdefer _ = fuzz.coverage_files.pop();
-
- gop.value_ptr.* = .{
- .coverage = std.debug.Coverage.init,
- .mapped_memory = undefined, // populated below
- .source_locations = undefined, // populated below
- .entry_points = .empty,
- .start_timestamp = ws.now(),
- .start_n_runs = undefined, // populated below
- };
- errdefer gop.value_ptr.coverage.deinit(gpa);
-
- const rebuilt_exe_path = run_step.rebuilt_executable.?;
- const target = run_step.producer.?.rootModuleTarget();
- var debug_info = std.debug.Info.load(
- gpa,
- io,
- rebuilt_exe_path,
- &gop.value_ptr.coverage,
- target.ofmt,
- target.cpu.arch,
- ) catch |err| {
- log.err("step '{s}': failed to load debug information for '{f}': {t}", .{
- run_step.step.name, rebuilt_exe_path, err,
- });
- return error.AlreadyReported;
- };
- defer debug_info.deinit(gpa);
-
- const coverage_file_path: Build.Cache.Path = .{
- .root_dir = run_step.step.owner.cache_root,
- .sub_path = "v/" ++ std.fmt.hex(coverage_id),
- };
- var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
- log.err("step '{s}': failed to load coverage file '{f}': {t}", .{
- run_step.step.name, coverage_file_path, err,
- });
- return error.AlreadyReported;
- };
- defer coverage_file.close(io);
-
- const file_size = coverage_file.length(io) catch |err| {
- log.err("unable to check len of coverage file '{f}': {t}", .{ coverage_file_path, err });
- return error.AlreadyReported;
- };
-
- const mapped_memory = std.posix.mmap(
- null,
- file_size,
- .{ .READ = true },
- .{ .TYPE = .SHARED },
- coverage_file.handle,
- 0,
- ) catch |err| {
- log.err("failed to map coverage file '{f}': {t}", .{ coverage_file_path, err });
- return error.AlreadyReported;
- };
- gop.value_ptr.mapped_memory = mapped_memory;
-
- const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
- const pcs = header.pcAddrs();
- const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
- errdefer gpa.free(source_locations);
-
- // Unfortunately the PCs array that LLVM gives us from the 8-bit PC
- // counters feature is not sorted.
- var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty;
- defer sorted_pcs.deinit(gpa);
- try sorted_pcs.resize(gpa, pcs.len);
- @memcpy(sorted_pcs.items(.pc), pcs);
- for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i);
- sorted_pcs.sortUnstable(struct {
- addrs: []const u64,
-
- pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
- return ctx.addrs[a_index] < ctx.addrs[b_index];
- }
- }{ .addrs = sorted_pcs.items(.pc) });
-
- debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
- log.err("failed to resolve addresses to source locations: {t}", .{err});
- return error.AlreadyReported;
- };
-
- for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl;
- gop.value_ptr.source_locations = source_locations;
- gop.value_ptr.start_n_runs = header.n_runs;
-
- ws.notifyUpdate();
-}
-
-fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void {
- const io = fuzz.io;
-
- try fuzz.coverage_mutex.lock(io);
- defer fuzz.coverage_mutex.unlock(io);
-
- const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?;
- const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
- const pcs = header.pcAddrs();
-
- // Since this pcs list is unsorted, we must linear scan for the best index.
- const index = i: {
- var best: usize = 0;
- for (pcs[1..], 1..) |elem_addr, i| {
- if (elem_addr == addr) break :i i;
- if (elem_addr > addr) continue;
- if (elem_addr > pcs[best]) best = i;
- }
- break :i best;
- };
- if (index >= pcs.len) {
- log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
- addr, pcs[0], pcs[pcs.len - 1],
- });
- return error.AlreadyReported;
- }
- if (false) {
- const sl = coverage_map.source_locations[index];
- const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
- if (pcs.len == 1) {
- log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{
- addr, file_name, sl.line, sl.column,
- });
- } else if (index == 0) {
- log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{
- addr, file_name, sl.line, sl.column, pcs[index + 1],
- });
- } else if (index == pcs.len - 1) {
- log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{
- addr, file_name, sl.line, sl.column, index, pcs[index - 1],
- });
- } else {
- log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
- addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
- });
- }
- }
- try coverage_map.entry_points.append(fuzz.gpa, @intCast(index));
-}
-
-pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
- assert(fuzz.mode == .limit);
- const io = fuzz.io;
-
- try fuzz.group.await(io);
- fuzz.group = .init;
-
- std.debug.print("======= FUZZING REPORT =======\n", .{});
- for (fuzz.msg_queue.items) |msg| {
- if (msg != .coverage) continue;
-
- const cov = msg.coverage;
- const coverage_file_path: std.Build.Cache.Path = .{
- .root_dir = cov.run.step.owner.cache_root,
- .sub_path = "v/" ++ std.fmt.hex(cov.id),
- };
- var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
- fatal("step '{s}': failed to load coverage file '{f}': {t}", .{
- cov.run.step.name, coverage_file_path, err,
- });
- };
- defer coverage_file.close(io);
-
- const fuzz_abi = std.Build.abi.fuzz;
- var rbuf: [0x1000]u8 = undefined;
- var r = coverage_file.reader(io, &rbuf);
-
- var header: fuzz_abi.SeenPcsHeader = undefined;
- r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
- fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{
- cov.run.step.name, coverage_file_path, err,
- });
- };
-
- if (header.pcs_len == 0) {
- fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{
- cov.run.step.name, coverage_file_path,
- });
- }
-
- var seen_count: usize = 0;
- const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
- for (0..chunk_count) |_| {
- const seen = r.interface.takeInt(usize, .little) catch |err| {
- fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{
- cov.run.step.name, coverage_file_path, err,
- });
- };
- seen_count += @popCount(seen);
- }
-
- const seen_f: f64 = @floatFromInt(seen_count);
- const total_f: f64 = @floatFromInt(header.pcs_len);
- const ratio = seen_f / total_f;
- std.debug.print(
- \\Step: {s}
- \\Fuzz test: "{s}" ({x})
- \\Runs: {} -> {}
- \\Unique runs: {} -> {}
- \\Coverage: {}/{} -> {}/{} ({:.02}%)
- \\
- , .{
- cov.run.step.name,
- cov.run.fuzz_tests.items[0],
- cov.id,
- cov.cumulative.runs,
- header.n_runs,
- cov.cumulative.unique,
- header.unique_runs,
- cov.cumulative.coverage,
- header.pcs_len,
- seen_count,
- header.pcs_len,
- ratio * 100,
- });
-
- std.debug.print("------------------------------\n", .{});
- }
- std.debug.print(
- \\Values are accumulated across multiple runs when preserving the cache.
- \\==============================
- \\
- , .{});
-}
diff --git a/lib/std/Build/Module.zig b/lib/std/Build/Module.zig
@@ -1,3 +1,11 @@
+const Module = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const LazyPath = std.Build.LazyPath;
+const Step = std.Build.Step;
+const ArrayList = std.ArrayList;
+
/// The one responsible for creating this module.
owner: *std.Build,
root_source_file: ?LazyPath,
@@ -65,18 +73,8 @@ pub const SystemLib = struct {
preferred_link_mode: std.builtin.LinkMode,
search_strategy: SystemLib.SearchStrategy,
- pub const UsePkgConfig = enum {
- /// Don't use pkg-config, just pass -lfoo where foo is name.
- no,
- /// Try to get information on how to link the library from pkg-config.
- /// If that fails, fall back to passing -lfoo where foo is name.
- yes,
- /// Try to get information on how to link the library from pkg-config.
- /// If that fails, error out.
- force,
- };
-
- pub const SearchStrategy = enum { paths_first, mode_first, no_fallback };
+ pub const UsePkgConfig = std.Build.Configuration.SystemLib.UsePkgConfig;
+ pub const SearchStrategy = std.Build.Configuration.SystemLib.SearchStrategy;
};
pub const CSourceLanguage = enum {
@@ -91,7 +89,8 @@ pub const CSourceLanguage = enum {
/// Assembly with the C preprocessor
assembly_with_preprocessor,
- pub fn internalIdentifier(self: CSourceLanguage) []const u8 {
+ /// The value passed to "-x" CLI flag of Clang.
+ pub fn clangIdentifier(self: CSourceLanguage) [:0]const u8 {
return switch (self) {
.c => "c",
.cpp => "c++",
@@ -119,10 +118,10 @@ pub const CSourceFile = struct {
/// By default, determines language of each file individually based on its file extension
language: ?CSourceLanguage = null,
- pub fn dupe(file: CSourceFile, b: *std.Build) CSourceFile {
+ pub fn dupe(file: CSourceFile, graph: *const std.Build.Graph) CSourceFile {
return .{
- .file = file.file.dupe(b),
- .flags = b.dupeStrings(file.flags),
+ .file = file.file.dupe(graph),
+ .flags = graph.dupeStrings(file.flags),
.language = file.language,
};
}
@@ -146,12 +145,13 @@ pub const RcSourceFile = struct {
/// as `/I <resolved path>`.
include_paths: []const LazyPath = &.{},
- pub fn dupe(file: RcSourceFile, b: *std.Build) RcSourceFile {
- const include_paths = b.allocator.alloc(LazyPath, file.include_paths.len) catch @panic("OOM");
- for (include_paths, file.include_paths) |*dest, lazy_path| dest.* = lazy_path.dupe(b);
+ pub fn dupe(file: RcSourceFile, graph: *const std.Build.Graph) RcSourceFile {
+ const arena = graph.arena;
+ const include_paths = arena.alloc(LazyPath, file.include_paths.len) catch @panic("OOM");
+ for (include_paths, file.include_paths) |*dest, lazy_path| dest.* = lazy_path.dupe(graph);
return .{
- .file = file.file.dupe(b),
- .flags = b.dupeStrings(file.flags),
+ .file = file.file.dupe(graph),
+ .flags = graph.dupeStrings(file.flags),
.include_paths = include_paths,
};
}
@@ -166,33 +166,6 @@ pub const IncludeDir = union(enum) {
other_step: *Step.Compile,
config_header_step: *Step.ConfigHeader,
embed_path: LazyPath,
-
- pub fn appendZigProcessFlags(
- include_dir: IncludeDir,
- b: *std.Build,
- zig_args: *std.array_list.Managed([]const u8),
- asking_step: ?*Step,
- ) !void {
- const flag: []const u8, const lazy_path: LazyPath = switch (include_dir) {
- // zig fmt: off
- .path => |lp| .{ "-I", lp },
- .path_system => |lp| .{ "-isystem", lp },
- .path_after => |lp| .{ "-idirafter", lp },
- .framework_path => |lp| .{ "-F", lp },
- .framework_path_system => |lp| .{ "-iframework", lp },
- .config_header_step => |ch| .{ "-I", ch.getOutputDir() },
- .other_step => |comp| .{ "-I", comp.installed_headers_include_tree.?.getDirectory() },
- // zig fmt: on
- .embed_path => |lazy_path| {
- // Special case: this is a single arg.
- const resolved = lazy_path.getPath3(b, asking_step);
- const arg = b.fmt("--embed-dir={f}", .{resolved});
- return zig_args.append(arg);
- },
- };
- const resolved_str = try lazy_path.getPath3(b, asking_step).toString(b.graph.arena);
- return zig_args.appendSlice(&.{ flag, resolved_str });
- }
};
pub const LinkFrameworkOptions = struct {
@@ -268,13 +241,14 @@ pub fn init(
owner: *std.Build,
value: union(enum) { options: CreateOptions, existing: *const Module },
) void {
- const allocator = owner.allocator;
+ const graph = owner.graph;
+ const arena = graph.arena;
switch (value) {
.options => |options| {
m.* = .{
.owner = owner,
- .root_source_file = if (options.root_source_file) |lp| lp.dupe(owner) else null,
+ .root_source_file = if (options.root_source_file) |lp| lp.dupe(graph) else null,
.import_table = .empty,
.resolved_target = options.target,
.optimize = options.optimize,
@@ -305,7 +279,7 @@ pub fn init(
.no_builtin = options.no_builtin,
};
- m.import_table.ensureUnusedCapacity(allocator, options.imports.len) catch @panic("OOM");
+ m.import_table.ensureUnusedCapacity(arena, options.imports.len) catch @panic("OOM");
for (options.imports) |dep| {
m.import_table.putAssumeCapacity(dep.name, dep.module);
}
@@ -317,15 +291,18 @@ pub fn init(
}
pub fn create(owner: *std.Build, options: CreateOptions) *Module {
- const m = owner.allocator.create(Module) catch @panic("OOM");
+ const graph = owner.graph;
+ const arena = graph.arena;
+ const m = arena.create(Module) catch @panic("OOM");
m.init(owner, .{ .options = options });
return m;
}
/// Adds an existing module to be used with `@import`.
pub fn addImport(m: *Module, name: []const u8, module: *Module) void {
- const b = m.owner;
- m.import_table.put(b.allocator, b.dupe(name), module) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.import_table.put(arena, graph.dupeString(name), module) catch @panic("OOM");
}
/// Creates a new module and adds it to be used with `@import`.
@@ -365,7 +342,8 @@ pub fn linkSystemLibrary(
name: []const u8,
options: LinkSystemLibraryOptions,
) void {
- const b = m.owner;
+ const graph = m.owner.graph;
+ const arena = graph.arena;
const target = m.requireKnownTarget();
if (std.zig.target.isLibCLibName(target, name)) {
@@ -377,9 +355,9 @@ pub fn linkSystemLibrary(
return;
}
- m.link_objects.append(b.allocator, .{
+ m.link_objects.append(arena, .{
.system_lib = .{
- .name = b.dupe(name),
+ .name = graph.dupeString(name),
.needed = options.needed,
.weak = options.weak,
.use_pkg_config = options.use_pkg_config,
@@ -390,8 +368,9 @@ pub fn linkSystemLibrary(
}
pub fn linkFramework(m: *Module, name: []const u8, options: LinkFrameworkOptions) void {
- const b = m.owner;
- m.frameworks.put(b.allocator, b.dupe(name), options) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.frameworks.put(arena, graph.dupeString(name), options) catch @panic("OOM");
}
pub const AddCSourceFilesOptions = struct {
@@ -407,7 +386,8 @@ pub const AddCSourceFilesOptions = struct {
/// Handy when you have many non-Zig source files and want them all to have the same flags.
pub fn addCSourceFiles(m: *Module, options: AddCSourceFilesOptions) void {
const b = m.owner;
- const allocator = b.allocator;
+ const graph = m.owner.graph;
+ const arena = graph.arena;
for (options.files) |path| {
if (std.fs.path.isAbsolute(path)) {
@@ -418,48 +398,50 @@ pub fn addCSourceFiles(m: *Module, options: AddCSourceFilesOptions) void {
}
}
- const c_source_files = allocator.create(CSourceFiles) catch @panic("OOM");
+ const c_source_files = arena.create(CSourceFiles) catch @panic("OOM");
c_source_files.* = .{
.root = options.root orelse b.path(""),
.files = b.dupeStrings(options.files),
.flags = b.dupeStrings(options.flags),
.language = options.language,
};
- m.link_objects.append(allocator, .{ .c_source_files = c_source_files }) catch @panic("OOM");
+ m.link_objects.append(arena, .{ .c_source_files = c_source_files }) catch @panic("OOM");
}
pub fn addCSourceFile(m: *Module, source: CSourceFile) void {
- const b = m.owner;
- const allocator = b.allocator;
- const c_source_file = allocator.create(CSourceFile) catch @panic("OOM");
- c_source_file.* = source.dupe(b);
- m.link_objects.append(allocator, .{ .c_source_file = c_source_file }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ const c_source_file = arena.create(CSourceFile) catch @panic("OOM");
+ c_source_file.* = source.dupe(graph);
+ m.link_objects.append(arena, .{ .c_source_file = c_source_file }) catch @panic("OOM");
}
/// Resource files must have the extension `.rc`.
/// Can be called regardless of target. The .rc file will be ignored
/// if the target object format does not support embedded resources.
pub fn addWin32ResourceFile(m: *Module, source: RcSourceFile) void {
- const b = m.owner;
- const allocator = b.allocator;
+ const graph = m.owner.graph;
+ const arena = graph.arena;
const target = m.requireKnownTarget();
// Only the PE/COFF format has a Resource Table, so for any other target
// the resource file is ignored.
if (target.ofmt != .coff) return;
- const rc_source_file = allocator.create(RcSourceFile) catch @panic("OOM");
- rc_source_file.* = source.dupe(b);
- m.link_objects.append(allocator, .{ .win32_resource_file = rc_source_file }) catch @panic("OOM");
+ const rc_source_file = arena.create(RcSourceFile) catch @panic("OOM");
+ rc_source_file.* = source.dupe(graph);
+ m.link_objects.append(arena, .{ .win32_resource_file = rc_source_file }) catch @panic("OOM");
}
pub fn addAssemblyFile(m: *Module, source: LazyPath) void {
- const b = m.owner;
- m.link_objects.append(b.allocator, .{ .assembly_file = source.dupe(b) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.link_objects.append(arena, .{ .assembly_file = source.dupe(graph) }) catch @panic("OOM");
}
pub fn addObjectFile(m: *Module, object: LazyPath) void {
- const b = m.owner;
- m.link_objects.append(b.allocator, .{ .static_path = object.dupe(b) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.link_objects.append(arena, .{ .static_path = object.dupe(graph) }) catch @panic("OOM");
}
pub fn addObject(m: *Module, object: *Step.Compile) void {
@@ -473,55 +455,63 @@ pub fn linkLibrary(m: *Module, library: *Step.Compile) void {
}
pub fn addAfterIncludePath(m: *Module, lazy_path: LazyPath) void {
- const b = m.owner;
- m.include_dirs.append(b.allocator, .{ .path_after = lazy_path.dupe(b) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.include_dirs.append(arena, .{ .path_after = lazy_path.dupe(graph) }) catch @panic("OOM");
}
pub fn addSystemIncludePath(m: *Module, lazy_path: LazyPath) void {
- const b = m.owner;
- m.include_dirs.append(b.allocator, .{ .path_system = lazy_path.dupe(b) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.include_dirs.append(arena, .{ .path_system = lazy_path.dupe(graph) }) catch @panic("OOM");
}
pub fn addIncludePath(m: *Module, lazy_path: LazyPath) void {
- const b = m.owner;
- m.include_dirs.append(b.allocator, .{ .path = lazy_path.dupe(b) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.include_dirs.append(arena, .{ .path = lazy_path.dupe(graph) }) catch @panic("OOM");
}
pub fn addConfigHeader(m: *Module, config_header: *Step.ConfigHeader) void {
- const allocator = m.owner.allocator;
- m.include_dirs.append(allocator, .{ .config_header_step = config_header }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.include_dirs.append(arena, .{ .config_header_step = config_header }) catch @panic("OOM");
}
pub fn addSystemFrameworkPath(m: *Module, directory_path: LazyPath) void {
- const b = m.owner;
- m.include_dirs.append(b.allocator, .{ .framework_path_system = directory_path.dupe(b) }) catch
- @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.include_dirs.append(arena, .{ .framework_path_system = directory_path.dupe(graph) }) catch @panic("OOM");
}
pub fn addFrameworkPath(m: *Module, directory_path: LazyPath) void {
- const b = m.owner;
- m.include_dirs.append(b.allocator, .{ .framework_path = directory_path.dupe(b) }) catch
- @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.include_dirs.append(arena, .{ .framework_path = directory_path.dupe(graph) }) catch @panic("OOM");
}
pub fn addEmbedPath(m: *Module, lazy_path: LazyPath) void {
- const b = m.owner;
- m.include_dirs.append(b.allocator, .{ .embed_path = lazy_path.dupe(b) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.include_dirs.append(arena, .{ .embed_path = lazy_path.dupe(graph) }) catch @panic("OOM");
}
pub fn addLibraryPath(m: *Module, directory_path: LazyPath) void {
- const b = m.owner;
- m.lib_paths.append(b.allocator, directory_path.dupe(b)) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.lib_paths.append(arena, directory_path.dupe(graph)) catch @panic("OOM");
}
pub fn addRPath(m: *Module, directory_path: LazyPath) void {
- const b = m.owner;
- m.rpaths.append(b.allocator, .{ .lazy_path = directory_path.dupe(b) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.rpaths.append(arena, .{ .lazy_path = directory_path.dupe(graph) }) catch @panic("OOM");
}
pub fn addRPathSpecial(m: *Module, bytes: []const u8) void {
- const b = m.owner;
- m.rpaths.append(b.allocator, .{ .special = b.dupe(bytes) }) catch @panic("OOM");
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.rpaths.append(arena, .{ .special = graph.dupeString(bytes) }) catch @panic("OOM");
}
/// Equvialent to the following C code, applied to all C source files owned by
@@ -532,130 +522,23 @@ pub fn addRPathSpecial(m: *Module, bytes: []const u8) void {
/// `name` and `value` need not live longer than the function call.
pub fn addCMacro(m: *Module, name: []const u8, value: []const u8) void {
const b = m.owner;
- m.c_macros.append(b.allocator, b.fmt("-D{s}={s}", .{ name, value })) catch @panic("OOM");
-}
-
-pub fn appendZigProcessFlags(
- m: *Module,
- zig_args: *std.array_list.Managed([]const u8),
- asking_step: ?*Step,
-) !void {
- const b = m.owner;
-
- try addFlag(zig_args, m.strip, "-fstrip", "-fno-strip");
- try addFlag(zig_args, m.single_threaded, "-fsingle-threaded", "-fno-single-threaded");
- try addFlag(zig_args, m.stack_check, "-fstack-check", "-fno-stack-check");
- try addFlag(zig_args, m.stack_protector, "-fstack-protector", "-fno-stack-protector");
- try addFlag(zig_args, m.omit_frame_pointer, "-fomit-frame-pointer", "-fno-omit-frame-pointer");
- try addFlag(zig_args, m.error_tracing, "-ferror-tracing", "-fno-error-tracing");
- try addFlag(zig_args, m.sanitize_thread, "-fsanitize-thread", "-fno-sanitize-thread");
- try addFlag(zig_args, m.fuzz, "-ffuzz", "-fno-fuzz");
- try addFlag(zig_args, m.valgrind, "-fvalgrind", "-fno-valgrind");
- try addFlag(zig_args, m.pic, "-fPIC", "-fno-PIC");
- try addFlag(zig_args, m.red_zone, "-mred-zone", "-mno-red-zone");
- try addFlag(zig_args, m.no_builtin, "-fno-builtin", "-fbuiltin");
-
- if (m.sanitize_c) |sc| switch (sc) {
- .off => try zig_args.append("-fno-sanitize-c"),
- .trap => try zig_args.append("-fsanitize-c=trap"),
- .full => try zig_args.append("-fsanitize-c=full"),
- };
-
- if (m.dwarf_format) |dwarf_format| {
- try zig_args.append(switch (dwarf_format) {
- .@"32" => "-gdwarf32",
- .@"64" => "-gdwarf64",
- });
- }
-
- if (m.unwind_tables) |unwind_tables| {
- try zig_args.append(switch (unwind_tables) {
- .none => "-fno-unwind-tables",
- .sync => "-funwind-tables",
- .async => "-fasync-unwind-tables",
- });
- }
-
- try zig_args.ensureUnusedCapacity(1);
- if (m.optimize) |optimize| switch (optimize) {
- .Debug => zig_args.appendAssumeCapacity("-ODebug"),
- .ReleaseSmall => zig_args.appendAssumeCapacity("-OReleaseSmall"),
- .ReleaseFast => zig_args.appendAssumeCapacity("-OReleaseFast"),
- .ReleaseSafe => zig_args.appendAssumeCapacity("-OReleaseSafe"),
- };
-
- if (m.code_model != .default) {
- try zig_args.append("-mcmodel");
- try zig_args.append(@tagName(m.code_model));
- }
-
- if (m.resolved_target) |*target| {
- // Communicate the query via CLI since it's more compact.
- if (!target.query.isNative()) {
- try zig_args.appendSlice(&.{
- "-target", try target.query.zigTriple(b.allocator),
- "-mcpu", try target.query.serializeCpuAlloc(b.allocator),
- });
- if (target.query.dynamic_linker) |*dynamic_linker| {
- if (dynamic_linker.get()) |dynamic_linker_path| {
- try zig_args.append("--dynamic-linker");
- try zig_args.append(dynamic_linker_path);
- } else {
- try zig_args.append("--no-dynamic-linker");
- }
- }
- }
- }
-
- for (m.export_symbol_names) |symbol_name| {
- try zig_args.append(b.fmt("--export={s}", .{symbol_name}));
- }
-
- for (m.include_dirs.items) |include_dir| {
- try include_dir.appendZigProcessFlags(b, zig_args, asking_step);
- }
-
- try zig_args.appendSlice(m.c_macros.items);
-
- try zig_args.ensureUnusedCapacity(2 * m.lib_paths.items.len);
- for (m.lib_paths.items) |lib_path| {
- zig_args.appendAssumeCapacity("-L");
- zig_args.appendAssumeCapacity(lib_path.getPath2(b, asking_step));
- }
-
- try zig_args.ensureUnusedCapacity(2 * m.rpaths.items.len);
- for (m.rpaths.items) |rpath| switch (rpath) {
- .lazy_path => |lp| {
- zig_args.appendAssumeCapacity("-rpath");
- zig_args.appendAssumeCapacity(lp.getPath2(b, asking_step));
- },
- .special => |bytes| {
- zig_args.appendAssumeCapacity("-rpath");
- zig_args.appendAssumeCapacity(bytes);
- },
- };
-}
-
-fn addFlag(
- args: *std.array_list.Managed([]const u8),
- opt: ?bool,
- then_name: []const u8,
- else_name: []const u8,
-) !void {
- const cond = opt orelse return;
- return args.append(if (cond) then_name else else_name);
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+ m.c_macros.append(arena, b.fmt("-D{s}={s}", .{ name, value })) catch @panic("OOM");
}
fn linkLibraryOrObject(m: *Module, other: *Step.Compile) void {
- const allocator = m.owner.allocator;
+ const graph = m.owner.graph;
+ const arena = graph.arena;
+
_ = other.getEmittedBin(); // Indicate there is a dependency on the outputted binary.
if (other.rootModuleTarget().os.tag == .windows and other.isDynamicLibrary()) {
_ = other.getEmittedImplib(); // Indicate dependency on the outputted implib.
}
- m.link_objects.append(allocator, .{ .other_step = other }) catch @panic("OOM");
- m.include_dirs.append(allocator, .{ .other_step = other }) catch @panic("OOM");
+ m.link_objects.append(arena, .{ .other_step = other }) catch @panic("OOM");
+ m.include_dirs.append(arena, .{ .other_step = other }) catch @panic("OOM");
}
fn requireKnownTarget(m: *Module) *const std.Target {
@@ -670,11 +553,9 @@ pub const Graph = struct {
names: []const []const u8,
};
-/// Intended to be used during the make phase only.
-///
-/// Given that `root` is the root `Module` of a compilation, return all `Module`s
-/// in the module graph, including `root` itself. `root` is guaranteed to be the
-/// first module in the returned slice.
+/// Given that `root` is the root `Module` of a compilation, return all
+/// `Module` in the module graph, including `root` itself. `root` is guaranteed
+/// to be the first module in the returned slice.
pub fn getGraph(root: *Module) Graph {
if (root.cached_graph.modules.len != 0) {
return root.cached_graph;
@@ -703,10 +584,3 @@ pub fn getGraph(root: *Module) Graph {
root.cached_graph = result;
return result;
}
-
-const Module = @This();
-const std = @import("std");
-const assert = std.debug.assert;
-const LazyPath = std.Build.LazyPath;
-const Step = std.Build.Step;
-const ArrayList = std.ArrayList;
diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig
@@ -1,33 +1,15 @@
const Step = @This();
-const builtin = @import("builtin");
const std = @import("../std.zig");
-const Io = std.Io;
const Build = std.Build;
-const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
-const Cache = Build.Cache;
-const Path = Cache.Path;
-const ArrayList = std.ArrayList;
+const Configuration = std.Build.Configuration;
-id: Id,
+tag: Configuration.Step.Tag,
name: []const u8,
owner: *Build,
-makeFn: MakeFn,
-dependencies: std.array_list.Managed(*Step),
-/// This field is empty during execution of the user's build script, and
-/// then populated during dependency loop checking in the build runner.
-dependants: ArrayList(*Step),
-/// Collects the set of files that retrigger this step to run.
-///
-/// This is used by the build system's implementation of `--watch` but it can
-/// also be potentially useful for IDEs to know what effects editing a
-/// particular file has.
-///
-/// Populated within `make`. Implementation may choose to clear and repopulate,
-/// retain previous value, or update.
-inputs: Inputs,
+dependencies: std.ArrayList(*Step),
/// Set this field to declare an upper bound on the amount of bytes of memory it will
/// take to run the step. Zero means no limit.
@@ -50,181 +32,60 @@ inputs: Inputs,
/// total system memory available.
max_rss: usize,
-state: State,
-pending_deps: u32,
-
-result_error_msgs: ArrayList([]const u8),
-result_error_bundle: std.zig.ErrorBundle,
-result_stderr: []const u8,
-result_cached: bool,
-result_duration_ns: ?u64,
-/// 0 means unavailable or not reported.
-result_peak_rss: usize,
-/// If the step is failed and this field is populated, this is the command which failed.
-/// This field may be populated even if the step succeeded.
-result_failed_command: ?[]const u8,
-test_results: TestResults,
-
/// The return address associated with creation of this step that can be useful
/// to print along with debugging messages.
debug_stack_trace: std.debug.StackTrace,
-pub const TestResults = struct {
- /// The total number of tests in the step. Every test has a "status" from the following:
- /// * passed
- /// * skipped
- /// * failed cleanly
- /// * crashed
- /// * timed out
- test_count: u32 = 0,
-
- /// The number of tests which were skipped (`error.SkipZigTest`).
- skip_count: u32 = 0,
- /// The number of tests which failed cleanly.
- fail_count: u32 = 0,
- /// The number of tests which terminated unexpectedly, i.e. crashed.
- crash_count: u32 = 0,
- /// The number of tests which timed out.
- timeout_count: u32 = 0,
-
- /// The number of detected memory leaks. The associated test may still have passed; indeed, *all*
- /// individual tests may have passed. However, the step as a whole fails if any test has leaks.
- leak_count: u32 = 0,
- /// The number of detected error logs. The associated test may still have passed; indeed, *all*
- /// individual tests may have passed. However, the step as a whole fails if any test logs errors.
- log_err_count: u32 = 0,
-
- pub fn isSuccess(tr: TestResults) bool {
- // all steps are success or skip
- return tr.fail_count == 0 and
- tr.crash_count == 0 and
- tr.timeout_count == 0 and
- // no (otherwise successful) step leaked memory or logged errors
- tr.leak_count == 0 and
- tr.log_err_count == 0;
- }
-
- /// Computes the number of tests which passed from the other values.
- pub fn passCount(tr: TestResults) u32 {
- return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count;
- }
-};
-
-pub const MakeOptions = struct {
- progress_node: std.Progress.Node,
- watch: bool,
- web_server: ?*Build.WebServer,
- /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds.
- unit_test_timeout_ns: ?u64,
- /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`.
- gpa: Allocator,
-};
-
-pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void;
-
-pub const State = enum {
- precheck_unstarted,
- precheck_started,
- /// This is also used to indicate "dirty" steps that have been modified
- /// after a previous build completed, in which case, the step may or may
- /// not have been completed before. Either way, one or more of its direct
- /// file system inputs have been modified, meaning that the step needs to
- /// be re-evaluated.
- precheck_done,
- dependency_failure,
- success,
- failure,
- /// This state indicates that the step did not complete, however, it also did not fail,
- /// and it is safe to continue executing its dependencies.
- skipped,
- /// This step was skipped because it specified a max_rss that exceeded the runner's maximum.
- /// It is not safe to run its dependencies.
- skipped_oom,
-};
-
-pub const Id = enum {
- top_level,
- compile,
- install_artifact,
- install_file,
- install_dir,
- remove_dir,
- fail,
- fmt,
- translate_c,
- write_file,
- update_source_files,
- run,
- check_file,
- check_object,
- config_header,
- objcopy,
- options,
- custom,
-
- pub fn Type(comptime id: Id) type {
- return switch (id) {
- .top_level => Build.TopLevelStep,
- .compile => Compile,
- .install_artifact => InstallArtifact,
- .install_file => InstallFile,
- .install_dir => InstallDir,
- .fail => Fail,
- .fmt => Fmt,
- .translate_c => TranslateC,
- .write_file => WriteFile,
- .update_source_files => UpdateSourceFiles,
- .run => Run,
- .check_file => CheckFile,
- .config_header => ConfigHeader,
- .objcopy => ObjCopy,
- .options => Options,
- .custom => @compileError("no type available for custom step"),
- };
- }
-};
+pub const Tag = Configuration.Step.Tag;
+
+pub fn Type(comptime tag: Tag) type {
+ return switch (tag) {
+ .check_file => CheckFile,
+ .compile => Compile,
+ .config_header => ConfigHeader,
+ .fail => Fail,
+ .find_program => FindProgram,
+ .fmt => Fmt,
+ .install_artifact => InstallArtifact,
+ .install_dir => InstallDir,
+ .install_file => InstallFile,
+ .obj_copy => ObjCopy,
+ .options => Options,
+ .run => Run,
+ .top_level => TopLevel,
+ .translate_c => TranslateC,
+ .update_source_files => UpdateSourceFiles,
+ .write_file => WriteFile,
+ };
+}
pub const CheckFile = @import("Step/CheckFile.zig");
+pub const Compile = @import("Step/Compile.zig");
pub const ConfigHeader = @import("Step/ConfigHeader.zig");
pub const Fail = @import("Step/Fail.zig");
+pub const FindProgram = @import("Step/FindProgram.zig");
pub const Fmt = @import("Step/Fmt.zig");
pub const InstallArtifact = @import("Step/InstallArtifact.zig");
pub const InstallDir = @import("Step/InstallDir.zig");
pub const InstallFile = @import("Step/InstallFile.zig");
pub const ObjCopy = @import("Step/ObjCopy.zig");
-pub const Compile = @import("Step/Compile.zig");
pub const Options = @import("Step/Options.zig");
pub const Run = @import("Step/Run.zig");
pub const TranslateC = @import("Step/TranslateC.zig");
-pub const WriteFile = @import("Step/WriteFile.zig");
pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
+pub const WriteFile = @import("Step/WriteFile.zig");
-pub const Inputs = struct {
- table: Table,
-
- pub const init: Inputs = .{
- .table = .{},
- };
-
- pub const Table = std.ArrayHashMapUnmanaged(Build.Cache.Path, Files, Build.Cache.Path.TableAdapter, false);
- /// The special file name "." means any changes inside the directory.
- pub const Files = ArrayList([]const u8);
-
- pub fn populated(inputs: *Inputs) bool {
- return inputs.table.count() != 0;
- }
+pub const TopLevel = struct {
+ pub const base_tag: Step.Tag = .top_level;
- pub fn clear(inputs: *Inputs, gpa: Allocator) void {
- for (inputs.table.values()) |*files| files.deinit(gpa);
- inputs.table.clearRetainingCapacity();
- }
+ step: Step,
+ description: []const u8,
};
pub const StepOptions = struct {
- id: Id,
+ tag: Tag,
name: []const u8,
owner: *Build,
- makeFn: MakeFn = makeNoOp,
first_ret_addr: ?usize = null,
max_rss: usize = 0,
};
@@ -233,97 +94,31 @@ pub fn init(options: StepOptions) Step {
const arena = options.owner.allocator;
return .{
- .id = options.id,
+ .tag = options.tag,
.name = arena.dupe(u8, options.name) catch @panic("OOM"),
.owner = options.owner,
- .makeFn = options.makeFn,
- .dependencies = std.array_list.Managed(*Step).init(arena),
- .dependants = .empty,
- .inputs = Inputs.init,
- .state = .precheck_unstarted,
- .pending_deps = undefined, // initialized by build runner
+ .dependencies = .empty,
.max_rss = options.max_rss,
.debug_stack_trace = blk: {
const addr_buf = arena.alloc(usize, options.owner.debug_stack_frames_count) catch @panic("OOM");
const first_ret_addr = options.first_ret_addr orelse @returnAddress();
break :blk std.debug.captureCurrentStackTrace(.{ .first_address = first_ret_addr }, addr_buf);
},
- .result_error_msgs = .empty,
- .result_error_bundle = std.zig.ErrorBundle.empty,
- .result_stderr = "",
- .result_cached = false,
- .result_duration_ns = null,
- .result_peak_rss = 0,
- .result_failed_command = null,
- .test_results = .{},
- };
-}
-
-/// If the Step's `make` function reports `error.MakeFailed`, it indicates they
-/// have already reported the error. Otherwise, we add a simple error report
-/// here.
-pub fn make(s: *Step, options: MakeOptions) error{ MakeFailed, MakeSkipped }!void {
- const arena = s.owner.allocator;
- const graph = s.owner.graph;
- const io = graph.io;
-
- var start_ts: ?Io.Timestamp = t: {
- if (!graph.time_report) break :t null;
- if (s.id == .compile) break :t null;
- if (s.id == .run and s.cast(Run).?.stdio == .zig_test) break :t null;
- break :t Io.Clock.awake.now(io);
- };
- const make_result = s.makeFn(s, options);
- if (start_ts) |*ts| {
- const duration = ts.untilNow(io, .awake);
- options.web_server.?.updateTimeReportGeneric(s, duration);
- }
-
- make_result catch |err| switch (err) {
- error.MakeFailed, error.MakeSkipped => |e| return e,
- else => {
- s.result_error_msgs.append(arena, @errorName(err)) catch @panic("OOM");
- return error.MakeFailed;
- },
};
-
- if (!s.test_results.isSuccess()) {
- return error.MakeFailed;
- }
-
- if (s.max_rss != 0 and s.result_peak_rss > s.max_rss) {
- const msg = std.fmt.allocPrint(arena, "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", .{
- s.result_peak_rss, s.max_rss,
- }) catch @panic("OOM");
- s.result_error_msgs.append(arena, msg) catch @panic("OOM");
- }
}
pub fn dependOn(step: *Step, other: *Step) void {
- step.dependencies.append(other) catch @panic("OOM");
-}
-
-fn makeNoOp(step: *Step, options: MakeOptions) anyerror!void {
- _ = options;
-
- var all_cached = true;
-
- for (step.dependencies.items) |dep| {
- all_cached = all_cached and dep.result_cached;
- }
-
- step.result_cached = all_cached;
+ const arena = step.owner.allocator;
+ step.dependencies.append(arena, other) catch @panic("OOM");
}
pub fn cast(step: *Step, comptime T: type) ?*T {
- if (step.id == T.base_id) {
- return @fieldParentPtr("step", step);
- }
+ if (step.tag == T.base_tag) return @fieldParentPtr("step", step);
return null;
}
/// For debugging purposes, prints identifying information about this Step.
-pub fn dump(step: *Step, t: Io.Terminal) void {
+pub fn dump(step: *Step, t: std.Io.Terminal) void {
const w = t.writer;
if (step.debug_stack_trace.return_addresses.len > 0) {
w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {};
@@ -337,682 +132,20 @@ pub fn dump(step: *Step, t: Io.Terminal) void {
}
}
-/// Populates `s.result_failed_command`.
-pub fn captureChildProcess(
- s: *Step,
- gpa: Allocator,
- progress_node: std.Progress.Node,
- argv: []const []const u8,
-) !std.process.RunResult {
- const graph = s.owner.graph;
- const arena = graph.arena;
- const io = graph.io;
-
- // If an error occurs, it's happened in this command:
- assert(s.result_failed_command == null);
- s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv);
-
- try handleChildProcUnsupported(s);
- try handleVerbose(s.owner, .inherit, argv);
-
- const result = std.process.run(arena, io, .{
- .argv = argv,
- .environ_map = &graph.environ_map,
- .progress_node = progress_node,
- }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err });
-
- if (result.stderr.len > 0) {
- try s.result_error_msgs.append(arena, result.stderr);
- }
-
- return result;
-}
-
-pub fn fail(step: *Step, comptime fmt: []const u8, args: anytype) error{ OutOfMemory, MakeFailed } {
- try step.addError(fmt, args);
- return error.MakeFailed;
-}
-
-pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void {
- const arena = step.owner.allocator;
- const msg = try std.fmt.allocPrint(arena, fmt, args);
- try step.result_error_msgs.append(arena, msg);
-}
-
-pub const ZigProcess = struct {
- child: std.process.Child,
- multi_reader_buffer: Io.File.MultiReader.Buffer(2),
- multi_reader: Io.File.MultiReader,
- progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn,
-
- pub const StreamEnum = enum { stdout, stderr };
-
- pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void {
- zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null;
- }
-
- pub fn deinit(zp: *ZigProcess, io: Io) void {
- zp.child.kill(io);
- zp.multi_reader.deinit();
- zp.* = undefined;
- }
-};
-
-/// Assumes that argv contains `--listen=-` and that the process being spawned
-/// is the zig compiler - the same version that compiled the build runner.
-/// Populates `s.result_failed_command`.
-pub fn evalZigProcess(
- s: *Step,
- argv: []const []const u8,
- prog_node: std.Progress.Node,
- watch: bool,
- web_server: ?*Build.WebServer,
- gpa: Allocator,
-) !?Path {
- const b = s.owner;
- const io = b.graph.io;
-
- // If an error occurs, it's happened in this command:
- assert(s.result_failed_command == null);
- s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv);
-
- if (s.getZigProcess()) |zp| update: {
- assert(watch);
- if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index);
- zp.progress_ipc_index = null;
- var exited = false;
- defer if (exited) {
- s.cast(Compile).?.zig_process = null;
- zp.deinit(io);
- gpa.destroy(zp);
- } else zp.saveState(prog_node);
- const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) {
- error.BrokenPipe, error.EndOfStream => |reason| {
- std.log.info("{s} restart required: {t}", .{ argv[0], reason });
- // Process restart required.
- const term = zp.child.wait(io) catch |e| {
- return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
- };
- _ = term;
- exited = true;
- break :update;
- },
- else => |e| return e,
- };
-
- if (s.result_error_bundle.errorMessageCount() > 0) {
- return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
- }
-
- if (s.result_error_msgs.items.len > 0 and result == null) {
- // Crash detected.
- const term = zp.child.wait(io) catch |e| {
- return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
- };
- s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
- exited = true;
- try handleChildProcessTerm(s, term);
- return error.MakeFailed;
- }
-
- return result;
- }
- assert(argv.len != 0);
-
- try handleChildProcUnsupported(s);
- try handleVerbose(s.owner, .inherit, argv);
-
- const zp = try gpa.create(ZigProcess);
- defer if (!watch) gpa.destroy(zp);
-
- zp.child = std.process.spawn(io, .{
- .argv = argv,
- .environ_map = &b.graph.environ_map,
- .stdin = .pipe,
- .stdout = .pipe,
- .stderr = .pipe,
- .request_resource_usage_statistics = true,
- .progress_node = prog_node,
- }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
-
- zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{
- zp.child.stdout.?, zp.child.stderr.?,
- });
- if (watch) s.cast(Compile).?.zig_process = zp;
- defer if (!watch) zp.deinit(io);
-
- const result = result: {
- defer if (watch) zp.saveState(prog_node);
- break :result try zigProcessUpdate(s, zp, watch, web_server, gpa);
- };
-
- if (!watch) {
- // Send EOF to stdin.
- zp.child.stdin.?.close(io);
- zp.child.stdin = null;
-
- const term = zp.child.wait(io) catch |err| {
- return s.fail("unable to wait for {s}: {t}", .{ argv[0], err });
- };
- s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
-
- // Special handling for Compile step that is expecting compile errors.
- if (s.cast(Compile)) |compile| switch (term) {
- .exited => {
- // Note that the exit code may be 0 in this case due to the
- // compiler server protocol.
- if (compile.expect_errors != null) {
- return error.NeedCompileErrorCheck;
- }
- },
- else => {},
- };
-
- try handleChildProcessTerm(s, term);
- }
-
- if (s.result_error_bundle.errorMessageCount() > 0) {
- return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
- }
-
- return result;
-}
-
-/// Wrapper around `Io.Dir.updateFile` that handles verbose and error output.
-pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus {
- const b = s.owner;
- const io = b.graph.io;
- const src_path = src_lazy_path.getPath3(b, s);
- try handleVerbose(b, .inherit, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path });
- return Io.Dir.updateFile(src_path.root_dir.handle, io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err|
- return s.fail("unable to update file from '{f}' to '{s}': {t}", .{ src_path, dest_path, err });
-}
-
-/// Wrapper around `Io.Dir.createDirPathStatus` that handles verbose and error output.
-pub fn installDir(s: *Step, dest_path: []const u8) !Io.Dir.CreatePathStatus {
- const b = s.owner;
- const io = b.graph.io;
- try handleVerbose(b, .inherit, &.{ "install", "-d", dest_path });
- return Io.Dir.cwd().createDirPathStatus(io, dest_path, .default_dir) catch |err|
- return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err });
-}
-
-fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.WebServer, gpa: Allocator) !?Path {
- const b = s.owner;
- const arena = b.allocator;
- const io = b.graph.io;
-
- const start_ts = Io.Clock.awake.now(io);
-
- try sendMessage(io, zp.child.stdin.?, .update);
- if (!watch) try sendMessage(io, zp.child.stdin.?, .exit);
-
- var result: ?Path = null;
- var eos_err: error{EndOfStream}!void = {};
-
- const stdout = zp.multi_reader.fileReader(0);
-
- while (true) {
- const Header = std.zig.Server.Message.Header;
- const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
- error.EndOfStream => break,
- error.ReadFailed => return stdout.err.?,
- };
- const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
- error.EndOfStream => |e| {
- // Better to report the crash with stderr below, but we set
- // this in case the child exits successfully while violating
- // this protocol.
- eos_err = e;
- break;
- },
- error.ReadFailed => return stdout.err.?,
- };
- switch (header.tag) {
- .zig_version => {
- if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
- return s.fail(
- "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
- .{ builtin.zig_version_string, body },
- );
- }
- },
- .error_bundle => {
- s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
- // This message indicates the end of the update.
- if (watch) break;
- },
- .emit_digest => {
- const EmitDigest = std.zig.Server.Message.EmitDigest;
- const emit_digest: *align(1) const EmitDigest = @ptrCast(body);
- s.result_cached = emit_digest.flags.cache_hit;
- const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
- result = .{
- .root_dir = b.cache_root,
- .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
- };
- },
- .file_system_inputs => {
- s.clearWatchInputs();
- var it = std.mem.splitScalar(u8, body, 0);
- while (it.next()) |prefixed_path| {
- const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1);
- const sub_path = try arena.dupe(u8, prefixed_path[1..]);
- const sub_path_dirname = std.fs.path.dirname(sub_path) orelse "";
- switch (prefix_index) {
- .cwd => {
- const path: Build.Cache.Path = .{
- .root_dir = Build.Cache.Directory.cwd(),
- .sub_path = sub_path_dirname,
- };
- try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
- },
- .zig_lib => zl: {
- if (s.cast(Step.Compile)) |compile| {
- if (compile.zig_lib_dir) |zig_lib_dir| {
- const lp = try zig_lib_dir.join(arena, sub_path);
- try addWatchInput(s, lp);
- break :zl;
- }
- }
- const path: Build.Cache.Path = .{
- .root_dir = s.owner.graph.zig_lib_directory,
- .sub_path = sub_path_dirname,
- };
- try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
- },
- .local_cache => {
- const path: Build.Cache.Path = .{
- .root_dir = b.cache_root,
- .sub_path = sub_path_dirname,
- };
- try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
- },
- .global_cache => {
- const path: Build.Cache.Path = .{
- .root_dir = s.owner.graph.global_cache_root,
- .sub_path = sub_path_dirname,
- };
- try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
- },
- }
- }
- },
- .time_report => if (web_server) |ws| {
- const TimeReport = std.zig.Server.Message.TimeReport;
- const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]);
- ws.updateTimeReportCompile(.{
- .compile = s.cast(Step.Compile).?,
- .use_llvm = tr.flags.use_llvm,
- .stats = tr.stats,
- .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()),
- .llvm_pass_timings_len = tr.llvm_pass_timings_len,
- .files_len = tr.files_len,
- .decls_len = tr.decls_len,
- .trailing = body[@sizeOf(TimeReport)..],
- });
- },
- else => {}, // ignore other messages
- }
- }
-
- s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds());
-
- const stderr_contents = zp.multi_reader.reader(1).buffered();
- if (stderr_contents.len > 0) {
- try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents));
- }
-
- try eos_err;
-
- return result;
-}
-
-pub fn getZigProcess(s: *Step) ?*ZigProcess {
- return switch (s.id) {
- .compile => s.cast(Compile).?.zig_process,
- else => null,
- };
-}
-
-fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
- const header: std.zig.Client.Message.Header = .{
- .tag = tag,
- .bytes_len = 0,
- };
- var w = file.writer(io, &.{});
- w.interface.writeStruct(header, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
-}
-
-pub fn handleVerbose(
- b: *Build,
- cwd: std.process.Child.Cwd,
- argv: []const []const u8,
-) error{OutOfMemory}!void {
- return handleVerbose2(b, cwd, null, argv);
-}
-
-pub fn handleVerbose2(
- b: *Build,
- cwd: std.process.Child.Cwd,
- opt_env: ?*const std.process.Environ.Map,
- argv: []const []const u8,
-) error{OutOfMemory}!void {
- if (b.verbose) {
- const graph = b.graph;
- // Intention of verbose is to print all sub-process command lines to
- // stderr before spawning them.
- const text = try allocPrintCmd(b.allocator, cwd, if (opt_env) |env| .{
- .child = env,
- .parent = &graph.environ_map,
- } else null, argv);
- std.debug.print("{s}\n", .{text});
- }
-}
-
-/// Asserts that the caller has already populated `s.result_failed_command`.
-pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void {
- if (!std.process.can_spawn) {
- return s.fail("unable to spawn process: host cannot spawn child processes", .{});
- }
-}
-
-/// Asserts that the caller has already populated `s.result_failed_command`.
-pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void {
- assert(s.result_failed_command != null);
- return switch (term) {
- .exited => |code| if (code != 0) s.fail("process exited with error code {d}", .{code}),
- .signal => |sig| s.fail("process terminated with signal {t}", .{sig}),
- .stopped => |sig| s.fail("process stopped with signal {t}", .{sig}),
- .unknown => s.fail("process terminated unexpectedly", .{}),
- };
-}
-
-pub fn allocPrintCmd(
- gpa: Allocator,
- cwd: std.process.Child.Cwd,
- opt_env: ?struct {
- child: *const std.process.Environ.Map,
- parent: *const std.process.Environ.Map,
- },
- argv: []const []const u8,
-) Allocator.Error![]u8 {
- const shell = struct {
- fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
- for (string) |c| {
- if (switch (c) {
- else => true,
- '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
- '=' => is_argv0,
- }) break;
- } else return writer.writeAll(string);
-
- try writer.writeByte('"');
- for (string) |c| {
- if (switch (c) {
- std.ascii.control_code.nul => break,
- '!', '"', '$', '\\', '`' => true,
- else => !std.ascii.isPrint(c),
- }) try writer.writeByte('\\');
- switch (c) {
- std.ascii.control_code.nul => unreachable,
- std.ascii.control_code.bel => try writer.writeByte('a'),
- std.ascii.control_code.bs => try writer.writeByte('b'),
- std.ascii.control_code.ht => try writer.writeByte('t'),
- std.ascii.control_code.lf => try writer.writeByte('n'),
- std.ascii.control_code.vt => try writer.writeByte('v'),
- std.ascii.control_code.ff => try writer.writeByte('f'),
- std.ascii.control_code.cr => try writer.writeByte('r'),
- std.ascii.control_code.esc => try writer.writeByte('E'),
- ' '...'~' => try writer.writeByte(c),
- else => try writer.print("{o:0>3}", .{c}),
- }
- }
- try writer.writeByte('"');
- }
- };
-
- var aw: Io.Writer.Allocating = .init(gpa);
- defer aw.deinit();
- const writer = &aw.writer;
- switch (cwd) {
- .inherit => {},
- .path => |path| writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory,
- .dir => @panic("TODO"),
- }
- if (opt_env) |env| {
- var it = env.child.iterator();
- while (it.next()) |entry| {
- const key = entry.key_ptr.*;
- const value = entry.value_ptr.*;
- if (env.parent.get(key)) |process_value| {
- if (std.mem.eql(u8, value, process_value)) continue;
- }
- writer.print("{s}=", .{key}) catch return error.OutOfMemory;
- shell.escape(writer, value, false) catch return error.OutOfMemory;
- writer.writeByte(' ') catch return error.OutOfMemory;
- }
- }
- shell.escape(writer, argv[0], true) catch return error.OutOfMemory;
- for (argv[1..]) |arg| {
- writer.writeByte(' ') catch return error.OutOfMemory;
- shell.escape(writer, arg, false) catch return error.OutOfMemory;
- }
- return aw.toOwnedSlice();
-}
-
-/// Prefer `cacheHitAndWatch` unless you already added watch inputs
-/// separately from using the cache system.
-pub fn cacheHit(s: *Step, man: *Build.Cache.Manifest) !bool {
- s.result_cached = man.hit() catch |err| return failWithCacheError(s, man, err);
- return s.result_cached;
-}
-
-/// Clears previous watch inputs, if any, and then populates watch inputs from
-/// the full set of files picked up by the cache manifest.
-///
-/// Must be accompanied with `writeManifestAndWatch`.
-pub fn cacheHitAndWatch(s: *Step, man: *Build.Cache.Manifest) !bool {
- const is_hit = man.hit() catch |err| return failWithCacheError(s, man, err);
- s.result_cached = is_hit;
- // The above call to hit() populates the manifest with files, so in case of
- // a hit, we need to populate watch inputs.
- if (is_hit) try setWatchInputsFromManifest(s, man);
- return is_hit;
-}
-
-fn failWithCacheError(
- s: *Step,
- man: *const Build.Cache.Manifest,
- err: Build.Cache.Manifest.HitError,
-) error{ OutOfMemory, Canceled, MakeFailed } {
- switch (err) {
- error.CacheCheckFailed => switch (man.diagnostic) {
- .none => unreachable,
- .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {t} {t}", .{
- man.diagnostic, e,
- }),
- .file_open, .file_stat, .file_read, .file_hash => |op| {
- const pp = man.files.keys()[op.file_index].prefixed_path;
- const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
- return s.fail("failed to check cache: '{s}{c}{s}' {t} {t}", .{
- prefix, std.fs.path.sep, pp.sub_path, man.diagnostic, op.err,
- });
- },
- },
- error.OutOfMemory, error.Canceled => |e| return e,
- error.InvalidFormat => return s.fail("failed to check cache: invalid manifest file format", .{}),
- }
-}
-
-/// Prefer `writeManifestAndWatch` unless you already added watch inputs
-/// separately from using the cache system.
-pub fn writeManifest(s: *Step, man: *Build.Cache.Manifest) !void {
- if (s.test_results.isSuccess()) {
- man.writeManifest() catch |err| {
- try s.addError("unable to write cache manifest: {t}", .{err});
- };
- }
-}
-
-/// Clears previous watch inputs, if any, and then populates watch inputs from
-/// the full set of files picked up by the cache manifest.
-///
-/// Must be accompanied with `cacheHitAndWatch`.
-pub fn writeManifestAndWatch(s: *Step, man: *Build.Cache.Manifest) !void {
- try writeManifest(s, man);
- try setWatchInputsFromManifest(s, man);
-}
-
-fn setWatchInputsFromManifest(s: *Step, man: *Build.Cache.Manifest) !void {
- const arena = s.owner.allocator;
- const prefixes = man.cache.prefixes();
- clearWatchInputs(s);
- for (man.files.keys()) |file| {
- // The file path data is freed when the cache manifest is cleaned up at the end of `make`.
- const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path);
- try addWatchInputFromPath(s, .{
- .root_dir = prefixes[file.prefixed_path.prefix],
- .sub_path = std.fs.path.dirname(sub_path) orelse "",
- }, std.fs.path.basename(sub_path));
- }
-}
-
-/// For steps that have a single input that never changes when re-running `make`.
-pub fn singleUnchangingWatchInput(step: *Step, lazy_path: Build.LazyPath) Allocator.Error!void {
- if (!step.inputs.populated()) try step.addWatchInput(lazy_path);
-}
-
-pub fn clearWatchInputs(step: *Step) void {
- const gpa = step.owner.allocator;
- step.inputs.clear(gpa);
-}
-
-/// Places a *file* dependency on the path.
-pub fn addWatchInput(step: *Step, lazy_file: Build.LazyPath) Allocator.Error!void {
- switch (lazy_file) {
- .src_path => |src_path| try addWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
- .dependency => |d| try addWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
- .cwd_relative => |path_string| {
- try addWatchInputFromPath(step, .{
- .root_dir = .{
- .path = null,
- .handle = Io.Dir.cwd(),
- },
- .sub_path = std.fs.path.dirname(path_string) orelse "",
- }, std.fs.path.basename(path_string));
- },
- // Nothing to watch because this dependency edge is modeled instead via `dependants`.
- .generated => {},
- }
-}
-
-/// Any changes inside the directory will trigger invalidation.
-///
-/// See also `addDirectoryWatchInputFromPath` which takes a `Build.Cache.Path` instead.
-///
-/// Paths derived from this directory should also be manually added via
-/// `addDirectoryWatchInputFromPath` if and only if this function returns
-/// `true`.
-pub fn addDirectoryWatchInput(step: *Step, lazy_directory: Build.LazyPath) Allocator.Error!bool {
- switch (lazy_directory) {
- .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
- .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
- .cwd_relative => |path_string| {
- try addDirectoryWatchInputFromPath(step, .{
- .root_dir = .{
- .path = null,
- .handle = Io.Dir.cwd(),
- },
- .sub_path = path_string,
- });
- },
- // Nothing to watch because this dependency edge is modeled instead via `dependants`.
- .generated => return false,
- }
- return true;
-}
-
-/// Any changes inside the directory will trigger invalidation.
-///
-/// See also `addDirectoryWatchInput` which takes a `Build.LazyPath` instead.
-///
-/// This function should only be called when it has been verified that the
-/// dependency on `path` is not already accounted for by a `Step` dependency.
-/// In other words, before calling this function, first check that the
-/// `Build.LazyPath` which this `path` is derived from is not `generated`.
-pub fn addDirectoryWatchInputFromPath(step: *Step, path: Build.Cache.Path) !void {
- return addWatchInputFromPath(step, path, ".");
-}
-
-fn addWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
- return addWatchInputFromPath(step, .{
- .root_dir = builder.build_root,
- .sub_path = std.fs.path.dirname(sub_path) orelse "",
- }, std.fs.path.basename(sub_path));
-}
-
-fn addDirectoryWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
- return addDirectoryWatchInputFromPath(step, .{
- .root_dir = builder.build_root,
- .sub_path = sub_path,
- });
-}
-
-fn addWatchInputFromPath(step: *Step, path: Build.Cache.Path, basename: []const u8) !void {
- const gpa = step.owner.allocator;
- const gop = try step.inputs.table.getOrPut(gpa, path);
- if (!gop.found_existing) gop.value_ptr.* = .empty;
- try gop.value_ptr.append(gpa, basename);
-}
-
-/// Implementation detail of file watching and forced rebuilds. Prepares the step for being re-evaluated.
-pub fn reset(step: *Step, gpa: Allocator) void {
- assert(step.state == .precheck_done);
-
- if (step.result_failed_command) |cmd| gpa.free(cmd);
-
- step.result_error_msgs.clearRetainingCapacity();
- step.result_stderr = "";
- step.result_cached = false;
- step.result_duration_ns = null;
- step.result_peak_rss = 0;
- step.result_failed_command = null;
- step.test_results = .{};
- step.clearWatchInputs();
-
- step.result_error_bundle.deinit(gpa);
- step.result_error_bundle = std.zig.ErrorBundle.empty;
-}
-
-/// Implementation detail of file watching. Prepares the step for being re-evaluated.
-/// Returns `true` if the step was newly invalidated, `false` if it was already invalidated.
-pub fn invalidateResult(step: *Step, gpa: Allocator) bool {
- if (step.state == .precheck_done) return false;
- assert(step.pending_deps == 0);
- step.state = .precheck_done;
- step.reset(gpa);
- for (step.dependants.items) |dependant| {
- _ = dependant.invalidateResult(gpa);
- dependant.pending_deps += 1;
- }
- return true;
-}
-
test {
_ = CheckFile;
+ _ = Compile;
+ _ = ConfigHeader;
_ = Fail;
+ _ = FindProgram;
_ = Fmt;
_ = InstallArtifact;
_ = InstallDir;
_ = InstallFile;
_ = ObjCopy;
- _ = Compile;
_ = Options;
_ = Run;
_ = TranslateC;
- _ = WriteFile;
_ = UpdateSourceFiles;
+ _ = WriteFile;
}
diff --git a/lib/std/Build/Step/CheckFile.zig b/lib/std/Build/Step/CheckFile.zig
@@ -1,7 +1,4 @@
//! Fail the build step if a file does not match certain checks.
-//! TODO: make this more flexible, supporting more kinds of checks.
-//! TODO: generalize the code in std.testing.expectEqualStrings and make this
-//! CheckFile step produce those helpful diagnostics when there is not a match.
const CheckFile = @This();
const std = @import("std");
@@ -9,83 +6,40 @@ const Io = std.Io;
const Step = std.Build.Step;
const fs = std.fs;
const mem = std.mem;
+const Configuration = std.Build.Configuration;
step: Step,
-expected_matches: []const []const u8,
-expected_exact: ?[]const u8,
-source: std.Build.LazyPath,
-max_bytes: usize = 20 * 1024 * 1024,
+file: std.Build.LazyPath,
+expected_matches: []const Configuration.Bytes,
+expected_exact: ?Configuration.Bytes,
+max_bytes: ?u32,
-pub const base_id: Step.Id = .check_file;
+pub const base_tag: Step.Tag = .check_file;
pub const Options = struct {
expected_matches: []const []const u8 = &.{},
expected_exact: ?[]const u8 = null,
+ max_bytes: ?u32 = null,
};
-pub fn create(
- owner: *std.Build,
- source: std.Build.LazyPath,
- options: Options,
-) *CheckFile {
- const check_file = owner.allocator.create(CheckFile) catch @panic("OOM");
+pub fn create(owner: *std.Build, file: std.Build.LazyPath, options: Options) *CheckFile {
+ const graph = owner.graph;
+ const check_file = graph.create(CheckFile);
check_file.* = .{
- .step = Step.init(.{
- .id = base_id,
+ .step = .init(.{
+ .tag = base_tag,
.name = "CheckFile",
.owner = owner,
- .makeFn = make,
}),
- .source = source.dupe(owner),
- .expected_matches = owner.dupeStrings(options.expected_matches),
- .expected_exact = options.expected_exact,
+ .file = file.dupe(graph),
+ .expected_matches = graph.addBytesList(options.expected_matches),
+ .expected_exact = if (options.expected_exact) |b| graph.addBytes(b) else null,
+ .max_bytes = options.max_bytes,
};
- check_file.source.addStepDependencies(&check_file.step);
+ file.addStepDependencies(&check_file.step);
return check_file;
}
pub fn setName(check_file: *CheckFile, name: []const u8) void {
check_file.step.name = name;
}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const b = step.owner;
- const io = b.graph.io;
- const check_file: *CheckFile = @fieldParentPtr("step", step);
- try step.singleUnchangingWatchInput(check_file.source);
-
- const src_path = check_file.source.getPath2(b, step);
- const contents = Io.Dir.cwd().readFileAlloc(io, src_path, b.allocator, .limited(check_file.max_bytes)) catch |err| {
- return step.fail("unable to read '{s}': {s}", .{
- src_path, @errorName(err),
- });
- };
-
- for (check_file.expected_matches) |expected_match| {
- if (mem.find(u8, contents, expected_match) == null) {
- return step.fail(
- \\
- \\========= expected to find: ===================
- \\{s}
- \\========= but file does not contain it: =======
- \\{s}
- \\===============================================
- , .{ expected_match, contents });
- }
- }
-
- if (check_file.expected_exact) |expected_exact| {
- if (!mem.eql(u8, expected_exact, contents)) {
- return step.fail(
- \\
- \\========= expected: =====================
- \\{s}
- \\========= but found: ====================
- \\{s}
- \\========= from the following file: ======
- \\{s}
- , .{ expected_exact, contents, src_path });
- }
- }
-}
diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig
@@ -1,4 +1,5 @@
const Compile = @This();
+
const builtin = @import("builtin");
const std = @import("std");
@@ -8,19 +9,15 @@ const fs = std.fs;
const assert = std.debug.assert;
const panic = std.debug.panic;
const StringHashMap = std.StringHashMap;
-const Sha256 = std.crypto.hash.sha2.Sha256;
const Allocator = std.mem.Allocator;
const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
-const PkgConfigPkg = std.Build.PkgConfigPkg;
-const PkgConfigError = std.Build.PkgConfigError;
-const RunError = std.Build.RunError;
const Module = std.Build.Module;
const InstallDir = std.Build.InstallDir;
-const GeneratedFile = std.Build.GeneratedFile;
const Path = std.Build.Cache.Path;
+const Configuration = std.Build.Configuration;
-pub const base_id: Step.Id = .compile;
+pub const base_tag: Step.Tag = .compile;
step: Step,
root_module: *Module,
@@ -28,13 +25,11 @@ root_module: *Module,
name: []const u8,
linker_script: ?LazyPath = null,
version_script: ?LazyPath = null,
+/// Deprecated.
out_filename: []const u8,
-out_lib_filename: []const u8,
linkage: ?std.builtin.LinkMode = null,
version: ?std.SemanticVersion,
kind: Kind,
-major_only_filename: ?[]const u8,
-name_only_filename: ?[]const u8,
formatted_panics: ?bool = null,
compress_debug_sections: std.zig.CompressDebugSections = .none,
verbose_link: bool,
@@ -47,6 +42,7 @@ export_memory: bool = false,
/// For WebAssembly targets, this will allow for undefined symbols to
/// be imported from the host environment.
import_symbols: bool = false,
+/// (WebAssembly) import function table from the host environment
import_table: bool = false,
export_table: bool = false,
initial_memory: ?u64 = null,
@@ -60,7 +56,7 @@ filters: []const []const u8,
test_runner: ?TestRunner,
wasi_exec_model: ?std.builtin.WasiExecModel = null,
-installed_headers: std.array_list.Managed(HeaderInstallation),
+installed_headers: std.ArrayList(HeaderInstallation),
/// This step is used to create an include tree that dependent modules can add to their include
/// search paths. Installed headers are copied to this step.
@@ -83,8 +79,6 @@ win32_manifest: ?LazyPath = null,
/// Set via options; intended to be read-only after that.
win32_module_definition: ?LazyPath = null,
-installed_path: ?[]const u8,
-
/// Base address for an executable image.
image_base: ?u64 = null,
@@ -93,9 +87,13 @@ libc_file: ?LazyPath = null,
each_lib_rpath: ?bool = null,
/// On ELF targets, this will emit a link section called ".note.gnu.build-id"
/// which can be used to coordinate a stripped binary with its debug symbols.
+///
/// As an example, the bloaty project refuses to work unless its inputs have
/// build ids, in order to prevent accidental mismatches.
+///
/// The default is to not include this section because it slows down linking.
+///
+/// This option overrides the CLI argument passed to `zig build`.
build_id: ?std.zig.BuildId = null,
/// Create a .eh_frame_hdr section and a PT_GNU_EH_FRAME segment in the ELF
@@ -147,8 +145,8 @@ link_z_defs: bool = false,
/// (Darwin) Install name for the dylib
install_name: ?[]const u8 = null,
-/// (Darwin) Path to entitlements file
-entitlements: ?[]const u8 = null,
+/// Must be passed in via `Options`.
+entitlements: ?LazyPath = null,
/// (Darwin) Size of the pagezero segment.
pagezero_size: ?u64 = null,
@@ -189,7 +187,7 @@ entry: Entry = .default,
/// List of symbols forced as undefined in the symbol table
/// thus forcing their resolution by the linker.
/// Corresponds to `-u <symbol>` for ELF/MachO and `/include:<symbol>` for COFF/PE.
-force_undefined_symbols: std.StringHashMap(void),
+force_undefined_symbols: std.StringArrayHashMapUnmanaged(void),
/// Overrides the default stack size
stack_size: ?u64 = null,
@@ -213,21 +211,8 @@ allow_so_scripts: ?bool = null,
/// otherwise.
expect_errors: ?ExpectedCompileErrors = null,
-emit_directory: ?*GeneratedFile,
-
-generated_docs: ?*GeneratedFile,
-generated_asm: ?*GeneratedFile,
-generated_bin: ?*GeneratedFile,
-generated_pdb: ?*GeneratedFile,
-// hack for stage2_x86_64 + coff
-generated_compiler_rt_dyn_lib: ?*GeneratedFile,
-generated_implib: ?*GeneratedFile,
-generated_llvm_bc: ?*GeneratedFile,
-generated_llvm_ir: ?*GeneratedFile,
-generated_h: ?*GeneratedFile,
-
-/// The maximum number of distinct errors within a compilation step
-/// Defaults to `std.math.maxInt(u16)`
+/// The maximum number of distinct errors within a compilation step Defaults to
+/// `std.math.maxInt(u16)`. Overrides the argument passed to `zig build`.
error_limit: ?u32 = null,
/// Computed during make().
@@ -235,10 +220,6 @@ is_linking_libc: bool = false,
/// Computed during make().
is_linking_libcpp: bool = false,
-/// Populated during the make phase when there is a long-lived compiler process.
-/// Managed by the build runner, not user build script.
-zig_process: ?*Step.ZigProcess,
-
/// Enables coverage instrumentation that is only useful if you are using third
/// party fuzzers that depend on it. Otherwise, slows down the instrumented
/// binary with unnecessary function calls.
@@ -253,6 +234,16 @@ zig_process: ?*Step.ZigProcess,
/// builtin fuzzer, see the `fuzz` flag in `Module`.
sanitize_coverage_trace_pc_guard: ?bool = null,
+emit_directory: Configuration.OptionalGeneratedFileIndex = .none,
+generated_docs: Configuration.OptionalGeneratedFileIndex = .none,
+generated_asm: Configuration.OptionalGeneratedFileIndex = .none,
+generated_bin: Configuration.OptionalGeneratedFileIndex = .none,
+generated_pdb: Configuration.OptionalGeneratedFileIndex = .none,
+generated_implib: Configuration.OptionalGeneratedFileIndex = .none,
+generated_llvm_bc: Configuration.OptionalGeneratedFileIndex = .none,
+generated_llvm_ir: Configuration.OptionalGeneratedFileIndex = .none,
+generated_h: Configuration.OptionalGeneratedFileIndex = .none,
+
pub const ExpectedCompileErrors = union(enum) {
contains: []const u8,
exact: []const []const u8,
@@ -292,22 +283,11 @@ pub const Options = struct {
win32_manifest: ?LazyPath = null,
/// Win32 module definition file.
win32_module_definition: ?LazyPath = null,
+ /// (Darwin) Path to entitlements file
+ entitlements: ?LazyPath = null,
};
-pub const Kind = enum {
- exe,
- lib,
- obj,
- @"test",
- test_obj,
-
- pub fn isTest(kind: Kind) bool {
- return switch (kind) {
- .exe, .lib, .obj => false,
- .@"test", .test_obj => true,
- };
- }
-};
+pub const Kind = Configuration.Step.Compile.Kind;
pub const HeaderInstallation = union(enum) {
file: File,
@@ -317,10 +297,10 @@ pub const HeaderInstallation = union(enum) {
source: LazyPath,
dest_rel_path: []const u8,
- pub fn dupe(file: File, b: *std.Build) File {
+ pub fn dupe(file: File, graph: *const std.Build.Graph) File {
return .{
- .source = file.source.dupe(b),
- .dest_rel_path = b.dupePath(file.dest_rel_path),
+ .source = file.source.dupe(graph),
+ .dest_rel_path = graph.dupePath(file.dest_rel_path),
};
}
};
@@ -338,19 +318,19 @@ pub const HeaderInstallation = union(enum) {
/// `exclude_extensions` takes precedence over `include_extensions`.
include_extensions: ?[]const []const u8 = &.{".h"},
- pub fn dupe(opts: Directory.Options, b: *std.Build) Directory.Options {
+ pub fn dupe(opts: Directory.Options, graph: *const std.Build.Graph) Directory.Options {
return .{
- .exclude_extensions = b.dupeStrings(opts.exclude_extensions),
- .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null,
+ .exclude_extensions = graph.dupeStrings(opts.exclude_extensions),
+ .include_extensions = if (opts.include_extensions) |incs| graph.dupeStrings(incs) else null,
};
}
};
- pub fn dupe(dir: Directory, b: *std.Build) Directory {
+ pub fn dupe(dir: Directory, graph: *const std.Build.Graph) Directory {
return .{
- .source = dir.source.dupe(b),
- .dest_rel_path = b.dupePath(dir.dest_rel_path),
- .options = dir.options.dupe(b),
+ .source = dir.source.dupe(graph),
+ .dest_rel_path = graph.dupePath(dir.dest_rel_path),
+ .options = dir.options.dupe(graph),
};
}
};
@@ -361,10 +341,10 @@ pub const HeaderInstallation = union(enum) {
};
}
- pub fn dupe(installation: HeaderInstallation, b: *std.Build) HeaderInstallation {
+ pub fn dupe(installation: HeaderInstallation, graph: *const std.Build.Graph) HeaderInstallation {
return switch (installation) {
- .file => |f| .{ .file = f.dupe(b) },
- .directory => |d| .{ .directory = d.dupe(b) },
+ .file => |f| .{ .file = f.dupe(graph) },
+ .directory => |d| .{ .directory = d.dupe(graph) },
};
}
};
@@ -378,6 +358,9 @@ pub const TestRunner = struct {
};
pub fn create(owner: *std.Build, options: Options) *Compile {
+ const graph = owner.graph;
+ const arena = graph.arena;
+
const name = owner.dupe(options.name);
if (mem.find(u8, name, "/") != null or mem.find(u8, name, "\\") != null) {
panic("invalid name: '{s}'. It looks like a file path, but it is supposed to be the library or application name.", .{name});
@@ -392,14 +375,17 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
if (options.kind.isTest() and mem.eql(u8, name, "test"))
@tagName(options.kind)
else
- owner.fmt("{s} {s}", .{ @tagName(options.kind), name }),
+ owner.fmt("{t} {s}", .{ options.kind, name }),
@tagName(options.root_module.optimize orelse .Debug),
- resolved_target.query.zigTriple(owner.allocator) catch @panic("OOM"),
+ resolved_target.query.zigTriple(arena) catch @panic("OOM"),
});
- const out_filename = std.zig.binNameAlloc(owner.allocator, .{
+ const out_filename = std.zig.binNameAlloc(arena, .{
.root_name = name,
- .target = target,
+ .cpu_arch = target.cpu.arch,
+ .os_tag = target.os.tag,
+ .ofmt = target.ofmt,
+ .abi = target.abi,
.output_mode = switch (options.kind) {
.lib => .Lib,
.obj, .test_obj => .Obj,
@@ -409,7 +395,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
.version = options.version,
}) catch @panic("OOM");
- const compile = owner.allocator.create(Compile) catch @panic("OOM");
+ const compile = arena.create(Compile) catch @panic("OOM");
compile.* = .{
.root_module = options.root_module,
.verbose_link = false,
@@ -418,52 +404,34 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
.kind = options.kind,
.name = name,
.step = .init(.{
- .id = base_id,
+ .tag = base_tag,
.name = step_name,
.owner = owner,
- .makeFn = make,
.max_rss = options.max_rss,
}),
.version = options.version,
.out_filename = out_filename,
- .out_lib_filename = undefined,
- .major_only_filename = null,
- .name_only_filename = null,
- .installed_headers = std.array_list.Managed(HeaderInstallation).init(owner.allocator),
+ .installed_headers = .empty,
.zig_lib_dir = null,
.exec_cmd_args = null,
.filters = options.filters,
.test_runner = null, // set below
.rdynamic = false,
- .installed_path = null,
- .force_undefined_symbols = StringHashMap(void).init(owner.allocator),
-
- .emit_directory = null,
- .generated_docs = null,
- .generated_asm = null,
- .generated_bin = null,
- .generated_pdb = null,
- .generated_compiler_rt_dyn_lib = null,
- .generated_implib = null,
- .generated_llvm_bc = null,
- .generated_llvm_ir = null,
- .generated_h = null,
+ .force_undefined_symbols = .empty,
.use_llvm = options.use_llvm,
.use_lld = options.use_lld,
.use_new_linker = null,
-
- .zig_process = null,
};
if (options.zig_lib_dir) |lp| {
- compile.zig_lib_dir = lp.dupe(compile.step.owner);
+ compile.zig_lib_dir = lp.dupe(graph);
lp.addStepDependencies(&compile.step);
}
if (options.test_runner) |runner| {
compile.test_runner = .{
- .path = runner.path.dupe(compile.step.owner),
+ .path = runner.path.dupe(graph),
.mode = runner.mode,
};
runner.path.addStepDependencies(&compile.step);
@@ -473,45 +441,21 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
// gets embedded, so for any other target the manifest file is just ignored.
if (target.ofmt == .coff) {
if (options.win32_manifest) |lp| {
- compile.win32_manifest = lp.dupe(compile.step.owner);
+ compile.win32_manifest = lp.dupe(graph);
lp.addStepDependencies(&compile.step);
}
if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) {
// Building a Win32 DLL, check for win32 .def file.
if (options.win32_module_definition) |lp| {
- compile.win32_module_definition = lp.dupe(compile.step.owner);
+ compile.win32_module_definition = lp.dupe(graph);
lp.addStepDependencies(&compile.step);
}
}
}
- if (compile.kind == .lib) {
- if (compile.linkage != null and compile.linkage.? == .static) {
- compile.out_lib_filename = compile.out_filename;
- } else if (compile.version) |version| {
- if (target.os.tag.isDarwin()) {
- compile.major_only_filename = owner.fmt("lib{s}.{d}.dylib", .{
- compile.name,
- version.major,
- });
- compile.name_only_filename = owner.fmt("lib{s}.dylib", .{compile.name});
- compile.out_lib_filename = compile.out_filename;
- } else if (target.os.tag == .windows) {
- compile.out_lib_filename = owner.fmt("{s}.lib", .{compile.name});
- } else {
- compile.major_only_filename = owner.fmt("lib{s}.so.{d}", .{ compile.name, version.major });
- compile.name_only_filename = owner.fmt("lib{s}.so", .{compile.name});
- compile.out_lib_filename = compile.out_filename;
- }
- } else {
- if (target.os.tag.isDarwin()) {
- compile.out_lib_filename = compile.out_filename;
- } else if (target.os.tag == .windows) {
- compile.out_lib_filename = owner.fmt("{s}.lib", .{compile.name});
- } else {
- compile.out_lib_filename = compile.out_filename;
- }
- }
+ if (options.entitlements) |lp| {
+ compile.entitlements = lp.dupe(graph);
+ lp.addStepDependencies(&compile.step);
}
return compile;
@@ -521,12 +465,13 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
/// When a module links with this artifact, all headers marked for installation are added to that
/// module's include search path.
pub fn installHeader(cs: *Compile, source: LazyPath, dest_rel_path: []const u8) void {
- const b = cs.step.owner;
+ const graph = cs.step.owner.graph;
+ const arena = graph.arena;
const installation: HeaderInstallation = .{ .file = .{
- .source = source.dupe(b),
- .dest_rel_path = b.dupePath(dest_rel_path),
+ .source = source.dupe(graph),
+ .dest_rel_path = graph.dupePath(dest_rel_path),
} };
- cs.installed_headers.append(installation) catch @panic("OOM");
+ cs.installed_headers.append(arena, installation) catch @panic("OOM");
cs.addHeaderInstallationToIncludeTree(installation);
installation.getSource().addStepDependencies(&cs.step);
}
@@ -540,13 +485,14 @@ pub fn installHeadersDirectory(
dest_rel_path: []const u8,
options: HeaderInstallation.Directory.Options,
) void {
- const b = cs.step.owner;
+ const graph = cs.step.owner.graph;
+ const arena = graph.arena;
const installation: HeaderInstallation = .{ .directory = .{
- .source = source.dupe(b),
- .dest_rel_path = b.dupePath(dest_rel_path),
- .options = options.dupe(b),
+ .source = source.dupe(graph),
+ .dest_rel_path = graph.dupePath(dest_rel_path),
+ .options = options.dupe(graph),
} };
- cs.installed_headers.append(installation) catch @panic("OOM");
+ cs.installed_headers.append(arena, installation) catch @panic("OOM");
cs.addHeaderInstallationToIncludeTree(installation);
installation.getSource().addStepDependencies(&cs.step);
}
@@ -563,9 +509,11 @@ pub fn installConfigHeader(cs: *Compile, config_header: *Step.ConfigHeader) void
/// module's include search path.
pub fn installLibraryHeaders(cs: *Compile, lib: *Compile) void {
assert(lib.kind == .lib);
+ const graph = cs.step.owner.graph;
+ const arena = graph.arena;
for (lib.installed_headers.items) |installation| {
- const installation_copy = installation.dupe(lib.step.owner);
- cs.installed_headers.append(installation_copy) catch @panic("OOM");
+ const installation_copy = installation.dupe(graph);
+ cs.installed_headers.append(arena, installation_copy) catch @panic("OOM");
cs.addHeaderInstallationToIncludeTree(installation_copy);
installation_copy.getSource().addStepDependencies(&cs.step);
}
@@ -612,20 +560,21 @@ pub fn addObjCopy(cs: *Compile, options: Step.ObjCopy.Options) *Step.ObjCopy {
}
pub fn setLinkerScript(compile: *Compile, source: LazyPath) void {
- const b = compile.step.owner;
- compile.linker_script = source.dupe(b);
+ const graph = compile.step.owner.graph;
+ compile.linker_script = source.dupe(graph);
source.addStepDependencies(&compile.step);
}
pub fn setVersionScript(compile: *Compile, source: LazyPath) void {
- const b = compile.step.owner;
- compile.version_script = source.dupe(b);
+ const graph = compile.step.owner.graph;
+ compile.version_script = source.dupe(graph);
source.addStepDependencies(&compile.step);
}
pub fn forceUndefinedSymbol(compile: *Compile, symbol_name: []const u8) void {
- const b = compile.step.owner;
- compile.force_undefined_symbols.put(b.dupe(symbol_name), {}) catch @panic("OOM");
+ const graph = compile.step.owner.graph;
+ const arena = graph.allocator;
+ compile.force_undefined_symbols.put(arena, graph.dupeString(symbol_name), {}) catch @panic("OOM");
}
/// Returns whether the library, executable, or object depends on a particular system library.
@@ -701,122 +650,6 @@ pub fn producesImplib(compile: *Compile) bool {
return compile.isDll();
}
-const PkgConfigResult = struct {
- cflags: []const []const u8,
- libs: []const []const u8,
-};
-
-/// Run pkg-config for the given library name and parse the output, returning the arguments
-/// that should be passed to zig to link the given library.
-pub fn runPkgConfig(step: *Step, lib_name: []const u8) !PkgConfigResult {
- const wl_rpath_prefix = "-Wl,-rpath,";
-
- const b = step.owner;
- const pkg_name = match: {
- // First we have to map the library name to pkg config name. Unfortunately,
- // there are several examples where this is not straightforward:
- // -lSDL2 -> pkg-config sdl2
- // -lgdk-3 -> pkg-config gdk-3.0
- // -latk-1.0 -> pkg-config atk
- // -lpulse -> pkg-config libpulse
- const pkgs = try getPkgConfigList(b);
-
- // Exact match means instant winner.
- for (pkgs) |pkg| {
- if (mem.eql(u8, pkg.name, lib_name)) {
- break :match pkg.name;
- }
- }
-
- // Next we'll try ignoring case.
- for (pkgs) |pkg| {
- if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
- break :match pkg.name;
- }
- }
-
- // Prefixed "lib" or suffixed ".0".
- for (pkgs) |pkg| {
- if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
- const prefix = pkg.name[0..pos];
- const suffix = pkg.name[pos + lib_name.len ..];
- if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
- if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
- break :match pkg.name;
- }
- }
-
- // Trimming "-1.0".
- if (mem.endsWith(u8, lib_name, "-1.0")) {
- const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
- for (pkgs) |pkg| {
- if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
- break :match pkg.name;
- }
- }
- }
-
- return error.PackageNotFound;
- };
-
- var code: u8 = undefined;
- const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
- const stdout = if (b.runAllowFail(&[_][]const u8{
- pkg_config_exe,
- pkg_name,
- "--cflags",
- "--libs",
- }, &code, .ignore)) |stdout| stdout else |err| switch (err) {
- error.ProcessTerminated => return error.PkgConfigCrashed,
- error.ExecNotSupported => return error.PkgConfigFailed,
- error.ExitCodeFailure => return error.PkgConfigFailed,
- error.FileNotFound => return error.PkgConfigNotInstalled,
- else => return err,
- };
-
- var zig_cflags: std.ArrayList([]const u8) = .empty;
- defer zig_cflags.deinit(b.allocator);
- var zig_libs: std.ArrayList([]const u8) = .empty;
- defer zig_libs.deinit(b.allocator);
-
- var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
- while (arg_it.next()) |arg| {
- if (mem.eql(u8, arg, "-I")) {
- const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_cflags.appendSlice(b.allocator, &.{ "-I", dir });
- } else if (mem.startsWith(u8, arg, "-I")) {
- try zig_cflags.append(b.allocator, arg);
- } else if (mem.eql(u8, arg, "-L")) {
- const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_libs.appendSlice(b.allocator, &.{ "-L", dir });
- } else if (mem.startsWith(u8, arg, "-L")) {
- try zig_libs.append(b.allocator, arg);
- } else if (mem.eql(u8, arg, "-l")) {
- const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_libs.appendSlice(b.allocator, &.{ "-l", lib });
- } else if (mem.startsWith(u8, arg, "-l")) {
- try zig_libs.append(b.allocator, arg);
- } else if (mem.eql(u8, arg, "-D")) {
- const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_cflags.appendSlice(b.allocator, &.{ "-D", macro });
- } else if (mem.startsWith(u8, arg, "-D")) {
- try zig_cflags.append(b.allocator, arg);
- } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
- try zig_cflags.appendSlice(b.allocator, &.{ "-rpath", arg[wl_rpath_prefix.len..] });
- } else if (b.debug_pkg_config) {
- return step.fail("unknown pkg-config flag '{s}'", .{arg});
- }
- }
-
- try zig_cflags.shrinkToLen(b.allocator);
- try zig_libs.shrinkToLen(b.allocator);
-
- return .{
- .cflags = zig_cflags.toOwnedSliceAssert(),
- .libs = zig_libs.toOwnedSliceAssert(),
- };
-}
-
pub fn setVerboseLink(compile: *Compile, value: bool) void {
compile.verbose_link = value;
}
@@ -826,22 +659,21 @@ pub fn setVerboseCC(compile: *Compile, value: bool) void {
}
pub fn setLibCFile(compile: *Compile, libc_file: ?LazyPath) void {
- const b = compile.step.owner;
+ const graph = compile.step.owner.graph;
if (libc_file) |f| {
- compile.libc_file = f.dupe(b);
+ compile.libc_file = f.dupe(graph);
f.addStepDependencies(&compile.step);
} else {
compile.libc_file = null;
}
}
-fn getEmittedFileGeneric(compile: *Compile, output_file: *?*GeneratedFile) LazyPath {
- if (output_file.*) |file| return .{ .generated = .{ .file = file } };
- const arena = compile.step.owner.allocator;
- const generated_file = arena.create(GeneratedFile) catch @panic("OOM");
- generated_file.* = .{ .step = &compile.step };
- output_file.* = generated_file;
- return .{ .generated = .{ .file = generated_file } };
+fn getEmittedFileGeneric(compile: *Compile, output_file: *Configuration.OptionalGeneratedFileIndex) LazyPath {
+ if (output_file.unwrap()) |index| return .{ .generated = .{ .index = index } };
+ const graph = compile.step.owner.graph;
+ const index = graph.addGeneratedFile(&compile.step);
+ output_file.* = .init(index);
+ return .{ .generated = .{ .index = index } };
}
/// Returns the path to the directory that contains the emitted binary file.
@@ -905,1175 +737,21 @@ pub fn getEmittedLlvmBc(compile: *Compile) LazyPath {
}
pub fn setExecCmd(compile: *Compile, args: []const ?[]const u8) void {
- const b = compile.step.owner;
+ const graph = compile.step.owner.graph;
+ const arena = graph.arena;
assert(compile.kind == .@"test");
- const duped_args = b.allocator.alloc(?[]u8, args.len) catch @panic("OOM");
+ const duped_args = arena.alloc(?[]u8, args.len) catch @panic("OOM");
for (args, 0..) |arg, i| {
- duped_args[i] = if (arg) |a| b.dupe(a) else null;
+ duped_args[i] = if (arg) |a| graph.dupeString(a) else null;
}
compile.exec_cmd_args = duped_args;
}
-const CliNamedModules = struct {
- modules: std.AutoArrayHashMapUnmanaged(*Module, void),
- names: std.StringArrayHashMapUnmanaged(void),
-
- /// Traverse the whole dependency graph and give every module a unique
- /// name, ideally one named after what it's called somewhere in the graph.
- /// It will help here to have both a mapping from module to name and a set
- /// of all the currently-used names.
- fn init(arena: Allocator, root_module: *Module) Allocator.Error!CliNamedModules {
- var compile: CliNamedModules = .{
- .modules = .{},
- .names = .{},
- };
- const graph = root_module.getGraph();
- {
- assert(graph.modules[0] == root_module);
- try compile.modules.put(arena, root_module, {});
- try compile.names.put(arena, "root", {});
- }
- for (graph.modules[1..], graph.names[1..]) |mod, orig_name| {
- var name = orig_name;
- var n: usize = 0;
- while (true) {
- const gop = try compile.names.getOrPut(arena, name);
- if (!gop.found_existing) {
- try compile.modules.putNoClobber(arena, mod, {});
- break;
- }
- name = try std.fmt.allocPrint(arena, "{s}{d}", .{ orig_name, n });
- n += 1;
- }
- }
- return compile;
- }
-};
-
-fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking_step: ?*Step) ![]const u8 {
- const step = &compile.step;
- const b = step.owner;
- const graph = b.graph;
- const io = graph.io;
- const maybe_path: ?*GeneratedFile = @field(compile, tag_name);
-
- const generated_file = maybe_path orelse {
- const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
- std.Build.dumpBadGetPathHelp(&compile.step, stderr.terminal(), compile.step.owner, asking_step) catch {};
- io.unlockStderr();
- @panic("missing emit option for " ++ tag_name);
- };
-
- const path = generated_file.path orelse {
- const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
- std.Build.dumpBadGetPathHelp(&compile.step, stderr.terminal(), compile.step.owner, asking_step) catch {};
- io.unlockStderr();
- @panic(tag_name ++ " is null. Is there a missing step dependency?");
- };
-
- return path;
-}
-
-fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
- const step = &compile.step;
- const b = step.owner;
- const arena = b.allocator;
-
- var zig_args = std.array_list.Managed([]const u8).init(arena);
- defer zig_args.deinit();
-
- try zig_args.append(b.graph.zig_exe);
-
- const cmd = switch (compile.kind) {
- .lib => "build-lib",
- .exe => "build-exe",
- .obj => "build-obj",
- .@"test" => "test",
- .test_obj => "test-obj",
- };
- try zig_args.append(cmd);
-
- if (b.reference_trace) |some| {
- try zig_args.append(try std.fmt.allocPrint(arena, "-freference-trace={d}", .{some}));
- }
- try addFlag(&zig_args, "allow-so-scripts", compile.allow_so_scripts orelse b.graph.allow_so_scripts);
-
- try addFlag(&zig_args, "llvm", compile.use_llvm);
- try addFlag(&zig_args, "lld", compile.use_lld);
- try addFlag(&zig_args, "new-linker", compile.use_new_linker);
-
- if (compile.root_module.resolved_target.?.query.ofmt) |ofmt| {
- try zig_args.append(try std.fmt.allocPrint(arena, "-ofmt={s}", .{@tagName(ofmt)}));
- }
-
- switch (compile.entry) {
- .default => {},
- .disabled => try zig_args.append("-fno-entry"),
- .enabled => try zig_args.append("-fentry"),
- .symbol_name => |entry_name| {
- try zig_args.append(try std.fmt.allocPrint(arena, "-fentry={s}", .{entry_name}));
- },
- }
-
- {
- var symbol_it = compile.force_undefined_symbols.keyIterator();
- while (symbol_it.next()) |symbol_name| {
- try zig_args.append("--force_undefined");
- try zig_args.append(symbol_name.*);
- }
- }
-
- if (compile.stack_size) |stack_size| {
- try zig_args.append("--stack");
- try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size}));
- }
-
- if (fuzz) {
- try zig_args.append("-ffuzz");
- }
-
- {
- // Stores system libraries that have already been seen for at least one
- // module, along with any arguments that need to be passed to the
- // compiler for each module individually.
- var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty;
- var frameworks: std.StringArrayHashMapUnmanaged(Module.LinkFrameworkOptions) = .empty;
-
- var prev_has_cflags = false;
- var prev_has_rcflags = false;
- var prev_search_strategy: Module.SystemLib.SearchStrategy = .paths_first;
- var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic;
- // Track the number of positional arguments so that a nice error can be
- // emitted if there is nothing to link.
- var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null);
-
- // Fully recursive iteration including dynamic libraries to detect
- // libc and libc++ linkage.
- for (compile.getCompileDependencies(true)) |some_compile| {
- for (some_compile.root_module.getGraph().modules) |mod| {
- if (mod.link_libc == true) compile.is_linking_libc = true;
- if (mod.link_libcpp == true) compile.is_linking_libcpp = true;
- }
- }
-
- var cli_named_modules = try CliNamedModules.init(arena, compile.root_module);
-
- // For this loop, don't chase dynamic libraries because their link
- // objects are already linked.
- for (compile.getCompileDependencies(false)) |dep_compile| {
- for (dep_compile.root_module.getGraph().modules) |mod| {
- // While walking transitive dependencies, if a given link object is
- // already included in a library, it should not redundantly be
- // placed on the linker line of the dependee.
- const my_responsibility = dep_compile == compile;
- const already_linked = !my_responsibility and dep_compile.isDynamicLibrary();
-
- // Inherit dependencies on darwin frameworks.
- if (!already_linked) {
- for (mod.frameworks.keys(), mod.frameworks.values()) |name, info| {
- try frameworks.put(arena, name, info);
- }
- }
-
- // Inherit dependencies on system libraries and static libraries.
- for (mod.link_objects.items) |link_object| {
- switch (link_object) {
- .static_path => |static_path| {
- if (my_responsibility) {
- try zig_args.append(static_path.getPath2(mod.owner, step));
- total_linker_objects += 1;
- }
- },
- .system_lib => |system_lib| {
- const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
- if (system_lib_gop.found_existing) {
- try zig_args.appendSlice(system_lib_gop.value_ptr.*);
- continue;
- } else {
- system_lib_gop.value_ptr.* = &.{};
- }
-
- if (already_linked)
- continue;
-
- if ((system_lib.search_strategy != prev_search_strategy or
- system_lib.preferred_link_mode != prev_preferred_link_mode) and
- compile.linkage != .static)
- {
- switch (system_lib.search_strategy) {
- .no_fallback => switch (system_lib.preferred_link_mode) {
- .dynamic => try zig_args.append("-search_dylibs_only"),
- .static => try zig_args.append("-search_static_only"),
- },
- .paths_first => switch (system_lib.preferred_link_mode) {
- .dynamic => try zig_args.append("-search_paths_first"),
- .static => try zig_args.append("-search_paths_first_static"),
- },
- .mode_first => switch (system_lib.preferred_link_mode) {
- .dynamic => try zig_args.append("-search_dylibs_first"),
- .static => try zig_args.append("-search_static_first"),
- },
- }
- prev_search_strategy = system_lib.search_strategy;
- prev_preferred_link_mode = system_lib.preferred_link_mode;
- }
-
- const prefix: []const u8 = prefix: {
- if (system_lib.needed) break :prefix "-needed-l";
- if (system_lib.weak) break :prefix "-weak-l";
- break :prefix "-l";
- };
- switch (system_lib.use_pkg_config) {
- .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })),
- .yes, .force => {
- if (runPkgConfig(&compile.step, system_lib.name)) |result| {
- try zig_args.appendSlice(result.cflags);
- try zig_args.appendSlice(result.libs);
- try seen_system_libs.put(arena, system_lib.name, result.cflags);
- } else |err| switch (err) {
- error.PkgConfigInvalidOutput,
- error.PkgConfigCrashed,
- error.PkgConfigFailed,
- error.PkgConfigNotInstalled,
- error.PackageNotFound,
- => switch (system_lib.use_pkg_config) {
- .yes => {
- // pkg-config failed, so fall back to linking the library
- // by name directly.
- try zig_args.append(b.fmt("{s}{s}", .{
- prefix,
- system_lib.name,
- }));
- },
- .force => {
- panic("pkg-config failed for library {s}", .{system_lib.name});
- },
- .no => unreachable,
- },
-
- else => |e| return e,
- }
- },
- }
- },
- .other_step => |other| {
- switch (other.kind) {
- .exe => return step.fail("cannot link with an executable build artifact", .{}),
- .@"test" => return step.fail("cannot link with a test", .{}),
- .obj, .test_obj => {
- const included_in_lib_or_obj = !my_responsibility and
- (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj);
- if (!already_linked and !included_in_lib_or_obj) {
- try zig_args.append(other.getEmittedBin().getPath2(b, step));
- total_linker_objects += 1;
- }
- },
- .lib => l: {
- const other_produces_implib = other.producesImplib();
- const other_is_static = other_produces_implib or other.isStaticLibrary();
-
- if (compile.isStaticLibrary() and other_is_static) {
- // Avoid putting a static library inside a static library.
- break :l;
- }
-
- // For DLLs, we must link against the implib.
- // For everything else, we directly link
- // against the library file.
- const full_path_lib = if (other_produces_implib)
- try other.getGeneratedFilePath("generated_implib", &compile.step)
- else
- try other.getGeneratedFilePath("generated_bin", &compile.step);
-
- try zig_args.append(full_path_lib);
- total_linker_objects += 1;
-
- if (other.linkage == .dynamic and
- compile.rootModuleTarget().os.tag != .windows)
- {
- if (fs.path.dirname(full_path_lib)) |dirname| {
- try zig_args.append("-rpath");
- try zig_args.append(dirname);
- }
- }
- },
- }
- },
- .assembly_file => |asm_file| l: {
- if (!my_responsibility) break :l;
-
- if (prev_has_cflags) {
- try zig_args.append("-cflags");
- try zig_args.append("--");
- prev_has_cflags = false;
- }
- try zig_args.append(asm_file.getPath2(mod.owner, step));
- total_linker_objects += 1;
- },
-
- .c_source_file => |c_source_file| l: {
- if (!my_responsibility) break :l;
-
- if (prev_has_cflags or c_source_file.flags.len != 0) {
- try zig_args.append("-cflags");
- for (c_source_file.flags) |arg| {
- try zig_args.append(arg);
- }
- try zig_args.append("--");
- }
- prev_has_cflags = (c_source_file.flags.len != 0);
-
- if (c_source_file.language) |lang| {
- try zig_args.append("-x");
- try zig_args.append(lang.internalIdentifier());
- }
-
- try zig_args.append(c_source_file.file.getPath2(mod.owner, step));
-
- if (c_source_file.language != null) {
- try zig_args.append("-x");
- try zig_args.append("none");
- }
- total_linker_objects += 1;
- },
-
- .c_source_files => |c_source_files| l: {
- if (!my_responsibility) break :l;
-
- if (prev_has_cflags or c_source_files.flags.len != 0) {
- try zig_args.append("-cflags");
- for (c_source_files.flags) |arg| {
- try zig_args.append(arg);
- }
- try zig_args.append("--");
- }
- prev_has_cflags = (c_source_files.flags.len != 0);
-
- if (c_source_files.language) |lang| {
- try zig_args.append("-x");
- try zig_args.append(lang.internalIdentifier());
- }
-
- const root_path = c_source_files.root.getPath2(mod.owner, step);
- for (c_source_files.files) |file| {
- try zig_args.append(b.pathJoin(&.{ root_path, file }));
- }
-
- if (c_source_files.language != null) {
- try zig_args.append("-x");
- try zig_args.append("none");
- }
-
- total_linker_objects += c_source_files.files.len;
- },
-
- .win32_resource_file => |rc_source_file| l: {
- if (!my_responsibility) break :l;
-
- if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) {
- if (prev_has_rcflags) {
- try zig_args.append("-rcflags");
- try zig_args.append("--");
- prev_has_rcflags = false;
- }
- } else {
- try zig_args.append("-rcflags");
- for (rc_source_file.flags) |arg| {
- try zig_args.append(arg);
- }
- for (rc_source_file.include_paths) |include_path| {
- try zig_args.append("/I");
- try zig_args.append(include_path.getPath2(mod.owner, step));
- }
- try zig_args.append("--");
- prev_has_rcflags = true;
- }
- try zig_args.append(rc_source_file.file.getPath2(mod.owner, step));
- total_linker_objects += 1;
- },
- }
- }
-
- // We need to emit the --mod argument here so that the above link objects
- // have the correct parent module, but only if the module is part of
- // this compilation.
- if (!my_responsibility) continue;
- if (cli_named_modules.modules.getIndex(mod)) |module_cli_index| {
- const module_cli_name = cli_named_modules.names.keys()[module_cli_index];
- try mod.appendZigProcessFlags(&zig_args, step);
-
- // --dep arguments
- try zig_args.ensureUnusedCapacity(mod.import_table.count() * 2);
- for (mod.import_table.keys(), mod.import_table.values()) |name, import| {
- const import_index = cli_named_modules.modules.getIndex(import).?;
- const import_cli_name = cli_named_modules.names.keys()[import_index];
- zig_args.appendAssumeCapacity("--dep");
- if (std.mem.eql(u8, import_cli_name, name)) {
- zig_args.appendAssumeCapacity(import_cli_name);
- } else {
- zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name }));
- }
- }
-
- // When the CLI sees a -M argument, it determines whether it
- // implies the existence of a Zig compilation unit based on
- // whether there is a root source file. If there is no root
- // source file, then this is not a zig compilation unit - it is
- // perhaps a set of linker objects, or C source files instead.
- // Linker objects are added to the CLI globally, while C source
- // files must have a module parent.
- if (mod.root_source_file) |lp| {
- const src = lp.getPath2(mod.owner, step);
- try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src }));
- } else if (moduleNeedsCliArg(mod)) {
- try zig_args.append(b.fmt("-M{s}", .{module_cli_name}));
- }
- }
- }
- }
-
- if (total_linker_objects == 0) {
- return step.fail("the linker needs one or more objects to link", .{});
- }
-
- for (frameworks.keys(), frameworks.values()) |name, info| {
- if (info.needed) {
- try zig_args.append("-needed_framework");
- } else if (info.weak) {
- try zig_args.append("-weak_framework");
- } else {
- try zig_args.append("-framework");
- }
- try zig_args.append(name);
- }
-
- if (compile.is_linking_libcpp) {
- try zig_args.append("-lc++");
- }
-
- if (compile.is_linking_libc) {
- try zig_args.append("-lc");
- }
- }
-
- if (compile.win32_manifest) |manifest_file| {
- try zig_args.append(manifest_file.getPath2(b, step));
- }
-
- if (compile.win32_module_definition) |module_file| {
- try zig_args.append(module_file.getPath2(b, step));
- }
-
- if (compile.image_base) |image_base| {
- try zig_args.append("--image-base");
- try zig_args.append(b.fmt("0x{x}", .{image_base}));
- }
-
- for (compile.filters) |filter| {
- try zig_args.append("--test-filter");
- try zig_args.append(filter);
- }
-
- if (compile.test_runner) |test_runner| {
- try zig_args.append("--test-runner");
- try zig_args.append(test_runner.path.getPath2(b, step));
- }
-
- for (b.debug_log_scopes) |log_scope| {
- try zig_args.append("--debug-log");
- try zig_args.append(log_scope);
- }
-
- if (b.debug_compile_errors) {
- try zig_args.append("--debug-compile-errors");
- }
-
- if (b.debug_incremental) {
- try zig_args.append("--debug-incremental");
- }
-
- if (b.verbose_air) try zig_args.append("--verbose-air");
- if (b.verbose_llvm_ir) |path| try zig_args.append(b.fmt("--verbose-llvm-ir={s}", .{path}));
- if (b.verbose_llvm_bc) |path| try zig_args.append(b.fmt("--verbose-llvm-bc={s}", .{path}));
- if (b.verbose_link or compile.verbose_link) try zig_args.append("--verbose-link");
- if (b.verbose_cc or compile.verbose_cc) try zig_args.append("--verbose-cc");
- if (b.verbose_llvm_cpu_features) try zig_args.append("--verbose-llvm-cpu-features");
- if (b.graph.time_report) try zig_args.append("--time-report");
-
- if (compile.generated_asm != null) try zig_args.append("-femit-asm");
- if (compile.generated_bin == null) try zig_args.append("-fno-emit-bin");
- if (compile.generated_docs != null) try zig_args.append("-femit-docs");
- if (compile.generated_implib != null) try zig_args.append("-femit-implib");
- if (compile.generated_llvm_bc != null) try zig_args.append("-femit-llvm-bc");
- if (compile.generated_llvm_ir != null) try zig_args.append("-femit-llvm-ir");
- if (compile.generated_h != null) try zig_args.append("-femit-h");
-
- try addFlag(&zig_args, "formatted-panics", compile.formatted_panics);
-
- switch (compile.compress_debug_sections) {
- .none => {},
- .zlib => try zig_args.append("--compress-debug-sections=zlib"),
- .zstd => try zig_args.append("--compress-debug-sections=zstd"),
- }
-
- if (compile.link_eh_frame_hdr) {
- try zig_args.append("--eh-frame-hdr");
- }
- if (compile.link_emit_relocs) {
- try zig_args.append("--emit-relocs");
- }
- if (compile.link_function_sections) {
- try zig_args.append("-ffunction-sections");
- }
- if (compile.link_data_sections) {
- try zig_args.append("-fdata-sections");
- }
- if (compile.link_gc_sections) |x| {
- try zig_args.append(if (x) "--gc-sections" else "--no-gc-sections");
- }
- if (!compile.linker_dynamicbase) {
- try zig_args.append("--no-dynamicbase");
- }
- if (compile.linker_allow_shlib_undefined) |x| {
- try zig_args.append(if (x) "-fallow-shlib-undefined" else "-fno-allow-shlib-undefined");
- }
- if (compile.link_z_notext) {
- try zig_args.append("-z");
- try zig_args.append("notext");
- }
- if (!compile.link_z_relro) {
- try zig_args.append("-z");
- try zig_args.append("norelro");
- }
- if (compile.link_z_lazy) {
- try zig_args.append("-z");
- try zig_args.append("lazy");
- }
- if (compile.link_z_common_page_size) |size| {
- try zig_args.append("-z");
- try zig_args.append(b.fmt("common-page-size={d}", .{size}));
- }
- if (compile.link_z_max_page_size) |size| {
- try zig_args.append("-z");
- try zig_args.append(b.fmt("max-page-size={d}", .{size}));
- }
- if (compile.link_z_defs) {
- try zig_args.append("-z");
- try zig_args.append("defs");
- }
-
- if (compile.libc_file) |libc_file| {
- try zig_args.append("--libc");
- try zig_args.append(libc_file.getPath2(b, step));
- } else if (b.libc_file) |libc_file| {
- try zig_args.append("--libc");
- try zig_args.append(libc_file);
- }
-
- try zig_args.append("--cache-dir");
- try zig_args.append(b.cache_root.path orelse ".");
-
- try zig_args.append("--global-cache-dir");
- try zig_args.append(b.graph.global_cache_root.path orelse ".");
-
- if (b.graph.debug_compiler_runtime_libs) |mode|
- try zig_args.append(b.fmt("--debug-rt={t}", .{mode}));
-
- try zig_args.append("--name");
- try zig_args.append(compile.name);
-
- if (compile.linkage) |some| switch (some) {
- .dynamic => try zig_args.append("-dynamic"),
- .static => try zig_args.append("-static"),
- };
- if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) {
- if (compile.version) |version| {
- try zig_args.append("--version");
- try zig_args.append(b.fmt("{f}", .{version}));
- }
-
- if (compile.rootModuleTarget().os.tag.isDarwin()) {
- const install_name = compile.install_name orelse b.fmt("@rpath/{s}{s}{s}", .{
- compile.rootModuleTarget().libPrefix(),
- compile.name,
- compile.rootModuleTarget().dynamicLibSuffix(),
- });
- try zig_args.append("-install_name");
- try zig_args.append(install_name);
- }
- }
-
- if (compile.entitlements) |entitlements| {
- try zig_args.appendSlice(&[_][]const u8{ "--entitlements", entitlements });
- }
- if (compile.pagezero_size) |pagezero_size| {
- const size = try std.fmt.allocPrint(arena, "{x}", .{pagezero_size});
- try zig_args.appendSlice(&[_][]const u8{ "-pagezero_size", size });
- }
- if (compile.headerpad_size) |headerpad_size| {
- const size = try std.fmt.allocPrint(arena, "{x}", .{headerpad_size});
- try zig_args.appendSlice(&[_][]const u8{ "-headerpad", size });
- }
- if (compile.headerpad_max_install_names) {
- try zig_args.append("-headerpad_max_install_names");
- }
- if (compile.dead_strip_dylibs) {
- try zig_args.append("-dead_strip_dylibs");
- }
- if (compile.force_load_objc) {
- try zig_args.append("-ObjC");
- }
- if (compile.discard_local_symbols) {
- try zig_args.append("--discard-all");
- }
-
- try addFlag(&zig_args, "compiler-rt", compile.bundle_compiler_rt);
- try addFlag(&zig_args, "ubsan-rt", compile.bundle_ubsan_rt);
- try addFlag(&zig_args, "dll-export-fns", compile.dll_export_fns);
- if (compile.rdynamic) {
- try zig_args.append("-rdynamic");
- }
- if (compile.import_memory) {
- try zig_args.append("--import-memory");
- }
- if (compile.export_memory) {
- try zig_args.append("--export-memory");
- }
- if (compile.import_symbols) {
- try zig_args.append("--import-symbols");
- }
- if (compile.import_table) {
- try zig_args.append("--import-table");
- }
- if (compile.export_table) {
- try zig_args.append("--export-table");
- }
- if (compile.initial_memory) |initial_memory| {
- try zig_args.append(b.fmt("--initial-memory={d}", .{initial_memory}));
- }
- if (compile.max_memory) |max_memory| {
- try zig_args.append(b.fmt("--max-memory={d}", .{max_memory}));
- }
- if (compile.shared_memory) {
- try zig_args.append("--shared-memory");
- }
- if (compile.global_base) |global_base| {
- try zig_args.append(b.fmt("--global-base={d}", .{global_base}));
- }
-
- if (compile.wasi_exec_model) |model| {
- try zig_args.append(b.fmt("-mexec-model={s}", .{@tagName(model)}));
- }
- if (compile.linker_script) |linker_script| {
- try zig_args.append("--script");
- try zig_args.append(linker_script.getPath2(b, step));
- }
-
- if (compile.version_script) |version_script| {
- try zig_args.append("--version-script");
- try zig_args.append(version_script.getPath2(b, step));
- }
- if (compile.linker_allow_undefined_version) |x| {
- try zig_args.append(if (x) "--undefined-version" else "--no-undefined-version");
- }
-
- if (compile.linker_enable_new_dtags) |enabled| {
- try zig_args.append(if (enabled) "--enable-new-dtags" else "--disable-new-dtags");
- }
-
- if (compile.kind == .@"test") {
- if (compile.exec_cmd_args) |exec_cmd_args| {
- for (exec_cmd_args) |cmd_arg| {
- if (cmd_arg) |arg| {
- try zig_args.append("--test-cmd");
- try zig_args.append(arg);
- } else {
- try zig_args.append("--test-cmd-bin");
- }
- }
- }
- }
-
- if (b.sysroot) |sysroot| {
- try zig_args.appendSlice(&[_][]const u8{ "--sysroot", sysroot });
- }
-
- // -I and -L arguments that appear after the last --mod argument apply to all modules.
- const cwd: Io.Dir = .cwd();
- const io = b.graph.io;
-
- for (b.search_prefixes.items) |search_prefix| {
- var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| {
- return step.fail("unable to open prefix directory '{s}': {s}", .{
- search_prefix, @errorName(err),
- });
- };
- defer prefix_dir.close(io);
-
- // Avoid passing -L and -I flags for nonexistent directories.
- // This prevents a warning, that should probably be upgraded to an error in Zig's
- // CLI parsing code, when the linker sees an -L directory that does not exist.
-
- if (prefix_dir.access(io, "lib", .{})) |_| {
- try zig_args.appendSlice(&.{
- "-L", b.pathJoin(&.{ search_prefix, "lib" }),
- });
- } else |err| switch (err) {
- error.FileNotFound => {},
- else => |e| return step.fail("unable to access '{s}/lib' directory: {s}", .{
- search_prefix, @errorName(e),
- }),
- }
-
- if (prefix_dir.access(io, "include", .{})) |_| {
- try zig_args.appendSlice(&.{
- "-I", b.pathJoin(&.{ search_prefix, "include" }),
- });
- } else |err| switch (err) {
- error.FileNotFound => {},
- else => |e| return step.fail("unable to access '{s}/include' directory: {s}", .{
- search_prefix, @errorName(e),
- }),
- }
- }
-
- if (compile.rc_includes != .any) {
- try zig_args.append("-rcincludes");
- try zig_args.append(@tagName(compile.rc_includes));
- }
-
- try addFlag(&zig_args, "each-lib-rpath", compile.each_lib_rpath);
-
- if (compile.build_id orelse b.build_id) |build_id| {
- try zig_args.append(switch (build_id) {
- .hexstring => |hs| b.fmt("--build-id=0x{x}", .{hs.toSlice()}),
- .none, .fast, .uuid, .sha1, .md5 => b.fmt("--build-id={s}", .{@tagName(build_id)}),
- });
- }
-
- const opt_zig_lib_dir = if (compile.zig_lib_dir) |dir|
- dir.getPath2(b, step)
- else if (b.graph.zig_lib_directory.path) |_|
- b.fmt("{f}", .{b.graph.zig_lib_directory})
- else
- null;
-
- if (opt_zig_lib_dir) |zig_lib_dir| {
- try zig_args.append("--zig-lib-dir");
- try zig_args.append(zig_lib_dir);
- }
-
- try addFlag(&zig_args, "PIE", compile.pie);
-
- if (compile.lto) |lto| {
- try zig_args.append(switch (lto) {
- .full => "-flto=full",
- .thin => "-flto=thin",
- .none => "-fno-lto",
- });
- }
-
- try addFlag(&zig_args, "sanitize-coverage-trace-pc-guard", compile.sanitize_coverage_trace_pc_guard);
-
- if (compile.subsystem) |subsystem| {
- try zig_args.append("--subsystem");
- try zig_args.append(@tagName(subsystem));
- }
-
- if (compile.mingw_unicode_entry_point) {
- try zig_args.append("-municode");
- }
-
- if (compile.error_limit) |err_limit| try zig_args.appendSlice(&.{
- "--error-limit", b.fmt("{d}", .{err_limit}),
- });
-
- try addFlag(&zig_args, "incremental", b.graph.incremental);
-
- try zig_args.append("--listen=-");
-
- // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux
- // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and
- // pass that to zig, e.g. via 'zig build-lib @args.rsp'
- // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html
- var args_length: usize = 0;
- for (zig_args.items) |arg| {
- args_length += arg.len + 1; // +1 to account for null terminator
- }
- if (args_length >= 30 * 1024) {
- try b.cache_root.handle.createDirPath(io, "args");
-
- const args_to_escape = zig_args.items[2..];
- var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len);
- arg_blk: for (args_to_escape) |arg| {
- for (arg, 0..) |c, arg_idx| {
- if (c == '\\' or c == '"') {
- // Slow path for arguments that need to be escaped. We'll need to allocate and copy
- var escaped: std.ArrayList(u8) = .empty;
- try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1);
- try escaped.appendSlice(arena, arg[0..arg_idx]);
- for (arg[arg_idx..]) |to_escape| {
- if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\');
- try escaped.append(arena, to_escape);
- }
- escaped_args.appendAssumeCapacity(escaped.items);
- continue :arg_blk;
- }
- }
- escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument
- }
-
- // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with
- // other zig build commands running in parallel.
- const partially_quoted = try std.mem.join(arena, "\" \"", escaped_args.items);
- const args = try std.mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" });
-
- var args_hash: [Sha256.digest_length]u8 = undefined;
- Sha256.hash(args, &args_hash, .{});
- var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined;
- _ = try std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash});
-
- const args_file = "args" ++ fs.path.sep_str ++ args_hex_hash;
- if (b.cache_root.handle.access(io, args_file, .{})) |_| {
- // The args file is already present from a previous run.
- } else |err| switch (err) {
- error.FileNotFound => {
- var af = b.cache_root.handle.createFileAtomic(io, args_file, .{
- .replace = false,
- .make_path = true,
- }) catch |e| return step.fail("failed creating tmp args file {f}{s}: {t}", .{
- b.cache_root, args_file, e,
- });
- defer af.deinit(io);
-
- af.file.writeStreamingAll(io, args) catch |e| {
- return step.fail("failed writing args data to tmp file {f}{s}: {t}", .{
- b.cache_root, args_file, e,
- });
- };
- // Note we can't clean up this file, not even after build
- // success, because that might interfere with another build
- // process that needs the same file.
- af.link(io) catch |e| switch (e) {
- error.PathAlreadyExists => {
- // The args file was created by another concurrent build process.
- },
- else => |other_err| return step.fail("failed linking tmp file {f}{s}: {t}", .{
- b.cache_root, args_file, other_err,
- }),
- };
- },
- else => |other_err| return other_err,
- }
-
- const resolved_args_file = try mem.concat(arena, u8, &.{
- "@",
- try b.cache_root.join(arena, &.{args_file}),
- });
-
- zig_args.shrinkRetainingCapacity(2);
- try zig_args.append(resolved_args_file);
- }
-
- return try zig_args.toOwnedSlice();
-}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- const b = step.owner;
- const compile: *Compile = @fieldParentPtr("step", step);
-
- const zig_args = try getZigArgs(compile, false);
-
- const maybe_output_dir = step.evalZigProcess(
- zig_args,
- options.progress_node,
- (b.graph.incremental == true) and (options.watch or options.web_server != null),
- options.web_server,
- options.gpa,
- ) catch |err| switch (err) {
- error.NeedCompileErrorCheck => {
- assert(compile.expect_errors != null);
- try checkCompileErrors(compile);
- return;
- },
- else => |e| return e,
- };
-
- // Update generated files
- if (maybe_output_dir) |output_dir| {
- if (compile.emit_directory) |lp| {
- lp.path = b.fmt("{f}", .{output_dir});
- }
-
- // zig fmt: off
- if (compile.generated_bin) |lp| lp.path = compile.outputPath(output_dir, .bin);
- if (compile.generated_pdb) |lp| lp.path = compile.outputPath(output_dir, .pdb);
- // hack for stage2_x86_64 + coff
- if (compile.generated_compiler_rt_dyn_lib) |lp| lp.path = compile.outputPath(output_dir, .compiler_rt_dyn_lib);
- if (compile.generated_implib) |lp| lp.path = compile.outputPath(output_dir, .implib);
- if (compile.generated_h) |lp| lp.path = compile.outputPath(output_dir, .h);
- if (compile.generated_docs) |lp| lp.path = compile.outputPath(output_dir, .docs);
- if (compile.generated_asm) |lp| lp.path = compile.outputPath(output_dir, .@"asm");
- if (compile.generated_llvm_ir) |lp| lp.path = compile.outputPath(output_dir, .llvm_ir);
- if (compile.generated_llvm_bc) |lp| lp.path = compile.outputPath(output_dir, .llvm_bc);
- // zig fmt: on
- }
-
- if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic and
- compile.version != null and compile.generated_bin != null and
- std.Build.wantSharedLibSymLinks(compile.rootModuleTarget()))
- {
- try doAtomicSymLinks(
- step,
- compile.getEmittedBin().getPath2(b, step),
- compile.major_only_filename.?,
- compile.name_only_filename.?,
- );
- }
-}
-fn outputPath(c: *Compile, out_dir: std.Build.Cache.Path, ea: std.zig.EmitArtifact) []const u8 {
- const arena = c.step.owner.graph.arena;
- const name = ea.cacheName(arena, .{
- .root_name = c.name,
- .target = &c.root_module.resolved_target.?.result,
- .output_mode = switch (c.kind) {
- .lib => .Lib,
- .obj, .test_obj => .Obj,
- .exe, .@"test" => .Exe,
- },
- .link_mode = c.linkage,
- .version = c.version,
- }) catch @panic("OOM");
- return out_dir.joinString(arena, name) catch @panic("OOM");
-}
-
-pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progress.Node) !Path {
- c.step.result_error_msgs.clearRetainingCapacity();
- c.step.result_stderr = "";
-
- c.step.result_error_bundle.deinit(gpa);
- c.step.result_error_bundle = std.zig.ErrorBundle.empty;
-
- if (c.step.result_failed_command) |cmd| {
- gpa.free(cmd);
- c.step.result_failed_command = null;
- }
-
- const zig_args = try getZigArgs(c, true);
- const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa);
- return maybe_output_bin_path.?;
-}
-
-pub fn doAtomicSymLinks(
- step: *Step,
- output_path: []const u8,
- filename_major_only: []const u8,
- filename_name_only: []const u8,
-) !void {
- const b = step.owner;
- const io = b.graph.io;
- const out_dir = fs.path.dirname(output_path) orelse ".";
- const out_basename = fs.path.basename(output_path);
- // sym link for libfoo.so.1 to libfoo.so.1.2.3
- const major_only_path = b.pathJoin(&.{ out_dir, filename_major_only });
- const cwd: Io.Dir = .cwd();
- cwd.symLinkAtomic(io, out_basename, major_only_path, .{}) catch |err| {
- return step.fail("unable to symlink {s} -> {s}: {s}", .{
- major_only_path, out_basename, @errorName(err),
- });
- };
- // sym link for libfoo.so to libfoo.so.1
- const name_only_path = b.pathJoin(&.{ out_dir, filename_name_only });
- cwd.symLinkAtomic(io, filename_major_only, name_only_path, .{}) catch |err| {
- return step.fail("Unable to symlink {s} -> {s}: {s}", .{
- name_only_path, filename_major_only, @errorName(err),
- });
- };
-}
-
-fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
- const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
- const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore);
- var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator);
- errdefer list.deinit();
- var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
- while (line_it.next()) |line| {
- if (mem.trim(u8, line, " \t").len == 0) continue;
- var tok_it = mem.tokenizeAny(u8, line, " \t");
- try list.append(PkgConfigPkg{
- .name = tok_it.next() orelse return error.PkgConfigInvalidOutput,
- .desc = tok_it.rest(),
- });
- }
- return list.toOwnedSlice();
-}
-
-fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
- if (b.pkg_config_pkg_list) |res| {
- return res;
- }
- var code: u8 = undefined;
- if (execPkgConfigList(b, &code)) |list| {
- b.pkg_config_pkg_list = list;
- return list;
- } else |err| {
- const result = switch (err) {
- error.ProcessTerminated => error.PkgConfigCrashed,
- error.ExecNotSupported => error.PkgConfigFailed,
- error.ExitCodeFailure => error.PkgConfigFailed,
- error.FileNotFound => error.PkgConfigNotInstalled,
- error.InvalidName => error.PkgConfigNotInstalled,
- error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
- else => return err,
- };
- b.pkg_config_pkg_list = result;
- return result;
- }
-}
-
-fn addFlag(args: *std.array_list.Managed([]const u8), comptime name: []const u8, opt: ?bool) !void {
- const cond = opt orelse return;
- try args.ensureUnusedCapacity(1);
- if (cond) {
- args.appendAssumeCapacity("-f" ++ name);
- } else {
- args.appendAssumeCapacity("-fno-" ++ name);
- }
-}
-
-fn checkCompileErrors(compile: *Compile) !void {
- // Clear this field so that it does not get printed by the build runner.
- const actual_eb = compile.step.result_error_bundle;
- compile.step.result_error_bundle = .empty;
-
- const arena = compile.step.owner.allocator;
-
- const actual_errors = ae: {
- var aw: std.Io.Writer.Allocating = .init(arena);
- defer aw.deinit();
- try actual_eb.renderToWriter(.{
- .include_reference_trace = false,
- .include_source_line = false,
- }, &aw.writer);
- break :ae try aw.toOwnedSlice();
- };
-
- // Render the expected lines into a string that we can compare verbatim.
- var expected_generated: std.ArrayList(u8) = .empty;
- const expect_errors = compile.expect_errors.?;
-
- var actual_line_it = mem.splitScalar(u8, actual_errors, '\n');
-
- // TODO merge this with the testing.expectEqualStrings logic, and also CheckFile
- switch (expect_errors) {
- .starts_with => |expect_starts_with| {
- if (std.mem.startsWith(u8, actual_errors, expect_starts_with)) return;
- return compile.step.fail(
- \\
- \\========= should start with: ============
- \\{s}
- \\========= but not found: ================
- \\{s}
- \\=========================================
- , .{ expect_starts_with, actual_errors });
- },
- .contains => |expect_line| {
- while (actual_line_it.next()) |actual_line| {
- if (!matchCompileError(actual_line, expect_line)) continue;
- return;
- }
-
- return compile.step.fail(
- \\
- \\========= should contain: ===============
- \\{s}
- \\========= but not found: ================
- \\{s}
- \\=========================================
- , .{ expect_line, actual_errors });
- },
- .stderr_contains => |expect_line| {
- const actual_stderr: []const u8 = if (compile.step.result_error_msgs.items.len > 0)
- compile.step.result_error_msgs.items[0]
- else
- &.{};
- compile.step.result_error_msgs.clearRetainingCapacity();
-
- var stderr_line_it = mem.splitScalar(u8, actual_stderr, '\n');
-
- while (stderr_line_it.next()) |actual_line| {
- if (!matchCompileError(actual_line, expect_line)) continue;
- return;
- }
-
- return compile.step.fail(
- \\
- \\========= should contain: ===============
- \\{s}
- \\========= but not found: ================
- \\{s}
- \\=========================================
- , .{ expect_line, actual_stderr });
- },
- .exact => |expect_lines| {
- for (expect_lines) |expect_line| {
- const actual_line = actual_line_it.next() orelse {
- try expected_generated.appendSlice(arena, expect_line);
- try expected_generated.append(arena, '\n');
- continue;
- };
- if (matchCompileError(actual_line, expect_line)) {
- try expected_generated.appendSlice(arena, actual_line);
- try expected_generated.append(arena, '\n');
- continue;
- }
- try expected_generated.appendSlice(arena, expect_line);
- try expected_generated.append(arena, '\n');
- }
-
- if (mem.eql(u8, expected_generated.items, actual_errors)) return;
-
- return compile.step.fail(
- \\
- \\========= expected: =====================
- \\{s}
- \\========= but found: ====================
- \\{s}
- \\=========================================
- , .{ expected_generated.items, actual_errors });
- },
- }
-}
-
-fn matchCompileError(actual: []const u8, expected: []const u8) bool {
- if (mem.endsWith(u8, actual, expected)) return true;
- if (mem.startsWith(u8, expected, ":?:?: ")) {
- if (mem.endsWith(u8, actual, expected[":?:?: ".len..])) return true;
- }
- // We scan for /?/ in expected line and if there is a match, we match everything
- // up to and after /?/.
- const expected_trim = mem.trim(u8, expected, " ");
- if (mem.find(u8, expected_trim, "/?/")) |index| {
- const actual_trim = mem.trim(u8, actual, " ");
- const lhs = expected_trim[0..index];
- const rhs = expected_trim[index + "/?/".len ..];
- if (mem.startsWith(u8, actual_trim, lhs) and mem.endsWith(u8, actual_trim, rhs)) return true;
- }
- return false;
-}
-
pub fn rootModuleTarget(c: *Compile) std.Target {
// The root module is always given a target, so we know this to be non-null.
return c.root_module.resolved_target.?.result;
}
-fn moduleNeedsCliArg(mod: *const Module) bool {
- return for (mod.link_objects.items) |o| switch (o) {
- .c_source_file, .c_source_files, .assembly_file, .win32_resource_file => break true,
- else => continue,
- } else false;
-}
-
/// Return the full set of `Step.Compile` which `start` depends on, recursively. `start` itself is
/// always returned as the first element. If `chase_dynamic` is `false`, then dynamic libraries are
/// not included, and their dependencies are not considered; if `chase_dynamic` is `true`, dynamic
diff --git a/lib/std/Build/Step/ConfigHeader.zig b/lib/std/Build/Step/ConfigHeader.zig
@@ -4,7 +4,20 @@ const std = @import("std");
const Io = std.Io;
const Step = std.Build.Step;
const Allocator = std.mem.Allocator;
-const Writer = std.Io.Writer;
+const Configuration = std.Build.Configuration;
+const allocPrint = std.fmt.allocPrint;
+
+step: Step,
+values: std.array_hash_map.String(Value) = .empty,
+/// This directory contains the generated file under the name `include_path`.
+generated_dir: Configuration.GeneratedFileIndex,
+
+style: Style,
+input_size_limit: ?u64,
+include_path: []const u8,
+include_guard: Configuration.OptionalString,
+
+pub const base_tag: Step.Tag = .config_header;
pub const Style = union(enum) {
/// A configure format supported by autotools that uses `#undef foo` to
@@ -37,70 +50,60 @@ pub const Value = union(enum) {
string: []const u8,
};
-step: Step,
-values: std.array_hash_map.String(Value),
-/// This directory contains the generated file under the name `include_path`.
-generated_dir: std.Build.GeneratedFile,
-
-style: Style,
-max_bytes: usize,
-include_path: []const u8,
-include_guard_override: ?[]const u8,
-
-pub const base_id: Step.Id = .config_header;
-
pub const Options = struct {
style: Style = .blank,
- max_bytes: usize = 2 * 1024 * 1024,
+ max_bytes: ?u64 = null,
include_path: ?[]const u8 = null,
+ include_guard: ?[]const u8 = null,
first_ret_addr: ?usize = null,
- include_guard_override: ?[]const u8 = null,
};
pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
- const config_header = owner.allocator.create(ConfigHeader) catch @panic("OOM");
-
- var include_path: []const u8 = "config.h";
-
- if (options.style.getPath()) |s| default_include_path: {
- const sub_path = switch (s) {
- .src_path => |sp| sp.sub_path,
- .generated => break :default_include_path,
- .cwd_relative => |sub_path| sub_path,
- .dependency => |dependency| dependency.sub_path,
- };
- const basename = std.fs.path.basename(sub_path);
- if (std.mem.endsWith(u8, basename, ".h.in")) {
- include_path = basename[0 .. basename.len - 3];
+ const graph = owner.graph;
+ const arena = graph.arena;
+ const wc = &graph.wip_configuration;
+ const config_header = graph.create(ConfigHeader);
+
+ const include_path: []const u8 = p: {
+ if (options.include_path) |p|
+ break :p graph.dupeString(p);
+
+ if (options.style.getPath()) |s| default: {
+ const sub_path = switch (s) {
+ .src_path => |sp| sp.sub_path,
+ .generated => break :default,
+ .cwd_relative => |sub_path| sub_path,
+ .relative => |r| r.sub_path,
+ .dependency => |dependency| dependency.sub_path,
+ };
+ const basename = Io.Dir.path.basename(sub_path);
+ if (std.mem.endsWith(u8, basename, ".h.in"))
+ break :p graph.dupeString(basename[0 .. basename.len - 3]);
}
- }
-
- if (options.include_path) |p| {
- include_path = p;
- }
+ break :p "config.h";
+ };
const name = if (options.style.getPath()) |s|
- owner.fmt("configure {s} header {s} to {s}", .{
- @tagName(options.style), s.getDisplayName(), include_path,
- })
+ allocPrint(arena, "configure {t} header {f} to {s}", .{
+ options.style, s, include_path,
+ }) catch @panic("OOM")
else
- owner.fmt("configure {s} header to {s}", .{ @tagName(options.style), include_path });
+ allocPrint(arena, "configure {t} header to {s}", .{
+ options.style, include_path,
+ }) catch @panic("OOM");
config_header.* = .{
.step = .init(.{
- .id = base_id,
+ .tag = base_tag,
.name = name,
.owner = owner,
- .makeFn = make,
.first_ret_addr = options.first_ret_addr orelse @returnAddress(),
}),
.style = options.style,
- .values = .empty,
-
- .max_bytes = options.max_bytes,
+ .input_size_limit = options.max_bytes,
.include_path = include_path,
- .include_guard_override = options.include_guard_override,
- .generated_dir = .{ .step = &config_header.step },
+ .include_guard = if (options.include_guard) |s| .init(wc.addString(s) catch @panic("OOM")) else .none,
+ .generated_dir = graph.addGeneratedFile(&config_header.step),
};
if (options.style.getPath()) |s| {
@@ -118,19 +121,6 @@ pub fn addValue(config_header: *ConfigHeader, name: []const u8, comptime T: type
return addValueInner(config_header, name, T, value) catch @panic("OOM");
}
-pub fn addValues(config_header: *ConfigHeader, values: anytype) void {
- inline for (@typeInfo(@TypeOf(values)).@"struct".fields) |field| {
- addValue(config_header, field.name, field.type, @field(values, field.name));
- }
-}
-
-pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath {
- return .{ .generated = .{ .file = &ch.generated_dir } };
-}
-pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath {
- return ch.getOutputDir().path(ch.step.owner, ch.include_path);
-}
-
fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: type, value: T) !void {
const arena = config_header.step.owner.allocator;
switch (@typeInfo(T)) {
@@ -182,895 +172,16 @@ fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: typ
}
}
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const b = step.owner;
- const config_header: *ConfigHeader = @fieldParentPtr("step", step);
- if (config_header.style.getPath()) |lp| try step.singleUnchangingWatchInput(lp);
-
- const gpa = b.allocator;
- const arena = b.allocator;
- const io = b.graph.io;
-
- var man = b.graph.cache.obtain();
- defer man.deinit();
-
- // Random bytes to make ConfigHeader unique. Refresh this with new
- // random bytes when ConfigHeader implementation is modified in a
- // non-backwards-compatible way.
- man.hash.add(@as(u32, 0xdef08d23));
- man.hash.addBytes(config_header.include_path);
- man.hash.addOptionalBytes(config_header.include_guard_override);
-
- var aw: Writer.Allocating = .init(gpa);
- defer aw.deinit();
- const bw = &aw.writer;
-
- const header_text = "This file was generated by ConfigHeader using the Zig Build System.";
- const c_generated_line = "/* " ++ header_text ++ " */\n";
- const asm_generated_line = "; " ++ header_text ++ "\n";
-
- switch (config_header.style) {
- .autoconf_undef, .autoconf_at => |file_source| {
- try bw.writeAll(c_generated_line);
- const src_path = file_source.getPath2(b, step);
- const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| {
- return step.fail("unable to read autoconf input file '{s}': {s}", .{
- src_path, @errorName(err),
- });
- };
- switch (config_header.style) {
- .autoconf_undef => try render_autoconf_undef(step, contents, bw, &config_header.values, src_path),
- .autoconf_at => try render_autoconf_at(step, contents, &aw, &config_header.values, src_path),
- else => unreachable,
- }
- },
- .cmake => |file_source| {
- try bw.writeAll(c_generated_line);
- const src_path = file_source.getPath2(b, step);
- const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| {
- return step.fail("unable to read cmake input file '{s}': {s}", .{
- src_path, @errorName(err),
- });
- };
- try render_cmake(step, contents, bw, config_header.values, src_path);
- },
- .blank => {
- try bw.writeAll(c_generated_line);
- try render_blank(gpa, bw, config_header.values, config_header.include_path, config_header.include_guard_override);
- },
- .nasm => {
- try bw.writeAll(asm_generated_line);
- try render_nasm(bw, config_header.values);
- },
- }
-
- const output = aw.written();
- man.hash.addBytes(output);
-
- if (try step.cacheHit(&man)) {
- const digest = man.final();
- config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
- return;
- }
-
- const digest = man.final();
-
- // If output_path has directory parts, deal with them. Example:
- // output_dir is zig-cache/o/HASH
- // output_path is libavutil/avconfig.h
- // We want to open directory zig-cache/o/HASH/libavutil/
- // but keep output_dir as zig-cache/o/HASH for -I include
- const sub_path = b.pathJoin(&.{ "o", &digest, config_header.include_path });
- const sub_path_dirname = std.fs.path.dirname(sub_path).?;
-
- b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| {
- return step.fail("unable to make path '{f}{s}': {s}", .{
- b.cache_root, sub_path_dirname, @errorName(err),
- });
- };
-
- b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = output }) catch |err| {
- return step.fail("unable to write file '{f}{s}': {s}", .{
- b.cache_root, sub_path, @errorName(err),
- });
- };
-
- config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
- try man.writeManifest();
-}
-
-fn render_autoconf_undef(
- step: *Step,
- contents: []const u8,
- bw: *Writer,
- values: *const std.array_hash_map.String(Value),
- src_path: []const u8,
-) !void {
- const build = step.owner;
- const allocator = build.allocator;
-
- var is_used: std.bit_set.Dynamic = try .initEmpty(allocator, values.count());
- defer is_used.deinit(allocator);
-
- var any_errors = false;
- var line_index: u32 = 0;
- var line_it = std.mem.splitScalar(u8, contents, '\n');
- while (line_it.next()) |line| : (line_index += 1) {
- if (!std.mem.startsWith(u8, line, "#")) {
- try bw.writeAll(line);
- try bw.writeByte('\n');
- continue;
- }
- var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
- const undef = it.next().?;
- if (!std.mem.eql(u8, undef, "undef")) {
- try bw.writeAll(line);
- try bw.writeByte('\n');
- continue;
- }
- const name = it.next().?;
- const index = values.getIndex(name) orelse {
- try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{
- src_path, line_index + 1, name,
- });
- any_errors = true;
- continue;
- };
- is_used.set(index);
- try renderValueC(bw, name, values.values()[index]);
- }
-
- var unused_value_it = is_used.iterator(.{ .kind = .unset });
- while (unused_value_it.next()) |index| {
- try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, values.keys()[index] });
- any_errors = true;
- }
-
- if (any_errors) {
- return error.MakeFailed;
- }
-}
-
-fn render_autoconf_at(
- step: *Step,
- contents: []const u8,
- aw: *Writer.Allocating,
- values: *const std.array_hash_map.String(Value),
- src_path: []const u8,
-) !void {
- const build = step.owner;
- const allocator = build.allocator;
- const bw = &aw.writer;
-
- const used = allocator.alloc(bool, values.count()) catch @panic("OOM");
- for (used) |*u| u.* = false;
- defer allocator.free(used);
-
- var any_errors = false;
- var line_index: u32 = 0;
- var line_it = std.mem.splitScalar(u8, contents, '\n');
- while (line_it.next()) |line| : (line_index += 1) {
- const last_line = line_it.index == line_it.buffer.len;
-
- const old_len = aw.written().len;
- expand_variables_autoconf_at(bw, line, values, used) catch |err| switch (err) {
- error.MissingValue => {
- const name = aw.written()[old_len..];
- defer aw.shrinkRetainingCapacity(old_len);
- try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{
- src_path, line_index + 1, name,
- });
- any_errors = true;
- continue;
- },
- else => {
- try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{
- src_path, line_index + 1, @errorName(err),
- });
- any_errors = true;
- continue;
- },
- };
- if (!last_line) try bw.writeByte('\n');
- }
-
- for (values.entries.slice().items(.key), used) |name, u| {
- if (!u) {
- try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, name });
- any_errors = true;
- }
- }
-
- if (any_errors) return error.MakeFailed;
-}
-
-fn render_cmake(
- step: *Step,
- contents: []const u8,
- bw: *Writer,
- values: std.array_hash_map.String(Value),
- src_path: []const u8,
-) !void {
- const build = step.owner;
- const allocator = build.allocator;
-
- var values_copy = try values.clone(allocator);
- defer values_copy.deinit(allocator);
-
- var any_errors = false;
- var line_index: u32 = 0;
- var line_it = std.mem.splitScalar(u8, contents, '\n');
- while (line_it.next()) |raw_line| : (line_index += 1) {
- const last_line = line_it.index == line_it.buffer.len;
-
- const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) {
- error.InvalidCharacter => {
- try step.addError("{s}:{d}: error: invalid character in a variable name", .{
- src_path, line_index + 1,
- });
- any_errors = true;
- continue;
- },
- else => {
- try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{
- src_path, line_index + 1, @errorName(err),
- });
- any_errors = true;
- continue;
- },
- };
- defer allocator.free(line);
-
- const line_start = std.mem.findNone(u8, line, " \t\r") orelse {
- try bw.writeAll(line);
- if (!last_line) try bw.writeByte('\n');
- continue;
- };
- const whitespace_prefix = line[0..line_start];
- const trimmed_line = line[line_start..];
-
- if (!std.mem.startsWith(u8, trimmed_line, "#")) {
- try bw.writeAll(line);
- if (!last_line) try bw.writeByte('\n');
- continue;
- }
-
- var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r");
- const cmakedefine = it.next().?;
- if (!std.mem.eql(u8, cmakedefine, "cmakedefine") and
- !std.mem.eql(u8, cmakedefine, "cmakedefine01"))
- {
- try bw.writeAll(line);
- if (!last_line) try bw.writeByte('\n');
- continue;
- }
-
- const booldefine = std.mem.eql(u8, cmakedefine, "cmakedefine01");
-
- const name = it.next() orelse {
- try step.addError("{s}:{d}: error: missing define name", .{
- src_path, line_index + 1,
- });
- any_errors = true;
- continue;
- };
- var value = values_copy.get(name) orelse blk: {
- if (booldefine) {
- break :blk Value{ .int = 0 };
- }
- break :blk Value.undef;
- };
-
- value = blk: {
- switch (value) {
- .boolean => |b| {
- if (!b) {
- break :blk Value.undef;
- }
- },
- .int => |i| {
- if (i == 0) {
- break :blk Value.undef;
- }
- },
- .string => |string| {
- if (string.len == 0) {
- break :blk Value.undef;
- }
- },
-
- else => {},
- }
- break :blk value;
- };
-
- if (booldefine) {
- value = blk: {
- switch (value) {
- .undef => {
- break :blk Value{ .boolean = false };
- },
- .defined => {
- break :blk Value{ .boolean = false };
- },
- .boolean => |b| {
- break :blk Value{ .boolean = b };
- },
- .int => |i| {
- break :blk Value{ .boolean = i != 0 };
- },
- .string => |string| {
- break :blk Value{ .boolean = string.len != 0 };
- },
-
- else => {
- break :blk Value{ .boolean = false };
- },
- }
- };
- } else if (value != Value.undef) {
- value = Value{ .ident = it.rest() };
- }
-
- try bw.writeAll(whitespace_prefix);
- try renderValueC(bw, name, value);
- }
-
- if (any_errors) {
- return error.HeaderConfigFailed;
- }
-}
-
-fn render_blank(
- gpa: std.mem.Allocator,
- bw: *Writer,
- defines: std.array_hash_map.String(Value),
- include_path: []const u8,
- include_guard_override: ?[]const u8,
-) !void {
- const include_guard_name = include_guard_override orelse blk: {
- const name = try gpa.dupe(u8, include_path);
- for (name) |*byte| {
- switch (byte.*) {
- 'a'...'z' => byte.* = byte.* - 'a' + 'A',
- 'A'...'Z', '0'...'9' => continue,
- else => byte.* = '_',
- }
- }
- break :blk name;
- };
- defer if (include_guard_override == null) gpa.free(include_guard_name);
-
- try bw.print(
- \\#ifndef {[0]s}
- \\#define {[0]s}
- \\
- , .{include_guard_name});
-
- const values = defines.values();
- for (defines.keys(), 0..) |name, i| try renderValueC(bw, name, values[i]);
-
- try bw.print(
- \\#endif /* {s} */
- \\
- , .{include_guard_name});
-}
-
-fn render_nasm(bw: *Writer, defines: std.array_hash_map.String(Value)) !void {
- for (defines.keys(), defines.values()) |name, value| try renderValueNasm(bw, name, value);
-}
-
-fn renderValueC(bw: *Writer, name: []const u8, value: Value) !void {
- switch (value) {
- .undef => try bw.print("/* #undef {s} */\n", .{name}),
- .defined => try bw.print("#define {s}\n", .{name}),
- .boolean => |b| try bw.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }),
- .int => |i| try bw.print("#define {s} {d}\n", .{ name, i }),
- .ident => |ident| try bw.print("#define {s} {s}\n", .{ name, ident }),
- // TODO: use C-specific escaping instead of zig string literals
- .string => |string| try bw.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
- }
-}
-
-fn renderValueNasm(bw: *Writer, name: []const u8, value: Value) !void {
- switch (value) {
- .undef => try bw.print("; %undef {s}\n", .{name}),
- .defined => try bw.print("%define {s}\n", .{name}),
- .boolean => |b| try bw.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }),
- .int => |i| try bw.print("%define {s} {d}\n", .{ name, i }),
- .ident => |ident| try bw.print("%define {s} {s}\n", .{ name, ident }),
- // TODO: use nasm-specific escaping instead of zig string literals
- .string => |string| try bw.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
- }
-}
-
-fn expand_variables_autoconf_at(
- bw: *Writer,
- contents: []const u8,
- values: *const std.array_hash_map.String(Value),
- used: []bool,
-) !void {
- const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
-
- var curr: usize = 0;
- var source_offset: usize = 0;
- while (curr < contents.len) : (curr += 1) {
- if (contents[curr] != '@') continue;
- if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
- if (close_pos == curr + 1) {
- // closed immediately, preserve as a literal
- continue;
- }
- const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
- if (valid_varname_end != close_pos) {
- // contains invalid characters, preserve as a literal
- continue;
- }
-
- const key = contents[curr + 1 .. close_pos];
- const index = values.getIndex(key) orelse {
- // Report the missing key to the caller.
- try bw.writeAll(key);
- return error.MissingValue;
- };
- const value = values.entries.slice().items(.value)[index];
- used[index] = true;
- try bw.writeAll(contents[source_offset..curr]);
- switch (value) {
- .undef, .defined => {},
- .boolean => |b| try bw.writeByte(@as(u8, '0') + @intFromBool(b)),
- .int => |i| try bw.print("{d}", .{i}),
- .ident, .string => |s| try bw.writeAll(s),
- }
-
- curr = close_pos;
- source_offset = close_pos + 1;
- }
- }
-
- try bw.writeAll(contents[source_offset..]);
-}
-
-fn expand_variables_cmake(
- allocator: Allocator,
- contents: []const u8,
- values: std.array_hash_map.String(Value),
-) ![]const u8 {
- var result: std.array_list.Managed(u8) = .init(allocator);
- errdefer result.deinit();
-
- const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-";
- const open_var = "${";
-
- var curr: usize = 0;
- var source_offset: usize = 0;
- const Position = struct {
- source: usize,
- target: usize,
- };
- var var_stack: std.array_list.Managed(Position) = .init(allocator);
- defer var_stack.deinit();
- loop: while (curr < contents.len) : (curr += 1) {
- switch (contents[curr]) {
- '@' => blk: {
- if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
- if (close_pos == curr + 1) {
- // closed immediately, preserve as a literal
- break :blk;
- }
- const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
- if (valid_varname_end != close_pos) {
- // contains invalid characters, preserve as a literal
- break :blk;
- }
-
- const key = contents[curr + 1 .. close_pos];
- const value = values.get(key) orelse return error.MissingValue;
- const missing = contents[source_offset..curr];
- try result.appendSlice(missing);
- switch (value) {
- .undef, .defined => {},
- .boolean => |b| {
- try result.append(if (b) '1' else '0');
- },
- .int => |i| {
- try result.print("{d}", .{i});
- },
- .ident, .string => |s| {
- try result.appendSlice(s);
- },
- }
-
- curr = close_pos;
- source_offset = close_pos + 1;
-
- continue :loop;
- }
- },
- '$' => blk: {
- const next = curr + 1;
- if (next == contents.len or contents[next] != '{') {
- // no open bracket detected, preserve as a literal
- break :blk;
- }
- const missing = contents[source_offset..curr];
- try result.appendSlice(missing);
- try result.appendSlice(open_var);
-
- source_offset = curr + open_var.len;
- curr = next;
- try var_stack.append(Position{
- .source = curr,
- .target = result.items.len - open_var.len,
- });
-
- continue :loop;
- },
- '}' => blk: {
- if (var_stack.items.len == 0) {
- // no open bracket, preserve as a literal
- break :blk;
- }
- const open_pos = var_stack.pop().?;
- if (source_offset == open_pos.source) {
- source_offset += open_var.len;
- }
- const missing = contents[source_offset..curr];
- try result.appendSlice(missing);
-
- const key_start = open_pos.target + open_var.len;
- const key = result.items[key_start..];
- if (key.len == 0) {
- return error.MissingKey;
- }
- const value = values.get(key) orelse return error.MissingValue;
- result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len);
- switch (value) {
- .undef, .defined => {},
- .boolean => |b| {
- try result.append(if (b) '1' else '0');
- },
- .int => |i| {
- try result.print("{d}", .{i});
- },
- .ident, .string => |s| {
- try result.appendSlice(s);
- },
- }
-
- source_offset = curr + 1;
-
- continue :loop;
- },
- '\\' => {
- // backslash is not considered a special character
- continue :loop;
- },
- else => {},
- }
-
- if (var_stack.items.len > 0 and std.mem.findScalar(u8, valid_varname_chars, contents[curr]) == null) {
- return error.InvalidCharacter;
- }
- }
-
- if (source_offset != contents.len) {
- const missing = contents[source_offset..];
- try result.appendSlice(missing);
+pub fn addValues(config_header: *ConfigHeader, values: anytype) void {
+ inline for (@typeInfo(@TypeOf(values)).@"struct".fields) |field| {
+ addValue(config_header, field.name, field.type, @field(values, field.name));
}
-
- return result.toOwnedSlice();
-}
-
-fn testReplaceVariablesAutoconfAt(
- allocator: Allocator,
- contents: []const u8,
- expected: []const u8,
- values: std.array_hash_map.String(Value),
-) !void {
- var aw: Writer.Allocating = .init(allocator);
- defer aw.deinit();
-
- const used = try allocator.alloc(bool, values.count());
- for (used) |*u| u.* = false;
- defer allocator.free(used);
-
- try expand_variables_autoconf_at(&aw.writer, contents, values, used);
-
- for (used) |u| if (!u) return error.UnusedValue;
- try std.testing.expectEqualStrings(expected, aw.written());
-}
-
-fn testReplaceVariablesCMake(
- allocator: Allocator,
- contents: []const u8,
- expected: []const u8,
- values: std.array_hash_map.String(Value),
-) !void {
- const actual = try expand_variables_cmake(allocator, contents, values);
- defer allocator.free(actual);
-
- try std.testing.expectEqualStrings(expected, actual);
-}
-
-test "expand_variables_autoconf_at simple cases" {
- const allocator = std.testing.allocator;
- var values: std.array_hash_map.String(Value) = .init(allocator);
- defer values.deinit();
-
- // empty strings are preserved
- try testReplaceVariablesAutoconfAt(allocator, "", "", values);
-
- // line with misc content is preserved
- try testReplaceVariablesAutoconfAt(allocator, "no substitution", "no substitution", values);
-
- // empty @ sigils are preserved
- try testReplaceVariablesAutoconfAt(allocator, "@", "@", values);
- try testReplaceVariablesAutoconfAt(allocator, "@@", "@@", values);
- try testReplaceVariablesAutoconfAt(allocator, "@@@", "@@@", values);
- try testReplaceVariablesAutoconfAt(allocator, "@@@@", "@@@@", values);
-
- // simple substitution
- try values.putNoClobber("undef", .undef);
- try testReplaceVariablesAutoconfAt(allocator, "@undef@", "", values);
- values.clearRetainingCapacity();
-
- try values.putNoClobber("defined", .defined);
- try testReplaceVariablesAutoconfAt(allocator, "@defined@", "", values);
- values.clearRetainingCapacity();
-
- try values.putNoClobber("true", Value{ .boolean = true });
- try testReplaceVariablesAutoconfAt(allocator, "@true@", "1", values);
- values.clearRetainingCapacity();
-
- try values.putNoClobber("false", Value{ .boolean = false });
- try testReplaceVariablesAutoconfAt(allocator, "@false@", "0", values);
- values.clearRetainingCapacity();
-
- try values.putNoClobber("int", Value{ .int = 42 });
- try testReplaceVariablesAutoconfAt(allocator, "@int@", "42", values);
- values.clearRetainingCapacity();
-
- try values.putNoClobber("ident", Value{ .string = "value" });
- try testReplaceVariablesAutoconfAt(allocator, "@ident@", "value", values);
- values.clearRetainingCapacity();
-
- try values.putNoClobber("string", Value{ .string = "text" });
- try testReplaceVariablesAutoconfAt(allocator, "@string@", "text", values);
- values.clearRetainingCapacity();
-
- // double packed substitution
- try values.putNoClobber("string", Value{ .string = "text" });
- try testReplaceVariablesAutoconfAt(allocator, "@string@@string@", "texttext", values);
- values.clearRetainingCapacity();
-
- // triple packed substitution
- try values.putNoClobber("int", Value{ .int = 42 });
- try values.putNoClobber("string", Value{ .string = "text" });
- try testReplaceVariablesAutoconfAt(allocator, "@string@@int@@string@", "text42text", values);
- values.clearRetainingCapacity();
-
- // double separated substitution
- try values.putNoClobber("int", Value{ .int = 42 });
- try testReplaceVariablesAutoconfAt(allocator, "@int@.@int@", "42.42", values);
- values.clearRetainingCapacity();
-
- // triple separated substitution
- try values.putNoClobber("true", Value{ .boolean = true });
- try values.putNoClobber("int", Value{ .int = 42 });
- try testReplaceVariablesAutoconfAt(allocator, "@int@.@true@.@int@", "42.1.42", values);
- values.clearRetainingCapacity();
-
- // misc prefix is preserved
- try values.putNoClobber("false", Value{ .boolean = false });
- try testReplaceVariablesAutoconfAt(allocator, "false is @false@", "false is 0", values);
- values.clearRetainingCapacity();
-
- // misc suffix is preserved
- try values.putNoClobber("true", Value{ .boolean = true });
- try testReplaceVariablesAutoconfAt(allocator, "@true@ is true", "1 is true", values);
- values.clearRetainingCapacity();
-
- // surrounding content is preserved
- try values.putNoClobber("int", Value{ .int = 42 });
- try testReplaceVariablesAutoconfAt(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values);
- values.clearRetainingCapacity();
-
- // incomplete key is preserved
- try testReplaceVariablesAutoconfAt(allocator, "@undef", "@undef", values);
-
- // unknown key leads to an error
- try std.testing.expectError(error.MissingValue, testReplaceVariablesAutoconfAt(allocator, "@bad@", "", values));
-
- // unused key leads to an error
- try values.putNoClobber("int", Value{ .int = 42 });
- try values.putNoClobber("false", Value{ .boolean = false });
- try std.testing.expectError(error.UnusedValue, testReplaceVariablesAutoconfAt(allocator, "@int", "", values));
- values.clearRetainingCapacity();
-}
-
-test "expand_variables_autoconf_at edge cases" {
- const allocator = std.testing.allocator;
- var values: std.array_hash_map.String(Value) = .init(allocator);
- defer values.deinit();
-
- // @-vars resolved only when they wrap valid characters, otherwise considered literals
- try values.putNoClobber("string", Value{ .string = "text" });
- try testReplaceVariablesAutoconfAt(allocator, "@@string@@", "@text@", values);
- values.clearRetainingCapacity();
-
- // expanded variables are considered strings after expansion
- try values.putNoClobber("string_at", Value{ .string = "@string@" });
- try testReplaceVariablesAutoconfAt(allocator, "@string_at@", "@string@", values);
- values.clearRetainingCapacity();
-}
-
-test "expand_variables_cmake simple cases" {
- const allocator = std.testing.allocator;
- var values: std.array_hash_map.String(Value) = .init(allocator);
- defer values.deinit();
-
- try values.putNoClobber("undef", .undef);
- try values.putNoClobber("defined", .defined);
- try values.putNoClobber("true", Value{ .boolean = true });
- try values.putNoClobber("false", Value{ .boolean = false });
- try values.putNoClobber("int", Value{ .int = 42 });
- try values.putNoClobber("ident", Value{ .string = "value" });
- try values.putNoClobber("string", Value{ .string = "text" });
-
- // empty strings are preserved
- try testReplaceVariablesCMake(allocator, "", "", values);
-
- // line with misc content is preserved
- try testReplaceVariablesCMake(allocator, "no substitution", "no substitution", values);
-
- // empty ${} wrapper leads to an error
- try std.testing.expectError(error.MissingKey, testReplaceVariablesCMake(allocator, "${}", "", values));
-
- // empty @ sigils are preserved
- try testReplaceVariablesCMake(allocator, "@", "@", values);
- try testReplaceVariablesCMake(allocator, "@@", "@@", values);
- try testReplaceVariablesCMake(allocator, "@@@", "@@@", values);
- try testReplaceVariablesCMake(allocator, "@@@@", "@@@@", values);
-
- // simple substitution
- try testReplaceVariablesCMake(allocator, "@undef@", "", values);
- try testReplaceVariablesCMake(allocator, "${undef}", "", values);
- try testReplaceVariablesCMake(allocator, "@defined@", "", values);
- try testReplaceVariablesCMake(allocator, "${defined}", "", values);
- try testReplaceVariablesCMake(allocator, "@true@", "1", values);
- try testReplaceVariablesCMake(allocator, "${true}", "1", values);
- try testReplaceVariablesCMake(allocator, "@false@", "0", values);
- try testReplaceVariablesCMake(allocator, "${false}", "0", values);
- try testReplaceVariablesCMake(allocator, "@int@", "42", values);
- try testReplaceVariablesCMake(allocator, "${int}", "42", values);
- try testReplaceVariablesCMake(allocator, "@ident@", "value", values);
- try testReplaceVariablesCMake(allocator, "${ident}", "value", values);
- try testReplaceVariablesCMake(allocator, "@string@", "text", values);
- try testReplaceVariablesCMake(allocator, "${string}", "text", values);
-
- // double packed substitution
- try testReplaceVariablesCMake(allocator, "@string@@string@", "texttext", values);
- try testReplaceVariablesCMake(allocator, "${string}${string}", "texttext", values);
-
- // triple packed substitution
- try testReplaceVariablesCMake(allocator, "@string@@int@@string@", "text42text", values);
- try testReplaceVariablesCMake(allocator, "@string@${int}@string@", "text42text", values);
- try testReplaceVariablesCMake(allocator, "${string}@int@${string}", "text42text", values);
- try testReplaceVariablesCMake(allocator, "${string}${int}${string}", "text42text", values);
-
- // double separated substitution
- try testReplaceVariablesCMake(allocator, "@int@.@int@", "42.42", values);
- try testReplaceVariablesCMake(allocator, "${int}.${int}", "42.42", values);
-
- // triple separated substitution
- try testReplaceVariablesCMake(allocator, "@int@.@true@.@int@", "42.1.42", values);
- try testReplaceVariablesCMake(allocator, "@int@.${true}.@int@", "42.1.42", values);
- try testReplaceVariablesCMake(allocator, "${int}.@true@.${int}", "42.1.42", values);
- try testReplaceVariablesCMake(allocator, "${int}.${true}.${int}", "42.1.42", values);
-
- // misc prefix is preserved
- try testReplaceVariablesCMake(allocator, "false is @false@", "false is 0", values);
- try testReplaceVariablesCMake(allocator, "false is ${false}", "false is 0", values);
-
- // misc suffix is preserved
- try testReplaceVariablesCMake(allocator, "@true@ is true", "1 is true", values);
- try testReplaceVariablesCMake(allocator, "${true} is true", "1 is true", values);
-
- // surrounding content is preserved
- try testReplaceVariablesCMake(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values);
- try testReplaceVariablesCMake(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values);
-
- // incomplete key is preserved
- try testReplaceVariablesCMake(allocator, "@undef", "@undef", values);
- try testReplaceVariablesCMake(allocator, "${undef", "${undef", values);
- try testReplaceVariablesCMake(allocator, "{undef}", "{undef}", values);
- try testReplaceVariablesCMake(allocator, "undef@", "undef@", values);
- try testReplaceVariablesCMake(allocator, "undef}", "undef}", values);
-
- // unknown key leads to an error
- try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@bad@", "", values));
- try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${bad}", "", values));
}
-test "expand_variables_cmake edge cases" {
- const allocator = std.testing.allocator;
- var values: std.array_hash_map.String(Value) = .init(allocator);
- defer values.deinit();
-
- // special symbols
- try values.putNoClobber("at", Value{ .string = "@" });
- try values.putNoClobber("dollar", Value{ .string = "$" });
- try values.putNoClobber("underscore", Value{ .string = "_" });
-
- // basic value
- try values.putNoClobber("string", Value{ .string = "text" });
-
- // proxy case values
- try values.putNoClobber("string_proxy", Value{ .string = "string" });
- try values.putNoClobber("string_at", Value{ .string = "@string@" });
- try values.putNoClobber("string_curly", Value{ .string = "{string}" });
- try values.putNoClobber("string_var", Value{ .string = "${string}" });
-
- // stack case values
- try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" });
- try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" });
-
- // @-vars resolved only when they wrap valid characters, otherwise considered literals
- try testReplaceVariablesCMake(allocator, "@@string@@", "@text@", values);
- try testReplaceVariablesCMake(allocator, "@${string}@", "@text@", values);
-
- // @-vars are resolved inside ${}-vars
- try testReplaceVariablesCMake(allocator, "${@string_proxy@}", "text", values);
-
- // expanded variables are considered strings after expansion
- try testReplaceVariablesCMake(allocator, "@string_at@", "@string@", values);
- try testReplaceVariablesCMake(allocator, "${string_at}", "@string@", values);
- try testReplaceVariablesCMake(allocator, "$@string_curly@", "${string}", values);
- try testReplaceVariablesCMake(allocator, "$${string_curly}", "${string}", values);
- try testReplaceVariablesCMake(allocator, "${string_var}", "${string}", values);
- try testReplaceVariablesCMake(allocator, "@string_var@", "${string}", values);
- try testReplaceVariablesCMake(allocator, "${dollar}{${string}}", "${text}", values);
- try testReplaceVariablesCMake(allocator, "@dollar@{${string}}", "${text}", values);
- try testReplaceVariablesCMake(allocator, "@dollar@{@string@}", "${text}", values);
-
- // when expanded variables contain invalid characters, they prevent further expansion
- try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${${string_var}}", "", values));
- try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${@string_var@}", "", values));
-
- // nested expanded variables are expanded from the inside out
- try testReplaceVariablesCMake(allocator, "${string${underscore}proxy}", "string", values);
- try testReplaceVariablesCMake(allocator, "${string@underscore@proxy}", "string", values);
-
- // nested vars are only expanded when ${} is closed
- try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@underscore@proxy@", "", values));
- try testReplaceVariablesCMake(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values);
- try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "", values));
- try testReplaceVariablesCMake(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values);
-
- // invalid characters lead to an error
- try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str*ing}", "", values));
- try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str$ing}", "", values));
- try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str@ing}", "", values));
+pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath {
+ return .{ .generated = .{ .index = ch.generated_dir } };
}
-test "expand_variables_cmake escaped characters" {
- const allocator = std.testing.allocator;
- var values: std.array_hash_map.String(Value) = .init(allocator);
- defer values.deinit();
-
- try values.putNoClobber("string", Value{ .string = "text" });
-
- // backslash is an invalid character for @ lookup
- try testReplaceVariablesCMake(allocator, "\\@string\\@", "\\@string\\@", values);
-
- // backslash is preserved, but doesn't affect ${} variable expansion
- try testReplaceVariablesCMake(allocator, "\\${string}", "\\text", values);
-
- // backslash breaks ${} opening bracket identification
- try testReplaceVariablesCMake(allocator, "$\\{string}", "$\\{string}", values);
-
- // backslash is skipped when checking for invalid characters, yet it mangles the key
- try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${string\\}", "", values));
+pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath {
+ return ch.getOutputDir().path(ch.step.owner, ch.include_path);
}
diff --git a/lib/std/Build/Step/Fail.zig b/lib/std/Build/Step/Fail.zig
@@ -1,35 +1,25 @@
//! Fail the build with a given message.
+const Fail = @This();
+
const std = @import("std");
const Step = std.Build.Step;
-const Fail = @This();
+const Configuration = std.Build.Configuration;
step: Step,
-error_msg: []const u8,
+error_msg: Configuration.String,
-pub const base_id: Step.Id = .fail;
+pub const base_tag: Step.Tag = .fail;
pub fn create(owner: *std.Build, error_msg: []const u8) *Fail {
- const fail = owner.allocator.create(Fail) catch @panic("OOM");
-
+ const graph = owner.graph;
+ const fail = graph.create(Fail);
fail.* = .{
- .step = Step.init(.{
- .id = base_id,
+ .step = .init(.{
+ .tag = base_tag,
.name = "fail",
.owner = owner,
- .makeFn = make,
}),
- .error_msg = owner.dupe(error_msg),
+ .error_msg = graph.addString(error_msg),
};
-
return fail;
}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options; // No progress to report.
-
- const fail: *Fail = @fieldParentPtr("step", step);
-
- try step.result_error_msgs.append(step.owner.allocator, fail.error_msg);
-
- return error.MakeFailed;
-}
diff --git a/lib/std/Build/Step/FindProgram.zig b/lib/std/Build/Step/FindProgram.zig
@@ -0,0 +1,31 @@
+const FindProgram = @This();
+
+const std = @import("std");
+const Step = std.Build.Step;
+const Configuration = std.Build.Configuration;
+
+step: Step,
+found_path: Configuration.GeneratedFileIndex,
+names: Configuration.StringList,
+
+pub const base_tag: Step.Tag = .find_program;
+
+pub const Options = struct {
+ names: []const []const u8,
+};
+
+pub fn create(owner: *std.Build, options: Options) *FindProgram {
+ const graph = owner.graph;
+ const wc = &graph.wip_configuration;
+ const fp = graph.create(FindProgram);
+ fp.* = .{
+ .step = .init(.{
+ .tag = base_tag,
+ .name = owner.fmt("find program {s} ({d} candidates)", .{ options.names[0], options.names.len }),
+ .owner = owner,
+ }),
+ .found_path = graph.addGeneratedFile(&fp.step),
+ .names = wc.addStringList(options.names) catch @panic("OOM"),
+ };
+ return fp;
+}
diff --git a/lib/std/Build/Step/Fmt.zig b/lib/std/Build/Step/Fmt.zig
@@ -1,81 +1,46 @@
//! This step has two modes:
//! * Modify mode: directly modify source files, formatting them in place.
//! * Check mode: fail the step if a non-conforming file is found.
+const Fmt = @This();
+
const std = @import("std");
const Step = std.Build.Step;
-const Fmt = @This();
+const LazyPath = std.Build.LazyPath;
+const Configuration = std.Build.Configuration;
step: Step,
-paths: []const []const u8,
-exclude_paths: []const []const u8,
+/// Intended to be read-only after the `Fmt` step is created.
+paths: []const LazyPath,
+/// Intended to be read-only after the `Fmt` step is created.
+exclude_paths: []const LazyPath,
check: bool,
-pub const base_id: Step.Id = .fmt;
+pub const base_tag: Step.Tag = .fmt;
pub const Options = struct {
- paths: []const []const u8 = &.{},
- exclude_paths: []const []const u8 = &.{},
+ paths: []const LazyPath = &.{},
+ exclude_paths: []const LazyPath = &.{},
/// If true, fails the build step when any non-conforming files are encountered.
check: bool = false,
};
pub fn create(owner: *std.Build, options: Options) *Fmt {
- const fmt = owner.allocator.create(Fmt) catch @panic("OOM");
- const name = if (options.check) "zig fmt --check" else "zig fmt";
+ const graph = owner.graph;
+ const fmt = graph.create(Fmt);
+
fmt.* = .{
- .step = Step.init(.{
- .id = base_id,
- .name = name,
+ .step = .init(.{
+ .tag = base_tag,
+ .name = if (options.check) "zig fmt --check" else "zig fmt",
.owner = owner,
- .makeFn = make,
}),
- .paths = owner.dupeStrings(options.paths),
- .exclude_paths = owner.dupeStrings(options.exclude_paths),
+ .paths = options.paths,
+ .exclude_paths = options.exclude_paths,
.check = options.check,
};
- return fmt;
-}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- const prog_node = options.progress_node;
-
- // TODO: if check=false, this means we are modifying source files in place, which
- // is an operation that could race against other operations also modifying source files
- // in place. In this case, this step should obtain a write lock while making those
- // modifications.
- const b = step.owner;
- const arena = b.allocator;
- const fmt: *Fmt = @fieldParentPtr("step", step);
+ for (options.paths) |lp| lp.addStepDependencies(&fmt.step);
+ for (options.exclude_paths) |lp| lp.addStepDependencies(&fmt.step);
- var argv: std.ArrayList([]const u8) = .empty;
- try argv.ensureUnusedCapacity(arena, 2 + 1 + fmt.paths.len + 2 * fmt.exclude_paths.len);
-
- argv.appendAssumeCapacity(b.graph.zig_exe);
- argv.appendAssumeCapacity("fmt");
-
- if (fmt.check) {
- argv.appendAssumeCapacity("--check");
- }
-
- for (fmt.paths) |p| {
- argv.appendAssumeCapacity(b.pathFromRoot(p));
- }
-
- for (fmt.exclude_paths) |p| {
- argv.appendAssumeCapacity("--exclude");
- argv.appendAssumeCapacity(b.pathFromRoot(p));
- }
-
- const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items);
- if (fmt.check) switch (run_result.term) {
- .exited => |code| if (code != 0 and run_result.stdout.len != 0) {
- var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n');
- while (it.next()) |bad_file_name| {
- try step.addError("{s}: non-conforming formatting", .{bad_file_name});
- }
- },
- else => {},
- };
- try step.handleChildProcessTerm(run_result.term);
+ return fmt;
}
diff --git a/lib/std/Build/Step/InstallArtifact.zig b/lib/std/Build/Step/InstallArtifact.zig
@@ -1,14 +1,14 @@
+const InstallArtifact = @This();
+
const std = @import("std");
const Step = std.Build.Step;
const InstallDir = std.Build.InstallDir;
-const InstallArtifact = @This();
-const fs = std.fs;
const LazyPath = std.Build.LazyPath;
step: Step,
dest_dir: ?InstallDir,
-dest_sub_path: []const u8,
+dest_sub_path: ?[]const u8,
emitted_bin: ?LazyPath,
implib_dir: ?InstallDir,
@@ -17,14 +17,10 @@ emitted_implib: ?LazyPath,
pdb_dir: ?InstallDir,
emitted_pdb: ?LazyPath,
-// hack for stage2_x86_64 + coff
-compiler_rt_dyn_lib_dir: ?InstallDir,
-emitted_compiler_rt_dyn_lib: ?LazyPath,
-
h_dir: ?InstallDir,
emitted_h: ?LazyPath,
-dylib_symlinks: ?DylibSymlinkInfo,
+dylib_symlinks: bool,
artifact: *Step.Compile,
@@ -33,7 +29,7 @@ const DylibSymlinkInfo = struct {
name_only_filename: []const u8,
};
-pub const base_id: Step.Id = .install_artifact;
+pub const base_tag: Step.Tag = .install_artifact;
pub const Options = struct {
/// Which installation directory to put the main output file into.
@@ -67,158 +63,47 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
},
.override => |o| o,
};
+ const pdb_dir: ?InstallDir = switch (options.pdb_dir) {
+ .disabled => null,
+ .default => if (artifact.producesPdbFile()) dest_dir else null,
+ .override => |o| o,
+ };
+ const implib_dir: ?InstallDir = switch (options.implib_dir) {
+ .disabled => null,
+ .default => if (artifact.producesImplib()) .lib else null,
+ .override => |o| o,
+ };
install_artifact.* = .{
.step = Step.init(.{
- .id = base_id,
+ .tag = base_tag,
.name = owner.fmt("install {s}", .{artifact.name}),
.owner = owner,
- .makeFn = make,
}),
.dest_dir = dest_dir,
- .pdb_dir = switch (options.pdb_dir) {
- .disabled => null,
- .default => if (artifact.producesPdbFile()) dest_dir else null,
- .override => |o| o,
- },
- .compiler_rt_dyn_lib_dir = switch (options.compiler_rt_dyn_lib_dir) {
- .disabled => null,
- .default => if (artifact.producesCompilerRtDynLib()) dest_dir else null,
- .override => |o| o,
- },
+ .pdb_dir = pdb_dir,
.h_dir = switch (options.h_dir) {
.disabled => null,
.default => if (artifact.kind == .lib) .header else null,
.override => |o| o,
},
- .implib_dir = switch (options.implib_dir) {
- .disabled => null,
- .default => if (artifact.producesImplib()) .lib else null,
- .override => |o| o,
- },
+ .implib_dir = implib_dir,
- .dylib_symlinks = if (options.dylib_symlinks orelse (dest_dir != null and
- artifact.isDynamicLibrary() and
- artifact.version != null and
- std.Build.wantSharedLibSymLinks(artifact.rootModuleTarget()))) .{
- .major_only_filename = artifact.major_only_filename.?,
- .name_only_filename = artifact.name_only_filename.?,
- } else null,
+ .dylib_symlinks = options.dylib_symlinks orelse (dest_dir != null and
+ artifact.isDynamicLibrary() and artifact.version != null and
+ std.Build.wantSharedLibSymLinks(artifact.rootModuleTarget())),
- .dest_sub_path = options.dest_sub_path orelse artifact.out_filename,
+ .dest_sub_path = options.dest_sub_path,
- .emitted_bin = null,
- .emitted_pdb = null,
- .emitted_compiler_rt_dyn_lib = null,
+ .emitted_bin = if (dest_dir != null) artifact.getEmittedBin() else null,
+ .emitted_pdb = if (pdb_dir != null) artifact.getEmittedPdb() else null,
+ // https://github.com/ziglang/zig/issues/9698
.emitted_h = null,
- .emitted_implib = null,
+ .emitted_implib = if (implib_dir != null) artifact.getEmittedImplib() else null,
.artifact = artifact,
};
install_artifact.step.dependOn(&artifact.step);
- if (install_artifact.dest_dir != null) install_artifact.emitted_bin = artifact.getEmittedBin();
- if (install_artifact.compiler_rt_dyn_lib_dir != null) install_artifact.emitted_compiler_rt_dyn_lib = artifact.getEmittedCompilerRtDynLib();
- if (install_artifact.pdb_dir != null) install_artifact.emitted_pdb = artifact.getEmittedPdb();
- // https://github.com/ziglang/zig/issues/9698
- //if (install_artifact.h_dir != null) install_artifact.emitted_h = artifact.getEmittedH();
- if (install_artifact.implib_dir != null) install_artifact.emitted_implib = artifact.getEmittedImplib();
-
return install_artifact;
}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const install_artifact: *InstallArtifact = @fieldParentPtr("step", step);
- const b = step.owner;
- const io = b.graph.io;
-
- var all_cached = true;
-
- if (install_artifact.dest_dir) |dest_dir| {
- const full_dest_path = b.getInstallPath(dest_dir, install_artifact.dest_sub_path);
- const p = try step.installFile(install_artifact.emitted_bin.?, full_dest_path);
- all_cached = all_cached and p == .fresh;
-
- if (install_artifact.dylib_symlinks) |dls| {
- try Step.Compile.doAtomicSymLinks(step, full_dest_path, dls.major_only_filename, dls.name_only_filename);
- }
-
- install_artifact.artifact.installed_path = full_dest_path;
- }
-
- if (install_artifact.compiler_rt_dyn_lib_dir) |compiler_rt_dir| {
- const full_compiler_rt_path = b.getInstallPath(compiler_rt_dir, install_artifact.emitted_compiler_rt_dyn_lib.?.basename(b, step));
- const p = try step.installFile(install_artifact.emitted_compiler_rt_dyn_lib.?, full_compiler_rt_path);
- all_cached = all_cached and p == .fresh;
- }
-
- if (install_artifact.implib_dir) |implib_dir| {
- const full_implib_path = b.getInstallPath(implib_dir, install_artifact.emitted_implib.?.basename(b, step));
- const p = try step.installFile(install_artifact.emitted_implib.?, full_implib_path);
- all_cached = all_cached and p == .fresh;
- }
-
- if (install_artifact.pdb_dir) |pdb_dir| {
- const full_pdb_path = b.getInstallPath(pdb_dir, install_artifact.emitted_pdb.?.basename(b, step));
- const p = try step.installFile(install_artifact.emitted_pdb.?, full_pdb_path);
- all_cached = all_cached and p == .fresh;
- }
-
- if (install_artifact.h_dir) |h_dir| {
- if (install_artifact.emitted_h) |emitted_h| {
- const full_h_path = b.getInstallPath(h_dir, emitted_h.basename(b, step));
- const p = try step.installFile(emitted_h, full_h_path);
- all_cached = all_cached and p == .fresh;
- }
-
- for (install_artifact.artifact.installed_headers.items) |installation| switch (installation) {
- .file => |file| {
- const full_h_path = b.getInstallPath(h_dir, file.dest_rel_path);
- const p = try step.installFile(file.source, full_h_path);
- all_cached = all_cached and p == .fresh;
- },
- .directory => |dir| {
- const src_dir_path = dir.source.getPath3(b, step);
- const full_h_prefix = b.getInstallPath(h_dir, dir.dest_rel_path);
-
- var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
- return step.fail("unable to open source directory '{f}': {s}", .{
- src_dir_path, @errorName(err),
- });
- };
- defer src_dir.close(io);
-
- var it = try src_dir.walk(b.allocator);
- next_entry: while (try it.next(io)) |entry| {
- for (dir.options.exclude_extensions) |ext| {
- if (std.mem.endsWith(u8, entry.path, ext)) continue :next_entry;
- }
- if (dir.options.include_extensions) |incs| {
- for (incs) |inc| {
- if (std.mem.endsWith(u8, entry.path, inc)) break;
- } else {
- continue :next_entry;
- }
- }
-
- const full_dest_path = b.pathJoin(&.{ full_h_prefix, entry.path });
- switch (entry.kind) {
- .directory => {
- try Step.handleVerbose(b, .inherit, &.{ "install", "-d", full_dest_path });
- const p = try step.installDir(full_dest_path);
- all_cached = all_cached and p == .existed;
- },
- .file => {
- const p = try step.installFile(try dir.source.join(b.allocator, entry.path), full_dest_path);
- all_cached = all_cached and p == .fresh;
- },
- else => continue,
- }
- }
- },
- };
- }
-
- step.result_cached = all_cached;
-}
diff --git a/lib/std/Build/Step/InstallDir.zig b/lib/std/Build/Step/InstallDir.zig
@@ -1,14 +1,15 @@
+const InstallDir = @This();
+
const std = @import("std");
const mem = std.mem;
const fs = std.fs;
const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
-const InstallDir = @This();
step: Step,
options: Options,
-pub const base_id: Step.Id = .install_dir;
+pub const base_tag: Step.Tag = .install_dir;
pub const Options = struct {
source_dir: LazyPath,
@@ -28,83 +29,29 @@ pub const Options = struct {
/// `@import("test.zig")` would be a compile error.
blank_extensions: []const []const u8 = &.{},
- fn dupe(opts: Options, b: *std.Build) Options {
+ fn dupe(opts: Options, graph: *const std.Build.Graph) Options {
return .{
- .source_dir = opts.source_dir.dupe(b),
- .install_dir = opts.install_dir.dupe(b),
- .install_subdir = b.dupe(opts.install_subdir),
- .exclude_extensions = b.dupeStrings(opts.exclude_extensions),
- .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null,
- .blank_extensions = b.dupeStrings(opts.blank_extensions),
+ .source_dir = opts.source_dir.dupe(graph),
+ .install_dir = opts.install_dir.dupe(graph),
+ .install_subdir = graph.dupeString(opts.install_subdir),
+ .exclude_extensions = graph.dupeStrings(opts.exclude_extensions),
+ .include_extensions = if (opts.include_extensions) |incs| graph.dupeStrings(incs) else null,
+ .blank_extensions = graph.dupeStrings(opts.blank_extensions),
};
}
};
pub fn create(owner: *std.Build, options: Options) *InstallDir {
const install_dir = owner.allocator.create(InstallDir) catch @panic("OOM");
+ const graph = owner.graph;
install_dir.* = .{
.step = Step.init(.{
- .id = base_id,
- .name = owner.fmt("install {s}/", .{options.source_dir.getDisplayName()}),
+ .tag = base_tag,
+ .name = owner.fmt("install {f}/", .{options.source_dir}),
.owner = owner,
- .makeFn = make,
}),
- .options = options.dupe(owner),
+ .options = options.dupe(graph),
};
options.source_dir.addStepDependencies(&install_dir.step);
return install_dir;
}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const b = step.owner;
- const io = b.graph.io;
- const install_dir: *InstallDir = @fieldParentPtr("step", step);
- step.clearWatchInputs();
- const arena = b.allocator;
- const dest_prefix = b.getInstallPath(install_dir.options.install_dir, install_dir.options.install_subdir);
- const src_dir_path = install_dir.options.source_dir.getPath3(b, step);
- const need_derived_inputs = try step.addDirectoryWatchInput(install_dir.options.source_dir);
- var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
- return step.fail("unable to open source directory '{f}': {t}", .{ src_dir_path, err });
- };
- defer src_dir.close(io);
- var it = try src_dir.walk(arena);
- var all_cached = true;
- next_entry: while (try it.next(io)) |entry| {
- for (install_dir.options.exclude_extensions) |ext| {
- if (mem.endsWith(u8, entry.path, ext)) continue :next_entry;
- }
- if (install_dir.options.include_extensions) |incs| {
- for (incs) |inc| {
- if (mem.endsWith(u8, entry.path, inc)) break;
- } else {
- continue :next_entry;
- }
- }
-
- const src_path = try install_dir.options.source_dir.join(b.allocator, entry.path);
- const dest_path = b.pathJoin(&.{ dest_prefix, entry.path });
- switch (entry.kind) {
- .directory => {
- if (need_derived_inputs) _ = try step.addDirectoryWatchInput(src_path);
- const p = try step.installDir(dest_path);
- all_cached = all_cached and p == .existed;
- },
- .file => {
- for (install_dir.options.blank_extensions) |ext| {
- if (mem.endsWith(u8, entry.path, ext)) {
- try b.truncateFile(dest_path);
- continue :next_entry;
- }
- }
-
- const p = try step.installFile(src_path, dest_path);
- all_cached = all_cached and p == .fresh;
- },
- else => continue,
- }
- }
-
- step.result_cached = all_cached;
-}
diff --git a/lib/std/Build/Step/InstallFile.zig b/lib/std/Build/Step/InstallFile.zig
@@ -1,17 +1,18 @@
+const InstallFile = @This();
+
const std = @import("std");
const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
const InstallDir = std.Build.InstallDir;
-const InstallFile = @This();
const assert = std.debug.assert;
-pub const base_id: Step.Id = .install_file;
-
step: Step,
source: LazyPath,
dir: InstallDir,
dest_rel_path: []const u8,
+pub const base_tag: Step.Tag = .install_file;
+
pub fn create(
owner: *std.Build,
source: LazyPath,
@@ -19,29 +20,19 @@ pub fn create(
dest_rel_path: []const u8,
) *InstallFile {
assert(dest_rel_path.len != 0);
- const install_file = owner.allocator.create(InstallFile) catch @panic("OOM");
+ const graph = owner.graph;
+ const arena = graph.arena;
+ const install_file = arena.create(InstallFile) catch @panic("OOM");
install_file.* = .{
.step = Step.init(.{
- .id = base_id,
- .name = owner.fmt("install {s} to {s}", .{ source.getDisplayName(), dest_rel_path }),
+ .tag = base_tag,
+ .name = owner.fmt("install {f} to {s}", .{ source, dest_rel_path }),
.owner = owner,
- .makeFn = make,
}),
- .source = source.dupe(owner),
- .dir = dir.dupe(owner),
- .dest_rel_path = owner.dupePath(dest_rel_path),
+ .source = source.dupe(graph),
+ .dir = dir.dupe(graph),
+ .dest_rel_path = graph.dupePath(dest_rel_path),
};
source.addStepDependencies(&install_file.step);
return install_file;
}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const b = step.owner;
- const install_file: *InstallFile = @fieldParentPtr("step", step);
- try step.singleUnchangingWatchInput(install_file.source);
-
- const full_dest_path = b.getInstallPath(install_file.dir, install_file.dest_rel_path);
- const p = try step.installFile(install_file.source, full_dest_path);
- step.result_cached = p == .fresh;
-}
diff --git a/lib/std/Build/Step/ObjCopy.zig b/lib/std/Build/Step/ObjCopy.zig
@@ -1,92 +1,43 @@
-const std = @import("std");
const ObjCopy = @This();
-const Allocator = std.mem.Allocator;
-const ArenaAllocator = std.heap.ArenaAllocator;
-const File = std.Io.File;
-const InstallDir = std.Build.InstallDir;
+const std = @import("std");
const Step = std.Build.Step;
-const elf = std.elf;
-const fs = std.fs;
-const sort = std.sort;
-
-pub const base_id: Step.Id = .objcopy;
-
-pub const RawFormat = enum {
- bin,
- hex,
- elf,
-};
-
-pub const Strip = enum {
- none,
- debug,
- debug_and_symbols,
-};
-
-pub const SectionFlags = packed struct {
- /// add SHF_ALLOC
- alloc: bool = false,
-
- /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing
- contents: bool = false,
-
- /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing (same as contents)
- load: bool = false,
+const Configuration = std.Build.Configuration;
- /// readonly: clear default SHF_WRITE flag
- readonly: bool = false,
-
- /// add SHF_EXECINSTR
- code: bool = false,
+step: Step,
+input_file: std.Build.LazyPath,
+basename: Configuration.OptionalString,
+output_file: Configuration.GeneratedFileIndex,
+debug_file: ?DebugFile,
- /// add SHF_EXCLUDE
- exclude: bool = false,
+format: ?Format,
+only_section: Configuration.OptionalString,
+pad_to: ?u64,
+strip: Strip,
+compress_debug: bool,
- /// add SHF_X86_64_LARGE. Fatal error if target is not x86_64
- large: bool = false,
+add_sections: std.ArrayList(AddSection) = .empty,
+update_sections: std.ArrayList(Configuration.Step.ObjCopy.UpdateSection) = .empty,
- /// add SHF_MERGE
- merge: bool = false,
+pub const base_tag: Step.Tag = .obj_copy;
- /// add SHF_STRINGS
- strings: bool = false,
-};
+pub const Format = enum { binary, hex, elf };
+pub const Strip = Configuration.Step.ObjCopy.Strip;
+pub const SectionFlags = Configuration.Step.ObjCopy.SectionFlags;
pub const AddSection = struct {
- section_name: []const u8,
+ section_name: Configuration.String,
file_path: std.Build.LazyPath,
};
-pub const SetSectionAlignment = struct {
- section_name: []const u8,
- alignment: u32,
-};
-
-pub const SetSectionFlags = struct {
- section_name: []const u8,
- flags: SectionFlags,
+pub const DebugFile = struct {
+ basename: Configuration.OptionalString,
+ output_file: Configuration.GeneratedFileIndex,
};
-step: Step,
-input_file: std.Build.LazyPath,
-basename: []const u8,
-output_file: std.Build.GeneratedFile,
-output_file_debug: ?std.Build.GeneratedFile,
-
-format: ?RawFormat,
-only_section: ?[]const u8,
-pad_to: ?u64,
-strip: Strip,
-compress_debug: bool,
-
-add_section: ?AddSection,
-set_section_alignment: ?SetSectionAlignment,
-set_section_flags: ?SetSectionFlags,
-
pub const Options = struct {
basename: ?[]const u8 = null,
- format: ?RawFormat = null,
+ format: ?Format = null,
only_section: ?[]const u8 = null,
pad_to: ?u64 = null,
@@ -96,148 +47,80 @@ pub const Options = struct {
/// Put the stripped out debug sections in a separate file.
/// note: the `basename` is baked into the elf file to specify the link to the separate debug file.
/// see https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
- extract_to_separate_file: bool = false,
+ ///
+ /// Makes `getOutputSeparatedDebug` return non-null.
+ separate_debug_file: ?SeparateDebugFile = null,
- add_section: ?AddSection = null,
- set_section_alignment: ?SetSectionAlignment = null,
- set_section_flags: ?SetSectionFlags = null,
+ pub const SeparateDebugFile = struct {
+ basename: ?[]const u8,
+ };
};
-pub fn create(
- owner: *std.Build,
- input_file: std.Build.LazyPath,
- options: Options,
-) *ObjCopy {
- const objcopy = owner.allocator.create(ObjCopy) catch @panic("OOM");
- objcopy.* = ObjCopy{
- .step = Step.init(.{
- .id = base_id,
- .name = owner.fmt("objcopy {s}", .{input_file.getDisplayName()}),
+pub fn create(owner: *std.Build, input_file: std.Build.LazyPath, options: Options) *ObjCopy {
+ const graph = owner.graph;
+ const wc = &graph.wip_configuration;
+ const oc = graph.create(ObjCopy);
+ oc.* = .{
+ .step = .init(.{
+ .tag = base_tag,
+ .name = owner.fmt("objcopy {f}", .{input_file}),
.owner = owner,
- .makeFn = make,
}),
.input_file = input_file,
- .basename = options.basename orelse input_file.getDisplayName(),
- .output_file = std.Build.GeneratedFile{ .step = &objcopy.step },
- .output_file_debug = if (options.strip != .none and options.extract_to_separate_file) std.Build.GeneratedFile{ .step = &objcopy.step } else null,
+ .basename = if (options.basename) |s| .init(wc.addString(s) catch @panic("OOM")) else .none,
+ .output_file = graph.addGeneratedFile(&oc.step),
+ .debug_file = if (options.separate_debug_file) |df| .{
+ .basename = if (df.basename) |s| .init(wc.addString(s) catch @panic("OOM")) else .none,
+ .output_file = graph.addGeneratedFile(&oc.step),
+ } else null,
.format = options.format,
- .only_section = options.only_section,
+ .only_section = if (options.only_section) |s| .init(wc.addString(s) catch @panic("OOM")) else .none,
.pad_to = options.pad_to,
.strip = options.strip,
.compress_debug = options.compress_debug,
- .add_section = options.add_section,
- .set_section_alignment = options.set_section_alignment,
- .set_section_flags = options.set_section_flags,
};
- input_file.addStepDependencies(&objcopy.step);
- return objcopy;
+ input_file.addStepDependencies(&oc.step);
+ return oc;
}
-pub fn getOutput(objcopy: *const ObjCopy) std.Build.LazyPath {
- return .{ .generated = .{ .file = &objcopy.output_file } };
+pub const UpdateSectionOptions = struct {
+ alignment: ?std.mem.Alignment = null,
+ flags: SectionFlags = .default,
+};
+
+pub fn updateSection(oc: *ObjCopy, section_name: []const u8, options: UpdateSectionOptions) void {
+ const graph = oc.owner.graph;
+ const arena = graph.arena;
+ const wc = &graph.wip_configuration;
+ oc.update_sections.append(arena, .{
+ .flags = .{
+ .section_flags = options.flags,
+ .alignment = .init(options.alignment),
+ },
+ .section_name = wc.addString(section_name) catch @panic("OOM"),
+ }) catch @panic("OOM");
}
-pub fn getOutputSeparatedDebug(objcopy: *const ObjCopy) ?std.Build.LazyPath {
- return if (objcopy.output_file_debug) |*file| .{ .generated = .{ .file = file } } else null;
+
+pub const AddSectionOptions = struct {
+ file_path: std.Build.LazyPath,
+};
+
+pub fn addSection(oc: *ObjCopy, section_name: []const u8, options: AddSectionOptions) void {
+ const graph = oc.owner.graph;
+ const arena = graph.arena;
+ const wc = &graph.wip_configuration;
+ oc.add_sections.append(arena, .{
+ .section_name = wc.addString(section_name) catch @panic("OOM"),
+ .file_path = options.file_path,
+ }) catch @panic("OOM");
+ options.file_path.addStepDependencies(&oc.step);
}
-fn make(step: *Step, options: Step.MakeOptions) !void {
- const prog_node = options.progress_node;
- const b = step.owner;
- const io = b.graph.io;
- const objcopy: *ObjCopy = @fieldParentPtr("step", step);
- try step.singleUnchangingWatchInput(objcopy.input_file);
-
- var man = b.graph.cache.obtain();
- defer man.deinit();
-
- const full_src_path = objcopy.input_file.getPath2(b, step);
- _ = try man.addFile(full_src_path, null);
- man.hash.addOptionalBytes(objcopy.only_section);
- man.hash.addOptional(objcopy.pad_to);
- man.hash.addOptional(objcopy.format);
- man.hash.add(objcopy.compress_debug);
- man.hash.add(objcopy.strip);
- man.hash.add(objcopy.output_file_debug != null);
-
- if (try step.cacheHit(&man)) {
- // Cache hit, skip subprocess execution.
- const digest = man.final();
- objcopy.output_file.path = try b.cache_root.join(b.allocator, &.{
- "o", &digest, objcopy.basename,
- });
- if (objcopy.output_file_debug) |*file| {
- file.path = try b.cache_root.join(b.allocator, &.{
- "o", &digest, b.fmt("{s}.debug", .{objcopy.basename}),
- });
- }
- return;
- }
-
- const digest = man.final();
- const cache_path = "o" ++ fs.path.sep_str ++ digest;
- const full_dest_path = try b.cache_root.join(b.allocator, &.{ cache_path, objcopy.basename });
- const full_dest_path_debug = try b.cache_root.join(b.allocator, &.{ cache_path, b.fmt("{s}.debug", .{objcopy.basename}) });
- b.cache_root.handle.createDirPath(io, cache_path) catch |err| {
- return step.fail("unable to make path {s}: {s}", .{ cache_path, @errorName(err) });
- };
+pub fn getOutput(oc: *const ObjCopy) std.Build.LazyPath {
+ return .{ .generated = .{ .index = oc.output_file } };
+}
- var argv = std.array_list.Managed([]const u8).init(b.allocator);
- try argv.appendSlice(&.{ b.graph.zig_exe, "objcopy" });
-
- if (objcopy.only_section) |only_section| {
- try argv.appendSlice(&.{ "-j", only_section });
- }
- switch (objcopy.strip) {
- .none => {},
- .debug => try argv.appendSlice(&.{"--strip-debug"}),
- .debug_and_symbols => try argv.appendSlice(&.{"--strip-all"}),
- }
- if (objcopy.pad_to) |pad_to| {
- try argv.appendSlice(&.{ "--pad-to", b.fmt("{d}", .{pad_to}) });
- }
- if (objcopy.format) |format| switch (format) {
- .bin => try argv.appendSlice(&.{ "-O", "binary" }),
- .hex => try argv.appendSlice(&.{ "-O", "hex" }),
- .elf => try argv.appendSlice(&.{ "-O", "elf" }),
- };
- if (objcopy.compress_debug) {
- try argv.appendSlice(&.{"--compress-debug-sections"});
- }
- if (objcopy.output_file_debug != null) {
- try argv.appendSlice(&.{b.fmt("--extract-to={s}", .{full_dest_path_debug})});
- }
- if (objcopy.add_section) |section| {
- try argv.append("--add-section");
- try argv.appendSlice(&.{b.fmt("{s}={s}", .{ section.section_name, section.file_path.getPath2(b, step) })});
- }
- if (objcopy.set_section_alignment) |set_align| {
- try argv.append("--set-section-alignment");
- try argv.appendSlice(&.{b.fmt("{s}={d}", .{ set_align.section_name, set_align.alignment })});
- }
- if (objcopy.set_section_flags) |set_flags| {
- const f = set_flags.flags;
- // trailing comma is allowed
- try argv.append("--set-section-flags");
- try argv.appendSlice(&.{b.fmt("{s}={s}{s}{s}{s}{s}{s}{s}{s}{s}", .{
- set_flags.section_name,
- if (f.alloc) "alloc," else "",
- if (f.contents) "contents," else "",
- if (f.load) "load," else "",
- if (f.readonly) "readonly," else "",
- if (f.code) "code," else "",
- if (f.exclude) "exclude," else "",
- if (f.large) "large," else "",
- if (f.merge) "merge," else "",
- if (f.strings) "strings," else "",
- })});
- }
-
- try argv.appendSlice(&.{ full_src_path, full_dest_path });
-
- try argv.append("--listen=-");
- _ = try step.evalZigProcess(argv.items, prog_node, false, options.web_server, options.gpa);
-
- objcopy.output_file.path = full_dest_path;
- if (objcopy.output_file_debug) |*file| file.path = full_dest_path_debug;
- try man.writeManifest();
+pub fn getOutputSeparatedDebug(oc: *const ObjCopy) ?std.Build.LazyPath {
+ const df = oc.debug_file orelse return null;
+ return .{ .generated = .{ .index = df.output_file } };
}
diff --git a/lib/std/Build/Step/Options.zig b/lib/std/Build/Step/Options.zig
@@ -1,37 +1,41 @@
const Options = @This();
+
const builtin = @import("builtin");
const std = @import("std");
const Io = std.Io;
const fs = std.fs;
const Step = std.Build.Step;
-const GeneratedFile = std.Build.GeneratedFile;
const LazyPath = std.Build.LazyPath;
-
-pub const base_id: Step.Id = .options;
+const Configuration = std.Build.Configuration;
step: Step,
-generated_file: GeneratedFile,
-
-contents: std.ArrayList(u8),
-args: std.ArrayList(Arg),
+generated_file: Configuration.GeneratedFileIndex,
+contents: std.ArrayList(u8) = .empty,
+args: std.ArrayList(Arg) = .empty,
encountered_types: std.StringHashMapUnmanaged(void),
+pub const base_tag: Step.Tag = .options;
+
+pub const Arg = struct {
+ name: Configuration.String,
+ path: LazyPath,
+};
+
pub fn create(owner: *std.Build) *Options {
- const options = owner.allocator.create(Options) catch @panic("OOM");
+ const graph = owner.graph;
+ const arena = graph.arena;
+
+ const options = arena.create(Options) catch @panic("OOM");
options.* = .{
.step = .init(.{
- .id = base_id,
+ .tag = base_tag,
.name = "options",
.owner = owner,
- .makeFn = make,
}),
- .generated_file = undefined,
- .contents = .empty,
- .args = .empty,
+ .generated_file = graph.addGeneratedFile(&options.step),
.encountered_types = .empty,
};
- options.generated_file = .{ .step = &options.step };
return options;
}
@@ -410,16 +414,14 @@ fn printStructValue(
}
}
-/// The value is the path in the cache dir.
-/// Adds a dependency automatically.
-pub fn addOptionPath(
- options: *Options,
- name: []const u8,
- path: LazyPath,
-) void {
- const arena = options.step.owner.allocator;
+/// The added option has type `[]const u8` and value of the provided path.
+pub fn addOptionPath(options: *Options, name: []const u8, path: LazyPath) void {
+ const graph = options.step.owner.graph;
+ const arena = graph.arena;
+ const wc = &graph.wip_configuration;
+
options.args.append(arena, .{
- .name = options.step.owner.dupe(name),
+ .name = try wc.addString(name),
.path = path.dupe(options.step.owner),
}) catch @panic("OOM");
path.addStepDependencies(&options.step);
@@ -434,242 +436,5 @@ pub fn createModule(options: *Options) *std.Build.Module {
/// Returns the main artifact of this Build Step which is a Zig source file
/// generated from the key-value pairs of the Options.
pub fn getOutput(options: *Options) LazyPath {
- return .{ .generated = .{ .file = &options.generated_file } };
-}
-
-fn make(step: *Step, make_options: Step.MakeOptions) !void {
- // This step completes so quickly that no progress reporting is necessary.
- _ = make_options;
-
- const b = step.owner;
- const io = b.graph.io;
- const options: *Options = @fieldParentPtr("step", step);
-
- for (options.args.items) |item| {
- options.addOption(
- []const u8,
- item.name,
- item.path.getPath2(b, step),
- );
- }
- if (!step.inputs.populated()) for (options.args.items) |item| {
- try step.addWatchInput(item.path);
- };
-
- const basename = "options.zig";
-
- // Hash contents to file name.
- var hash = b.graph.cache.hash;
- // Random bytes to make unique. Refresh this with new random bytes when
- // implementation is modified in a non-backwards-compatible way.
- hash.add(@as(u32, 0xad95e922));
- hash.addBytes(options.contents.items);
- const sub_path = "c" ++ fs.path.sep_str ++ hash.final() ++ fs.path.sep_str ++ basename;
-
- options.generated_file.path = try b.cache_root.join(b.allocator, &.{sub_path});
-
- // Optimize for the hot path. Stat the file, and if it already exists,
- // cache hit.
- if (b.cache_root.handle.access(io, sub_path, .{})) |_| {
- // This is the hot path, success.
- step.result_cached = true;
- return;
- } else |outer_err| switch (outer_err) {
- error.FileNotFound => {
- var atomic_file = b.cache_root.handle.createFileAtomic(io, sub_path, .{
- .replace = false,
- .make_path = true,
- }) catch |err| return step.fail("failed to create temporary path for '{f}{s}': {t}", .{
- b.cache_root, sub_path, err,
- });
- defer atomic_file.deinit(io);
-
- atomic_file.file.writeStreamingAll(io, options.contents.items) catch |err| {
- return step.fail("failed to write options to temporary path for '{f}{s}': {t}", .{
- b.cache_root, sub_path, err,
- });
- };
-
- atomic_file.link(io) catch |err| switch (err) {
- error.PathAlreadyExists => {
- step.result_cached = true;
- return;
- },
- else => return step.fail("failed to link temporary file into '{f}{s}': {t}", .{
- b.cache_root, sub_path, err,
- }),
- };
- },
- else => |e| return step.fail("unable to access options file '{f}{s}': {t}", .{
- b.cache_root, sub_path, e,
- }),
- }
-}
-
-const Arg = struct {
- name: []const u8,
- path: LazyPath,
-};
-
-test Options {
- if (builtin.os.tag == .wasi) return error.SkipZigTest;
-
- const io = std.testing.io;
-
- var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
- defer arena.deinit();
-
- const cwd = try std.process.currentPathAlloc(io, std.testing.allocator);
- defer std.testing.allocator.free(cwd);
-
- var graph: std.Build.Graph = .{
- .io = io,
- .arena = arena.allocator(),
- .cache = .{
- .io = io,
- .gpa = arena.allocator(),
- .manifest_dir = Io.Dir.cwd(),
- .cwd = cwd,
- },
- .zig_exe = "test",
- .environ_map = std.process.Environ.Map.init(arena.allocator()),
- .global_cache_root = .{ .path = "test", .handle = Io.Dir.cwd() },
- .host = .{
- .query = .{},
- .result = try std.zig.system.resolveTargetQuery(io, .{}),
- },
- .zig_lib_directory = std.Build.Cache.Directory.cwd(),
- .time_report = false,
- };
-
- var builder = try std.Build.create(
- &graph,
- .{ .path = "test", .handle = Io.Dir.cwd() },
- .{ .path = "test", .handle = Io.Dir.cwd() },
- &.{},
- );
-
- const options = builder.addOptions();
-
- const KeywordEnum = enum {
- @"0.8.1",
- };
-
- const NormalEnum = enum {
- foo,
- bar,
- };
-
- const nested_array = [2][2]u16{
- [2]u16{ 300, 200 },
- [2]u16{ 300, 200 },
- };
- const nested_slice: []const []const u16 = &[_][]const u16{ &nested_array[0], &nested_array[1] };
-
- const NormalStruct = struct {
- hello: ?[]const u8,
- world: bool = true,
- };
-
- const NestedStruct = struct {
- normal_struct: NormalStruct,
- normal_enum: NormalEnum = .foo,
- };
-
- options.addOption(usize, "option1", 1);
- options.addOption(?usize, "option2", null);
- options.addOption(?usize, "option3", 3);
- options.addOption(comptime_int, "option4", 4);
- options.addOption(comptime_float, "option5", 5.01);
- options.addOption([]const u8, "string", "zigisthebest");
- options.addOption(?[]const u8, "optional_string", null);
- options.addOption([2][2]u16, "nested_array", nested_array);
- options.addOption([]const []const u16, "nested_slice", nested_slice);
- options.addOption(KeywordEnum, "keyword_enum", .@"0.8.1");
- options.addOption(std.SemanticVersion, "semantic_version", try std.SemanticVersion.parse("0.1.2-foo+bar"));
- options.addOption(NormalEnum, "normal1_enum", NormalEnum.foo);
- options.addOption(NormalEnum, "normal2_enum", NormalEnum.bar);
- options.addOption(NormalStruct, "normal1_struct", NormalStruct{
- .hello = "foo",
- });
- options.addOption(NormalStruct, "normal2_struct", NormalStruct{
- .hello = null,
- .world = false,
- });
- options.addOption(NestedStruct, "nested_struct", NestedStruct{
- .normal_struct = .{ .hello = "bar" },
- });
-
- try std.testing.expectEqualStrings(
- \\pub const option1: usize = 1;
- \\pub const option2: ?usize = null;
- \\pub const option3: ?usize = 3;
- \\pub const option4: comptime_int = 4;
- \\pub const option5: comptime_float = 5.01;
- \\pub const string: []const u8 = "zigisthebest";
- \\pub const optional_string: ?[]const u8 = null;
- \\pub const nested_array: [2][2]u16 = [2][2]u16 {
- \\ [2]u16 {
- \\ 300,
- \\ 200,
- \\ },
- \\ [2]u16 {
- \\ 300,
- \\ 200,
- \\ },
- \\};
- \\pub const nested_slice: []const []const u16 = &[_][]const u16 {
- \\ &[_]u16 {
- \\ 300,
- \\ 200,
- \\ },
- \\ &[_]u16 {
- \\ 300,
- \\ 200,
- \\ },
- \\};
- \\pub const @"Build.Step.Options.decltest.Options.KeywordEnum" = enum (u0) {
- \\ @"0.8.1" = 0,
- \\};
- \\pub const keyword_enum: @"Build.Step.Options.decltest.Options.KeywordEnum" = .@"0.8.1";
- \\pub const semantic_version: @import("std").SemanticVersion = .{
- \\ .major = 0,
- \\ .minor = 1,
- \\ .patch = 2,
- \\ .pre = "foo",
- \\ .build = "bar",
- \\};
- \\pub const @"Build.Step.Options.decltest.Options.NormalEnum" = enum (u1) {
- \\ foo = 0,
- \\ bar = 1,
- \\};
- \\pub const normal1_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo;
- \\pub const normal2_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .bar;
- \\pub const @"Build.Step.Options.decltest.Options.NormalStruct" = struct {
- \\ hello: ?[]const u8,
- \\ world: bool = true,
- \\};
- \\pub const normal1_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{
- \\ .hello = "foo",
- \\ .world = true,
- \\};
- \\pub const normal2_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{
- \\ .hello = null,
- \\ .world = false,
- \\};
- \\pub const @"Build.Step.Options.decltest.Options.NestedStruct" = struct {
- \\ normal_struct: @"Build.Step.Options.decltest.Options.NormalStruct",
- \\ normal_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo,
- \\};
- \\pub const nested_struct: @"Build.Step.Options.decltest.Options.NestedStruct" = .{
- \\ .normal_struct = .{
- \\ .hello = "bar",
- \\ .world = true,
- \\ },
- \\ .normal_enum = .foo,
- \\};
- \\
- , options.contents.items);
-
- _ = try std.zig.Ast.parse(arena.allocator(), try options.contents.toOwnedSliceSentinel(arena.allocator(), 0), .zig);
+ return .{ .generated = .{ .index = options.generated_file } };
}
diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig
@@ -11,8 +11,9 @@ const process = std.process;
const EnvMap = std.process.Environ.Map;
const assert = std.debug.assert;
const Path = std.Build.Cache.Path;
+const Configuration = std.Build.Configuration;
-pub const base_id: Step.Id = .run;
+pub const base_tag: Step.Tag = .run;
step: Step,
@@ -84,36 +85,13 @@ stdio_limit: std.Io.Limit,
captured_stdout: ?*CapturedStdIo,
captured_stderr: ?*CapturedStdIo,
-dep_output_file: ?*Output,
-
has_side_effects: bool,
-
-/// If this is a Zig unit test binary, this tracks the names of the unit
-/// tests that are also fuzz tests. Indexes cannot be used as they may
-/// change between reruns.
-fuzz_tests: std.ArrayList([]const u8),
-cached_test_metadata: ?CachedTestMetadata = null,
-
-/// Populated during the fuzz phase if this run step corresponds to a unit test
-/// executable that contains fuzz tests.
-rebuilt_executable: ?Path,
+test_runner_mode: bool = false,
/// If this Run step was produced by a Compile step, it is tracked here.
producer: ?*Step.Compile,
-pub const Color = enum {
- /// `CLICOLOR_FORCE` is set, and `NO_COLOR` is unset.
- enable,
- /// `NO_COLOR` is set, and `CLICOLOR_FORCE` is unset.
- disable,
- /// If the build runner is using color, equivalent to `.enable`. Otherwise, equivalent to `.disable`.
- inherit,
- /// If stderr is captured or checked, equivalent to `.disable`. Otherwise, equivalent to `.inherit`.
- auto,
- /// The build runner does not modify the `CLICOLOR_FORCE` or `NO_COLOR` environment variables.
- /// They are treated like normal variables, so can be controlled through `setEnvironmentVariable`.
- manual,
-};
+pub const Color = std.Build.Configuration.Step.Run.Color;
pub const StdIn = union(enum) {
none,
@@ -159,9 +137,12 @@ pub const Arg = union(enum) {
lazy_path: PrefixedLazyPath,
decorated_directory: DecoratedLazyPath,
file_content: PrefixedLazyPath,
- bytes: []u8,
+ bytes: []const u8,
output_file: *Output,
+ output_file_dep: *Output,
output_directory: *Output,
+ /// The arguments passed after "--" on the "zig build" CLI.
+ passthru,
};
pub const PrefixedArtifact = struct {
@@ -181,7 +162,7 @@ pub const DecoratedLazyPath = struct {
};
pub const Output = struct {
- generated_file: std.Build.GeneratedFile,
+ generated_file: Configuration.GeneratedFileIndex,
prefix: []const u8,
basename: []const u8,
};
@@ -197,22 +178,16 @@ pub const CapturedStdIo = struct {
trim_whitespace: TrimWhitespace = .none,
};
- pub const TrimWhitespace = enum {
- none,
- all,
- leading,
- trailing,
- };
+ pub const TrimWhitespace = std.Build.Configuration.Step.Run.TrimWhitespace;
};
pub fn create(owner: *std.Build, name: []const u8) *Run {
const run = owner.allocator.create(Run) catch @panic("OOM");
run.* = .{
.step = .init(.{
- .id = base_id,
+ .tag = base_tag,
.name = name,
.owner = owner,
- .makeFn = make,
}),
.argv = .empty,
.cwd = null,
@@ -227,10 +202,7 @@ pub fn create(owner: *std.Build, name: []const u8) *Run {
.stdio_limit = .unlimited,
.captured_stdout = null,
.captured_stderr = null,
- .dep_output_file = null,
.has_side_effects = false,
- .fuzz_tests = .empty,
- .rebuilt_executable = null,
.producer = null,
};
return run;
@@ -242,13 +214,9 @@ pub fn setName(run: *Run, name: []const u8) void {
}
pub fn enableTestRunnerMode(run: *Run) void {
- const b = run.step.owner;
+ if (run.test_runner_mode) return;
run.stdio = .zig_test;
- run.addPrefixedDirectoryArg("--cache-dir=", .{ .cwd_relative = b.cache_root.path orelse "." });
- run.addArgs(&.{
- b.fmt("--seed=0x{x}", .{b.graph.random_seed}),
- "--listen=-",
- });
+ run.test_runner_mode = true;
}
pub fn addArtifactArg(run: *Run, artifact: *Step.Compile) void {
@@ -256,13 +224,14 @@ pub fn addArtifactArg(run: *Run, artifact: *Step.Compile) void {
}
pub fn addPrefixedArtifactArg(run: *Run, prefix: []const u8, artifact: *Step.Compile) void {
- const b = run.step.owner;
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
const prefixed_artifact: PrefixedArtifact = .{
- .prefix = b.dupe(prefix),
+ .prefix = graph.dupeString(prefix),
.artifact = artifact,
};
- run.argv.append(b.allocator, .{ .artifact = prefixed_artifact }) catch @panic("OOM");
+ run.argv.append(arena, .{ .artifact = prefixed_artifact }) catch @panic("OOM");
const bin_file = artifact.getEmittedBin();
bin_file.addStepDependencies(&run.step);
@@ -273,21 +242,23 @@ pub fn addPrefixedArtifactArg(run: *Run, prefix: []const u8, artifact: *Step.Com
/// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs
/// throughout the build system.
///
+/// `sub_path` is the name of the generated output file which may have zero or
+/// more path components.
+///
/// Related:
/// * `addPrefixedOutputFileArg` - same thing but prepends a string to the argument
/// * `addFileArg` - for input files given to the child process
-pub fn addOutputFileArg(run: *Run, basename: []const u8) std.Build.LazyPath {
- return run.addPrefixedOutputFileArg("", basename);
+pub fn addOutputFileArg(run: *Run, sub_path: []const u8) std.Build.LazyPath {
+ return run.addPrefixedOutputFileArg("", sub_path);
}
/// Provides a file path as a command line argument to the command being run.
-/// Asserts `basename` is not empty.
///
-/// For example, a prefix of "-o" and basename of "output.txt" will result in
+/// For example, a prefix of "-o" and `sub_path` of "output.txt" will result in
/// the child process seeing something like this: "-ozig-cache/.../output.txt"
///
/// The child process will see a single argument, regardless of whether the
-/// prefix or basename have spaces.
+/// prefix or `sub_path` have spaces.
///
/// The returned `std.Build.LazyPath` can be used as inputs to other APIs
/// throughout the build system.
@@ -298,24 +269,30 @@ pub fn addOutputFileArg(run: *Run, basename: []const u8) std.Build.LazyPath {
pub fn addPrefixedOutputFileArg(
run: *Run,
prefix: []const u8,
- basename: []const u8,
+ /// The name of the generated output file which may have zero or more path
+ /// components.
+ ///
+ /// Asserted to be non-empty.
+ sub_path: []const u8,
) std.Build.LazyPath {
const b = run.step.owner;
- if (basename.len == 0) @panic("basename must not be empty");
+ const graph = b.graph;
+ const arena = graph.arena;
+ assert(sub_path.len != 0);
- const output = b.allocator.create(Output) catch @panic("OOM");
+ const output = graph.create(Output);
output.* = .{
- .prefix = b.dupe(prefix),
- .basename = b.dupe(basename),
- .generated_file = .{ .step = &run.step },
+ .prefix = graph.dupeString(prefix),
+ .basename = graph.dupeString(sub_path),
+ .generated_file = graph.addGeneratedFile(&run.step),
};
- run.argv.append(b.allocator, .{ .output_file = output }) catch @panic("OOM");
+ run.argv.append(arena, .{ .output_file = output }) catch @panic("OOM");
if (run.rename_step_with_output_arg) {
- run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename }));
+ run.setName(b.fmt("{s} ({s})", .{ run.step.name, sub_path }));
}
- return .{ .generated = .{ .file = &output.generated_file } };
+ return .{ .generated = .{ .index = output.generated_file } };
}
/// Appends an input file to the command line arguments.
@@ -344,13 +321,14 @@ pub fn addFileArg(run: *Run, lp: std.Build.LazyPath) void {
/// * `addFileArg` - same thing but without the prefix
/// * `addOutputFileArg` - for files generated by the child process
pub fn addPrefixedFileArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void {
- const b = run.step.owner;
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
const prefixed_file_source: PrefixedLazyPath = .{
- .prefix = b.dupe(prefix),
- .lazy_path = lp.dupe(b),
+ .prefix = graph.dupeString(prefix),
+ .lazy_path = lp.dupe(graph),
};
- run.argv.append(b.allocator, .{ .lazy_path = prefixed_file_source }) catch @panic("OOM");
+ run.argv.append(arena, .{ .lazy_path = prefixed_file_source }) catch @panic("OOM");
lp.addStepDependencies(&run.step);
}
@@ -391,7 +369,8 @@ pub fn addFileContentArg(run: *Run, lp: std.Build.LazyPath) void {
/// Related:
/// * `addFileContentArg` - same thing but without the prefix
pub fn addPrefixedFileContentArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void {
- const b = run.step.owner;
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
// Some parts of this step's configure phase API rely on the first argument being somewhat
// transparent/readable, but the content of the file specified by `lp` remains completely
@@ -401,10 +380,10 @@ pub fn addPrefixedFileContentArg(run: *Run, prefix: []const u8, lp: std.Build.La
}
const prefixed_file_source: PrefixedLazyPath = .{
- .prefix = b.dupe(prefix),
- .lazy_path = lp.dupe(b),
+ .prefix = graph.dupeString(prefix),
+ .lazy_path = lp.dupe(graph),
};
- run.argv.append(b.allocator, .{ .file_content = prefixed_file_source }) catch @panic("OOM");
+ run.argv.append(arena, .{ .file_content = prefixed_file_source }) catch @panic("OOM");
lp.addStepDependencies(&run.step);
}
@@ -441,21 +420,22 @@ pub fn addPrefixedOutputDirectoryArg(
basename: []const u8,
) std.Build.LazyPath {
if (basename.len == 0) @panic("basename must not be empty");
- const b = run.step.owner;
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
- const output = b.allocator.create(Output) catch @panic("OOM");
+ const output = arena.create(Output) catch @panic("OOM");
output.* = .{
- .prefix = b.dupe(prefix),
- .basename = b.dupe(basename),
- .generated_file = .{ .step = &run.step },
+ .prefix = graph.dupeString(prefix),
+ .basename = graph.dupeString(basename),
+ .generated_file = graph.addGeneratedFile(&run.step),
};
- run.argv.append(b.allocator, .{ .output_directory = output }) catch @panic("OOM");
+ run.argv.append(arena, .{ .output_directory = output }) catch @panic("OOM");
if (run.rename_step_with_output_arg) {
- run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename }));
+ run.setName(std.fmt.allocPrint(arena, "{s} ({s})", .{ run.step.name, basename }) catch @panic("OOM"));
}
- return .{ .generated = .{ .file = &output.generated_file } };
+ return .{ .generated = .{ .index = output.generated_file } };
}
pub fn addDirectoryArg(run: *Run, lazy_directory: std.Build.LazyPath) void {
@@ -463,10 +443,11 @@ pub fn addDirectoryArg(run: *Run, lazy_directory: std.Build.LazyPath) void {
}
pub fn addPrefixedDirectoryArg(run: *Run, prefix: []const u8, lazy_directory: std.Build.LazyPath) void {
- const b = run.step.owner;
- run.argv.append(b.allocator, .{ .decorated_directory = .{
- .prefix = b.dupe(prefix),
- .lazy_path = lazy_directory.dupe(b),
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
+ run.argv.append(arena, .{ .decorated_directory = .{
+ .prefix = graph.dupeString(prefix),
+ .lazy_path = lazy_directory.dupe(graph),
.suffix = "",
} }) catch @panic("OOM");
lazy_directory.addStepDependencies(&run.step);
@@ -478,11 +459,12 @@ pub fn addDecoratedDirectoryArg(
lazy_directory: std.Build.LazyPath,
suffix: []const u8,
) void {
- const b = run.step.owner;
- run.argv.append(b.allocator, .{ .decorated_directory = .{
- .prefix = b.dupe(prefix),
- .lazy_path = lazy_directory.dupe(b),
- .suffix = b.dupe(suffix),
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
+ run.argv.append(arena, .{ .decorated_directory = .{
+ .prefix = graph.dupeString(prefix),
+ .lazy_path = lazy_directory.dupe(graph),
+ .suffix = graph.dupeString(suffix),
} }) catch @panic("OOM");
lazy_directory.addStepDependencies(&run.step);
}
@@ -496,34 +478,63 @@ pub fn addDepFileOutputArg(run: *Run, basename: []const u8) std.Build.LazyPath {
/// Add a prefixed path argument to a dep file (.d) for the child process to
/// write its discovered additional dependencies.
-/// Only one dep file argument is allowed by instance.
pub fn addPrefixedDepFileOutputArg(run: *Run, prefix: []const u8, basename: []const u8) std.Build.LazyPath {
const b = run.step.owner;
- assert(run.dep_output_file == null);
+ const graph = b.graph;
+ const arena = graph.arena;
- const dep_file = b.allocator.create(Output) catch @panic("OOM");
+ const dep_file = arena.create(Output) catch @panic("OOM");
dep_file.* = .{
- .prefix = b.dupe(prefix),
- .basename = b.dupe(basename),
- .generated_file = .{ .step = &run.step },
+ .prefix = graph.dupeString(prefix),
+ .basename = graph.dupeString(basename),
+ .generated_file = graph.addGeneratedFile(&run.step),
};
- run.dep_output_file = dep_file;
+ run.argv.append(arena, .{ .output_file_dep = dep_file }) catch @panic("OOM");
- run.argv.append(b.allocator, .{ .output_file = dep_file }) catch @panic("OOM");
-
- return .{ .generated = .{ .file = &dep_file.generated_file } };
+ return .{ .generated = .{ .index = dep_file.generated_file } };
}
+/// Appends the contents of `arg`, verbatim, to the command line that will be
+/// passed to the process being run.
+///
+/// If `arg` is an input file, `addFileInput` (or related function) must be
+/// used instead to ensure correct cache behavior.
+///
+/// If `arg` is an output file, `addOutputFileArg` (or related function) must
+/// be used instead to ensure correct cache behavior.
pub fn addArg(run: *Run, arg: []const u8) void {
- const b = run.step.owner;
- run.argv.append(b.allocator, .{ .bytes = b.dupe(arg) }) catch @panic("OOM");
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
+ run.argv.append(arena, .{ .bytes = graph.dupeString(arg) }) catch @panic("OOM");
}
+/// Appends each of `args`, verbatim, to the command line that will be passed
+/// to the process being run.
+///
+/// If any element of `args` is an input file, `addFileInput` must be used
+/// instead to ensure correct cache behavior.
+///
+/// If any element of `args` is an output file, `addOutputFileArg` (or related
+/// function) must be used instead to ensure correct cache behavior.
pub fn addArgs(run: *Run, args: []const []const u8) void {
for (args) |arg| run.addArg(arg);
}
+/// Appends the extra arguments provided to `zig build` to the command line
+/// that will be passed to the process being run.
+///
+/// This causes the step to be considered to have side effects, disabling
+/// caching.
+///
+/// In the example command `zig build run -- arg1 arg2`, "arg1" and "arg2" will
+/// be passed to the process being run.
+pub fn addPassthruArgs(run: *Run) void {
+ const graph = run.step.owner.graph;
+ const arena = graph.arena;
+ run.argv.append(arena, .passthru) catch @panic("OOM");
+}
+
pub fn setStdIn(run: *Run, stdin: StdIn) void {
switch (stdin) {
.lazy_path => |lazy_path| lazy_path.addStepDependencies(&run.step),
@@ -533,8 +544,9 @@ pub fn setStdIn(run: *Run, stdin: StdIn) void {
}
pub fn setCwd(run: *Run, cwd: Build.LazyPath) void {
+ const graph = run.step.owner.graph;
cwd.addStepDependencies(&run.step);
- run.cwd = cwd.dupe(run.step.owner);
+ run.cwd = cwd.dupe(graph);
}
pub fn clearEnvironment(run: *Run) void {
@@ -560,7 +572,7 @@ pub fn addPathDir(run: *Run, search_path: []const u8) void {
.decorated_directory => false,
.file_content => unreachable, // not allowed as first arg
.bytes => |bytes| std.mem.endsWith(u8, bytes, ".exe"),
- .output_file, .output_directory => false,
+ .output_file, .output_file_dep, .output_directory => false,
};
const key = if (use_wine) "WINEPATH" else "PATH";
const prev_path = environ_map.get(key);
@@ -604,24 +616,28 @@ pub fn removeEnvironmentVariable(run: *Run, key: []const u8) void {
/// Adds a check for exact stderr match. Does not add any other checks.
pub fn expectStdErrEqual(run: *Run, bytes: []const u8) void {
- run.addCheck(.{ .expect_stderr_exact = run.step.owner.dupe(bytes) });
+ const graph = run.step.owner.graph;
+ run.addCheck(.{ .expect_stderr_exact = graph.dupeString(bytes) });
}
pub fn expectStdErrMatch(run: *Run, bytes: []const u8) void {
- run.addCheck(.{ .expect_stderr_match = run.step.owner.dupe(bytes) });
+ const graph = run.step.owner.graph;
+ run.addCheck(.{ .expect_stderr_match = graph.dupeString(bytes) });
}
/// Adds a check for exact stdout match as well as a check for exit code 0, if
/// there is not already an expected termination check.
pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void {
- run.addCheck(.{ .expect_stdout_exact = run.step.owner.dupe(bytes) });
+ const graph = run.step.owner.graph;
+ run.addCheck(.{ .expect_stdout_exact = graph.dupeString(bytes) });
if (!run.hasTermCheck()) run.expectExitCode(0);
}
/// Adds a check for stdout match as well as a check for exit code 0, if there
/// is not already an expected termination check.
pub fn expectStdOutMatch(run: *Run, bytes: []const u8) void {
- run.addCheck(.{ .expect_stdout_match = run.step.owner.dupe(bytes) });
+ const graph = run.step.owner.graph;
+ run.addCheck(.{ .expect_stdout_match = graph.dupeString(bytes) });
if (!run.hasTermCheck()) run.expectExitCode(0);
}
@@ -656,20 +672,22 @@ pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPa
assert(run.stdio != .zig_test);
const b = run.step.owner;
+ const graph = b.graph;
+ const arena = graph.arena;
- if (run.captured_stderr) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } };
+ if (run.captured_stderr) |captured| return .{ .generated = .{ .index = captured.output.generated_file } };
- const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM");
+ const captured = arena.create(CapturedStdIo) catch @panic("OOM");
captured.* = .{
.output = .{
.prefix = "",
- .basename = if (options.basename) |basename| b.dupe(basename) else "stderr",
- .generated_file = .{ .step = &run.step },
+ .basename = if (options.basename) |basename| graph.dupeString(basename) else "stderr",
+ .generated_file = graph.addGeneratedFile(&run.step),
},
.trim_whitespace = options.trim_whitespace,
};
run.captured_stderr = captured;
- return .{ .generated = .{ .file = &captured.output.generated_file } };
+ return .{ .generated = .{ .index = captured.output.generated_file } };
}
pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath {
@@ -677,20 +695,22 @@ pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPa
assert(run.stdio != .zig_test);
const b = run.step.owner;
+ const graph = b.graph;
+ const arena = graph.arena;
- if (run.captured_stdout) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } };
+ if (run.captured_stdout) |captured| return .{ .generated = .{ .index = captured.output.generated_file } };
- const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM");
+ const captured = arena.create(CapturedStdIo) catch @panic("OOM");
captured.* = .{
.output = .{
.prefix = "",
- .basename = if (options.basename) |basename| b.dupe(basename) else "stdout",
- .generated_file = .{ .step = &run.step },
+ .basename = if (options.basename) |basename| graph.dupeString(basename) else "stdout",
+ .generated_file = graph.addGeneratedFile(&run.step),
},
.trim_whitespace = options.trim_whitespace,
};
run.captured_stdout = captured;
- return .{ .generated = .{ .file = &captured.output.generated_file } };
+ return .{ .generated = .{ .index = captured.output.generated_file } };
}
/// Adds an additional input files that, when modified, indicates that this Run
@@ -698,2111 +718,9 @@ pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPa
/// If the Run step is determined to have side-effects, the Run step is always
/// executed when it appears in the build graph, regardless of whether this
/// file has been modified.
-pub fn addFileInput(self: *Run, file_input: std.Build.LazyPath) void {
- file_input.addStepDependencies(&self.step);
- self.file_inputs.append(self.step.owner.allocator, file_input.dupe(self.step.owner)) catch @panic("OOM");
-}
-
-/// Returns whether the Run step has side effects *other than* updating the output arguments.
-fn hasSideEffects(run: Run) bool {
- if (run.has_side_effects) return true;
- return switch (run.stdio) {
- .infer_from_args => !run.hasAnyOutputArgs(),
- .inherit => true,
- .check => false,
- .zig_test => false,
- };
-}
-
-fn hasAnyOutputArgs(run: Run) bool {
- if (run.captured_stdout != null) return true;
- if (run.captured_stderr != null) return true;
- for (run.argv.items) |arg| switch (arg) {
- .output_file, .output_directory => return true,
- else => continue,
- };
- return false;
-}
-
-fn checksContainStdout(checks: []const StdIo.Check) bool {
- for (checks) |check| switch (check) {
- .expect_stderr_exact,
- .expect_stderr_match,
- .expect_term,
- => continue,
-
- .expect_stdout_exact,
- .expect_stdout_match,
- => return true,
- };
- return false;
-}
-
-fn checksContainStderr(checks: []const StdIo.Check) bool {
- for (checks) |check| switch (check) {
- .expect_stdout_exact,
- .expect_stdout_match,
- .expect_term,
- => continue,
-
- .expect_stderr_exact,
- .expect_stderr_match,
- => return true,
- };
- return false;
-}
-
-/// If `path` is cwd-relative, make it relative to the cwd of the child instead.
-///
-/// Whenever a path is included in the argv of a child, it should be put through this function first
-/// to make sure the child doesn't see paths relative to a cwd other than its own.
-fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 {
- const b = run.step.owner;
- const graph = b.graph;
+pub fn addFileInput(run: *Run, file_input: std.Build.LazyPath) void {
+ const graph = run.step.owner.graph;
const arena = graph.arena;
-
- const path_str = path.toString(arena) catch @panic("OOM");
- if (Dir.path.isAbsolute(path_str)) {
- // Absolute paths don't need changing.
- return path_str;
- }
- const child_cwd_rel: []const u8 = rel: {
- const child_lazy_cwd = run.cwd orelse break :rel path_str;
- const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(arena) catch @panic("OOM");
- // Convert it from relative to *our* cwd, to relative to the *child's* cwd.
- break :rel Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str) catch @panic("OOM");
- };
- // Not every path can be made relative, e.g. if the path and the child cwd are on different
- // disk designators on Windows. In that case, `relative` will return an absolute path which we can
- // just return.
- if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel;
-
- // We're not done yet. In some cases this path must be prefixed with './':
- // * On POSIX, the executable name cannot be a single component like 'foo'
- // * Some executables might treat a leading '-' like a flag, which we must avoid
- // There's no harm in it, so just *always* apply this prefix.
- return Dir.path.join(arena, &.{ ".", child_cwd_rel }) catch @panic("OOM");
-}
-
-const IndexedOutput = struct {
- index: usize,
- tag: @typeInfo(Arg).@"union".tag_type.?,
- output: *Output,
-};
-fn make(step: *Step, options: Step.MakeOptions) !void {
- const b = step.owner;
- const io = b.graph.io;
- const arena = b.allocator;
- const run: *Run = @fieldParentPtr("step", step);
- const has_side_effects = run.hasSideEffects();
-
- var argv_list = std.array_list.Managed([]const u8).init(arena);
- var output_placeholders = std.array_list.Managed(IndexedOutput).init(arena);
-
- var man = b.graph.cache.obtain();
- defer man.deinit();
-
- if (run.environ_map) |environ_map| {
- for (environ_map.keys(), environ_map.values()) |key, value| {
- man.hash.addBytes(key);
- man.hash.addBytes(value);
- }
- }
-
- man.hash.add(run.color);
- man.hash.add(run.disable_zig_progress);
-
- for (run.argv.items) |arg| {
- switch (arg) {
- .bytes => |bytes| {
- try argv_list.append(bytes);
- man.hash.addBytes(bytes);
- },
- .lazy_path => |file| {
- const file_path = file.lazy_path.getPath3(b, step);
- try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
- man.hash.addBytes(file.prefix);
- _ = try man.addFilePath(file_path, null);
- },
- .decorated_directory => |dd| {
- const file_path = dd.lazy_path.getPath3(b, step);
- const resolved_arg = b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix });
- try argv_list.append(resolved_arg);
- man.hash.addBytes(resolved_arg);
- },
- .file_content => |file_plp| {
- const file_path = file_plp.lazy_path.getPath3(b, step);
-
- var result: std.Io.Writer.Allocating = .init(arena);
- errdefer result.deinit();
- result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
-
- const file = file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}) catch |err| {
- return step.fail(
- "unable to open input file '{f}': {t}",
- .{ file_path, err },
- );
- };
- defer file.close(io);
-
- var buf: [1024]u8 = undefined;
- var file_reader = file.reader(io, &buf);
- _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
- error.ReadFailed => return step.fail(
- "failed to read from '{f}': {t}",
- .{ file_path, file_reader.err.? },
- ),
- error.WriteFailed => return error.OutOfMemory,
- };
-
- try argv_list.append(result.written());
- man.hash.addBytes(file_plp.prefix);
- _ = try man.addFilePath(file_path, null);
- },
- .artifact => |pa| {
- const artifact = pa.artifact;
-
- if (artifact.rootModuleTarget().os.tag == .windows) {
- // On Windows we don't have rpaths so we have to add .dll search paths to PATH
- run.addPathForDynLibs(artifact);
- }
- const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?;
-
- try argv_list.append(b.fmt("{s}{s}", .{
- pa.prefix,
- run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
- }));
-
- _ = try man.addFile(file_path, null);
- },
- .output_file, .output_directory => |output| {
- man.hash.addBytes(output.prefix);
- man.hash.addBytes(output.basename);
- // Add a placeholder into the argument list because we need the
- // manifest hash to be updated with all arguments before the
- // object directory is computed.
- try output_placeholders.append(.{
- .index = argv_list.items.len,
- .tag = arg,
- .output = output,
- });
- _ = try argv_list.addOne();
- },
- }
- }
-
- switch (run.stdin) {
- .bytes => |bytes| {
- man.hash.addBytes(bytes);
- },
- .lazy_path => |lazy_path| {
- const file_path = lazy_path.getPath2(b, step);
- _ = try man.addFile(file_path, null);
- },
- .none => {},
- }
-
- if (run.captured_stdout) |captured| {
- man.hash.addBytes(captured.output.basename);
- man.hash.add(captured.trim_whitespace);
- }
-
- if (run.captured_stderr) |captured| {
- man.hash.addBytes(captured.output.basename);
- man.hash.add(captured.trim_whitespace);
- }
-
- hashStdIo(&man.hash, run.stdio);
-
- for (run.file_inputs.items) |lazy_path| {
- _ = try man.addFile(lazy_path.getPath2(b, step), null);
- }
-
- if (run.cwd) |cwd| {
- const cwd_path = cwd.getPath3(b, step);
- _ = man.hash.addBytes(try cwd_path.toString(arena));
- }
-
- if (!has_side_effects and try step.cacheHitAndWatch(&man)) {
- // cache hit, skip running command
- const digest = man.final();
-
- try populateGeneratedPaths(
- arena,
- output_placeholders.items,
- run.captured_stdout,
- run.captured_stderr,
- b.cache_root,
- &digest,
- );
-
- step.result_cached = true;
- return;
- }
-
- const dep_output_file = run.dep_output_file orelse {
- // We already know the final output paths, use them directly.
- const digest = if (has_side_effects)
- man.hash.final()
- else
- man.final();
-
- try populateGeneratedPaths(
- arena,
- output_placeholders.items,
- run.captured_stdout,
- run.captured_stderr,
- b.cache_root,
- &digest,
- );
-
- const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest;
- for (output_placeholders.items) |placeholder| {
- const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename });
- const output_sub_dir_path = switch (placeholder.tag) {
- .output_file => Dir.path.dirname(output_sub_path).?,
- .output_directory => output_sub_path,
- else => unreachable,
- };
- b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| {
- return step.fail("unable to make path '{f}{s}': {s}", .{
- b.cache_root, output_sub_dir_path, @errorName(err),
- });
- };
- const arg_output_path = run.convertPathArg(.{
- .root_dir = .cwd(),
- .sub_path = placeholder.output.generated_file.getPath(),
- });
- argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0)
- arg_output_path
- else
- b.fmt("{s}{s}", .{ placeholder.output.prefix, arg_output_path });
- }
-
- try runCommand(run, argv_list.items, has_side_effects, output_dir_path, options, null);
- if (!has_side_effects) try step.writeManifestAndWatch(&man);
- return;
- };
-
- // We do not know the final output paths yet, use temp paths to run the command.
- var rand_int: u64 = undefined;
- io.random(@ptrCast(&rand_int));
- const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
-
- for (output_placeholders.items) |placeholder| {
- const output_components = .{ tmp_dir_path, placeholder.output.basename };
- const output_sub_path = b.pathJoin(&output_components);
- const output_sub_dir_path = switch (placeholder.tag) {
- .output_file => Dir.path.dirname(output_sub_path).?,
- .output_directory => output_sub_path,
- else => unreachable,
- };
- b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| {
- return step.fail("unable to make path '{f}{s}': {s}", .{
- b.cache_root, output_sub_dir_path, @errorName(err),
- });
- };
- const raw_output_path: Build.Cache.Path = .{
- .root_dir = b.cache_root,
- .sub_path = b.pathJoin(&output_components),
- };
- placeholder.output.generated_file.path = raw_output_path.toString(b.graph.arena) catch @panic("OOM");
- argv_list.items[placeholder.index] = b.fmt("{s}{s}", .{
- placeholder.output.prefix,
- run.convertPathArg(raw_output_path),
- });
- }
-
- try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, options, null);
-
- const dep_file_dir = Dir.cwd();
- const dep_file_basename = dep_output_file.generated_file.getPath2(b, step);
- if (has_side_effects)
- try man.addDepFile(dep_file_dir, dep_file_basename)
- else
- try man.addDepFilePost(dep_file_dir, dep_file_basename);
-
- const digest = if (has_side_effects)
- man.hash.final()
- else
- man.final();
-
- const any_output = output_placeholders.items.len > 0 or
- run.captured_stdout != null or run.captured_stderr != null;
-
- // Rename into place
- if (any_output) {
- const o_sub_path = "o" ++ Dir.path.sep_str ++ &digest;
-
- b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |err| switch (err) {
- Dir.RenameError.DirNotEmpty => {
- b.cache_root.handle.deleteTree(io, o_sub_path) catch |del_err| {
- return step.fail("unable to remove dir '{f}'{s}: {t}", .{
- b.cache_root, tmp_dir_path, del_err,
- });
- };
- b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |retry_err| {
- return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{
- b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, retry_err,
- });
- };
- },
- else => return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{
- b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, err,
- }),
- };
- }
-
- if (!has_side_effects) try step.writeManifestAndWatch(&man);
-
- try populateGeneratedPaths(
- arena,
- output_placeholders.items,
- run.captured_stdout,
- run.captured_stderr,
- b.cache_root,
- &digest,
- );
-}
-
-pub fn rerunInFuzzMode(
- run: *Run,
- fuzz: *std.Build.Fuzz,
- prog_node: std.Progress.Node,
-) !void {
- const step = &run.step;
- const b = step.owner;
- const io = b.graph.io;
- const arena = b.allocator;
- var argv_list: std.ArrayList([]const u8) = .empty;
- for (run.argv.items) |arg| {
- switch (arg) {
- .bytes => |bytes| {
- try argv_list.append(arena, bytes);
- },
- .lazy_path => |file| {
- const file_path = file.lazy_path.getPath3(b, step);
- try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
- },
- .decorated_directory => |dd| {
- const file_path = dd.lazy_path.getPath3(b, step);
- try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }));
- },
- .file_content => |file_plp| {
- const file_path = file_plp.lazy_path.getPath3(b, step);
-
- var result: std.Io.Writer.Allocating = .init(arena);
- errdefer result.deinit();
- result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
-
- const file = try file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{});
- defer file.close(io);
-
- var buf: [1024]u8 = undefined;
- var file_reader = file.reader(io, &buf);
- _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
- error.ReadFailed => return file_reader.err.?,
- error.WriteFailed => return error.OutOfMemory,
- };
-
- try argv_list.append(arena, result.written());
- },
- .artifact => |pa| {
- const artifact = pa.artifact;
- const file_path: []const u8 = p: {
- if (artifact == run.producer.?) break :p b.fmt("{f}", .{run.rebuilt_executable.?});
- break :p artifact.installed_path orelse artifact.generated_bin.?.path.?;
- };
- try argv_list.append(arena, b.fmt("{s}{s}", .{
- pa.prefix,
- run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
- }));
- },
- .output_file, .output_directory => unreachable,
- }
- }
-
- if (run.step.result_failed_command) |cmd| {
- fuzz.gpa.free(cmd);
- run.step.result_failed_command = null;
- }
-
- const has_side_effects = false;
- var rand_int: u64 = undefined;
- io.random(@ptrCast(&rand_int));
- const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
- try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{
- .progress_node = prog_node,
- .watch = undefined, // not used by `runCommand`
- .web_server = null, // only needed for time reports
- .unit_test_timeout_ns = null, // don't time out fuzz tests for now
- .gpa = fuzz.gpa,
- }, .{
- .fuzz = fuzz,
- });
-}
-
-fn populateGeneratedPaths(
- arena: std.mem.Allocator,
- output_placeholders: []const IndexedOutput,
- captured_stdout: ?*CapturedStdIo,
- captured_stderr: ?*CapturedStdIo,
- cache_root: Build.Cache.Directory,
- digest: *const Build.Cache.HexDigest,
-) !void {
- for (output_placeholders) |placeholder| {
- placeholder.output.generated_file.path = try cache_root.join(arena, &.{
- "o", digest, placeholder.output.basename,
- });
- }
-
- if (captured_stdout) |captured| {
- captured.output.generated_file.path = try cache_root.join(arena, &.{
- "o", digest, captured.output.basename,
- });
- }
-
- if (captured_stderr) |captured| {
- captured.output.generated_file.path = try cache_root.join(arena, &.{
- "o", digest, captured.output.basename,
- });
- }
-}
-
-fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
- if (term) |t| switch (t) {
- .exited => |code| try w.print("exited with code {d}", .{code}),
- .signal => |sig| try w.print("terminated with signal {t}", .{sig}),
- .stopped => |sig| try w.print("stopped with signal {t}", .{sig}),
- .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
- } else {
- try w.writeAll("exited with any code");
- }
-}
-fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) {
- return .{ .data = term };
-}
-
-fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
- return if (expected) |e| switch (e) {
- .exited => |expected_code| switch (actual) {
- .exited => |actual_code| expected_code == actual_code,
- else => false,
- },
- .signal => |expected_sig| switch (actual) {
- .signal => |actual_sig| expected_sig == actual_sig,
- else => false,
- },
- .stopped => |expected_sig| switch (actual) {
- .stopped => |actual_sig| expected_sig == actual_sig,
- else => false,
- },
- .unknown => |expected_code| switch (actual) {
- .unknown => |actual_code| expected_code == actual_code,
- else => false,
- },
- } else switch (actual) {
- .exited => true,
- else => false,
- };
-}
-
-const FuzzContext = struct {
- fuzz: *std.Build.Fuzz,
-};
-
-fn runCommand(
- run: *Run,
- argv: []const []const u8,
- has_side_effects: bool,
- output_dir_path: []const u8,
- options: Step.MakeOptions,
- fuzz_context: ?FuzzContext,
-) !void {
- const step = &run.step;
- const b = step.owner;
- const arena = b.allocator;
- const gpa = options.gpa;
- const io = b.graph.io;
-
- const cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, step) } else .inherit;
-
- try step.handleChildProcUnsupported();
- try Step.handleVerbose2(step.owner, cwd, run.environ_map, argv);
-
- const allow_skip = switch (run.stdio) {
- .check, .zig_test => run.skip_foreign_checks,
- else => false,
- };
-
- var interp_argv = std.array_list.Managed([]const u8).init(b.allocator);
- defer interp_argv.deinit();
-
- var environ_map: EnvMap = env: {
- const orig = run.environ_map orelse &b.graph.environ_map;
- break :env try orig.clone(gpa);
- };
- defer environ_map.deinit();
-
- const opt_generic_result = spawnChildAndCollect(run, argv, &environ_map, has_side_effects, options, fuzz_context) catch |err| term: {
- // InvalidExe: cpu arch mismatch
- // FileNotFound: can happen with a wrong dynamic linker path
- if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
- // TODO: learn the target from the binary directly rather than from
- // relying on it being a Compile step. This will make this logic
- // work even for the edge case that the binary was produced by a
- // third party.
- const exe = switch (run.argv.items[0]) {
- .artifact => |exe| exe.artifact,
- else => break :interpret,
- };
- switch (exe.kind) {
- .exe, .@"test" => {},
- else => break :interpret,
- }
-
- const root_target = exe.rootModuleTarget();
- const need_cross_libc = exe.is_linking_libc and
- (root_target.isGnuLibC() or (root_target.isMuslLibC() and exe.linkage == .dynamic));
- const other_target = exe.root_module.resolved_target.?.result;
- switch (std.zig.system.getExternalExecutor(io, &b.graph.host.result, &other_target, .{
- .qemu_fixes_dl = need_cross_libc and b.libc_runtimes_dir != null,
- .link_libc = exe.is_linking_libc,
- })) {
- .native, .rosetta => {
- if (allow_skip) return error.MakeSkipped;
- break :interpret;
- },
- .wine => |bin_name| {
- if (b.enable_wine) {
- try interp_argv.append(bin_name);
- try interp_argv.appendSlice(argv);
-
- // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but
- // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.
- if (environ_map.get("WINEDEBUG") == null) {
- try environ_map.put("WINEDEBUG", "-all");
- }
- } else {
- return failForeign(run, "-fwine", argv[0], exe);
- }
- },
- .qemu => |bin_name| {
- if (b.enable_qemu) {
- try interp_argv.append(bin_name);
-
- if (need_cross_libc) {
- if (b.libc_runtimes_dir) |dir| {
- try interp_argv.append("-L");
- try interp_argv.append(b.pathJoin(&.{
- dir,
- try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple(
- b.allocator,
- root_target.cpu.arch,
- root_target.os.tag,
- root_target.abi,
- ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple(
- b.allocator,
- root_target.cpu.arch,
- root_target.abi,
- ) else unreachable,
- }));
- } else return failForeign(run, "--libc-runtimes", argv[0], exe);
- }
-
- try interp_argv.appendSlice(argv);
- } else return failForeign(run, "-fqemu", argv[0], exe);
- },
- .darling => |bin_name| {
- if (b.enable_darling) {
- try interp_argv.append(bin_name);
- try interp_argv.appendSlice(argv);
- } else {
- return failForeign(run, "-fdarling", argv[0], exe);
- }
- },
- .wasmtime => |bin_name| {
- if (b.enable_wasmtime) {
- try interp_argv.append(bin_name);
- try interp_argv.append("--dir=.");
- // Wasmtime doeesn't inherit environment variables from the parent process
- // by default. '-S inherit-env' was added in Wasmtime version 20.
- try interp_argv.append("-Sinherit-env");
- try interp_argv.append(argv[0]);
- try interp_argv.appendSlice(argv[1..]);
- } else {
- return failForeign(run, "-fwasmtime", argv[0], exe);
- }
- },
- .bad_dl => |foreign_dl| {
- if (allow_skip) return error.MakeSkipped;
-
- const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)";
-
- return step.fail(
- \\the host system is unable to execute binaries from the target
- \\ because the host dynamic linker is '{s}',
- \\ while the target dynamic linker is '{s}'.
- \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step
- , .{ host_dl, foreign_dl });
- },
- .bad_os_or_cpu => {
- if (allow_skip) return error.MakeSkipped;
-
- const host_name = try b.graph.host.result.zigTriple(b.allocator);
- const foreign_name = try root_target.zigTriple(b.allocator);
-
- return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{
- host_name, foreign_name,
- });
- },
- }
-
- if (root_target.os.tag == .windows) {
- // On Windows we don't have rpaths so we have to add .dll search paths to PATH
- run.addPathForDynLibs(exe);
- }
-
- gpa.free(step.result_failed_command.?);
- step.result_failed_command = null;
- try Step.handleVerbose2(step.owner, cwd, run.environ_map, interp_argv.items);
-
- break :term spawnChildAndCollect(run, interp_argv.items, &environ_map, has_side_effects, options, fuzz_context) catch |e| {
- if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
- if (e == error.MakeFailed) return error.MakeFailed; // error already reported
- return step.fail("unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e });
- };
- }
- if (err == error.MakeFailed) return error.MakeFailed; // error already reported
-
- return step.fail("failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err });
- };
-
- const generic_result = opt_generic_result orelse {
- assert(run.stdio == .zig_test);
- // Specific errors have already been reported, and test results are populated. All we need
- // to do is report step failure if any test failed.
- if (!step.test_results.isSuccess()) return error.MakeFailed;
- return;
- };
-
- assert(fuzz_context == null);
- assert(run.stdio != .zig_test);
-
- // Capture stdout and stderr to GeneratedFile objects.
- const Stream = struct {
- captured: ?*CapturedStdIo,
- bytes: ?[]const u8,
- };
- for ([_]Stream{
- .{
- .captured = run.captured_stdout,
- .bytes = generic_result.stdout,
- },
- .{
- .captured = run.captured_stderr,
- .bytes = generic_result.stderr,
- },
- }) |stream| {
- if (stream.captured) |captured| {
- const output_components = .{ output_dir_path, captured.output.basename };
- const output_path = try b.cache_root.join(arena, &output_components);
- captured.output.generated_file.path = output_path;
-
- const sub_path = b.pathJoin(&output_components);
- const sub_path_dirname = Dir.path.dirname(sub_path).?;
- b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| {
- return step.fail("unable to make path '{f}{s}': {s}", .{
- b.cache_root, sub_path_dirname, @errorName(err),
- });
- };
- const data = switch (captured.trim_whitespace) {
- .none => stream.bytes.?,
- .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace),
- .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace),
- .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace),
- };
- b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = data }) catch |err| {
- return step.fail("unable to write file '{f}{s}': {s}", .{
- b.cache_root, sub_path, @errorName(err),
- });
- };
- }
- }
-
- switch (run.stdio) {
- .zig_test => unreachable,
- .check => |checks| for (checks.items) |check| switch (check) {
- .expect_stderr_exact => |expected_bytes| {
- if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) {
- return step.fail(
- \\========= expected this stderr: =========
- \\{s}
- \\========= but found: ====================
- \\{s}
- , .{
- expected_bytes,
- generic_result.stderr.?,
- });
- }
- },
- .expect_stderr_match => |match| {
- if (mem.find(u8, generic_result.stderr.?, match) == null) {
- return step.fail(
- \\========= expected to find in stderr: =========
- \\{s}
- \\========= but stderr does not contain it: =====
- \\{s}
- , .{
- match,
- generic_result.stderr.?,
- });
- }
- },
- .expect_stdout_exact => |expected_bytes| {
- if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) {
- return step.fail(
- \\========= expected this stdout: =========
- \\{s}
- \\========= but found: ====================
- \\{s}
- , .{
- expected_bytes,
- generic_result.stdout.?,
- });
- }
- },
- .expect_stdout_match => |match| {
- if (mem.find(u8, generic_result.stdout.?, match) == null) {
- return step.fail(
- \\========= expected to find in stdout: =========
- \\{s}
- \\========= but stdout does not contain it: =====
- \\{s}
- , .{
- match,
- generic_result.stdout.?,
- });
- }
- },
- .expect_term => |expected_term| {
- if (!termMatches(expected_term, generic_result.term)) {
- return step.fail("process {f} (expected {f})", .{
- fmtTerm(generic_result.term),
- fmtTerm(expected_term),
- });
- }
- },
- },
- else => {
- // On failure, report captured stderr like normal standard error output.
- const bad_exit = switch (generic_result.term) {
- .exited => |code| code != 0,
- .signal, .stopped, .unknown => true,
- };
- if (bad_exit) {
- if (generic_result.stderr) |bytes| {
- run.step.result_stderr = bytes;
- }
- }
-
- try step.handleChildProcessTerm(generic_result.term);
- },
- }
-}
-
-const EvalGenericResult = struct {
- term: process.Child.Term,
- stdout: ?[]const u8,
- stderr: ?[]const u8,
-};
-
-fn spawnChildAndCollect(
- run: *Run,
- argv: []const []const u8,
- environ_map: *EnvMap,
- has_side_effects: bool,
- options: Step.MakeOptions,
- fuzz_context: ?FuzzContext,
-) !?EvalGenericResult {
- const b = run.step.owner;
- const graph = b.graph;
- const io = graph.io;
-
- if (fuzz_context != null) {
- assert(!has_side_effects);
- assert(run.stdio == .zig_test);
- }
-
- const child_cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, &run.step) } else .inherit;
-
- // If an error occurs, it's caused by this command:
- assert(run.step.result_failed_command == null);
- run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{
- .child = environ_map,
- .parent = &graph.environ_map,
- }, argv);
-
- var spawn_options: process.SpawnOptions = .{
- .argv = argv,
- .cwd = child_cwd,
- .environ_map = environ_map,
- .request_resource_usage_statistics = true,
- .stdin = if (run.stdin != .none) s: {
- assert(run.stdio != .inherit);
- break :s .pipe;
- } else switch (run.stdio) {
- .infer_from_args => if (has_side_effects) .inherit else .ignore,
- .inherit => .inherit,
- .check => .ignore,
- .zig_test => .pipe,
- },
- .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) {
- .infer_from_args => if (has_side_effects) .inherit else .ignore,
- .inherit => .inherit,
- .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore,
- .zig_test => .pipe,
- },
- .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) {
- .infer_from_args => if (has_side_effects) .inherit else .pipe,
- .inherit => .inherit,
- .check => .pipe,
- .zig_test => .pipe,
- },
- };
-
- if (run.stdio == .zig_test) {
- const started: Io.Clock.Timestamp = .now(io, .awake);
- const result = evalZigTest(run, spawn_options, options, fuzz_context) catch |err| switch (err) {
- error.Canceled => |e| return e,
- else => |e| e,
- };
- run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
- try result;
- return null;
- } else {
- const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit;
- if (!run.disable_zig_progress and !inherit) {
- spawn_options.progress_node = options.progress_node;
- }
- const terminal_mode: Io.Terminal.Mode = if (inherit) m: {
- const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
- break :m stderr.terminal_mode;
- } else .no_color;
- defer if (inherit) io.unlockStderr();
- try setColorEnvironmentVariables(run, environ_map, terminal_mode);
-
- const started: Io.Clock.Timestamp = .now(io, .awake);
- const result = evalGeneric(run, spawn_options) catch |err| switch (err) {
- error.Canceled => |e| return e,
- else => |e| e,
- };
- run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
- return try result;
- }
-}
-
-fn setColorEnvironmentVariables(run: *Run, environ_map: *EnvMap, terminal_mode: Io.Terminal.Mode) !void {
- color: switch (run.color) {
- .manual => {},
- .enable => {
- try environ_map.put("CLICOLOR_FORCE", "1");
- _ = environ_map.swapRemove("NO_COLOR");
- },
- .disable => {
- try environ_map.put("NO_COLOR", "1");
- _ = environ_map.swapRemove("CLICOLOR_FORCE");
- },
- .inherit => switch (terminal_mode) {
- .no_color, .windows_api => continue :color .disable,
- .escape_codes => continue :color .enable,
- },
- .auto => {
- const capture_stderr = run.captured_stderr != null or switch (run.stdio) {
- .check => |checks| checksContainStderr(checks.items),
- .infer_from_args, .inherit, .zig_test => false,
- };
- if (capture_stderr) {
- continue :color .disable;
- } else {
- continue :color .inherit;
- }
- },
- }
-}
-
-const StdioPollEnum = enum { stdout, stderr };
-
-fn evalZigTest(
- run: *Run,
- spawn_options: process.SpawnOptions,
- options: Step.MakeOptions,
- fuzz_context: ?FuzzContext,
-) !void {
- if (fuzz_context != null) {
- try evalFuzzTest(run, spawn_options, options, fuzz_context.?);
- return;
- }
-
- const step_owner = run.step.owner;
- const gpa = step_owner.allocator;
- const arena = step_owner.allocator;
- const io = step_owner.graph.io;
-
- // We will update this every time a child runs.
- run.step.result_peak_rss = 0;
-
- var test_results: Step.TestResults = .{
- .test_count = 0,
- .skip_count = 0,
- .fail_count = 0,
- .crash_count = 0,
- .timeout_count = 0,
- .leak_count = 0,
- .log_err_count = 0,
- };
- var test_metadata: ?TestMetadata = null;
-
- while (true) {
- var child = try process.spawn(io, spawn_options);
- var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
- var multi_reader: Io.File.MultiReader = undefined;
- multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
- var child_killed = false;
- defer if (!child_killed) {
- child.kill(io);
- multi_reader.deinit();
- run.step.result_peak_rss = @max(
- run.step.result_peak_rss,
- child.resource_usage_statistics.getMaxRss() orelse 0,
- );
- };
-
- switch (try waitZigTest(
- run,
- &child,
- options,
- &multi_reader,
- &test_metadata,
- &test_results,
- )) {
- .write_failed => |err| {
- // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured
- // all available stderr to make our error output as useful as possible.
- const stderr_fr = multi_reader.fileReader(1);
- while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) {
- error.ReadFailed => return stderr_fr.err.?,
- error.EndOfStream => {},
- }
- run.step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered());
-
- // Clean up everything and wait for the child to exit.
- child.stdin.?.close(io);
- child.stdin = null;
- multi_reader.deinit();
- child_killed = true;
- const term = try child.wait(io);
- run.step.result_peak_rss = @max(
- run.step.result_peak_rss,
- child.resource_usage_statistics.getMaxRss() orelse 0,
- );
-
- // The individual unit test results are irrelevant: the test runner itself broke!
- // Fail immediately without populating `s.test_results`.
- return run.step.fail("unable to write stdin ({t}); test process unexpectedly {f}", .{ err, fmtTerm(term) });
- },
- .no_poll => |no_poll| {
- // This might be a success (we requested exit and the child dutifully closed stdout) or
- // a crash of some kind. Either way, the child will terminate by itself -- wait for it.
- const stderr_reader = multi_reader.reader(1);
- const stderr_owned = try arena.dupe(u8, stderr_reader.buffered());
-
- // Clean up everything and wait for the child to exit.
- child.stdin.?.close(io);
- child.stdin = null;
- multi_reader.deinit();
- child_killed = true;
- const term = try child.wait(io);
- run.step.result_peak_rss = @max(
- run.step.result_peak_rss,
- child.resource_usage_statistics.getMaxRss() orelse 0,
- );
-
- if (no_poll.active_test_index) |test_index| {
- // A test was running, so this is definitely a crash. Report it against that
- // test, and continue to the next test.
- test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed;
- test_results.crash_count += 1;
- try run.step.addError("'{s}' {f}{s}{s}", .{
- test_metadata.?.testName(test_index),
- fmtTerm(term),
- if (stderr_owned.len != 0) " with stderr:\n" else "",
- std.mem.trim(u8, stderr_owned, "\n"),
- });
- continue;
- }
-
- // Report an error if the child terminated uncleanly or if we were still trying to run more tests.
- run.step.result_stderr = stderr_owned;
- const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32);
- if (!tests_done or !termMatches(.{ .exited = 0 }, term)) {
- // The individual unit test results are irrelevant: the test runner itself broke!
- // Fail immediately without populating `s.test_results`.
- return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)});
- }
-
- // We're done with all of the tests! Commit the test results and return.
- run.step.test_results = test_results;
- if (test_metadata) |tm| {
- run.cached_test_metadata = tm.toCachedTestMetadata();
- if (options.web_server) |ws| {
- if (run.step.owner.graph.time_report) {
- ws.updateTimeReportRunTest(
- run,
- &run.cached_test_metadata.?,
- tm.ns_per_test,
- );
- }
- }
- }
- return;
- },
- .timeout => |timeout| {
- const stderr_reader = multi_reader.reader(1);
- const stderr = stderr_reader.buffered();
- stderr_reader.tossBuffered();
- if (timeout.active_test_index) |test_index| {
- // A test was running. Report the timeout against that test, and continue on to
- // the next test.
- test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed;
- test_results.timeout_count += 1;
- try run.step.addError("'{s}' timed out after {f}{s}{s}", .{
- test_metadata.?.testName(test_index),
- Io.Duration{ .nanoseconds = timeout.ns_elapsed },
- if (stderr.len != 0) " with stderr:\n" else "",
- std.mem.trim(u8, stderr, "\n"),
- });
- continue;
- }
- // Just log an error and let the child be killed.
- run.step.result_stderr = try arena.dupe(u8, stderr);
- // The individual unit test results in `results` are irrelevant: the test runner
- // is broken! Fail immediately without populating `s.test_results`.
- return run.step.fail("test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }});
- },
- }
- comptime unreachable;
- }
-}
-
-/// Reads stdout of a Zig test process until a termination condition is reached:
-/// * A write fails, indicating the child unexpectedly closed stdin
-/// * A test (or a response from the test runner) times out
-/// * The wait fails, indicating the child closed stdout and stderr
-fn waitZigTest(
- run: *Run,
- child: *process.Child,
- options: Step.MakeOptions,
- multi_reader: *Io.File.MultiReader,
- opt_metadata: *?TestMetadata,
- results: *Step.TestResults,
-) !union(enum) {
- write_failed: anyerror,
- no_poll: struct {
- active_test_index: ?u32,
- ns_elapsed: u64,
- },
- timeout: struct {
- active_test_index: ?u32,
- ns_elapsed: u64,
- },
-} {
- const gpa = run.step.owner.allocator;
- const arena = run.step.owner.allocator;
- const io = run.step.owner.graph.io;
-
- var sub_prog_node: ?std.Progress.Node = null;
- defer if (sub_prog_node) |n| n.end();
-
- if (opt_metadata.*) |*md| {
- // Previous unit test process died or was killed; we're continuing where it left off
- requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
- } else {
- // Running unit tests normally
- run.fuzz_tests.clearRetainingCapacity();
- sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err };
- }
-
- var active_test_index: ?u32 = null;
-
- var last_update: Io.Clock.Timestamp = .now(io, .awake);
-
- // This timeout is used when we're waiting on the test runner itself rather than a user-specified
- // test. For instance, if the test runner leaves this much time between us requesting a test to
- // start and it acknowledging the test starting, we terminate the child and raise an error. This
- // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
- const response_timeout: Io.Clock.Duration = t: {
- const ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
- break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) };
- };
- const test_timeout: ?Io.Clock.Duration = if (options.unit_test_timeout_ns) |ns| .{
- .clock = .awake,
- .raw = .fromNanoseconds(ns),
- } else null;
-
- const stdout = multi_reader.reader(0);
- const stderr = multi_reader.reader(1);
- const Header = std.zig.Server.Message.Header;
-
- while (true) {
- const timeout: Io.Timeout = t: {
- const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
- const duration = opt_duration orelse break :t .none;
- break :t .{ .deadline = last_update.addDuration(duration) };
- };
-
- // This block is exited when `stdout` contains enough bytes for a `Header`.
- header_ready: {
- if (stdout.buffered().len >= @sizeOf(Header)) {
- // We already have one, no need to poll!
- break :header_ready;
- }
-
- multi_reader.fill(64, timeout) catch |err| switch (err) {
- error.Timeout => return .{ .timeout = .{
- .active_test_index = active_test_index,
- .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
- } },
- error.EndOfStream => return .{ .no_poll = .{
- .active_test_index = active_test_index,
- .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
- } },
- else => |e| return e,
- };
-
- continue;
- }
- // There is definitely a header available now -- read it.
- const header = stdout.takeStruct(Header, .little) catch unreachable;
-
- while (stdout.buffered().len < header.bytes_len) {
- multi_reader.fill(64, timeout) catch |err| switch (err) {
- error.Timeout => return .{ .timeout = .{
- .active_test_index = active_test_index,
- .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
- } },
- error.EndOfStream => return .{ .no_poll = .{
- .active_test_index = active_test_index,
- .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
- } },
- else => |e| return e,
- };
- }
-
- const body = stdout.take(header.bytes_len) catch unreachable;
- var body_r: std.Io.Reader = .fixed(body);
- switch (header.tag) {
- .zig_version => {
- if (!std.mem.eql(u8, builtin.zig_version_string, body)) return run.step.fail(
- "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
- .{ builtin.zig_version_string, body },
- );
- },
- .test_metadata => {
- // `metadata` would only be populated if we'd already seen a `test_metadata`, but we
- // only request it once (and importantly, we don't re-request it if we kill and
- // restart the test runner).
- assert(opt_metadata.* == null);
-
- const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable;
- results.test_count = tm_hdr.tests_len;
-
- const names = try arena.alloc(u32, results.test_count);
- for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
-
- const expected_panic_msgs = try arena.alloc(u32, results.test_count);
- for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
-
- const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable;
-
- options.progress_node.setEstimatedTotalItems(names.len);
- opt_metadata.* = .{
- .string_bytes = try arena.dupe(u8, string_bytes),
- .ns_per_test = try arena.alloc(u64, results.test_count),
- .names = names,
- .expected_panic_msgs = expected_panic_msgs,
- .next_index = 0,
- .prog_node = options.progress_node,
- };
- @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
-
- active_test_index = null;
- last_update = .now(io, .awake);
-
- requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
- },
- .test_started => {
- active_test_index = opt_metadata.*.?.next_index - 1;
- last_update = .now(io, .awake);
- },
- .test_results => {
- const md = &opt_metadata.*.?;
-
- const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable;
- assert(tr_hdr.index == active_test_index);
-
- switch (tr_hdr.flags.status) {
- .pass => {},
- .skip => results.skip_count +|= 1,
- .fail => results.fail_count +|= 1,
- }
- const leak_count = tr_hdr.flags.leak_count;
- const log_err_count = tr_hdr.flags.log_err_count;
- results.leak_count +|= leak_count;
- results.log_err_count +|= log_err_count;
-
- if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
-
- if (tr_hdr.flags.status == .fail) {
- const name = md.testName(tr_hdr.index);
- const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
- stderr.tossBuffered();
- if (stderr_bytes.len == 0) {
- try run.step.addError("'{s}' failed without output", .{name});
- } else {
- try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes });
- }
- } else if (leak_count > 0) {
- const name = md.testName(tr_hdr.index);
- const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
- stderr.tossBuffered();
- try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
- } else if (log_err_count > 0) {
- const name = md.testName(tr_hdr.index);
- const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
- stderr.tossBuffered();
- try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
- }
-
- active_test_index = null;
-
- const now: Io.Clock.Timestamp = .now(io, .awake);
- md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
- last_update = now;
-
- requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
- },
- else => {}, // ignore other messages
- }
- }
-}
-
-const FuzzTestRunner = struct {
- run: *Run,
- ctx: FuzzContext,
- coverage_id: ?u64,
-
- instances: []Instance,
- /// The indexes of this are layed out such that it is effectively an array
- /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr.
- batch: Io.Batch,
- /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter.
- pending_broadcasts: std.ArrayList(u8),
- broadcast: std.ArrayList(u8),
- broadcast_undelivered: u32,
-
- const Instance = struct {
- child: process.Child,
- message: std.ArrayListAligned(u8, .@"4"),
- broadcast_written: usize,
- stderr: std.ArrayList(u8),
- stdin_vec: [1][]u8,
- stdout_vec: [1][]u8,
- stderr_vec: [1][]u8,
- progress_node: std.Progress.Node,
-
- fn messageHeader(instance: *Instance) InHeader {
- assert(instance.message.items.len >= @sizeOf(InHeader));
- const header_ptr: *InHeader = @ptrCast(instance.message.items);
- var header = header_ptr.*;
- if (std.builtin.Endian.native != .little) {
- std.mem.byteSwapAllFields(InHeader, &header);
- }
- return header;
- }
- };
-
- const PendingBroadcastFooter = struct {
- from_id: u32,
- body_len: u32,
- };
-
- const InHeader = std.zig.Server.Message.Header;
- const OutHeader = std.zig.Client.Message.Header;
-
- const stdin_i = 0;
- const stdout_i = 1;
- const stderr_i = 2;
-
- fn init(
- run: *Run,
- ctx: FuzzContext,
- progress_node: std.Progress.Node,
- spawn_options: process.SpawnOptions,
- ) !FuzzTestRunner {
- const step_owner = run.step.owner;
- const gpa = step_owner.allocator;
- const io = step_owner.graph.io;
-
- const n_instances = switch (ctx.fuzz.mode) {
- .forever => step_owner.graph.max_jobs orelse @min(
- std.Thread.getCpuCount() catch 1,
- (std.math.maxInt(u32) - 2) / 3,
- ),
- .limit => 1,
- };
- const instances = try gpa.alloc(Instance, n_instances);
- errdefer gpa.free(instances);
- const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3);
- errdefer gpa.free(batch_storage);
-
- @memset(instances, .{
- .child = undefined,
- .message = .empty,
- .broadcast_written = undefined,
- .stderr = .empty,
- .stdin_vec = undefined,
- .stdout_vec = undefined,
- .stderr_vec = undefined,
- .progress_node = undefined,
- });
- for (0.., instances) |id, *instance| {
- errdefer for (instances[0..id]) |*spawned| {
- spawned.child.kill(io);
- spawned.progress_node.end();
- };
- instance.child = try process.spawn(io, spawn_options);
- instance.progress_node = progress_node.start("starting fuzzer", 0);
- }
-
- return .{
- .run = run,
- .ctx = ctx,
- .coverage_id = null,
-
- .instances = instances,
- .batch = .init(batch_storage),
- .pending_broadcasts = .empty,
- .broadcast = .empty,
- .broadcast_undelivered = 0,
- };
- }
-
- fn deinit(f: *FuzzTestRunner) void {
- const step_owner = f.run.step.owner;
- const gpa = step_owner.allocator;
- const io = step_owner.graph.io;
-
- f.batch.cancel(io);
- gpa.free(f.batch.storage);
- var total_rss: usize = 0;
- for (f.instances) |*instance| {
- instance.child.kill(io);
- instance.message.deinit(gpa);
- instance.stderr.deinit(gpa);
- instance.progress_node.end();
- total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0;
- }
- f.run.step.result_peak_rss = @max(f.run.step.result_peak_rss, total_rss);
- gpa.free(f.instances);
- }
-
- fn startInstances(f: *FuzzTestRunner) !void {
- const step_owner = f.run.step.owner;
- const io = step_owner.graph.io;
-
- for (0.., f.instances) |id, *instance| {
- const id32: u32 = @intCast(id);
- (switch (f.ctx.fuzz.mode) {
- .forever => sendRunFuzzTestMessage(
- io,
- instance.child.stdin.?,
- f.run.fuzz_tests.items,
- .forever,
- id32,
- ),
- .limit => |limit| sendRunFuzzTestMessage(
- io,
- instance.child.stdin.?,
- f.run.fuzz_tests.items,
- .iterations,
- limit.amount,
- ),
- }) catch |write_err| {
- // The runner unexpectedly closed stdin, which means it crashed during initialization.
- // Clean up everything and wait for the child to exit.
- instance.child.stdin.?.close(io);
- instance.child.stdin = null;
- const term = try instance.child.wait(io);
- return f.run.step.fail(
- "unable to write stdin ({t}); test process unexpectedly {f}",
- .{ write_err, fmtTerm(term) },
- );
- };
-
- try f.addStdoutRead(id32, @sizeOf(InHeader));
- try f.addStderrRead(id32);
- }
- }
-
- fn listen(f: *FuzzTestRunner) !void {
- const step_owner = f.run.step.owner;
- const io = step_owner.graph.io;
-
- while (true) {
- try f.batch.awaitConcurrent(io, .none);
- while (f.batch.next()) |completion| {
- const id = completion.index / 3;
- const result = completion.result;
- switch (completion.index % 3) {
- 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) {
- // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
- // that all stderr is collected.
- error.BrokenPipe => continue,
- else => |write_e| return write_e,
- }),
- 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) {
- // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
- // that all stderr is collected.
- error.EndOfStream => continue,
- else => |read_e| return read_e,
- }),
- 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) {
- error.EndOfStream => return f.instanceEos(id),
- else => |read_e| return read_e,
- }),
- else => unreachable,
- }
- }
- }
- }
-
- fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
- const step_owner = f.run.step.owner;
- const gpa = step_owner.allocator;
- const io = step_owner.graph.io;
- const instance = &f.instances[id];
-
- instance.message.items.len += n;
- const total_read = instance.message.items.len;
- if (total_read < @sizeOf(InHeader)) {
- try f.addStdoutRead(id, @sizeOf(InHeader));
- return;
- }
-
- const header = instance.messageHeader();
- const body = instance.message.items[@sizeOf(InHeader)..];
- if (body.len != header.bytes_len) {
- try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len);
- return;
- }
-
- switch (header.tag) {
- .zig_version => {
- if (!std.mem.eql(u8, builtin.zig_version_string, body)) return f.run.step.fail(
- "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
- .{ builtin.zig_version_string, body },
- );
- },
- .coverage_id => {
- var body_r: Io.Reader = .fixed(body);
- f.coverage_id = body_r.takeInt(u64, .little) catch unreachable;
- const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable;
- const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable;
- const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable;
-
- const fuzz = f.ctx.fuzz;
- fuzz.queue_mutex.lockUncancelable(io);
- defer fuzz.queue_mutex.unlock(io);
- try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{
- .id = f.coverage_id.?,
- .cumulative = .{
- .runs = cumulative_runs,
- .unique = cumulative_unique,
- .coverage = cumulative_coverage,
- },
- .run = f.run,
- } });
- fuzz.queue_cond.signal(io);
- },
- .fuzz_start_addr => {
- var body_r: Io.Reader = .fixed(body);
- const fuzz = f.ctx.fuzz;
- const addr = body_r.takeInt(u64, .little) catch unreachable;
-
- fuzz.queue_mutex.lockUncancelable(io);
- defer fuzz.queue_mutex.unlock(io);
- try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{
- .addr = addr,
- .coverage_id = f.coverage_id.?,
- } });
- fuzz.queue_cond.signal(io);
- },
- .fuzz_test_change => {
- const test_i = std.mem.readInt(u32, body[0..4], .little);
- instance.progress_node.setName(f.run.fuzz_tests.items[test_i]);
- },
- .broadcast_fuzz_input => {
- if (f.instances.len == 1) {
- // No other processes to broadcast to.
- } else if (f.broadcast_undelivered == 0) {
- try f.instanceBroadcast(id, body);
- } else {
- const footer: PendingBroadcastFooter = .{
- .from_id = id,
- .body_len = @intCast(body.len),
- };
- // There is another broadcast in progress so add this one to the queue.
- const size = @sizeOf(PendingBroadcastFooter) + body.len;
- try f.pending_broadcasts.ensureUnusedCapacity(gpa, size);
- f.pending_broadcasts.appendSliceAssumeCapacity(body);
- f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer));
- }
- },
- else => {}, // ignore other messages
- }
-
- instance.message.clearRetainingCapacity();
- try f.addStdoutRead(id, @sizeOf(InHeader));
- }
-
- fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
- const instance = &f.instances[id];
- instance.stderr.items.len += n;
- try f.addStderrRead(id);
- }
-
- fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void {
- const instance = &f.instances[id];
-
- instance.broadcast_written += n;
- if (instance.broadcast_written == f.broadcast.items.len) {
- f.broadcast_undelivered -= 1;
- if (f.broadcast_undelivered == 0) {
- try f.broadcastComplete();
- }
- } else {
- f.addStdinWrite(id);
- }
- }
-
- fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void {
- const step_owner = f.run.step.owner;
- const gpa = step_owner.allocator;
- const instance = &f.instances[id];
-
- try instance.message.ensureTotalCapacity(gpa, end);
- const start = instance.message.items.len;
- instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]};
- f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{
- .file = instance.child.stdout.?,
- .data = &instance.stdout_vec,
- } });
- }
-
- fn addStderrRead(f: *FuzzTestRunner, id: u32) !void {
- const step_owner = f.run.step.owner;
- const gpa = step_owner.allocator;
- const instance = &f.instances[id];
-
- try instance.stderr.ensureUnusedCapacity(gpa, 1);
- instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()};
- f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{
- .file = instance.child.stderr.?,
- .data = &instance.stderr_vec,
- } });
- }
-
- fn addStdinWrite(f: *FuzzTestRunner, id: u32) void {
- const instance = &f.instances[id];
-
- assert(f.broadcast.items.len != instance.broadcast_written);
- instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]};
- f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{
- .file = instance.child.stdin.?,
- .data = &instance.stdin_vec,
- } });
- }
-
- fn instanceEos(f: *FuzzTestRunner, id: u32) !void {
- const step_owner = f.run.step.owner;
- const io = step_owner.graph.io;
- const instance = &f.instances[id];
-
- instance.child.stdin.?.close(io);
- instance.child.stdin = null;
- const term = try instance.child.wait(io);
- if (!termMatches(.{ .exited = 0 }, term)) {
- f.run.step.result_stderr = try f.mergedStderr();
- try f.saveCrash(id, term);
- return f.run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)});
- }
- }
-
- fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void {
- const step = &f.run.step;
- const b = step.owner;
- const io = b.graph.io;
-
- if (f.coverage_id == null) return;
-
- // Search for the input file corresponding to the instance
- const InputHeader = Build.abi.fuzz.MmapInputHeader;
- var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined;
- var in_r: Io.File.Reader = undefined;
- var in_f: Io.File = undefined;
- var in_name_buf: [12]u8 = undefined;
- var in_name: []const u8 = undefined;
- var i: u32 = 0;
- const header: InputHeader = while (true) : ({
- if (i == std.math.maxInt(u32)) return;
- i += 1;
- }) {
- const name_prefix = "f" ++ Io.Dir.path.sep_str ++ "in";
- in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable;
- in_f = b.cache_root.handle.openFile(io, in_name, .{
- .lock = .exclusive,
- .lock_nonblocking = true,
- }) catch |e| switch (e) {
- error.FileNotFound => return,
- error.WouldBlock => continue, // Can not be from
- // the crashed instance since it is still locked.
- else => return step.fail("failed to open file '{f}{s}': {t}", .{
- b.cache_root, in_name, e,
- }),
- };
-
- in_r = in_f.readerStreaming(io, &in_r_buf);
- const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| {
- in_f.close(io);
- switch (e) {
- error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{
- b.cache_root, in_name, in_r.err.?,
- }),
- error.EndOfStream => continue,
- }
- };
-
- if (header.pc_digest == f.coverage_id.? and
- header.instance_id == id and
- header.test_i < f.run.fuzz_tests.items.len)
- {
- break header;
- }
-
- in_f.close(io);
- };
- defer in_f.close(io);
-
- // Save it to a seperate file
- const crash_name = "f" ++ Io.Dir.path.sep_str ++ "crash";
- const out = b.cache_root.handle.createFile(io, crash_name, .{
- .lock = .exclusive, // Multiple run steps could have found a crash at the same time
- }) catch |e| return step.fail("failed to create file '{f}{s}': {t}", .{
- b.cache_root, crash_name, e,
- });
- defer out.close(io);
-
- var out_w_buf: [512]u8 = undefined;
- var out_w = out.writerStreaming(io, &out_w_buf);
- _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) {
- error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{
- b.cache_root, in_name, in_r.err.?,
- }),
- error.WriteFailed => return step.fail("failed to write file '{f}{s}': {t}", .{
- b.cache_root, crash_name, out_w.err.?,
- }),
- };
-
- return f.run.step.fail("test '{s}' {f}; input saved to '{f}{s}'", .{
- f.run.fuzz_tests.items[header.test_i],
- fmtTerm(term),
- b.cache_root,
- crash_name,
- });
- }
-
- fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void {
- assert(f.instances.len > 1);
- assert(f.broadcast_undelivered == 0); // no other broadcast is progress
- assert(f.broadcast.items.len == 0);
- assert(from_id < f.instances.len);
-
- const step_owner = f.run.step.owner;
- const gpa = step_owner.allocator;
-
- var out_header: OutHeader = .{
- .tag = .new_fuzz_input,
- .bytes_len = @intCast(bytes.len),
- };
- if (std.builtin.Endian.native != .little) {
- std.mem.byteSwapAllFields(OutHeader, &out_header);
- }
- try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len);
- f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header));
- f.broadcast.appendSliceAssumeCapacity(bytes);
-
- f.broadcast_undelivered = @intCast(f.instances.len - 1);
- for (0.., f.instances) |to_id, *instance| {
- if (to_id == from_id) continue;
- instance.broadcast_written = 0;
- f.addStdinWrite(@intCast(to_id));
- }
- }
-
- fn broadcastComplete(f: *FuzzTestRunner) !void {
- assert(f.instances.len > 1);
- assert(f.broadcast_undelivered == 0);
- f.broadcast.clearRetainingCapacity();
-
- const pending = &f.pending_broadcasts;
- if (pending.items.len != 0) {
- // Another broadcast is pending; copy it over to `broadcast`
-
- const footer_len = @sizeOf(PendingBroadcastFooter);
- const footer_bytes = pending.items[pending.items.len - footer_len ..];
- const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes);
- pending.items.len -= footer_len;
-
- const body = pending.items[pending.items.len - footer.body_len ..];
- try f.instanceBroadcast(footer.from_id, body);
- pending.items.len -= body.len;
- }
- }
-
- fn mergedStderr(f: *FuzzTestRunner) std.mem.Allocator.Error![]const u8 {
- const step_owner = f.run.step.owner;
- const arena = step_owner.allocator;
-
- // Collect any available stderr
- while (f.batch.next()) |completion| {
- if (completion.index % 3 != 2) continue;
- const len = completion.result.file_read_streaming catch continue;
- f.instances[completion.index / 3].stderr.items.len += len;
- }
-
- var stderr_len: usize = 0;
- for (f.instances) |*instance| stderr_len += instance.stderr.items.len;
- const stderr = try arena.alloc(u8, stderr_len);
-
- stderr_len = 0;
- for (f.instances) |*instance| {
- @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items);
- stderr_len += instance.stderr.items.len;
- }
- return stderr;
- }
-};
-
-fn evalFuzzTest(
- run: *Run,
- spawn_options: process.SpawnOptions,
- options: Step.MakeOptions,
- fuzz_context: FuzzContext,
-) !void {
- var f: FuzzTestRunner = try .init(run, fuzz_context, options.progress_node, spawn_options);
- defer f.deinit();
- try f.startInstances();
- try f.listen();
-}
-
-const TestMetadata = struct {
- names: []const u32,
- ns_per_test: []u64,
- expected_panic_msgs: []const u32,
- string_bytes: []const u8,
- next_index: u32,
- prog_node: std.Progress.Node,
-
- fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata {
- return .{
- .names = tm.names,
- .string_bytes = tm.string_bytes,
- };
- }
-
- fn testName(tm: TestMetadata, index: u32) []const u8 {
- return tm.toCachedTestMetadata().testName(index);
- }
-};
-
-pub const CachedTestMetadata = struct {
- names: []const u32,
- string_bytes: []const u8,
-
- pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 {
- return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0);
- }
-};
-
-fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
- while (metadata.next_index < metadata.names.len) {
- const i = metadata.next_index;
- metadata.next_index += 1;
-
- if (metadata.expected_panic_msgs[i] != 0) continue;
-
- const name = metadata.testName(i);
- if (sub_prog_node.*) |n| n.end();
- sub_prog_node.* = metadata.prog_node.start(name, 0);
-
- try sendRunTestMessage(io, in, .run_test, i);
- return;
- } else {
- metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done
- try sendMessage(io, in, .exit);
- }
-}
-
-fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
- const header: std.zig.Client.Message.Header = .{
- .tag = tag,
- .bytes_len = 0,
- };
- var w = file.writerStreaming(io, &.{});
- w.interface.writeStruct(header, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
-}
-
-fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void {
- const header: std.zig.Client.Message.Header = .{
- .tag = tag,
- .bytes_len = 4,
- };
- var w = file.writerStreaming(io, &.{});
- w.interface.writeStruct(header, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
-}
-
-fn sendRunFuzzTestMessage(
- io: Io,
- file: Io.File,
- test_names: []const []const u8,
- kind: std.Build.abi.fuzz.LimitKind,
- amount_or_instance: u64,
-) !void {
- const header: std.zig.Client.Message.Header = .{
- .tag = .start_fuzzing,
- .bytes_len = 1 + 8 + 4 + count: {
- var c: u32 = @intCast(test_names.len * 4);
- for (test_names) |name| {
- c += @intCast(name.len);
- }
- break :count c;
- },
- };
- var w = file.writerStreaming(io, &.{});
- w.interface.writeStruct(header, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- for (test_names) |test_name| {
- w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- w.interface.writeAll(test_name) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- }
-}
-
-fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult {
- const b = run.step.owner;
- const io = b.graph.io;
- const arena = b.allocator;
- const gpa = b.allocator;
-
- var child = try process.spawn(io, spawn_options);
- defer child.kill(io);
-
- switch (run.stdin) {
- .bytes => |bytes| {
- child.stdin.?.writeStreamingAll(io, bytes) catch |err| {
- return run.step.fail("unable to write stdin: {t}", .{err});
- };
- child.stdin.?.close(io);
- child.stdin = null;
- },
- .lazy_path => |lazy_path| {
- const path = lazy_path.getPath3(b, &run.step);
- const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| {
- return run.step.fail("unable to open stdin file: {t}", .{err});
- };
- defer file.close(io);
- // TODO https://github.com/ziglang/zig/issues/23955
- var read_buffer: [1024]u8 = undefined;
- var file_reader = file.reader(io, &read_buffer);
- var write_buffer: [1024]u8 = undefined;
- var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer);
- _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
- error.ReadFailed => return run.step.fail("failed to read from {f}: {t}", .{
- path, file_reader.err.?,
- }),
- error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
- stdin_writer.err.?,
- }),
- };
- stdin_writer.interface.flush() catch |err| switch (err) {
- error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
- stdin_writer.err.?,
- }),
- };
- child.stdin.?.close(io);
- child.stdin = null;
- },
- .none => {},
- }
-
- var stdout_bytes: ?[]const u8 = null;
- var stderr_bytes: ?[]const u8 = null;
-
- if (child.stdout) |stdout| {
- if (child.stderr) |stderr| {
- var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
- var multi_reader: Io.File.MultiReader = undefined;
- multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr });
- defer multi_reader.deinit();
-
- const stdout_reader = multi_reader.reader(0);
- const stderr_reader = multi_reader.reader(1);
-
- while (multi_reader.fill(64, .none)) |_| {
- if (run.stdio_limit.toInt()) |limit| {
- if (stdout_reader.buffered().len > limit)
- return error.StdoutStreamTooLong;
- if (stderr_reader.buffered().len > limit)
- return error.StderrStreamTooLong;
- }
- } else |err| switch (err) {
- error.Timeout => unreachable,
- error.EndOfStream => {},
- else => |e| return e,
- }
-
- try multi_reader.checkAnyError();
-
- // TODO: this string can leak since alloc below can return error.
- stdout_bytes = try multi_reader.toOwnedSlice(0);
- // TODO: this string can leak since its allocated using gpa and `try child.wait(io)` below can fail.
- stderr_bytes = try multi_reader.toOwnedSlice(1);
- } else {
- var stdout_reader = stdout.readerStreaming(io, &.{});
- stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
- error.OutOfMemory => |e| return e,
- error.ReadFailed => return stdout_reader.err.?,
- error.StreamTooLong => return error.StdoutStreamTooLong,
- };
- }
- } else if (child.stderr) |stderr| {
- var stderr_reader = stderr.readerStreaming(io, &.{});
- stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
- error.OutOfMemory => |e| return e,
- error.ReadFailed => return stderr_reader.err.?,
- error.StreamTooLong => return error.StderrStreamTooLong,
- };
- }
-
- if (stderr_bytes) |bytes| if (bytes.len > 0) {
- // Treat stderr as an error message.
- const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) {
- .check => |checks| !checksContainStderr(checks.items),
- else => true,
- };
- if (stderr_is_diagnostic) {
- run.step.result_stderr = bytes;
- }
- };
-
- run.step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0;
-
- return .{
- .term = try child.wait(io),
- .stdout = stdout_bytes,
- .stderr = stderr_bytes,
- };
-}
-
-fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void {
- const b = run.step.owner;
- const compiles = artifact.getCompileDependencies(true);
- for (compiles) |compile| {
- if (compile.root_module.resolved_target.?.result.os.tag == .windows and
- compile.isDynamicLibrary())
- {
- addPathDir(run, Dir.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?);
- }
- }
-}
-
-fn failForeign(
- run: *Run,
- suggested_flag: []const u8,
- argv0: []const u8,
- exe: *Step.Compile,
-) error{ MakeFailed, MakeSkipped, OutOfMemory } {
- switch (run.stdio) {
- .check, .zig_test => {
- if (run.skip_foreign_checks)
- return error.MakeSkipped;
-
- const b = run.step.owner;
- const host_name = try b.graph.host.result.zigTriple(b.allocator);
- const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator);
-
- return run.step.fail(
- \\unable to spawn foreign binary '{s}' ({s}) on host system ({s})
- \\ consider using {s} or enabling skip_foreign_checks in the Run step
- , .{ argv0, foreign_name, host_name, suggested_flag });
- },
- else => {
- return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0});
- },
- }
-}
-
-fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void {
- switch (stdio) {
- .infer_from_args, .inherit, .zig_test => {},
- .check => |checks| for (checks.items) |check| {
- hh.add(@as(std.meta.Tag(StdIo.Check), check));
- switch (check) {
- .expect_stderr_exact,
- .expect_stderr_match,
- .expect_stdout_exact,
- .expect_stdout_match,
- => |s| hh.addBytes(s),
-
- .expect_term => |term| {
- hh.add(@as(std.meta.Tag(process.Child.Term), term));
- switch (term) {
- inline .exited, .signal, .stopped => |x| hh.add(x),
- .unknown => |x| hh.add(x),
- }
- },
- }
- },
- }
+ file_input.addStepDependencies(&run.step);
+ run.file_inputs.append(arena, file_input.dupe(graph)) catch @panic("OOM");
}
diff --git a/lib/std/Build/Step/TranslateC.zig b/lib/std/Build/Step/TranslateC.zig
@@ -1,24 +1,25 @@
+const TranslateC = @This();
+
const std = @import("std");
-const Step = std.Build.Step;
-const LazyPath = std.Build.LazyPath;
const fs = std.fs;
const mem = std.mem;
-
-const TranslateC = @This();
-
-pub const base_id: Step.Id = .translate_c;
+const allocPrint = std.fmt.allocPrint;
+const Step = std.Build.Step;
+const LazyPath = std.Build.LazyPath;
+const Configuration = std.Build.Configuration;
step: Step,
source: std.Build.LazyPath,
-include_dirs: std.array_list.Managed(std.Build.Module.IncludeDir),
-system_libs: std.ArrayList(std.Build.Module.SystemLib),
-c_macros: std.array_list.Managed([]const u8),
-out_basename: []const u8,
+include_dirs: std.ArrayList(std.Build.Module.IncludeDir) = .empty,
+system_libs: std.ArrayList(std.Build.Module.SystemLib) = .empty,
+c_macros: std.ArrayList(Configuration.String) = .empty,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
-output_file: std.Build.GeneratedFile,
+output_file: Configuration.GeneratedFileIndex,
link_libc: bool,
+pub const base_tag: Step.Tag = .translate_c;
+
pub const Options = struct {
root_source_file: std.Build.LazyPath,
target: std.Build.ResolvedTarget,
@@ -27,24 +28,20 @@ pub const Options = struct {
};
pub fn create(owner: *std.Build, options: Options) *TranslateC {
- const translate_c = owner.allocator.create(TranslateC) catch @panic("OOM");
- const source = options.root_source_file.dupe(owner);
+ const graph = owner.graph;
+ const translate_c = graph.create(TranslateC);
+ const source = options.root_source_file.dupe(graph);
translate_c.* = .{
- .step = Step.init(.{
- .id = base_id,
+ .step = .init(.{
+ .tag = base_tag,
.name = "translate-c",
.owner = owner,
- .makeFn = make,
}),
.source = source,
- .include_dirs = std.array_list.Managed(std.Build.Module.IncludeDir).init(owner.allocator),
- .c_macros = std.array_list.Managed([]const u8).init(owner.allocator),
- .out_basename = undefined,
.target = options.target,
.optimize = options.optimize,
- .output_file = .{ .step = &translate_c.step },
+ .output_file = graph.addGeneratedFile(&translate_c.step),
.link_libc = options.link_libc,
- .system_libs = .empty,
};
source.addStepDependencies(&translate_c.step);
return translate_c;
@@ -59,7 +56,7 @@ pub const AddExecutableOptions = struct {
};
pub fn getOutput(translate_c: *TranslateC) std.Build.LazyPath {
- return .{ .generated = .{ .file = &translate_c.output_file } };
+ return .{ .generated = .{ .index = translate_c.output_file } };
}
/// Creates a module from the translated source and adds it to the package's
@@ -87,8 +84,8 @@ pub fn createModule(translate_c: *TranslateC) *std.Build.Module {
}
fn setUpModule(translate_c: *TranslateC, module: *std.Build.Module) *std.Build.Module {
- const b = translate_c.step.owner;
- const arena = b.graph.arena;
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
if (translate_c.link_libc) module.link_libc = true;
@@ -100,42 +97,49 @@ fn setUpModule(translate_c: *TranslateC, module: *std.Build.Module) *std.Build.M
}
pub fn addAfterIncludePath(translate_c: *TranslateC, lazy_path: LazyPath) void {
- const b = translate_c.step.owner;
- translate_c.include_dirs.append(.{ .path_after = lazy_path.dupe(b) }) catch
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ translate_c.include_dirs.append(arena, .{ .path_after = lazy_path.dupe(graph) }) catch
@panic("OOM");
lazy_path.addStepDependencies(&translate_c.step);
}
pub fn addSystemIncludePath(translate_c: *TranslateC, lazy_path: LazyPath) void {
- const b = translate_c.step.owner;
- translate_c.include_dirs.append(.{ .path_system = lazy_path.dupe(b) }) catch
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ translate_c.include_dirs.append(arena, .{ .path_system = lazy_path.dupe(graph) }) catch
@panic("OOM");
lazy_path.addStepDependencies(&translate_c.step);
}
pub fn addIncludePath(translate_c: *TranslateC, lazy_path: LazyPath) void {
- const b = translate_c.step.owner;
- translate_c.include_dirs.append(.{ .path = lazy_path.dupe(b) }) catch
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ translate_c.include_dirs.append(arena, .{ .path = lazy_path.dupe(graph) }) catch
@panic("OOM");
lazy_path.addStepDependencies(&translate_c.step);
}
pub fn addConfigHeader(translate_c: *TranslateC, config_header: *Step.ConfigHeader) void {
- translate_c.include_dirs.append(.{ .config_header_step = config_header }) catch
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ translate_c.include_dirs.append(arena, .{ .config_header_step = config_header }) catch
@panic("OOM");
translate_c.step.dependOn(&config_header.step);
}
pub fn addSystemFrameworkPath(translate_c: *TranslateC, directory_path: LazyPath) void {
- const b = translate_c.step.owner;
- translate_c.include_dirs.append(.{ .framework_path_system = directory_path.dupe(b) }) catch
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ translate_c.include_dirs.append(arena, .{ .framework_path_system = directory_path.dupe(graph) }) catch
@panic("OOM");
directory_path.addStepDependencies(&translate_c.step);
}
pub fn addFrameworkPath(translate_c: *TranslateC, directory_path: LazyPath) void {
- const b = translate_c.step.owner;
- translate_c.include_dirs.append(.{ .framework_path = directory_path.dupe(b) }) catch
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ translate_c.include_dirs.append(arena, .{ .framework_path = directory_path.dupe(graph) }) catch
@panic("OOM");
directory_path.addStepDependencies(&translate_c.step);
}
@@ -151,135 +155,21 @@ pub fn addCheckFile(translate_c: *TranslateC, expected_matches: []const []const
/// If the value is omitted, it is set to 1.
/// `name` and `value` need not live longer than the function call.
pub fn defineCMacro(translate_c: *TranslateC, name: []const u8, value: ?[]const u8) void {
- const macro = translate_c.step.owner.fmt("{s}={s}", .{ name, value orelse "1" });
- translate_c.c_macros.append(macro) catch @panic("OOM");
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ const wc = &graph.wip_configuration;
+ const macro = allocPrint(arena, "{s}={s}", .{ name, value orelse "1" }) catch @panic("OOM");
+ const macro_string = wc.addString(macro) catch @panic("OOM");
+ translate_c.c_macros.append(arena, macro_string) catch @panic("OOM");
}
-/// name_and_value looks like [name]=[value]. If the value is omitted, it is set to 1.
+/// name_and_value looks like [name]=[value].
pub fn defineCMacroRaw(translate_c: *TranslateC, name_and_value: []const u8) void {
- translate_c.c_macros.append(translate_c.step.owner.dupe(name_and_value)) catch @panic("OOM");
-}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- const prog_node = options.progress_node;
- const b = step.owner;
- const translate_c: *TranslateC = @fieldParentPtr("step", step);
- const arena = b.graph.arena;
-
- var argv_list = std.array_list.Managed([]const u8).init(b.allocator);
- try argv_list.append(b.graph.zig_exe);
- try argv_list.append("translate-c");
- if (translate_c.link_libc) {
- try argv_list.append("-lc");
- }
-
- try argv_list.append("--cache-dir");
- try argv_list.append(b.cache_root.path orelse ".");
-
- try argv_list.append("--global-cache-dir");
- try argv_list.append(b.graph.global_cache_root.path orelse ".");
-
- if (!translate_c.target.query.isNative()) {
- try argv_list.append("-target");
- try argv_list.append(try translate_c.target.query.zigTriple(b.allocator));
- }
-
- switch (translate_c.optimize) {
- .Debug => {}, // Skip since it's the default.
- else => try argv_list.append(b.fmt("-O{s}", .{@tagName(translate_c.optimize)})),
- }
-
- for (translate_c.include_dirs.items) |include_dir| {
- try include_dir.appendZigProcessFlags(b, &argv_list, step);
- }
-
- for (translate_c.c_macros.items) |c_macro| {
- try argv_list.append("-D");
- try argv_list.append(c_macro);
- }
-
- var prev_search_strategy: std.Build.Module.SystemLib.SearchStrategy = .paths_first;
- var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic;
-
- for (translate_c.system_libs.items) |*system_lib| {
- var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty;
- const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
- if (system_lib_gop.found_existing) {
- try argv_list.appendSlice(system_lib_gop.value_ptr.*);
- continue;
- } else {
- system_lib_gop.value_ptr.* = &.{};
- }
-
- if (system_lib.search_strategy != prev_search_strategy or
- system_lib.preferred_link_mode != prev_preferred_link_mode)
- {
- switch (system_lib.search_strategy) {
- .no_fallback => switch (system_lib.preferred_link_mode) {
- .dynamic => try argv_list.append("-search_dylibs_only"),
- .static => try argv_list.append("-search_static_only"),
- },
- .paths_first => switch (system_lib.preferred_link_mode) {
- .dynamic => try argv_list.append("-search_paths_first"),
- .static => try argv_list.append("-search_paths_first_static"),
- },
- .mode_first => switch (system_lib.preferred_link_mode) {
- .dynamic => try argv_list.append("-search_dylibs_first"),
- .static => try argv_list.append("-search_static_first"),
- },
- }
- prev_search_strategy = system_lib.search_strategy;
- prev_preferred_link_mode = system_lib.preferred_link_mode;
- }
-
- const prefix: []const u8 = prefix: {
- if (system_lib.needed) break :prefix "-needed-l";
- if (system_lib.weak) break :prefix "-weak-l";
- break :prefix "-l";
- };
- switch (system_lib.use_pkg_config) {
- .no => try argv_list.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })),
- .yes, .force => {
- if (Step.Compile.runPkgConfig(&translate_c.step, system_lib.name)) |result| {
- try argv_list.appendSlice(result.cflags);
- try argv_list.appendSlice(result.libs);
- try seen_system_libs.put(arena, system_lib.name, result.cflags);
- } else |err| switch (err) {
- error.PkgConfigInvalidOutput,
- error.PkgConfigCrashed,
- error.PkgConfigFailed,
- error.PkgConfigNotInstalled,
- error.PackageNotFound,
- => switch (system_lib.use_pkg_config) {
- .yes => {
- // pkg-config failed, so fall back to linking the library
- // by name directly.
- try argv_list.append(b.fmt("{s}{s}", .{
- prefix,
- system_lib.name,
- }));
- },
- .force => {
- std.debug.panic("pkg-config failed for library {s}", .{system_lib.name});
- },
- .no => unreachable,
- },
-
- else => |e| return e,
- }
- },
- }
- }
-
- const c_source_path = translate_c.source.getPath2(b, step);
- try argv_list.append(c_source_path);
-
- try argv_list.append("--listen=-");
- const output_dir = try step.evalZigProcess(argv_list.items, prog_node, false, options.web_server, options.gpa);
-
- const basename = std.fs.path.stem(std.fs.path.basename(c_source_path));
- translate_c.out_basename = b.fmt("{s}.zig", .{basename});
- translate_c.output_file.path = output_dir.?.joinString(b.allocator, translate_c.out_basename) catch @panic("OOM");
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ const wc = &graph.wip_configuration;
+ const macro_string = wc.addString(name_and_value) catch @panic("OOM");
+ translate_c.c_macros.append(arena, macro_string) catch @panic("OOM");
}
pub fn linkSystemLibrary(
@@ -287,9 +177,10 @@ pub fn linkSystemLibrary(
name: []const u8,
options: std.Build.Module.LinkSystemLibraryOptions,
) void {
- const b = translate_c.step.owner;
- translate_c.system_libs.append(b.allocator, .{
- .name = b.dupe(name),
+ const graph = translate_c.step.owner.graph;
+ const arena = graph.arena;
+ translate_c.system_libs.append(arena, .{
+ .name = graph.dupeString(name),
.needed = options.needed,
.weak = options.weak,
.use_pkg_config = options.use_pkg_config,
diff --git a/lib/std/Build/Step/UpdateSourceFiles.zig b/lib/std/Build/Step/UpdateSourceFiles.zig
@@ -1,116 +1,63 @@
-//! Writes data to paths relative to the package root, effectively mutating the
-//! package's source files. Be careful with the latter functionality; it should
-//! not be used during the normal build process, but as a utility run by a
-//! developer with intention to update source files, which will then be
-//! committed to version control.
const UpdateSourceFiles = @This();
const std = @import("std");
-const Io = std.Io;
const Step = std.Build.Step;
-const fs = std.fs;
-const ArrayList = std.ArrayList;
+const Configuration = std.Build.Configuration;
step: Step,
-output_source_files: std.ArrayList(OutputSourceFile),
+embeds: std.ArrayList(Embed) = .empty,
+copies: std.ArrayList(Copy) = .empty,
-pub const base_id: Step.Id = .update_source_files;
+pub const base_tag: Step.Tag = .update_source_files;
-pub const OutputSourceFile = struct {
- contents: Contents,
- sub_path: []const u8,
-};
-
-pub const Contents = union(enum) {
- bytes: []const u8,
- copy: std.Build.LazyPath,
-};
+pub const Embed = Step.WriteFile.Embed;
+pub const Copy = Step.WriteFile.Copy;
pub fn create(owner: *std.Build) *UpdateSourceFiles {
- const usf = owner.allocator.create(UpdateSourceFiles) catch @panic("OOM");
+ const graph = owner.graph;
+ const usf = graph.create(UpdateSourceFiles);
usf.* = .{
- .step = Step.init(.{
- .id = base_id,
+ .step = .init(.{
+ .tag = base_tag,
.name = "UpdateSourceFiles",
.owner = owner,
- .makeFn = make,
}),
- .output_source_files = .empty,
};
return usf;
}
-/// A path relative to the package root.
+/// Overwrites a path relative to the build root with the contents of another file.
///
-/// Be careful with this because it updates source files. This should not be
-/// used as part of the normal build process, but as a utility occasionally
-/// run by a developer with intent to modify source files and then commit
-/// those changes to version control.
-pub fn addCopyFileToSource(usf: *UpdateSourceFiles, source: std.Build.LazyPath, sub_path: []const u8) void {
- const b = usf.step.owner;
- usf.output_source_files.append(b.allocator, .{
- .contents = .{ .copy = source },
- .sub_path = sub_path,
+/// Because it updates source files, this should not be used as part of the
+/// normal build process, but as a utility occasionally run by a developer with
+/// intent to modify source files and then commit those changes to version
+/// control.
+pub fn addCopyFileToSource(usf: *UpdateSourceFiles, src_file: std.Build.LazyPath, sub_path: []const u8) void {
+ const graph = usf.step.owner.graph;
+ const wc = &graph.wip_configuration;
+ const arena = graph.arena;
+
+ usf.copies.append(arena, .{
+ .sub_path = wc.addString(sub_path) catch @panic("OOM"),
+ .src_file = src_file.dupe(graph),
}) catch @panic("OOM");
- source.addStepDependencies(&usf.step);
+
+ src_file.addStepDependencies(&usf.step);
}
-/// A path relative to the package root.
+/// Overwrites a path relative to the package root with the provided bytes.
///
-/// Be careful with this because it updates source files. This should not be
-/// used as part of the normal build process, but as a utility occasionally
-/// run by a developer with intent to modify source files and then commit
-/// those changes to version control.
-pub fn addBytesToSource(usf: *UpdateSourceFiles, bytes: []const u8, sub_path: []const u8) void {
- const b = usf.step.owner;
- usf.output_source_files.append(b.allocator, .{
- .contents = .{ .bytes = bytes },
- .sub_path = sub_path,
+/// Because it updates source files, this should not be used as part of the
+/// normal build process, but as a utility occasionally run by a developer with
+/// intent to modify source files and then commit those changes to version
+/// control.
+pub fn addBytesToSource(usf: *UpdateSourceFiles, contents: []const u8, sub_path: []const u8) void {
+ const graph = usf.step.owner.graph;
+ const wc = &graph.wip_configuration;
+ const arena = graph.arena;
+
+ usf.embeds.append(arena, .{
+ .sub_path = wc.addString(sub_path) catch @panic("OOM"),
+ .contents = wc.addBytes(contents) catch @panic("OOM"),
}) catch @panic("OOM");
}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const b = step.owner;
- const io = b.graph.io;
- const usf: *UpdateSourceFiles = @fieldParentPtr("step", step);
-
- var any_miss = false;
- for (usf.output_source_files.items) |output_source_file| {
- if (fs.path.dirname(output_source_file.sub_path)) |dirname| {
- b.build_root.handle.createDirPath(io, dirname) catch |err| {
- return step.fail("unable to make path '{f}{s}': {t}", .{ b.build_root, dirname, err });
- };
- }
- switch (output_source_file.contents) {
- .bytes => |bytes| {
- b.build_root.handle.writeFile(io, .{ .sub_path = output_source_file.sub_path, .data = bytes }) catch |err| {
- return step.fail("unable to write file '{f}{s}': {t}", .{
- b.build_root, output_source_file.sub_path, err,
- });
- };
- any_miss = true;
- },
- .copy => |file_source| {
- if (!step.inputs.populated()) try step.addWatchInput(file_source);
-
- const source_path = file_source.getPath2(b, step);
- const prev_status = Io.Dir.updateFile(
- .cwd(),
- io,
- source_path,
- b.build_root.handle,
- output_source_file.sub_path,
- .{},
- ) catch |err| {
- return step.fail("unable to update file from '{s}' to '{f}{s}': {t}", .{
- source_path, b.build_root, output_source_file.sub_path, err,
- });
- };
- any_miss = any_miss or prev_status == .stale;
- },
- }
- }
-
- step.result_cached = !any_miss;
-}
diff --git a/lib/std/Build/Step/WriteFile.zig b/lib/std/Build/Step/WriteFile.zig
@@ -4,21 +4,17 @@
const WriteFile = @This();
const std = @import("std");
-const Io = std.Io;
-const Dir = std.Io.Dir;
const Step = std.Build.Step;
-const ArrayList = std.ArrayList;
-const assert = std.debug.assert;
+const Configuration = std.Build.Configuration;
step: Step,
-
-/// The elements here are pointers because we need stable pointers for the GeneratedFile field.
-files: std.ArrayList(File),
-directories: std.ArrayList(Directory),
-generated_directory: std.Build.GeneratedFile,
+embeds: std.ArrayList(Embed) = .empty,
+copies: std.ArrayList(Copy) = .empty,
+directories: std.ArrayList(Directory) = .empty,
+generated_directory: Configuration.GeneratedFileIndex,
mode: Mode = .whole_cached,
-pub const base_id: Step.Id = .write_file;
+pub const base_tag: Step.Tag = .write_file;
pub const Mode = union(enum) {
/// Default mode. Integrates with the cache system. The directory should be
@@ -37,363 +33,152 @@ pub const Mode = union(enum) {
mutate: std.Build.LazyPath,
};
-pub const File = struct {
- sub_path: []const u8,
- contents: Contents,
-};
-
-pub const Directory = struct {
- source: std.Build.LazyPath,
- sub_path: []const u8,
- options: Options,
-
- pub const Options = struct {
- /// File paths that end in any of these suffixes will be excluded from copying.
- exclude_extensions: []const []const u8 = &.{},
- /// Only file paths that end in any of these suffixes will be included in copying.
- /// `null` means that all suffixes will be included.
- /// `exclude_extensions` takes precedence over `include_extensions`.
- include_extensions: ?[]const []const u8 = null,
-
- pub fn dupe(opts: Options, b: *std.Build) Options {
- return .{
- .exclude_extensions = b.dupeStrings(opts.exclude_extensions),
- .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null,
- };
- }
+pub const Embed = Configuration.Step.WriteFile.Embed;
- pub fn pathIncluded(opts: Options, path: []const u8) bool {
- for (opts.exclude_extensions) |ext| {
- if (std.mem.endsWith(u8, path, ext))
- return false;
- }
- if (opts.include_extensions) |incs| {
- for (incs) |inc| {
- if (std.mem.endsWith(u8, path, inc))
- return true;
- } else {
- return false;
- }
- }
- return true;
- }
- };
+pub const Copy = struct {
+ sub_path: Configuration.String,
+ src_file: std.Build.LazyPath,
};
-pub const Contents = union(enum) {
- bytes: []const u8,
- copy: std.Build.LazyPath,
+pub const Directory = struct {
+ sub_path: Configuration.String,
+ src_path: std.Build.LazyPath,
+ exclude_extensions: Configuration.OptionalStringList,
+ include_extensions: Configuration.OptionalStringList,
};
pub fn create(owner: *std.Build) *WriteFile {
- const write_file = owner.allocator.create(WriteFile) catch @panic("OOM");
- write_file.* = .{
- .step = Step.init(.{
- .id = base_id,
+ const graph = owner.graph;
+ const wf = graph.create(WriteFile);
+ wf.* = .{
+ .step = .init(.{
+ .tag = base_tag,
.name = "WriteFile",
.owner = owner,
- .makeFn = make,
}),
- .files = .empty,
- .directories = .empty,
- .generated_directory = .{ .step = &write_file.step },
+ .generated_directory = graph.addGeneratedFile(&wf.step),
};
- return write_file;
+ return wf;
}
-pub fn add(write_file: *WriteFile, sub_path: []const u8, bytes: []const u8) std.Build.LazyPath {
- const b = write_file.step.owner;
- const gpa = b.allocator;
- const file = File{
- .sub_path = b.dupePath(sub_path),
- .contents = .{ .bytes = b.dupe(bytes) },
- };
- write_file.files.append(gpa, file) catch @panic("OOM");
- write_file.maybeUpdateName();
+/// Writes `contents` to a file at `sub_path` relative to the output
+/// directory.
+///
+/// `sub_path` may be a basename, or it may include subdirectories, which are
+/// created as needed.
+pub fn add(wf: *WriteFile, sub_path: []const u8, contents: []const u8) std.Build.LazyPath {
+ const graph = wf.step.owner.graph;
+ const wc = &graph.wip_configuration;
+ const arena = graph.arena;
+
+ wf.embeds.append(arena, .{
+ .sub_path = wc.addString(sub_path) catch @panic("OOM"),
+ .contents = wc.addBytes(contents) catch @panic("OOM"),
+ }) catch @panic("OOM");
+
+ wf.maybeUpdateName();
+
return .{
.generated = .{
- .file = &write_file.generated_directory,
- .sub_path = file.sub_path,
+ .index = wf.generated_directory,
+ .sub_path = graph.dupeString(sub_path),
},
};
}
-/// Place the file into the generated directory within the local cache,
-/// along with all the rest of the files added to this step. The parameter
-/// here is the destination path relative to the local cache directory
-/// associated with this WriteFile. It may be a basename, or it may
-/// include sub-directories, in which case this step will ensure the
-/// required sub-path exists.
-/// This is the option expected to be used most commonly with `addCopyFile`.
-pub fn addCopyFile(write_file: *WriteFile, source: std.Build.LazyPath, sub_path: []const u8) std.Build.LazyPath {
- const b = write_file.step.owner;
- const gpa = b.allocator;
- const file = File{
- .sub_path = b.dupePath(sub_path),
- .contents = .{ .copy = source },
- };
- write_file.files.append(gpa, file) catch @panic("OOM");
+/// Copies the provided file to `sub_path` relative to the output directory.
+///
+/// `sub_path` may be a basename, or it may include subdirectories, which are
+/// created as needed.
+pub fn addCopyFile(wf: *WriteFile, src_file: std.Build.LazyPath, sub_path: []const u8) std.Build.LazyPath {
+ const graph = wf.step.owner.graph;
+ const wc = &graph.wip_configuration;
+ const arena = graph.arena;
- write_file.maybeUpdateName();
- source.addStepDependencies(&write_file.step);
- return .{
- .generated = .{
- .file = &write_file.generated_directory,
- .sub_path = file.sub_path,
- },
- };
+ wf.copies.append(arena, .{
+ .sub_path = wc.addString(sub_path) catch @panic("OOM"),
+ .src_file = src_file.dupe(graph),
+ }) catch @panic("OOM");
+
+ wf.maybeUpdateName();
+
+ src_file.addStepDependencies(&wf.step);
+
+ return .{ .generated = .{
+ .index = wf.generated_directory,
+ .sub_path = graph.dupePath(sub_path),
+ } };
}
-/// Copy files matching the specified exclude/include patterns to the specified subdirectory
-/// relative to this step's generated directory.
+pub const CopyDirectoryOptions = struct {
+ /// File paths that end in any of these suffixes will be excluded from copying.
+ exclude_extensions: []const []const u8 = &.{},
+ /// Only file paths that end in any of these suffixes will be included in copying.
+ /// `null` means that all suffixes will be included.
+ /// `exclude_extensions` takes precedence over `include_extensions`.
+ include_extensions: ?[]const []const u8 = null,
+};
+
+/// Copy files matching the specified exclude/include patterns to the specified
+/// subdirectory relative to this step's generated directory.
+///
/// The returned value is a lazy path to the generated subdirectory.
pub fn addCopyDirectory(
- write_file: *WriteFile,
- source: std.Build.LazyPath,
+ wf: *WriteFile,
+ src_path: std.Build.LazyPath,
sub_path: []const u8,
- options: Directory.Options,
+ options: CopyDirectoryOptions,
) std.Build.LazyPath {
- const b = write_file.step.owner;
- const gpa = b.allocator;
- const dir = Directory{
- .source = source.dupe(b),
- .sub_path = b.dupePath(sub_path),
- .options = options.dupe(b),
- };
- write_file.directories.append(gpa, dir) catch @panic("OOM");
+ const graph = wf.step.owner.graph;
+ const wc = &graph.wip_configuration;
+ const arena = graph.arena;
+
+ wf.directories.append(arena, .{
+ .sub_path = wc.addString(sub_path) catch @panic("OOM"),
+ .src_path = src_path.dupe(graph),
+ .exclude_extensions = if (options.exclude_extensions.len != 0)
+ .init(wc.addStringList(options.exclude_extensions) catch @panic("OOM"))
+ else
+ .none,
+ .include_extensions = if (options.include_extensions) |list|
+ .init(wc.addStringList(list) catch @panic("OOM"))
+ else
+ .none,
+ }) catch @panic("OOM");
+
+ wf.maybeUpdateName();
+
+ src_path.addStepDependencies(&wf.step);
- write_file.maybeUpdateName();
- source.addStepDependencies(&write_file.step);
return .{
.generated = .{
- .file = &write_file.generated_directory,
- .sub_path = dir.sub_path,
+ .index = wf.generated_directory,
+ .sub_path = graph.dupePath(sub_path),
},
};
}
/// Returns a `LazyPath` representing the base directory that contains all the
/// files from this `WriteFile`.
-pub fn getDirectory(write_file: *WriteFile) std.Build.LazyPath {
- return .{ .generated = .{ .file = &write_file.generated_directory } };
+pub fn getDirectory(wf: *WriteFile) std.Build.LazyPath {
+ return .{ .generated = .{ .index = wf.generated_directory } };
}
-fn maybeUpdateName(write_file: *WriteFile) void {
- if (write_file.files.items.len == 1 and write_file.directories.items.len == 0) {
+fn maybeUpdateName(wf: *WriteFile) void {
+ const graph = wf.step.owner.graph;
+ const wc = &graph.wip_configuration;
+ const files_count = wf.embeds.items.len + wf.copies.items.len;
+ if (files_count == 1 and wf.directories.items.len == 0) {
// First time adding a file; update name.
- if (std.mem.eql(u8, write_file.step.name, "WriteFile")) {
- write_file.step.name = write_file.step.owner.fmt("WriteFile {s}", .{write_file.files.items[0].sub_path});
+ const sub_path = if (wf.embeds.items.len == 1) wf.embeds.items[0].sub_path else wf.copies.items[0].sub_path;
+ if (std.mem.eql(u8, wf.step.name, "WriteFile")) {
+ wf.step.name = wf.step.owner.fmt("WriteFile {s}", .{wc.stringSlice(sub_path)});
}
- } else if (write_file.directories.items.len == 1 and write_file.files.items.len == 0) {
+ } else if (wf.directories.items.len == 1 and files_count == 0) {
// First time adding a directory; update name.
- if (std.mem.eql(u8, write_file.step.name, "WriteFile")) {
- write_file.step.name = write_file.step.owner.fmt("WriteFile {s}", .{write_file.directories.items[0].sub_path});
- }
- }
-}
-
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const b = step.owner;
- const graph = b.graph;
- const io = graph.io;
- const arena = b.allocator;
- const gpa = graph.cache.gpa;
- const write_file: *WriteFile = @fieldParentPtr("step", step);
-
- const open_dir_cache = try arena.alloc(Io.Dir, write_file.directories.items.len);
- var open_dirs_count: usize = 0;
- defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]);
-
- switch (write_file.mode) {
- .whole_cached => {
- step.clearWatchInputs();
-
- // The cache is used here not really as a way to speed things up - because writing
- // the data to a file would probably be very fast - but as a way to find a canonical
- // location to put build artifacts.
-
- // If, for example, a hard-coded path was used as the location to put WriteFile
- // files, then two WriteFiles executing in parallel might clobber each other.
-
- var man = b.graph.cache.obtain();
- defer man.deinit();
-
- for (write_file.files.items) |file| {
- man.hash.addBytes(file.sub_path);
-
- switch (file.contents) {
- .bytes => |bytes| {
- man.hash.addBytes(bytes);
- },
- .copy => |lazy_path| {
- const path = lazy_path.getPath3(b, step);
- _ = try man.addFilePath(path, null);
- try step.addWatchInput(lazy_path);
- },
- }
- }
-
- for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| {
- man.hash.addBytes(dir.sub_path);
- for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext);
- if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc);
-
- const need_derived_inputs = try step.addDirectoryWatchInput(dir.source);
- const src_dir_path = dir.source.getPath3(b, step);
-
- var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
- return step.fail("unable to open source directory '{f}': {s}", .{
- src_dir_path, @errorName(err),
- });
- };
- open_dir_cache_elem.* = src_dir;
- open_dirs_count += 1;
-
- var it = try src_dir.walk(gpa);
- defer it.deinit();
- while (try it.next(io)) |entry| {
- if (!dir.options.pathIncluded(entry.path)) continue;
-
- switch (entry.kind) {
- .directory => {
- if (need_derived_inputs) {
- const entry_path = try src_dir_path.join(arena, entry.path);
- try step.addDirectoryWatchInputFromPath(entry_path);
- }
- },
- .file => {
- const entry_path = try src_dir_path.join(arena, entry.path);
- _ = try man.addFilePath(entry_path, null);
- },
- else => continue,
- }
- }
- }
-
- if (try step.cacheHit(&man)) {
- const digest = man.final();
- write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest });
- assert(step.result_cached);
- return;
- }
-
- const digest = man.final();
- const cache_path = "o" ++ Dir.path.sep_str ++ digest;
-
- write_file.generated_directory.path = try b.cache_root.join(arena, &.{cache_path});
-
- try operate(write_file, open_dir_cache, .{
- .root_dir = b.cache_root,
- .sub_path = cache_path,
- });
-
- try step.writeManifest(&man);
- },
- .tmp => {
- step.result_cached = false;
-
- var rand_int: u64 = undefined;
- io.random(@ptrCast(&rand_int));
- const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
-
- write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path});
-
- try operate(write_file, open_dir_cache, .{
- .root_dir = b.cache_root,
- .sub_path = tmp_dir_sub_path,
- });
- },
- .mutate => |lp| {
- step.result_cached = false;
- const root_path = try lp.getPath4(b, step);
- write_file.generated_directory.path = try root_path.toString(arena);
- try operate(write_file, open_dir_cache, root_path);
- },
- }
-}
-
-fn operate(write_file: *WriteFile, open_dir_cache: []const Io.Dir, root_path: std.Build.Cache.Path) !void {
- const step = &write_file.step;
- const b = step.owner;
- const io = b.graph.io;
- const gpa = b.graph.cache.gpa;
- const arena = b.allocator;
-
- var cache_dir = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err|
- return step.fail("unable to make path {f}: {t}", .{ root_path, err });
- defer cache_dir.close(io);
-
- for (write_file.files.items) |file| {
- if (Dir.path.dirname(file.sub_path)) |dirname| {
- cache_dir.createDirPath(io, dirname) catch |err| {
- return step.fail("unable to make path '{f}{c}{s}': {t}", .{
- root_path, Dir.path.sep, dirname, err,
- });
- };
- }
- switch (file.contents) {
- .bytes => |bytes| {
- cache_dir.writeFile(io, .{ .sub_path = file.sub_path, .data = bytes }) catch |err| {
- return step.fail("unable to write file '{f}{c}{s}': {t}", .{
- root_path, Dir.path.sep, file.sub_path, err,
- });
- };
- },
- .copy => |file_source| {
- const source_path = file_source.getPath2(b, step);
- const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir, file.sub_path, .{}) catch |err| {
- return step.fail("unable to update file from '{s}' to '{f}{c}{s}': {t}", .{
- source_path, root_path, Dir.path.sep, file.sub_path, err,
- });
- };
- // At this point we already will mark the step as a cache miss.
- // But this is kind of a partial cache hit since individual
- // file copies may be avoided. Oh well, this information is
- // discarded.
- _ = prev_status;
- },
- }
- }
-
- for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| {
- const src_dir_path = dir.source.getPath3(b, step);
- const dest_dirname = dir.sub_path;
-
- if (dest_dirname.len != 0) {
- cache_dir.createDirPath(io, dest_dirname) catch |err| {
- return step.fail("unable to make path '{f}{c}{s}': {t}", .{
- root_path, Dir.path.sep, dest_dirname, err,
- });
- };
- }
-
- var it = try already_open_dir.walk(gpa);
- defer it.deinit();
- while (try it.next(io)) |entry| {
- if (!dir.options.pathIncluded(entry.path)) continue;
-
- const src_entry_path = try src_dir_path.join(arena, entry.path);
- const dest_path = b.pathJoin(&.{ dest_dirname, entry.path });
- switch (entry.kind) {
- .directory => try cache_dir.createDirPath(io, dest_path),
- .file => {
- const prev_status = Io.Dir.updateFile(
- src_entry_path.root_dir.handle,
- io,
- src_entry_path.sub_path,
- cache_dir,
- dest_path,
- .{},
- ) catch |err| {
- return step.fail("unable to update file from '{f}' to '{f}{c}{s}': {t}", .{
- src_entry_path, root_path, Dir.path.sep, dest_path, err,
- });
- };
- _ = prev_status;
- },
- else => continue,
- }
+ const dir_name = wc.stringSlice(wf.directories.items[0].sub_path);
+ if (std.mem.eql(u8, wf.step.name, "WriteFile")) {
+ wf.step.name = wf.step.owner.fmt("WriteFile {s}", .{dir_name});
}
}
}
diff --git a/lib/std/Build/Watch.zig b/lib/std/Build/Watch.zig
@@ -1,968 +0,0 @@
-const builtin = @import("builtin");
-
-const std = @import("../std.zig");
-const Io = std.Io;
-const Step = std.Build.Step;
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-const fatal = std.process.fatal;
-const Watch = @This();
-const FsEvents = @import("Watch/FsEvents.zig");
-
-os: Os,
-/// The number to show as the number of directories being watched.
-dir_count: usize,
-// These fields are common to most implementations so are kept here for simplicity.
-// They are `undefined` on implementations which do not utilize then.
-dir_table: DirTable,
-generation: Generation,
-
-pub const have_impl = Os != void;
-
-/// Key is the directory to watch which contains one or more files we are
-/// interested in noticing changes to.
-///
-/// Value is generation.
-const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false);
-
-/// Special key of "." means any changes in this directory trigger the steps.
-const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet);
-const StepSet = std.AutoArrayHashMapUnmanaged(*Step, Generation);
-
-const Generation = u8;
-
-const Hash = std.hash.Wyhash;
-const Cache = std.Build.Cache;
-
-const Os = switch (builtin.os.tag) {
- .linux => struct {
- const posix = std.posix;
-
- /// Keyed differently but indexes correspond 1:1 with `dir_table`.
- handle_table: HandleTable,
- /// fanotify file descriptors are keyed by mount id since marks
- /// are limited to a single filesystem.
- poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd),
-
- const MountId = i32;
- const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false);
-
- const fan_mask: std.os.linux.fanotify.MarkMask = .{
- .CLOSE_WRITE = true,
- .CREATE = true,
- .DELETE = true,
- .DELETE_SELF = true,
- .EVENT_ON_CHILD = true,
- .MOVED_FROM = true,
- .MOVED_TO = true,
- .MOVE_SELF = true,
- .ONDIR = true,
- };
-
- const FileHandle = struct {
- handle: *align(1) std.os.linux.file_handle,
-
- fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
- const bytes = lfh.slice();
- const new_ptr = try gpa.alignedAlloc(
- u8,
- .of(std.os.linux.file_handle),
- @sizeOf(std.os.linux.file_handle) + bytes.len,
- );
- const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
- new_header.* = lfh.handle.*;
- const new: FileHandle = .{ .handle = new_header };
- @memcpy(new.slice(), lfh.slice());
- return new;
- }
-
- fn destroy(lfh: FileHandle, gpa: Allocator) void {
- const ptr: [*]u8 = @ptrCast(lfh.handle);
- const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
- return gpa.free(allocated_slice);
- }
-
- fn slice(lfh: FileHandle) []u8 {
- const ptr: [*]u8 = &lfh.handle.f_handle;
- return ptr[0..lfh.handle.handle_bytes];
- }
-
- const Adapter = struct {
- pub fn hash(self: Adapter, a: FileHandle) u32 {
- _ = self;
- const unsigned_type: u32 = @bitCast(a.handle.handle_type);
- return @truncate(Hash.hash(unsigned_type, a.slice()));
- }
- pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
- _ = self;
- _ = b_index;
- return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
- }
- };
- };
-
- fn init(cwd_path: []const u8) !Watch {
- _ = cwd_path;
- return .{
- .dir_table = .{},
- .dir_count = 0,
- .os = switch (builtin.os.tag) {
- .linux => .{
- .handle_table = .{},
- .poll_fds = .{},
- },
- else => {},
- },
- .generation = 0,
- };
- }
-
- fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
- var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
- var buf: [std.fs.max_path_bytes]u8 = undefined;
- const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
- path.sub_path,
- }) catch return error.NameTooLong;
- const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
- stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
- try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID);
- const stack_lfh: FileHandle = .{ .handle = stack_ptr };
- return stack_lfh.clone(gpa);
- }
-
- fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool {
- const fanotify = std.os.linux.fanotify;
- const M = fanotify.event_metadata;
- var events_buf: [256 + 4096]u8 = undefined;
- var any_dirty = false;
- while (true) {
- var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) {
- error.WouldBlock => return any_dirty,
- else => |e| return e,
- };
- var meta: [*]align(1) M = @ptrCast(&events_buf);
- while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
- len -= meta[0].event_len;
- meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
- }) {
- assert(meta[0].vers == M.VERSION);
- if (meta[0].mask.Q_OVERFLOW) {
- any_dirty = true;
- std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
- markAllFilesDirty(w, gpa);
- return true;
- }
- const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
- switch (fid.hdr.info_type) {
- .DFID_NAME => {
- const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
- const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
- const file_name = std.mem.span(file_name_z);
- const lfh: FileHandle = .{ .handle = file_handle };
- if (w.os.handle_table.getPtr(lfh)) |value| {
- if (value.reaction_set.getPtr(".")) |glob_set|
- any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
- if (value.reaction_set.getPtr(file_name)) |step_set|
- any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
- }
- },
- else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
- }
- }
- }
- }
-
- fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
- // Add missing marks and note persisted ones.
- for (steps) |step| {
- for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
- const reaction_set = rs: {
- const gop = try w.dir_table.getOrPut(gpa, path);
- if (!gop.found_existing) {
- var mount_id: MountId = undefined;
- const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
- error.FileNotFound => {
- std.debug.assert(w.dir_table.swapRemove(path));
- continue;
- },
- else => return err,
- };
- const fan_fd = blk: {
- const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id);
- if (!fd_gop.found_existing) {
- const fan_fd = std.posix.fanotify_init(.{
- .CLASS = .NOTIF,
- .CLOEXEC = true,
- .NONBLOCK = true,
- .REPORT_NAME = true,
- .REPORT_DIR_FID = true,
- .REPORT_FID = true,
- .REPORT_TARGET_FID = true,
- }, 0) catch |err| switch (err) {
- error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}),
- else => |e| return e,
- };
- fd_gop.value_ptr.* = .{
- .fd = fan_fd,
- .events = std.posix.POLL.IN,
- .revents = undefined,
- };
- }
- break :blk fd_gop.value_ptr.*.fd;
- };
- // `dir_handle` may already be present in the table in
- // the case that we have multiple Cache.Path instances
- // that compare inequal but ultimately point to the same
- // directory on the file system.
- // In such case, we must revert adding this directory, but keep
- // the additions to the step set.
- const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle);
- if (dh_gop.found_existing) {
- _ = w.dir_table.pop();
- } else {
- assert(dh_gop.index == gop.index);
- dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
- posix.fanotify_mark(fan_fd, .{
- .ADD = true,
- .ONLYDIR = true,
- }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| {
- fatal("unable to watch {f}: {s}", .{ path, @errorName(err) });
- };
- }
- break :rs &dh_gop.value_ptr.reaction_set;
- }
- break :rs &w.os.handle_table.values()[gop.index].reaction_set;
- };
- for (files.items) |basename| {
- const gop = try reaction_set.getOrPut(gpa, basename);
- if (!gop.found_existing) gop.value_ptr.* = .{};
- try gop.value_ptr.put(gpa, step, w.generation);
- }
- }
- }
-
- {
- // Remove marks for files that are no longer inputs.
- var i: usize = 0;
- while (i < w.os.handle_table.entries.len) {
- {
- const reaction_set = &w.os.handle_table.values()[i].reaction_set;
- var step_set_i: usize = 0;
- while (step_set_i < reaction_set.entries.len) {
- const step_set = &reaction_set.values()[step_set_i];
- var dirent_i: usize = 0;
- while (dirent_i < step_set.entries.len) {
- const generations = step_set.values();
- if (generations[dirent_i] == w.generation) {
- dirent_i += 1;
- continue;
- }
- step_set.swapRemoveAt(dirent_i);
- }
- if (step_set.entries.len > 0) {
- step_set_i += 1;
- continue;
- }
- reaction_set.swapRemoveAt(step_set_i);
- }
- if (reaction_set.entries.len > 0) {
- i += 1;
- continue;
- }
- }
-
- const path = w.dir_table.keys()[i];
-
- const mount_id = w.os.handle_table.values()[i].mount_id;
- const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd;
- posix.fanotify_mark(fan_fd, .{
- .REMOVE = true,
- .ONLYDIR = true,
- }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) {
- error.FileNotFound => {}, // Expected, harmless.
- else => |e| std.log.warn("unable to unwatch '{f}': {s}", .{ path, @errorName(e) }),
- };
-
- w.dir_table.swapRemoveAt(i);
- w.os.handle_table.swapRemoveAt(i);
- }
- w.generation +%= 1;
- }
- w.dir_count = w.dir_table.count();
- }
-
- fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
- _ = io;
- const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms());
- if (events_len == 0)
- return .timeout;
- for (w.os.poll_fds.values()) |poll_fd| {
- if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try markDirtySteps(w, gpa, poll_fd.fd))
- return .dirty;
- }
- return .clean;
- }
- },
- .windows => struct {
- const windows = std.os.windows;
-
- /// Keyed differently but indexes correspond 1:1 with `dir_table`.
- handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false),
- ready_dirs: std.DoublyLinkedList,
-
- const FileId = struct {
- volumeSerialNumber: windows.ULONG,
- indexNumber: windows.LARGE_INTEGER,
- };
-
- const Directory = struct {
- reaction_set: ReactionSet,
- id: FileId,
- file: Io.File,
- state: enum { idle, listening, ready },
- iosb: windows.IO_STATUS_BLOCK,
- // 64 KB is the packet size limit when monitoring over a network.
- // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks
- buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)),
- ready_node: std.DoublyLinkedList.Node,
-
- /// Start listening for events, buffer field will be overwritten eventually.
- fn startListening(dir: *Directory, w: *Watch) !void {
- assert(dir.file.flags.nonblocking);
- assert(dir.state == .idle);
- switch (windows.ntdll.NtNotifyChangeDirectoryFileEx(
- dir.file.handle,
- null,
- ¬ifyApc,
- w,
- &dir.iosb,
- &dir.buffer,
- dir.buffer.len,
- .{
- .FILE_NAME = true,
- .DIR_NAME = true,
- .SIZE = true,
- .LAST_WRITE = true,
- .CREATION = true,
- },
- .FALSE,
- .Notify,
- )) {
- .SUCCESS, .PENDING => dir.state = .listening,
- .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported,
- else => |status| return windows.unexpectedStatus(status),
- }
- }
-
- fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void {
- const w: *Watch = @ptrCast(@alignCast(apc_context));
- const dir: *Directory = @fieldParentPtr("iosb", iosb);
- assert(iosb.u.Status != .PENDING);
- assert(dir.state == .listening);
- w.os.ready_dirs.append(&dir.ready_node);
- dir.state = .ready;
- }
-
- fn init(gpa: Allocator, path: Cache.Path) !*Directory {
- // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW)
- // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW.
- var dir_handle: windows.HANDLE = undefined;
- const root_fd = path.root_dir.handle.handle;
- const sub_path = path.subPathOrDot();
- const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call
- var iosb: windows.IO_STATUS_BLOCK = undefined;
- switch (windows.ntdll.NtCreateFile(
- &dir_handle,
- .{
- .SPECIFIC = .{ .FILE_DIRECTORY = .{
- .LIST = true,
- } },
- .STANDARD = .{ .SYNCHRONIZE = true },
- .GENERIC = .{ .READ = true },
- },
- &.{
- .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd,
- .ObjectName = @constCast(&sub_path_w.string()),
- },
- &iosb,
- null,
- .{},
- .VALID_FLAGS,
- .OPEN,
- .{
- .DIRECTORY_FILE = true,
- .IO = .ASYNCHRONOUS,
- .OPEN_FOR_BACKUP_INTENT = true,
- },
- null,
- 0,
- )) {
- .SUCCESS => {},
- .OBJECT_NAME_INVALID => return error.BadPathName,
- .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
- .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
- .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
- .NOT_A_DIRECTORY => return error.NotDir,
- // This can happen if the directory has 'List folder contents' permission set to 'Deny'
- .ACCESS_DENIED => return error.AccessDenied,
- .INVALID_PARAMETER => unreachable,
- else => |rc| return windows.unexpectedStatus(rc),
- }
- assert(dir_handle != windows.INVALID_HANDLE_VALUE);
- errdefer windows.CloseHandle(dir_handle);
-
- const dir_id = try getFileId(dir_handle);
-
- const dir = try gpa.create(Directory);
- dir.* = .{
- .reaction_set = .empty,
- .id = dir_id,
- .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } },
- .state = .idle,
- .iosb = undefined,
- .buffer = undefined,
- .ready_node = undefined,
- };
- return dir;
- }
-
- fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void {
- state: switch (dir.state) {
- .idle => {},
- .listening => {
- var cancel_iosb: windows.IO_STATUS_BLOCK = undefined;
- _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb);
- while (switch (dir.state) {
- .idle => unreachable,
- .listening => true,
- .ready => false,
- }) Io.Threaded.waitForApcOrAlert();
- continue :state .ready;
- },
- .ready => w.os.ready_dirs.remove(&dir.ready_node),
- }
- windows.CloseHandle(dir.file.handle);
- gpa.destroy(dir);
- }
-
- /// Useful to make `*Directory` a key in `std.ArrayHashMap`.
- const TableAdapter = struct {
- pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 {
- return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber)));
- }
- pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool {
- _ = rhs_index;
- return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and
- lhs_dir.id.indexNumber == rhs_dir.id.indexNumber;
- }
- };
- };
-
- fn init(cwd_path: []const u8) !Watch {
- _ = cwd_path;
- return .{
- .dir_table = .{},
- .dir_count = 0,
- .os = switch (builtin.os.tag) {
- .windows => .{
- .handle_table = .empty,
- .ready_dirs = .{},
- },
- else => {},
- },
- .generation = 0,
- };
- }
-
- fn getFileId(handle: windows.HANDLE) !FileId {
- var file_id: FileId = undefined;
- var io_status: windows.IO_STATUS_BLOCK = undefined;
- var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined;
- switch (windows.ntdll.NtQueryVolumeInformationFile(
- handle,
- &io_status,
- &volume_info,
- @sizeOf(windows.FILE.FS_VOLUME_INFORMATION),
- .Volume,
- )) {
- .SUCCESS => {},
- // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
- // size provided. This is treated as success because the type of variable-length information that this would be relevant for
- // (name, volume name, etc) we don't care about.
- .BUFFER_OVERFLOW => {},
- else => |rc| return windows.unexpectedStatus(rc),
- }
- file_id.volumeSerialNumber = volume_info.VolumeSerialNumber;
- var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined;
- switch (windows.ntdll.NtQueryInformationFile(
- handle,
- &io_status,
- &internal_info,
- @sizeOf(windows.FILE.INTERNAL_INFORMATION),
- .Internal,
- )) {
- .SUCCESS => {},
- else => |rc| return windows.unexpectedStatus(rc),
- }
- file_id.indexNumber = internal_info.IndexNumber;
- return file_id;
- }
-
- fn markDirtySteps(w: *Watch, gpa: Allocator, dir: *Directory) !bool {
- var any_dirty = false;
- const bytes_returned = dir.iosb.Information;
- if (bytes_returned == 0) {
- std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
- markAllFilesDirty(w, gpa);
- try dir.startListening(w);
- return true;
- }
- var file_name_buf: [std.fs.max_path_bytes]u8 = undefined;
- var offset: usize = 0;
- while (true) {
- const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset]));
- const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())];
- if (dir.reaction_set.getPtr(".")) |glob_set|
- any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
- if (dir.reaction_set.getPtr(file_name)) |step_set|
- any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
- if (notify.NextEntryOffset == 0)
- break;
-
- offset += notify.NextEntryOffset;
- }
-
- // We call this now since at this point we have finished reading dir.buffer.
- try dir.startListening(w);
- return any_dirty;
- }
-
- fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
- // Add missing marks and note persisted ones.
- for (steps) |step| {
- for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
- const dir = dir: {
- const gop = try w.dir_table.getOrPut(gpa, path);
- if (!gop.found_existing) {
- const dir: *Directory = try .init(gpa, path);
- errdefer dir.deinit(gpa, w);
- // `dir.id` may already be present in the table in
- // the case that we have multiple Cache.Path instances
- // that compare inequal but ultimately point to the same
- // directory on the file system.
- // In such case, we must revert adding this directory, but keep
- // the additions to the step set.
- const dh_gop = try w.os.handle_table.getOrPut(gpa, dir);
- if (dh_gop.found_existing) {
- dir.deinit(gpa, w);
- _ = w.dir_table.pop();
- break :dir w.os.handle_table.keys()[dh_gop.index];
- } else {
- assert(dh_gop.index == gop.index);
- try dir.startListening(w);
- break :dir dir;
- }
- }
- break :dir w.os.handle_table.keys()[gop.index];
- };
- for (files.items) |basename| {
- const gop = try dir.reaction_set.getOrPut(gpa, basename);
- if (!gop.found_existing) gop.value_ptr.* = .{};
- try gop.value_ptr.put(gpa, step, w.generation);
- }
- }
- }
-
- {
- // Remove marks for files that are no longer inputs.
- var i: usize = 0;
- while (i < w.os.handle_table.entries.len) {
- const dir = w.os.handle_table.keys()[i];
- {
- var step_set_i: usize = 0;
- while (step_set_i < dir.reaction_set.entries.len) {
- const step_set = &dir.reaction_set.values()[step_set_i];
- var dirent_i: usize = 0;
- while (dirent_i < step_set.entries.len) {
- const generations = step_set.values();
- if (generations[dirent_i] == w.generation) {
- dirent_i += 1;
- continue;
- }
- step_set.swapRemoveAt(dirent_i);
- }
- if (step_set.entries.len > 0) {
- step_set_i += 1;
- continue;
- }
- dir.reaction_set.swapRemoveAt(step_set_i);
- }
- if (dir.reaction_set.entries.len > 0) {
- i += 1;
- continue;
- }
- }
-
- w.dir_table.swapRemoveAt(i);
- w.os.handle_table.swapRemoveAt(i);
- dir.deinit(gpa, w);
- }
- w.generation +%= 1;
- }
- w.dir_count = w.dir_table.count();
- }
-
- fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
- for (0..2) |attempt| {
- while (w.os.ready_dirs.popFirst()) |ready_node| {
- const dir: *Directory = @fieldParentPtr("ready_node", ready_node);
- assert(dir.state == .ready);
- dir.state = .idle;
- switch (dir.iosb.u.Status) {
- .SUCCESS => return if (try markDirtySteps(w, gpa, dir)) .dirty else .clean,
- .PENDING => unreachable,
- .CANCELLED => {},
- else => |status| return windows.unexpectedStatus(status),
- }
- try dir.startListening(w);
- }
- try io.checkCancel();
- if (attempt == 1) return .timeout;
- const delay_interval: windows.LARGE_INTEGER = switch (timeout) {
- .none => std.math.minInt(windows.LARGE_INTEGER),
- .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100),
- };
- _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval);
- } else unreachable;
- }
- },
- .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct {
- const posix = std.posix;
-
- kq_fd: i32,
- /// Indexes correspond 1:1 with `dir_table`.
- handles: std.MultiArrayList(struct {
- rs: ReactionSet,
- /// If the corresponding dir_table Path has sub_path == "", then it
- /// suffices as the open directory handle, and this value will be
- /// -1. Otherwise, it needs to be opened in update(), and will be
- /// stored here.
- dir_fd: i32,
- }),
-
- const dir_open_flags: posix.O = f: {
- var f: posix.O = .{
- .ACCMODE = .RDONLY,
- .NOFOLLOW = false,
- .DIRECTORY = true,
- .CLOEXEC = true,
- };
- if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true;
- if (@hasField(posix.O, "PATH")) f.PATH = true;
- break :f f;
- };
-
- const EV = std.c.EV;
- const NOTE = std.c.NOTE;
-
- fn init(cwd_path: []const u8) !Watch {
- _ = cwd_path;
- return .{
- .dir_table = .{},
- .dir_count = 0,
- .os = .{
- .kq_fd = try Io.Kqueue.createFileDescriptor(),
- .handles = .empty,
- },
- .generation = 0,
- };
- }
-
- fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
- const handles = &w.os.handles;
- for (steps) |step| {
- for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
- const reaction_set = rs: {
- const gop = try w.dir_table.getOrPut(gpa, path);
- if (!gop.found_existing) {
- const skip_open_dir = path.sub_path.len == 0;
- const dir_fd = if (skip_open_dir)
- path.root_dir.handle.handle
- else
- posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| {
- fatal("failed to open directory {f}: {t}", .{ path, err });
- };
- // Empirically the dir has to stay open or else no events are triggered.
- errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd);
- const changes = [1]posix.Kevent{.{
- .ident = @bitCast(@as(isize, dir_fd)),
- .filter = std.c.EVFILT.VNODE,
- .flags = EV.ADD | EV.ENABLE | EV.CLEAR,
- .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
- .data = 0,
- .udata = gop.index,
- }};
- _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null);
- assert(handles.len == gop.index);
- try handles.append(gpa, .{
- .rs = .{},
- .dir_fd = if (skip_open_dir) -1 else dir_fd,
- });
- }
-
- break :rs &handles.items(.rs)[gop.index];
- };
- for (files.items) |basename| {
- const gop = try reaction_set.getOrPut(gpa, basename);
- if (!gop.found_existing) gop.value_ptr.* = .{};
- try gop.value_ptr.put(gpa, step, w.generation);
- }
- }
- }
-
- {
- // Remove marks for files that are no longer inputs.
- var i: usize = 0;
- while (i < handles.len) {
- {
- const reaction_set = &handles.items(.rs)[i];
- var step_set_i: usize = 0;
- while (step_set_i < reaction_set.entries.len) {
- const step_set = &reaction_set.values()[step_set_i];
- var dirent_i: usize = 0;
- while (dirent_i < step_set.entries.len) {
- const generations = step_set.values();
- if (generations[dirent_i] == w.generation) {
- dirent_i += 1;
- continue;
- }
- step_set.swapRemoveAt(dirent_i);
- }
- if (step_set.entries.len > 0) {
- step_set_i += 1;
- continue;
- }
- reaction_set.swapRemoveAt(step_set_i);
- }
- if (reaction_set.entries.len > 0) {
- i += 1;
- continue;
- }
- }
-
- // If the sub_path == "" then this patch has already the
- // dir fd that we need to use as the ident to remove the
- // event. If it was opened above with openat() then we need
- // to access that data via the dir_fd field.
- const path = w.dir_table.keys()[i];
- const dir_fd = if (path.sub_path.len == 0)
- path.root_dir.handle.handle
- else
- handles.items(.dir_fd)[i];
- assert(dir_fd != -1);
-
- // The changelist also needs to update the udata field of the last
- // event, since we are doing a swap remove, and we store the dir_table
- // index in the udata field.
- const last_dir_fd = fd: {
- const last_path = w.dir_table.keys()[handles.len - 1];
- const last_dir_fd = if (last_path.sub_path.len == 0)
- last_path.root_dir.handle.handle
- else
- handles.items(.dir_fd)[handles.len - 1];
- assert(last_dir_fd != -1);
- break :fd last_dir_fd;
- };
- const changes = [_]posix.Kevent{
- .{
- .ident = @bitCast(@as(isize, dir_fd)),
- .filter = std.c.EVFILT.VNODE,
- .flags = EV.DELETE,
- .fflags = 0,
- .data = 0,
- .udata = i,
- },
- .{
- .ident = @bitCast(@as(isize, last_dir_fd)),
- .filter = std.c.EVFILT.VNODE,
- .flags = EV.ADD,
- .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
- .data = 0,
- .udata = i,
- },
- };
- const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes;
- _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null);
- if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd);
-
- w.dir_table.swapRemoveAt(i);
- handles.swapRemove(i);
- }
- w.generation +%= 1;
- }
- w.dir_count = w.dir_table.count();
- }
-
- fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
- _ = io;
- var timespec_buffer: posix.timespec = undefined;
- var event_buffer: [100]posix.Kevent = undefined;
- var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(×pec_buffer));
- if (n == 0) return .timeout;
- const reaction_sets = w.os.handles.items(.rs);
- var any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], false);
- timespec_buffer = .{ .sec = 0, .nsec = 0 };
- while (n == event_buffer.len) {
- n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, ×pec_buffer);
- if (n == 0) break;
- any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], any_dirty);
- }
- return if (any_dirty) .dirty else .clean;
- }
-
- fn markDirtySteps(
- gpa: Allocator,
- reaction_sets: []ReactionSet,
- events: []const std.c.Kevent,
- start_any_dirty: bool,
- ) bool {
- var any_dirty = start_any_dirty;
- for (events) |event| {
- const index: usize = @intCast(event.udata);
- const reaction_set = &reaction_sets[index];
- // If we knew the basename of the changed file, here we would
- // mark only the step set dirty, and possibly the glob set:
- //if (reaction_set.getPtr(".")) |glob_set|
- // any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
- //if (reaction_set.getPtr(file_name)) |step_set|
- // any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
- // However we don't know the file name so just mark all the
- // sets dirty for this directory.
- for (reaction_set.values()) |*step_set| {
- any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
- }
- }
- return any_dirty;
- }
- },
- .macos => struct {
- fse: FsEvents,
-
- fn init(cwd_path: []const u8) !Watch {
- return .{
- .os = .{ .fse = try .init(cwd_path) },
- .dir_count = 0,
- .dir_table = undefined,
- .generation = undefined,
- };
- }
- fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
- try w.os.fse.setPaths(gpa, steps);
- w.dir_count = w.os.fse.watch_roots.len;
- }
- fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
- _ = io;
- return w.os.fse.wait(gpa, switch (timeout) {
- .none => null,
- .ms => |ms| @as(u64, ms) * std.time.ns_per_ms,
- });
- }
- },
- else => void,
-};
-
-pub fn init(cwd_path: []const u8) !Watch {
- return Os.init(cwd_path);
-}
-
-pub const Match = struct {
- /// Relative to the watched directory, the file path that triggers this
- /// match.
- basename: []const u8,
- /// The step to re-run when file corresponding to `basename` is changed.
- step: *Step,
-
- pub const Context = struct {
- pub fn hash(self: Context, a: Match) u32 {
- _ = self;
- var hasher = Hash.init(0);
- std.hash.autoHash(&hasher, a.step);
- hasher.update(a.basename);
- return @truncate(hasher.final());
- }
- pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool {
- _ = self;
- _ = b_index;
- return a.step == b.step and std.mem.eql(u8, a.basename, b.basename);
- }
- };
-};
-
-fn markAllFilesDirty(w: *Watch, gpa: Allocator) void {
- for (switch (builtin.os.tag) {
- .windows => w.os.handle_table.keys(),
- else => w.os.handle_table.values(),
- }) |item| {
- const reaction_set = switch (builtin.os.tag) {
- .linux, .windows => item.reaction_set,
- else => item,
- };
- for (reaction_set.values()) |step_set| {
- for (step_set.keys()) |step| {
- _ = step.invalidateResult(gpa);
- }
- }
- }
-}
-
-fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
- var this_any_dirty = false;
- for (step_set.keys()) |step| {
- if (step.invalidateResult(gpa)) this_any_dirty = true;
- }
- return any_dirty or this_any_dirty;
-}
-
-pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
- return Os.update(w, gpa, steps);
-}
-
-pub const Timeout = union(enum) {
- none,
- ms: u16,
-
- pub fn to_i32_ms(t: Timeout) i32 {
- return switch (t) {
- .none => -1,
- .ms => |ms| ms,
- };
- }
-
- pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec {
- return switch (t) {
- .none => null,
- .ms => |ms_u16| {
- const ms: isize = ms_u16;
- buf.* = .{
- .sec = @divTrunc(ms, std.time.ms_per_s),
- .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms,
- };
- return buf;
- },
- };
- }
-};
-
-pub const WaitResult = enum {
- timeout,
- /// File system watching triggered on files that were marked as inputs to at least one Step.
- /// Relevant steps have been marked dirty.
- dirty,
- /// File system watching triggered but none of the events were relevant to
- /// what we are listening to. There is nothing to do.
- clean,
-};
-
-pub fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
- return Os.wait(w, gpa, io, timeout);
-}
diff --git a/lib/std/Build/Watch/FsEvents.zig b/lib/std/Build/Watch/FsEvents.zig
@@ -1,479 +0,0 @@
-//! An implementation of file-system watching based on the `FSEventStream` API in macOS.
-//! While macOS supports kqueue, it does not allow detecting changes to files without
-//! placing watches on each individual file, meaning FD limits are reached incredibly
-//! quickly. The File System Events API works differently: it implements *recursive*
-//! directory watches, managed by a system service. Rather than being in libc, the API is
-//! exposed by the CoreServices framework. To avoid a compile dependency on the framework
-//! bundle, we dynamically load CoreServices with `std.DynLib`.
-//!
-//! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been
-//! made to keep that specialization to a minimum. Other use cases could be served with
-//! relatively minimal modifications to the `watch_paths` field and its usages (in
-//! particular the `setPaths` function). We avoid using the global GCD dispatch queue in
-//! favour of creating our own and synchronizing with an explicit semaphore, meaning this
-//! logic is thread-safe and does not affect process-global state.
-//!
-//! In theory, this API is quite good at avoiding filesystem race conditions. In practice,
-//! the logic that would avoid them is currently disabled, because the build system kind
-//! of relies on them at the time of writing to avoid redundant work -- see the comment at
-//! the top of `wait` for details.
-
-const enable_debug_logs = false;
-
-core_services: std.DynLib,
-resolved_symbols: ResolvedSymbols,
-
-paths_arena: std.heap.ArenaAllocator.State,
-/// The roots of the recursive watches. FSEvents has relatively small limits on the number
-/// of watched paths, so this slice must not be too long. The paths themselves are allocated
-/// into `paths_arena`, but this slice is allocated into the GPA.
-watch_roots: [][:0]const u8,
-/// All of the paths being watched. Value is the set of steps which depend on the file/directory.
-/// Keys and values are in `paths_arena`, but this map is allocated into the GPA.
-watch_paths: std.StringArrayHashMapUnmanaged([]const *std.Build.Step),
-
-/// The semaphore we use to block the thread calling `wait` until the callback determines a relevant
-/// event has occurred. This is retained across `wait` calls for simplicity and efficiency.
-waiting_semaphore: dispatch.semaphore_t,
-/// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the
-/// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained
-/// across `wait` calls for simplicity and efficiency.
-dispatch_queue: dispatch.queue_t,
-/// In theory, this field avoids race conditions. In practice, it is essentially unused at the time
-/// of writing. See the comment at the start of `wait` for details.
-since_event: FSEventStreamEventId,
-
-cwd_path: []const u8,
-
-/// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols
-/// is not present, `init` will close the framework and return an error.
-const ResolvedSymbols = struct {
- FSEventStreamCreate: *const fn (
- allocator: CFAllocatorRef,
- callback: FSEventStreamCallback,
- ctx: ?*const FSEventStreamContext,
- paths_to_watch: CFArrayRef,
- since_when: FSEventStreamEventId,
- latency: CFTimeInterval,
- flags: FSEventStreamCreateFlags,
- ) callconv(.c) FSEventStreamRef,
- FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void,
- FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool,
- FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void,
- FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void,
- FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void,
- FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId,
- FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId,
- CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void,
- CFArrayCreate: *const fn (
- allocator: CFAllocatorRef,
- values: [*]const usize,
- num_values: CFIndex,
- call_backs: ?*const CFArrayCallBacks,
- ) callconv(.c) CFArrayRef,
- CFStringCreateWithCString: *const fn (
- alloc: CFAllocatorRef,
- c_str: [*:0]const u8,
- encoding: CFStringEncoding,
- ) callconv(.c) CFStringRef,
- CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef,
- kCFAllocatorUseContext: *const CFAllocatorRef,
-};
-
-pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents {
- var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch
- return error.OpenFrameworkFailed;
- errdefer core_services.close();
-
- var resolved_symbols: ResolvedSymbols = undefined;
- inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| {
- @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol;
- }
-
- return .{
- .core_services = core_services,
- .resolved_symbols = resolved_symbols,
- .paths_arena = .{},
- .watch_roots = &.{},
- .watch_paths = .empty,
- .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources,
- .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources,
- // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order
- // to notice any changes which happened during said work.
- .since_event = resolved_symbols.FSEventsGetCurrentEventId(),
- .cwd_path = cwd_path,
- };
-}
-
-pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void {
- fse.waiting_semaphore.as_object().release();
- fse.dispatch_queue.as_object().release();
- fse.core_services.close(io);
-
- gpa.free(fse.watch_roots);
- fse.watch_paths.deinit(gpa);
- {
- var paths_arena = fse.paths_arena.promote(gpa);
- paths_arena.deinit();
- }
-}
-
-pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) !void {
- var paths_arena_instance = fse.paths_arena.promote(gpa);
- defer fse.paths_arena = paths_arena_instance.state;
- const paths_arena = paths_arena_instance.allocator();
-
- var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty;
- defer need_dirs.deinit(gpa);
-
- fse.watch_paths.clearRetainingCapacity();
-
- // We take `step` by pointer for a slight memory optimization in a moment.
- for (steps) |*step| {
- for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| {
- const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{
- fse.cwd_path, path.root_dir.path orelse ".", path.sub_path,
- });
- try need_dirs.put(gpa, resolved_dir, {});
- for (files.items) |file_name| {
- const watch_path = if (std.mem.eql(u8, file_name, "."))
- resolved_dir
- else
- try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name });
- const gop = try fse.watch_paths.getOrPut(gpa, watch_path);
- if (gop.found_existing) {
- const old_steps = gop.value_ptr.*;
- const new_steps = try paths_arena.alloc(*std.Build.Step, old_steps.len + 1);
- @memcpy(new_steps[0..old_steps.len], old_steps);
- new_steps[old_steps.len] = step.*;
- gop.value_ptr.* = new_steps;
- } else {
- // This is why we captured `step` by pointer! We can avoid allocating a slice of one
- // step in the arena in the common case where a file is referenced by only one step.
- gop.value_ptr.* = step[0..1];
- }
- }
- }
- }
-
- {
- // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar").
- // To eliminate these, we'll re-add directories in order of path length with a redundancy check.
- const old_dirs = try gpa.dupe([]const u8, need_dirs.keys());
- defer gpa.free(old_dirs);
- std.mem.sort([]const u8, old_dirs, {}, struct {
- fn lessThan(ctx: void, a: []const u8, b: []const u8) bool {
- ctx;
- return std.mem.lessThan(u8, a, b);
- }
- }.lessThan);
- need_dirs.clearRetainingCapacity();
- for (old_dirs) |dir_path| {
- var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
- while (it.next()) |component| {
- if (need_dirs.contains(component.path)) {
- // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added
- break;
- }
- } else {
- need_dirs.putAssumeCapacityNoClobber(dir_path, {});
- }
- }
- }
-
- // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very
- // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/`
- // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit
- // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be
- // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below*
- // that known limit.
- if (need_dirs.count() > 2048) {
- // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P
- if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{});
- fse.watch_roots = try gpa.realloc(fse.watch_roots, 1);
- fse.watch_roots[0] = "/";
- } else {
- fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count());
- for (fse.watch_roots, need_dirs.keys()) |*out, in| {
- out.* = try paths_arena.dupeSentinel(u8, in, 0);
- }
- }
- if (enable_debug_logs) {
- watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len });
- for (fse.watch_roots) |dir_path| {
- watch_log.debug("- '{s}'", .{dir_path});
- }
- }
-}
-
-pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!std.Build.Watch.WaitResult {
- if (fse.watch_roots.len == 0) @panic("nothing to watch");
-
- const rs = fse.resolved_symbols;
-
- // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds
- // to occur, because one step modifies a file which is an input to another step. The solution
- // to this problem will probably be either:
- //
- // a) Don't include the output of one step as a watch input of another; only mark external
- // files as watch inputs. Or...
- //
- // b) Note the current event ID when a step begins, and disregard events preceding that ID
- // when considering whether to dirty that step in `eventCallback`.
- //
- // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does
- // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those
- // too at the time of writing, so this is kind of expected.
- fse.since_event = .since_now;
-
- const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{
- .version = 0,
- .info = @constCast(&gpa),
- .retain = null,
- .release = null,
- .copy_description = null,
- .allocate = &cf_alloc_callbacks.allocate,
- .reallocate = &cf_alloc_callbacks.reallocate,
- .deallocate = &cf_alloc_callbacks.deallocate,
- .preferred_size = null,
- }) orelse return error.OutOfMemory;
- defer rs.CFRelease(cf_allocator);
-
- const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len);
- @memset(cf_paths, null);
- defer {
- for (cf_paths) |o| if (o) |p| rs.CFRelease(p);
- gpa.free(cf_paths);
- }
- for (fse.watch_roots, cf_paths) |raw_path, *cf_path| {
- cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8);
- }
- const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null);
- defer rs.CFRelease(cf_paths_array);
-
- const callback_ctx: EventCallbackCtx = .{
- .fse = fse,
- .gpa = gpa,
- };
- const event_stream = rs.FSEventStreamCreate(
- null,
- &eventCallback,
- &.{
- .version = 0,
- .info = @constCast(&callback_ctx),
- .retain = null,
- .release = null,
- .copy_description = null,
- },
- cf_paths_array,
- fse.since_event,
- 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events
- .{ .watch_root = true, .file_events = true },
- );
- defer rs.FSEventStreamRelease(event_stream);
- rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue);
- defer rs.FSEventStreamInvalidate(event_stream);
- if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed;
- defer rs.FSEventStreamStop(event_stream);
- const result = fse.waiting_semaphore.wait(timeout: {
- const ns = timeout_ns orelse break :timeout .FOREVER;
- break :timeout .time(.NOW, @intCast(ns));
- });
- return switch (result) {
- 0 => .dirty,
- else => .timeout,
- };
-}
-
-const cf_alloc_callbacks = struct {
- const log = std.log.scoped(.cf_alloc);
- fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
- if (enable_debug_logs) log.debug("allocate {d}", .{size});
- _ = hint;
- const gpa: *const Allocator = @ptrCast(@alignCast(info));
- const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null;
- const metadata: *usize = @ptrCast(mem);
- metadata.* = @intCast(size);
- return mem[@sizeOf(usize)..].ptr;
- }
- fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
- if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size });
- _ = hint;
- if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL
- const gpa: *const Allocator = @ptrCast(@alignCast(info));
- const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
- const old_size = @as(*const usize, @ptrCast(old_base)).*;
- const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
- const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null;
- const metadata: *usize = @ptrCast(new_mem);
- metadata.* = @intCast(new_size);
- return new_mem[@sizeOf(usize)..].ptr;
- }
- fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void {
- if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr});
- const gpa: *const Allocator = @ptrCast(@alignCast(info));
- const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
- const old_size = @as(*const usize, @ptrCast(old_base)).*;
- const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
- gpa.free(old_mem);
- }
-};
-
-const EventCallbackCtx = struct {
- fse: *FsEvents,
- gpa: Allocator,
-};
-
-fn eventCallback(
- stream: ConstFSEventStreamRef,
- client_callback_info: ?*anyopaque,
- num_events: usize,
- events_paths_ptr: *anyopaque,
- events_flags_ptr: [*]const FSEventStreamEventFlags,
- events_ids_ptr: [*]const FSEventStreamEventId,
-) callconv(.c) void {
- const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info));
- const fse = ctx.fse;
- const gpa = ctx.gpa;
- const rs = fse.resolved_symbols;
- const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr));
- const events_paths = events_paths_ptr_casted[0..num_events];
- const events_ids = events_ids_ptr[0..num_events];
- const events_flags = events_flags_ptr[0..num_events];
- var any_dirty = false;
- for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| {
- _ = event_id;
- if (event_flags.history_done) continue; // sentinel
- const event_path = std.mem.span(event_path_nts);
- switch (event_flags.must_scan_sub_dirs) {
- false => {
- if (fse.watch_paths.get(event_path)) |steps| {
- assert(steps.len > 0);
- for (steps) |s| {
- if (s.invalidateResult(gpa)) any_dirty = true;
- }
- }
- if (std.fs.path.dirname(event_path)) |event_dirname| {
- // Modifying '/foo/bar' triggers the watch on '/foo'.
- if (fse.watch_paths.get(event_dirname)) |steps| {
- assert(steps.len > 0);
- for (steps) |s| {
- if (s.invalidateResult(gpa)) any_dirty = true;
- }
- }
- }
- },
- true => {
- // This is unlikely, but can occasionally happen when bottlenecked: events have been
- // coalesced into one. We want to see if any of these events are actually relevant
- // to us. The only way we can reasonably do that in this rare edge case is iterate
- // the watch paths and see if any is under this directory. That's acceptable because
- // we would otherwise kick off a rebuild which would be clearing those paths anyway.
- const changed_path = std.fs.path.dirname(event_path) orelse event_path;
- for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| {
- if (dirStartsWith(watching_path, changed_path)) {
- for (steps) |s| {
- if (s.invalidateResult(gpa)) any_dirty = true;
- }
- }
- }
- },
- }
- }
- if (any_dirty) {
- fse.since_event = rs.FSEventStreamGetLatestEventId(stream);
- _ = fse.waiting_semaphore.signal();
- }
-}
-fn dirStartsWith(path: []const u8, prefix: []const u8) bool {
- if (std.mem.eql(u8, path, prefix)) return true;
- if (!std.mem.startsWith(u8, path, prefix)) return false;
- if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar`
- return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar`
-}
-
-const CFAllocatorRef = ?*const opaque {};
-const CFArrayRef = *const opaque {};
-const CFStringRef = *const opaque {};
-const CFTimeInterval = f64;
-const CFIndex = i32;
-const CFOptionFlags = enum(u32) { _ };
-const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque;
-const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void;
-const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef;
-const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
-const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
-const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void;
-const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex;
-const CFAllocatorContext = extern struct {
- version: CFIndex,
- info: ?*anyopaque,
- retain: ?CFAllocatorRetainCallBack,
- release: ?CFAllocatorReleaseCallBack,
- copy_description: ?CFAllocatorCopyDescriptionCallBack,
- allocate: CFAllocatorAllocateCallBack,
- reallocate: ?CFAllocatorReallocateCallBack,
- deallocate: ?CFAllocatorDeallocateCallBack,
- preferred_size: ?CFAllocatorPreferredSizeCallBack,
-};
-const CFArrayCallBacks = opaque {};
-const CFStringEncoding = enum(u32) {
- invalid_id = std.math.maxInt(u32),
- mac_roman = 0,
- windows_latin_1 = 0x500,
- iso_latin_1 = 0x201,
- next_step_latin = 0xB01,
- ascii = 0x600,
- unicode = 0x100,
- utf8 = 0x8000100,
- non_lossy_ascii = 0xBFF,
-};
-
-const FSEventStreamRef = *opaque {};
-const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child;
-const FSEventStreamCallback = *const fn (
- stream: ConstFSEventStreamRef,
- client_callback_info: ?*anyopaque,
- num_events: usize,
- event_paths: *anyopaque,
- event_flags: [*]const FSEventStreamEventFlags,
- event_ids: [*]const FSEventStreamEventId,
-) callconv(.c) void;
-const FSEventStreamContext = extern struct {
- version: CFIndex,
- info: ?*anyopaque,
- retain: ?CFAllocatorRetainCallBack,
- release: ?CFAllocatorReleaseCallBack,
- copy_description: ?CFAllocatorCopyDescriptionCallBack,
-};
-const FSEventStreamEventId = enum(u64) {
- since_now = std.math.maxInt(u64),
- _,
-};
-const FSEventStreamCreateFlags = packed struct(u32) {
- use_cf_types: bool = false,
- no_defer: bool = false,
- watch_root: bool = false,
- ignore_self: bool = false,
- file_events: bool = false,
- _: u27 = 0,
-};
-const FSEventStreamEventFlags = packed struct(u32) {
- must_scan_sub_dirs: bool,
- user_dropped: bool,
- kernel_dropped: bool,
- event_ids_wrapped: bool,
- history_done: bool,
- root_changed: bool,
- mount: bool,
- unmount: bool,
- _: u24 = 0,
-};
-
-const dispatch = std.c.dispatch;
-const std = @import("std");
-const Io = std.Io;
-const assert = std.debug.assert;
-const Allocator = std.mem.Allocator;
-const watch_log = std.log.scoped(.watch);
-const FsEvents = @This();
diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig
@@ -1,926 +0,0 @@
-gpa: Allocator,
-graph: *const Build.Graph,
-all_steps: []const *Build.Step,
-listen_address: net.IpAddress,
-root_prog_node: std.Progress.Node,
-watch: bool,
-
-tcp_server: ?net.Server,
-serve_task: ?Io.Future(Io.Cancelable!void),
-
-/// Uses `Io.Clock.awake`.
-base_timestamp: Io.Timestamp,
-/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
-step_names_trailing: []u8,
-
-/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
-/// Accessed atomically.
-step_status_bits: []u8,
-
-fuzz: ?Fuzz,
-time_report_mutex: Io.Mutex,
-time_report_msgs: [][]u8,
-time_report_update_times: []i64,
-
-build_status: std.atomic.Value(abi.BuildStatus),
-/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
-/// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so
-/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it
-/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For
-/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes,
-/// because this value changes quickly so this would result in constantly spamming all clients with
-/// an unreasonable number of packets.
-update_id: std.atomic.Value(u32),
-
-runner_request_mutex: Io.Mutex,
-runner_request_ready_cond: Io.Condition,
-runner_request_empty_cond: Io.Condition,
-runner_request: ?RunnerRequest,
-
-/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
-/// on a fixed interval of this many milliseconds.
-const default_update_interval_ms = 500;
-
-pub const base_clock: Io.Clock = .awake;
-
-/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
-pub fn notifyUpdate(ws: *WebServer) void {
- _ = ws.update_id.rmw(.Add, 1, .release);
- ws.graph.io.futexWake(u32, &ws.update_id.raw, 16);
-}
-
-pub const Options = struct {
- gpa: Allocator,
- graph: *const std.Build.Graph,
- all_steps: []const *Build.Step,
- root_prog_node: std.Progress.Node,
- watch: bool,
- listen_address: net.IpAddress,
- base_timestamp: Io.Clock.Timestamp,
-};
-pub fn init(opts: Options) WebServer {
- // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent`
- // instead of threads, so that the web server can function in single-threaded builds.
- comptime assert(!builtin.single_threaded);
- assert(opts.base_timestamp.clock == base_clock);
-
- const all_steps = opts.all_steps;
-
- const step_names_trailing = opts.gpa.alloc(u8, len: {
- var name_bytes: usize = 0;
- for (all_steps) |step| name_bytes += step.name.len;
- break :len name_bytes + all_steps.len * 4;
- }) catch @panic("out of memory");
- {
- const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
- var idx: usize = all_steps.len * 4;
- for (all_steps, step_name_lens) |step, *name_len| {
- name_len.* = @intCast(step.name.len);
- @memcpy(step_names_trailing[idx..][0..step.name.len], step.name);
- idx += step.name.len;
- }
- assert(idx == step_names_trailing.len);
- }
-
- const step_status_bits = opts.gpa.alloc(
- u8,
- std.math.divCeil(usize, all_steps.len, 4) catch unreachable,
- ) catch @panic("out of memory");
- @memset(step_status_bits, 0);
-
- const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0;
- const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory");
- const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory");
- @memset(time_report_msgs, &.{});
- @memset(time_report_update_times, std.math.minInt(i64));
-
- return .{
- .gpa = opts.gpa,
- .graph = opts.graph,
- .all_steps = all_steps,
- .listen_address = opts.listen_address,
- .root_prog_node = opts.root_prog_node,
- .watch = opts.watch,
-
- .tcp_server = null,
- .serve_task = null,
-
- .base_timestamp = opts.base_timestamp.raw,
- .step_names_trailing = step_names_trailing,
-
- .step_status_bits = step_status_bits,
-
- .fuzz = null,
- .time_report_mutex = .init,
- .time_report_msgs = time_report_msgs,
- .time_report_update_times = time_report_update_times,
-
- .build_status = .init(.idle),
- .update_id = .init(0),
-
- .runner_request_mutex = .init,
- .runner_request_ready_cond = .init,
- .runner_request_empty_cond = .init,
- .runner_request = null,
- };
-}
-pub fn deinit(ws: *WebServer) void {
- const gpa = ws.gpa;
- const io = ws.graph.io;
-
- gpa.free(ws.step_names_trailing);
- gpa.free(ws.step_status_bits);
-
- if (ws.fuzz) |*f| f.deinit();
- for (ws.time_report_msgs) |msg| gpa.free(msg);
- gpa.free(ws.time_report_msgs);
- gpa.free(ws.time_report_update_times);
-
- if (ws.serve_task) |t| {
- if (ws.tcp_server) |*s| s.stream.close(io);
- t.await();
- }
- if (ws.tcp_server) |*s| s.deinit();
-
- gpa.free(ws.step_names_trailing);
-}
-pub fn start(ws: *WebServer) error{AlreadyReported}!void {
- assert(ws.tcp_server == null);
- assert(ws.serve_task == null);
- const io = ws.graph.io;
-
- ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
- log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err });
- return error.AlreadyReported;
- };
- ws.serve_task = io.concurrent(serve, .{ws}) catch |err| {
- log.err("unable to spawn web server thread: {t}", .{err});
- ws.tcp_server.?.deinit(io);
- ws.tcp_server = null;
- return error.AlreadyReported;
- };
-
- log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address});
- if (ws.listen_address.getPort() == 0) {
- log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address});
- }
-}
-fn serve(ws: *WebServer) Io.Cancelable!void {
- const io = ws.graph.io;
- var group: Io.Group = .init;
- defer group.cancel(io);
- while (true) {
- var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) {
- error.Canceled => |e| return e,
- else => |e| {
- log.err("failed to accept connection: {t}", .{e});
- return;
- },
- };
- group.concurrent(io, accept, .{ ws, stream }) catch |err| {
- log.err("unable to spawn connection thread: {t}", .{err});
- stream.close(io);
- continue;
- };
- }
-}
-
-pub fn startBuild(ws: *WebServer) void {
- if (ws.fuzz) |*fuzz| {
- fuzz.deinit();
- ws.fuzz = null;
- }
- for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
- ws.build_status.store(.running, .monotonic);
- ws.notifyUpdate();
-}
-
-pub fn updateStepStatus(ws: *WebServer, step: *Build.Step, new_status: abi.StepUpdate.Status) void {
- const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
- if (s == step) break @intCast(i);
- } else unreachable;
- const ptr = &ws.step_status_bits[step_idx / 4];
- const bit_offset: u3 = @intCast((step_idx % 4) * 2);
- const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
- const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset;
- _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
- ws.notifyUpdate();
-}
-
-pub fn finishBuild(ws: *WebServer, opts: struct {
- fuzz: bool,
-}) void {
- if (opts.fuzz) {
- switch (builtin.os.tag) {
- // Current implementation depends on two things that need to be ported to Windows:
- // * Memory-mapping to share data between the fuzzer and build runner.
- // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
- // many addresses to source locations).
- .windows => std.process.fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
- else => {},
- }
- if (@bitSizeOf(usize) != 64) {
- // Current implementation depends on posix.mmap()'s second
- // parameter, `length: usize`, being compatible with file system's
- // u64 return value. This is not the case on 32-bit platforms.
- // Affects or affected by issues #5185, #22523, and #22464.
- std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
- }
-
- assert(ws.fuzz == null);
-
- ws.build_status.store(.fuzz_init, .monotonic);
- ws.notifyUpdate();
-
- ws.fuzz = Fuzz.init(
- ws.gpa,
- ws.graph.io,
- ws.all_steps,
- ws.root_prog_node,
- .{ .forever = .{ .ws = ws } },
- ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
- ws.fuzz.?.start();
- }
-
- ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic);
- ws.notifyUpdate();
-}
-
-pub fn now(s: *const WebServer) i64 {
- const io = s.graph.io;
- const ts = base_clock.now(io);
- return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds());
-}
-
-fn accept(ws: *WebServer, stream: net.Stream) void {
- const io = ws.graph.io;
- defer {
- // `net.Stream.close` wants to helpfully overwrite `stream` with
- // `undefined`, but it cannot do so since it is an immutable parameter.
- var copy = stream;
- copy.close(io);
- }
- var send_buffer: [4096]u8 = undefined;
- var recv_buffer: [4096]u8 = undefined;
- var connection_reader = stream.reader(io, &recv_buffer);
- var connection_writer = stream.writer(io, &send_buffer);
- var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface);
-
- while (true) {
- var request = server.receiveHead() catch |err| switch (err) {
- error.HttpConnectionClosing => return,
- else => return log.err("failed to receive http request: {t}", .{err}),
- };
- switch (request.upgradeRequested()) {
- .websocket => |opt_key| {
- const key = opt_key orelse return log.err("missing websocket key", .{});
- var web_socket = request.respondWebSocket(.{ .key = key }) catch {
- return log.err("failed to respond web socket: {t}", .{connection_writer.err.?});
- };
- ws.serveWebSocket(&web_socket) catch |err| {
- log.err("failed to serve websocket: {t}", .{err});
- return;
- };
- comptime unreachable;
- },
- .other => |name| return log.err("unknown upgrade request: {s}", .{name}),
- .none => {
- ws.serveRequest(&request) catch |err| switch (err) {
- error.AlreadyReported => return,
- else => {
- log.err("failed to serve '{s}': {t}", .{ request.head.target, err });
- return;
- },
- };
- },
- }
- }
-}
-
-fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
- const io = ws.graph.io;
-
- var prev_build_status = ws.build_status.load(.monotonic);
-
- const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len);
- defer ws.gpa.free(prev_step_status_bits);
- for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| {
- copy.* = @atomicLoad(u8, shared, .monotonic);
- }
-
- var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock });
- defer recv_thread.cancel(io);
-
- {
- const hello_header: abi.Hello = .{
- .status = prev_build_status,
- .flags = .{
- .time_report = ws.graph.time_report,
- },
- .timestamp = ws.now(),
- .steps_len = @intCast(ws.all_steps.len),
- };
- var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits };
- try sock.writeMessageVec(&bufs, .binary);
- }
-
- var prev_fuzz: Fuzz.Previous = .init;
- var prev_time: i64 = std.math.minInt(i64);
- while (true) {
- const start_time = ws.now();
- const start_update_id = ws.update_id.load(.acquire);
-
- if (ws.fuzz) |*fuzz| {
- try fuzz.sendUpdate(sock, &prev_fuzz);
- }
-
- {
- try ws.time_report_mutex.lock(io);
- defer ws.time_report_mutex.unlock(io);
- for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| {
- if (update_time <= prev_time) continue;
- // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so
- // that we don't hold up the build system on the client accepting this packet.
- const owned_msg = try ws.gpa.dupe(u8, msg);
- defer ws.gpa.free(owned_msg);
- // Temporarily unlock, then re-lock after the message is sent.
- ws.time_report_mutex.unlock(io);
- defer ws.time_report_mutex.lockUncancelable(io);
- try sock.writeMessage(owned_msg, .binary);
- }
- }
-
- {
- const build_status = ws.build_status.load(.monotonic);
- if (build_status != prev_build_status) {
- prev_build_status = build_status;
- const msg: abi.StatusUpdate = .{ .new = build_status };
- try sock.writeMessage(@ptrCast(&msg), .binary);
- }
- }
-
- for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
- const cur_byte = @atomicLoad(u8, shared, .monotonic);
- if (prev_byte.* == cur_byte) continue;
- const cur: [4]abi.StepUpdate.Status = .{
- @enumFromInt(@as(u2, @truncate(cur_byte >> 0))),
- @enumFromInt(@as(u2, @truncate(cur_byte >> 2))),
- @enumFromInt(@as(u2, @truncate(cur_byte >> 4))),
- @enumFromInt(@as(u2, @truncate(cur_byte >> 6))),
- };
- const prev: [4]abi.StepUpdate.Status = .{
- @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))),
- @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))),
- @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))),
- @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))),
- };
- for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| {
- const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } };
- if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary);
- }
- prev_byte.* = cur_byte;
- }
-
- prev_time = start_time;
-
- const old_cp = io.swapCancelProtection(.blocked);
- defer _ = io.swapCancelProtection(old_cp);
- io.futexWaitTimeout(
- u32,
- &ws.update_id.raw,
- start_update_id,
- .{ .duration = .{
- .clock = .awake,
- .raw = .fromMilliseconds(default_update_interval_ms),
- } },
- ) catch |err| switch (err) {
- error.Canceled => unreachable,
- };
- }
-}
-fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
- const io = ws.graph.io;
-
- while (true) {
- const msg = sock.readSmallMessage() catch return;
- if (msg.opcode != .binary) continue;
- if (msg.data.len == 0) continue;
- const tag: abi.ToServerTag = @enumFromInt(msg.data[0]);
- switch (tag) {
- _ => continue,
- .rebuild => while (true) {
- ws.runner_request_mutex.lock(io) catch |err| switch (err) {
- error.Canceled => return,
- };
- defer ws.runner_request_mutex.unlock(io);
- if (ws.runner_request == null) {
- ws.runner_request = .rebuild;
- ws.runner_request_ready_cond.signal(io);
- break;
- }
- ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return;
- },
- }
- }
-}
-
-fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void {
- // Strip an optional leading '/debug' component from the request.
- const target: []const u8, const debug: bool = target: {
- if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true };
- if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true };
- if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true };
- break :target .{ req.head.target, false };
- };
-
- if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html");
- if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript");
- if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css");
- if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css");
- if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast);
-
- if (ws.fuzz) |*fuzz| {
- if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req);
- }
-
- try req.respond("not found", .{
- .status = .not_found,
- .extra_headers = &.{
- .{ .name = "Content-Type", .value = "text/plain" },
- },
- });
-}
-
-fn serveLibFile(
- ws: *WebServer,
- request: *http.Server.Request,
- sub_path: []const u8,
- content_type: []const u8,
-) !void {
- return serveFile(ws, request, .{
- .root_dir = ws.graph.zig_lib_directory,
- .sub_path = sub_path,
- }, content_type);
-}
-fn serveClientWasm(
- ws: *WebServer,
- req: *http.Server.Request,
- optimize_mode: std.builtin.OptimizeMode,
-) !void {
- var arena_state: std.heap.ArenaAllocator = .init(ws.gpa);
- defer arena_state.deinit();
- const arena = arena_state.allocator();
-
- // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page.
- const bin_path = try buildClientWasm(ws, arena, optimize_mode);
- return serveFile(ws, req, bin_path, "application/wasm");
-}
-
-pub fn serveFile(
- ws: *WebServer,
- request: *http.Server.Request,
- path: Cache.Path,
- content_type: []const u8,
-) !void {
- const gpa = ws.gpa;
- const io = ws.graph.io;
- // The desired API is actually sendfile, which will require enhancing http.Server.
- // We load the file with every request so that the user can make changes to the file
- // and refresh the HTML page without restarting this server.
- const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| {
- log.err("failed to read '{f}': {t}", .{ path, err });
- return error.AlreadyReported;
- };
- defer gpa.free(file_contents);
- try request.respond(file_contents, .{
- .extra_headers = &.{
- .{ .name = "Content-Type", .value = content_type },
- cache_control_header,
- },
- });
-}
-pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
- const graph = ws.graph;
- const io = graph.io;
-
- var send_buffer: [0x4000]u8 = undefined;
- var response = try request.respondStreaming(&send_buffer, .{
- .respond_options = .{
- .extra_headers = &.{
- .{ .name = "Content-Type", .value = "application/x-tar" },
- cache_control_header,
- },
- },
- });
-
- var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
-
- for (paths) |path| {
- var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| {
- log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
- continue;
- };
- defer file.close(io);
- const stat = try file.stat(io);
- var read_buffer: [1024]u8 = undefined;
- var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size);
-
- // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can
- // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI:
- // it turns out the WASM treats the first path component as the module name, typically
- // resulting in modules named "" and "src". The compiler needs to tell the build system
- // about the module graph so that the build system can correctly encode this information in
- // the tar file.
- //
- // Additionally, this needs to ensure that all path separators for both prefix and
- // sub_path are using the POSIX-style `/` on platforms that don't use it as their native
- // path separator.
- archiver.prefix = path.root_dir.path orelse graph.cache.cwd;
- try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds()));
- }
-
- // intentionally not calling `archiver.finishPedantically`
- try response.end();
-}
-
-fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path {
- const root_name = "build-web";
- const arch_os_abi = "wasm32-freestanding";
- const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
-
- const gpa = ws.gpa;
- const graph = ws.graph;
- const io = graph.io;
-
- const main_src_path: Cache.Path = .{
- .root_dir = graph.zig_lib_directory,
- .sub_path = "build-web/main.zig",
- };
- const walk_src_path: Cache.Path = .{
- .root_dir = graph.zig_lib_directory,
- .sub_path = "docs/wasm/Walk.zig",
- };
- const html_render_src_path: Cache.Path = .{
- .root_dir = graph.zig_lib_directory,
- .sub_path = "docs/wasm/html_render.zig",
- };
-
- var argv: std.ArrayList([]const u8) = .empty;
-
- try argv.appendSlice(arena, &.{
- graph.zig_exe, "build-exe", //
- "-fno-entry", //
- "-O", @tagName(optimize), //
- "-target", arch_os_abi, //
- "-mcpu", cpu_features, //
- "--cache-dir", graph.global_cache_root.path orelse ".", //
- "--global-cache-dir", graph.global_cache_root.path orelse ".", //
- "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
- "--name", root_name, //
- "-rdynamic", //
- "-fsingle-threaded", //
- "--dep", "Walk", //
- "--dep", "html_render", //
- try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), //
- try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), //
- "--dep", "Walk", //
- try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), //
- "--listen=-",
- });
-
- var child = try std.process.spawn(io, .{
- .argv = argv.items,
- .environ_map = &graph.environ_map,
- .stdin = .pipe,
- .stdout = .pipe,
- .stderr = .pipe,
- });
- defer child.kill(io);
-
- var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited });
- defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
-
- var stdout_buffer: [512]u8 = undefined;
- var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
- const stdout = &stdout_reader.interface;
-
- {
- var w = child.stdin.?.writer(io, &.{});
- w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
- error.WriteFailed => return w.err.?,
- };
- }
-
- const Header = std.zig.Server.Message.Header;
-
- var result: ?Cache.Path = null;
- var result_error_bundle = std.zig.ErrorBundle.empty;
- var body_buffer: std.ArrayList(u8) = .empty;
- defer body_buffer.deinit(gpa);
-
- while (true) {
- const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
- error.ReadFailed => |e| return e,
- error.EndOfStream => break,
- };
- body_buffer.clearRetainingCapacity();
- try stdout.appendExact(gpa, &body_buffer, header.bytes_len);
- const body = body_buffer.items;
-
- switch (header.tag) {
- .zig_version => {
- if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
- return error.ZigProtocolVersionMismatch;
- }
- },
- .error_bundle => {
- result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
- },
- .emit_digest => {
- const EmitDigest = std.zig.Server.Message.EmitDigest;
- const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
- if (!ebp_hdr.flags.cache_hit) {
- log.info("source changes detected; rebuilt wasm component", .{});
- }
- const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
- result = .{
- .root_dir = graph.global_cache_root,
- .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
- };
- },
- else => {}, // ignore other messages
- }
- }
-
- const stderr_contents = try stderr_task.await(io);
- if (stderr_contents.len > 0) {
- std.debug.print("{s}", .{stderr_contents});
- }
-
- // Send EOF to stdin.
- child.stdin.?.close(io);
- child.stdin = null;
-
- switch (try child.wait(io)) {
- .exited => |code| {
- if (code != 0) {
- log.err(
- "the following command exited with error code {d}:\n{s}",
- .{ code, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
- );
- return error.WasmCompilationFailed;
- }
- },
- .signal => |sig| {
- log.err(
- "the following command terminated with signal {t}:\n{s}",
- .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
- );
- return error.WasmCompilationFailed;
- },
- .stopped => |sig| {
- log.err(
- "the following command stopped unexpectedly with signal {t}:\n{s}",
- .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
- );
- return error.WasmCompilationFailed;
- },
- .unknown => {
- log.err(
- "the following command terminated unexpectedly:\n{s}",
- .{try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items)},
- );
- return error.WasmCompilationFailed;
- },
- }
-
- if (result_error_bundle.errorMessageCount() > 0) {
- try result_error_bundle.renderToStderr(io, .{}, .auto);
- log.err("the following command failed with {d} compilation errors:\n{s}", .{
- result_error_bundle.errorMessageCount(),
- try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
- });
- return error.WasmCompilationFailed;
- }
-
- const base_path = result orelse {
- log.err("child process failed to report result\n{s}", .{
- try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
- });
- return error.WasmCompilationFailed;
- };
- const bin_name = try std.zig.binNameAlloc(arena, .{
- .root_name = root_name,
- .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
- .arch_os_abi = arch_os_abi,
- .cpu_features = cpu_features,
- }) catch unreachable) catch unreachable),
- .output_mode = .Exe,
- });
- return base_path.join(arena, bin_name);
-}
-
-fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
- var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
- return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
- error.ReadFailed => return file_reader.err.?,
- else => |e| return e,
- };
-}
-
-pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
- compile: *Build.Step.Compile,
-
- use_llvm: bool,
- stats: abi.time_report.CompileResult.Stats,
- ns_total: u64,
-
- llvm_pass_timings_len: u32,
- files_len: u32,
- decls_len: u32,
-
- /// The trailing data of `abi.time_report.CompileResult`, except the step name.
- trailing: []const u8,
-}) void {
- const gpa = ws.gpa;
- const io = ws.graph.io;
-
- const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
- if (s == &opts.compile.step) break @intCast(i);
- } else unreachable;
-
- const old_buf = old: {
- ws.time_report_mutex.lock(io) catch return;
- defer ws.time_report_mutex.unlock(io);
- const old = ws.time_report_msgs[step_idx];
- ws.time_report_msgs[step_idx] = &.{};
- break :old old;
- };
- const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
-
- const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]);
- out_header.* = .{
- .step_idx = step_idx,
- .flags = .{
- .use_llvm = opts.use_llvm,
- },
- .stats = opts.stats,
- .ns_total = opts.ns_total,
- .llvm_pass_timings_len = opts.llvm_pass_timings_len,
- .files_len = opts.files_len,
- .decls_len = opts.decls_len,
- };
- @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
-
- {
- ws.time_report_mutex.lock(io) catch return;
- defer ws.time_report_mutex.unlock(io);
- assert(ws.time_report_msgs[step_idx].len == 0);
- ws.time_report_msgs[step_idx] = buf;
- ws.time_report_update_times[step_idx] = ws.now();
- }
- ws.notifyUpdate();
-}
-
-pub fn updateTimeReportGeneric(ws: *WebServer, step: *Build.Step, duration: Io.Duration) void {
- const gpa = ws.gpa;
- const io = ws.graph.io;
-
- const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
- if (s == step) break @intCast(i);
- } else unreachable;
-
- const old_buf = old: {
- ws.time_report_mutex.lock(io) catch return;
- defer ws.time_report_mutex.unlock(io);
- const old = ws.time_report_msgs[step_idx];
- ws.time_report_msgs[step_idx] = &.{};
- break :old old;
- };
- const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
- const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf);
- out.* = .{
- .step_idx = step_idx,
- .ns_total = @intCast(duration.toNanoseconds()),
- };
- {
- ws.time_report_mutex.lock(io) catch return;
- defer ws.time_report_mutex.unlock(io);
- assert(ws.time_report_msgs[step_idx].len == 0);
- ws.time_report_msgs[step_idx] = buf;
- ws.time_report_update_times[step_idx] = ws.now();
- }
- ws.notifyUpdate();
-}
-
-pub fn updateTimeReportRunTest(
- ws: *WebServer,
- run: *Build.Step.Run,
- tests: *const Build.Step.Run.CachedTestMetadata,
- ns_per_test: []const u64,
-) void {
- const gpa = ws.gpa;
- const io = ws.graph.io;
-
- const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
- if (s == &run.step) break @intCast(i);
- } else unreachable;
-
- assert(tests.names.len == ns_per_test.len);
- const tests_len: u32 = @intCast(tests.names.len);
-
- const new_len: u64 = len: {
- var names_len: u64 = 0;
- for (0..tests_len) |i| {
- names_len += tests.testName(@intCast(i)).len + 1;
- }
- break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
- };
- const old_buf = old: {
- ws.time_report_mutex.lock(io) catch return;
- defer ws.time_report_mutex.unlock(io);
- const old = ws.time_report_msgs[step_idx];
- ws.time_report_msgs[step_idx] = &.{};
- break :old old;
- };
- const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
-
- const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]);
- out_header.* = .{
- .step_idx = step_idx,
- .tests_len = tests_len,
- };
- var offset: usize = @sizeOf(abi.time_report.RunTestResult);
- const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]);
- @memcpy(ns_per_test_out, ns_per_test);
- offset += tests_len * 8;
- for (0..tests_len) |i| {
- const name = tests.testName(@intCast(i));
- @memcpy(buf[offset..][0..name.len], name);
- buf[offset..][name.len] = 0;
- offset += name.len + 1;
- }
- assert(offset == buf.len);
-
- {
- ws.time_report_mutex.lock(io) catch return;
- defer ws.time_report_mutex.unlock(io);
- assert(ws.time_report_msgs[step_idx].len == 0);
- ws.time_report_msgs[step_idx] = buf;
- ws.time_report_update_times[step_idx] = ws.now();
- }
- ws.notifyUpdate();
-}
-
-const RunnerRequest = union(enum) {
- rebuild,
-};
-pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
- const io = ws.graph.io;
- ws.runner_request_mutex.lock(io) catch return;
- defer ws.runner_request_mutex.unlock(io);
- if (ws.runner_request) |req| {
- ws.runner_request = null;
- ws.runner_request_empty_cond.signal();
- return req;
- }
- return null;
-}
-pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
- const io = ws.graph.io;
- try ws.runner_request_mutex.lock(io);
- defer ws.runner_request_mutex.unlock(io);
- while (true) {
- if (ws.runner_request) |req| {
- ws.runner_request = null;
- ws.runner_request_empty_cond.signal(io);
- return req;
- }
- try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex);
- }
-}
-
-const cache_control_header: http.Header = .{
- .name = "Cache-Control",
- .value = "max-age=0, must-revalidate",
-};
-
-const builtin = @import("builtin");
-
-const std = @import("std");
-const Io = std.Io;
-const net = std.Io.net;
-const assert = std.debug.assert;
-const mem = std.mem;
-const log = std.log.scoped(.web_server);
-const Allocator = std.mem.Allocator;
-const Build = std.Build;
-const Cache = Build.Cache;
-const Fuzz = Build.Fuzz;
-const abi = Build.abi;
-const http = std.http;
-
-const WebServer = @This();
diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig
@@ -409,7 +409,10 @@ pub const PathNameError = error{
};
pub const AccessError = error{
+ /// The requested `AccessOptions` would be denied to the file, or search
+ /// permission is denied for one of the directories in the path prefix.
AccessDenied,
+ /// Write permission was requested but the file is immutable.
PermissionDenied,
FileNotFound,
InputOutput,
diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig
@@ -1172,6 +1172,14 @@ pub fn printValue(
},
else => invalidFmtError(fmt, value),
},
+ 'q' => switch (@typeInfo(T)) {
+ .pointer => |info| switch (info.size) {
+ .one, .slice => return printStringEscaped(w, value),
+ .many, .c => return printStringEscaped(w, std.mem.span(value)),
+ },
+ .array => return printStringEscaped(w, &value),
+ else => invalidFmtError(fmt, value),
+ },
'B' => switch (@typeInfo(T)) {
.int, .comptime_int => return w.printByteSize(value, .decimal, options),
.@"struct" => return value.formatByteSize(w, .decimal),
@@ -1448,6 +1456,14 @@ fn printEnumNonexhaustive(w: *Writer, value: anytype) Error!void {
try w.writeByte(')');
}
+/// Prints a double quote, then escapes a string according to Zig string
+/// literal rules, then a double quote.
+pub fn printStringEscaped(w: *Writer, bytes: []const u8) Error!void {
+ try w.writeByte('"');
+ try std.zig.stringEscape(bytes, w);
+ try w.writeByte('"');
+}
+
pub fn printVector(
w: *Writer,
comptime fmt: []const u8,
@@ -2102,6 +2118,11 @@ test "printFloat with comptime_float" {
try testing.expectFmt("1", "{}", .{1.0});
}
+test "{q} format string" {
+ const data: []const u8 = "i\tlike\"cheese\x00\x05cheese";
+ try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data});
+}
+
fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void {
var buffer: [100]u8 = undefined;
var w: Writer = .fixed(&buffer);
diff --git a/lib/std/Target.zig b/lib/std/Target.zig
@@ -1668,13 +1668,13 @@ pub const Cpu = struct {
};
}
- pub fn parseCpuModel(arch: Arch, cpu_name: []const u8) !*const Cpu.Model {
+ pub fn parseCpuModel(arch: Arch, cpu_name: []const u8) ?*const Cpu.Model {
for (arch.allCpuModels()) |cpu| {
if (std.mem.eql(u8, cpu_name, cpu.name)) {
return cpu;
}
}
- return error.UnknownCpuModel;
+ return null;
}
pub fn endian(arch: Arch) std.builtin.Endian {
diff --git a/lib/std/Target/Query.zig b/lib/std/Target/Query.zig
@@ -282,7 +282,7 @@ pub fn parse(args: ParseOptions) !Query {
} else if (mem.eql(u8, cpu_name, "baseline")) {
result.cpu_model = .baseline;
} else {
- result.cpu_model = .{ .explicit = try arch.parseCpuModel(cpu_name) };
+ result.cpu_model = .{ .explicit = arch.parseCpuModel(cpu_name) orelse return error.UnknownCpuModel };
}
while (index < cpu_features.len) {
diff --git a/lib/std/array_list.zig b/lib/std/array_list.zig
@@ -1391,12 +1391,19 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type {
return self.allocatedSlice()[self.items.len..];
}
- /// Returns the last element from the list, or `null` if the list is empty.
+ /// Deprecated in favor of `last`.
pub fn getLast(self: Self) ?T {
if (self.items.len == 0) return null;
return self.items[self.items.len - 1];
}
+ /// Returns a pointer to the last element from the list, or `null` if
+ /// the list is empty.
+ pub fn last(self: Self) ?*T {
+ if (self.items.len == 0) return null;
+ return &self.items[self.items.len - 1];
+ }
+
/// Called when memory growth is necessary. Returns a capacity larger than
/// minimum that grows super-linearly.
pub fn growCapacity(minimum: usize) usize {
@@ -2378,17 +2385,16 @@ test "Managed(?u32).pop()" {
try testing.expect(list.pop() == null);
}
-test "Managed(u32).getLast()" {
+test "last" {
const a = testing.allocator;
- var list = Managed(u32).init(a);
- defer list.deinit();
+ var list: ArrayList(u32) = .empty;
+ defer list.deinit(a);
- try testing.expectEqual(list.getLast(), null);
+ try testing.expectEqual(list.last(), null);
- try list.append(2);
- const const_list = list;
- try testing.expectEqual(const_list.getLast().?, 2);
+ try list.append(a, 2);
+ try testing.expectEqual(list.last().?.*, 2);
}
test "return OutOfMemory when capacity would exceed maximum usize integer value" {
diff --git a/lib/std/lang.zig b/lib/std/lang.zig
@@ -93,7 +93,7 @@ pub const AtomicRmwOp = enum {
///
/// This data structure is used by the Zig language code generation and
/// therefore must be kept in sync with the compiler implementation.
-pub const CodeModel = enum {
+pub const CodeModel = enum(u4) {
default,
extreme,
kernel,
@@ -873,7 +873,7 @@ pub const OutputMode = enum {
/// This data structure is used by the Zig language code generation and
/// therefore must be kept in sync with the compiler implementation.
-pub const LinkMode = enum {
+pub const LinkMode = enum(u1) {
static,
dynamic,
};
diff --git a/lib/std/mem/Allocator.zig b/lib/std/mem/Allocator.zig
@@ -169,7 +169,7 @@ pub fn create(a: Allocator, comptime T: type) Error!*T {
const ptr = comptime std.mem.alignBackward(usize, math.maxInt(usize), @alignOf(T));
return @ptrFromInt(ptr);
}
- const ptr: *T = @ptrCast(try a.allocBytesWithAlignment(.of(T), @sizeOf(T), @returnAddress()));
+ const ptr: *T = @ptrCast(try a.allocBytesAligned(.of(T), @sizeOf(T), @returnAddress()));
return ptr;
}
@@ -285,10 +285,10 @@ fn allocWithSizeAndAlignment(
return_address: usize,
) Error![*]align(alignment.toByteUnits()) u8 {
const byte_count = math.mul(usize, size, n) catch return error.OutOfMemory;
- return self.allocBytesWithAlignment(alignment, byte_count, return_address);
+ return self.allocBytesAligned(alignment, byte_count, return_address);
}
-fn allocBytesWithAlignment(
+pub fn allocBytesAligned(
self: Allocator,
comptime alignment: Alignment,
byte_count: usize,
diff --git a/lib/std/process.zig b/lib/std/process.zig
@@ -1113,3 +1113,10 @@ test protectMemory {
protectMemory(&test_page, .{}) catch return error.SkipZigTest;
protectMemory(&test_page, .{ .read = true, .write = true }) catch return error.SkipZigTest;
}
+
+test {
+ _ = Child;
+ _ = Args;
+ _ = Environ;
+ _ = Preopens;
+}
diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig
@@ -96,6 +96,22 @@ pub const Term = union(enum) {
signal: std.posix.SIG,
stopped: std.posix.SIG,
unknown: u32,
+
+ pub fn success(t: Term) bool {
+ return switch (t) {
+ .exited => |code| code == 0,
+ else => false,
+ };
+ }
+
+ pub fn format(t: Term, w: *Io.Writer) Io.Writer.Error!void {
+ switch (t) {
+ .exited => |code| return w.print("exited with code {d}", .{code}),
+ .signal => |sig| return w.print("terminated with signal {t}", .{sig}),
+ .stopped => |sig| return w.print("stopped with signal {t}", .{sig}),
+ .unknown => return w.writeAll("terminated unexpectedly"),
+ }
+ }
};
pub const Cwd = union(enum) {
@@ -135,3 +151,7 @@ pub fn wait(child: *Child, io: Io) WaitError!Term {
assert(child.id != null);
return io.vtable.childWait(io.userdata, child);
}
+
+test {
+ _ = Term;
+}
diff --git a/lib/std/process/Environ.zig b/lib/std/process/Environ.zig
@@ -96,6 +96,7 @@ pub const WindowsBlock = struct {
}
};
+/// Each key and each value are allocated independently and owned by this data structure.
pub const Map = struct {
array_hash_map: ArrayHashMap,
allocator: Allocator,
@@ -340,9 +341,6 @@ pub const Map = struct {
/// Returns a full copy of `em` allocated with `gpa`, which is not necessarily
/// the same allocator used to allocate `em`.
pub fn clone(m: *const Map, gpa: Allocator) Allocator.Error!Map {
- // Since we need to dupe the keys and values, the only way for error handling to not be a
- // nightmare is to add keys to an empty map one-by-one. This could be avoided if this
- // abstraction were a bit less... OOP-esque.
var new: Map = .init(gpa);
errdefer new.deinit();
try new.array_hash_map.ensureUnusedCapacity(gpa, m.array_hash_map.count());
@@ -352,6 +350,32 @@ pub const Map = struct {
return new;
}
+ /// Adds all the key-value pairs from `other` into this `m`.
+ pub fn putAll(m: *Map, other: *const Map) Allocator.Error!void {
+ const gpa = m.allocator;
+ try m.array_hash_map.ensureUnusedCapacity(gpa, other.array_hash_map.count());
+ const start = m.count();
+ errdefer while (m.array_hash_map.count() > start) {
+ const kv = m.array_hash_map.pop().?;
+ gpa.free(kv.key);
+ gpa.free(kv.value);
+ };
+ for (other.array_hash_map.keys(), other.array_hash_map.values()) |key, value| {
+ try m.put(key, value);
+ }
+ }
+
+ /// Set the length to zero, freeing all key and value memory, not freeing
+ /// the allocation for the entries.
+ pub fn clearRetainingCapacity(m: *Map) void {
+ const gpa = m.allocator;
+ for (m.array_hash_map.keys(), m.array_hash_map.values()) |k, v| {
+ gpa.free(k);
+ gpa.free(v);
+ }
+ m.array_hash_map.clearRetainingCapacity();
+ }
+
/// Creates a null-delimited environment variable block in the format
/// expected by POSIX, from a hash map plus options.
pub fn createPosixBlock(
diff --git a/lib/std/zig.zig b/lib/std/zig.zig
@@ -33,6 +33,7 @@ pub const AstRlAnnotate = @import("zig/AstRlAnnotate.zig");
pub const LibCInstallation = @import("zig/LibCInstallation.zig");
pub const WindowsSdk = @import("zig/WindowsSdk.zig");
pub const LibCDirs = @import("zig/LibCDirs.zig");
+pub const PkgConfig = @import("zig/PkgConfig.zig");
pub const target = @import("zig/target.zig");
pub const llvm = @import("zig/llvm.zig");
@@ -146,7 +147,10 @@ pub fn lineDelta(source: []const u8, start: usize, end: usize) isize {
pub const BinNameOptions = struct {
root_name: []const u8,
- target: *const std.Target,
+ cpu_arch: std.Target.Cpu.Arch,
+ os_tag: std.Target.Os.Tag,
+ ofmt: std.Target.ObjectFormat,
+ abi: std.Target.Abi,
output_mode: std.builtin.OutputMode,
link_mode: ?std.builtin.LinkMode = null,
version: ?std.SemanticVersion = null,
@@ -155,10 +159,12 @@ pub const BinNameOptions = struct {
/// Returns the standard file system basename of a binary generated by the Zig compiler.
pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMemory}![]u8 {
const root_name = options.root_name;
- const t = options.target;
- switch (t.ofmt) {
+ switch (options.ofmt) {
.coff => switch (options.output_mode) {
- .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, t.exeFileExt() }),
+ .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{
+ root_name,
+ options.os_tag.exeFileExt(options.cpu_arch),
+ }),
.Lib => {
const suffix = switch (options.link_mode orelse .static) {
.static => ".lib",
@@ -173,16 +179,16 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe
.Lib => {
switch (options.link_mode orelse .static) {
.static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
- t.libPrefix(), root_name,
+ options.os_tag.libPrefix(options.abi), root_name,
}),
.dynamic => {
if (options.version) |ver| {
return std.fmt.allocPrint(allocator, "{s}{s}.so.{d}.{d}.{d}", .{
- t.libPrefix(), root_name, ver.major, ver.minor, ver.patch,
+ options.os_tag.libPrefix(options.abi), root_name, ver.major, ver.minor, ver.patch,
});
} else {
return std.fmt.allocPrint(allocator, "{s}{s}.so", .{
- t.libPrefix(), root_name,
+ options.os_tag.libPrefix(options.abi), root_name,
});
}
},
@@ -195,16 +201,16 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe
.Lib => {
switch (options.link_mode orelse .static) {
.static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
- t.libPrefix(), root_name,
+ options.os_tag.libPrefix(options.abi), root_name,
}),
.dynamic => {
if (options.version) |ver| {
return std.fmt.allocPrint(allocator, "{s}{s}.{d}.{d}.{d}.dylib", .{
- t.libPrefix(), root_name, ver.major, ver.minor, ver.patch,
+ options.os_tag.libPrefix(options.abi), root_name, ver.major, ver.minor, ver.patch,
});
} else {
return std.fmt.allocPrint(allocator, "{s}{s}.dylib", .{
- t.libPrefix(), root_name,
+ options.os_tag.libPrefix(options.abi), root_name,
});
}
},
@@ -213,11 +219,14 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe
.Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}),
},
.wasm => switch (options.output_mode) {
- .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, t.exeFileExt() }),
+ .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{
+ root_name,
+ options.os_tag.exeFileExt(options.cpu_arch),
+ }),
.Lib => {
switch (options.link_mode orelse .static) {
.static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
- t.libPrefix(), root_name,
+ options.os_tag.libPrefix(options.abi), root_name,
}),
.dynamic => return std.fmt.allocPrint(allocator, "{s}.wasm", .{root_name}),
}
@@ -231,10 +240,10 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe
.plan9 => switch (options.output_mode) {
.Exe => return allocator.dupe(u8, root_name),
.Obj => return std.fmt.allocPrint(allocator, "{s}{s}", .{
- root_name, t.ofmt.fileExt(t.cpu.arch),
+ root_name, options.ofmt.fileExt(options.cpu_arch),
}),
.Lib => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
- t.libPrefix(), root_name,
+ options.os_tag.libPrefix(options.abi), root_name,
}),
},
}
@@ -374,9 +383,9 @@ pub const Subsystem = enum {
pub const EfiRuntimeDriver: Subsystem = .efi_runtime_driver;
};
-pub const CompressDebugSections = enum { none, zlib, zstd };
+pub const CompressDebugSections = enum(u2) { none, zlib, zstd };
-pub const RcIncludes = enum {
+pub const RcIncludes = enum(u2) {
/// Use MSVC if available, fall back to MinGW.
any,
/// Use MSVC include paths (MSVC install + Windows SDK, must be present on the system).
@@ -672,7 +681,7 @@ pub fn putAstErrorsIntoBundle(
pub fn resolveTargetQueryOrFatal(io: Io, target_query: std.Target.Query) std.Target {
return std.zig.system.resolveTargetQuery(io, target_query) catch |err|
- std.process.fatal("unable to resolve target: {s}", .{@errorName(err)});
+ std.process.fatal("unable to resolve target: {t}", .{err});
}
pub fn parseTargetQueryOrReportFatalError(
@@ -747,7 +756,6 @@ pub const EnvVar = enum {
ZIG_LOCAL_PKG_DIR,
ZIG_LIB_DIR,
ZIG_LIBC,
- ZIG_BUILD_RUNNER,
ZIG_BUILD_ERROR_STYLE,
ZIG_BUILD_MULTILINE_ERRORS,
ZIG_VERBOSE_LINK,
@@ -764,6 +772,7 @@ pub const EnvVar = enum {
CPLUS_INCLUDE_PATH,
LIBRARY_PATH,
CC,
+ PKG_CONFIG,
// Terminal integration
NO_COLOR,
@@ -1157,6 +1166,74 @@ pub const ClangCliParam = struct {
}
};
+pub const AllocPrintCmdOptions = struct {
+ cwd: ?[]const u8 = null,
+ parent_env: ?*const std.process.Environ.Map = null,
+ child_env: ?*const std.process.Environ.Map = null,
+};
+
+pub fn allocPrintCmd(gpa: Allocator, argv: []const []const u8, options: AllocPrintCmdOptions) Allocator.Error![]u8 {
+ const shell = struct {
+ fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
+ for (string) |c| {
+ if (switch (c) {
+ else => true,
+ '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
+ '=' => is_argv0,
+ }) break;
+ } else return writer.writeAll(string);
+
+ try writer.writeByte('"');
+ for (string) |c| {
+ if (switch (c) {
+ std.ascii.control_code.nul => break,
+ '!', '"', '$', '\\', '`' => true,
+ else => !std.ascii.isPrint(c),
+ }) try writer.writeByte('\\');
+ switch (c) {
+ std.ascii.control_code.nul => unreachable,
+ std.ascii.control_code.bel => try writer.writeByte('a'),
+ std.ascii.control_code.bs => try writer.writeByte('b'),
+ std.ascii.control_code.ht => try writer.writeByte('t'),
+ std.ascii.control_code.lf => try writer.writeByte('n'),
+ std.ascii.control_code.vt => try writer.writeByte('v'),
+ std.ascii.control_code.ff => try writer.writeByte('f'),
+ std.ascii.control_code.cr => try writer.writeByte('r'),
+ std.ascii.control_code.esc => try writer.writeByte('E'),
+ ' '...'~' => try writer.writeByte(c),
+ else => try writer.print("{o:0>3}", .{c}),
+ }
+ }
+ try writer.writeByte('"');
+ }
+ };
+
+ var aw: Io.Writer.Allocating = .init(gpa);
+ defer aw.deinit();
+ const writer = &aw.writer;
+ if (options.cwd) |path| {
+ writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory;
+ }
+ if (options.child_env) |child_env| {
+ for (child_env.keys(), child_env.values()) |key, value| {
+ if (options.parent_env) |parent_env| {
+ if (parent_env.get(key)) |process_value| {
+ if (std.mem.eql(u8, value, process_value)) continue;
+ }
+ }
+ writer.print("{s}=", .{key}) catch return error.OutOfMemory;
+ shell.escape(writer, value, false) catch return error.OutOfMemory;
+ writer.writeByte(' ') catch return error.OutOfMemory;
+ }
+ }
+ shell.escape(writer, argv[0], true) catch return error.OutOfMemory;
+ for (argv[1..]) |arg| {
+ writer.writeByte(' ') catch return error.OutOfMemory;
+ shell.escape(writer, arg, false) catch return error.OutOfMemory;
+ }
+ return aw.toOwnedSlice();
+}
+
test {
_ = Ast;
_ = AstRlAnnotate;
diff --git a/lib/std/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig
@@ -13,6 +13,7 @@ const Target = std.Target;
const fs = std.fs;
const Allocator = std.mem.Allocator;
const Path = std.Build.Cache.Path;
+const Cache = std.Build.Cache;
const log = std.log.scoped(.libc_installation);
const Environ = std.process.Environ;
@@ -990,7 +991,7 @@ pub fn resolveCrtPaths(
target: *const std.Target,
) error{ OutOfMemory, LibCInstallationMissingCrtDir }!CrtPaths {
const crt_dir_path: Path = .{
- .root_dir = std.Build.Cache.Directory.cwd(),
+ .root_dir = Cache.Directory.cwd(),
.sub_path = lci.crt_dir orelse return error.LibCInstallationMissingCrtDir,
};
switch (target.os.tag) {
@@ -1016,7 +1017,7 @@ pub fn resolveCrtPaths(
},
.haiku, .serenity => {
const gcc_dir_path: Path = .{
- .root_dir = std.Build.Cache.Directory.cwd(),
+ .root_dir = Cache.Directory.cwd(),
.sub_path = lci.gcc_dir orelse return error.LibCInstallationMissingCrtDir,
};
return .{
@@ -1038,3 +1039,16 @@ pub fn resolveCrtPaths(
},
}
}
+
+pub fn addToHash(opt_lci: ?*const LibCInstallation, hh: *Cache.HashHelper, abi: std.Target.Abi) void {
+ const lci = opt_lci orelse return hh.add(false);
+ hh.add(true);
+ hh.addOptionalBytes(lci.crt_dir);
+ switch (abi) {
+ .msvc, .itanium => {
+ hh.addOptionalBytes(lci.msvc_lib_dir);
+ hh.addOptionalBytes(lci.kernel32_lib_dir);
+ },
+ else => {},
+ }
+}
diff --git a/lib/std/zig/PkgConfig.zig b/lib/std/zig/PkgConfig.zig
@@ -0,0 +1,146 @@
+//! The more reusable pieces of the build system's pkg-config integration logic.
+const PkgConfig = @This();
+
+const std = @import("../std.zig");
+const mem = std.mem;
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+
+all: []const Pkg,
+
+pub const Pkg = struct {
+ name: []const u8,
+ desc: []const u8,
+};
+
+pub const InitError = Allocator.Error || error{InvalidPkgConfigOutput};
+
+pub const Diagnostic = struct {
+ invalid_line_index: usize,
+ invalid_line: []const u8,
+};
+
+/// Parses the output of `pkg-config --list-all`.
+pub fn init(arena: Allocator, stdout: []const u8, diagnostic: ?*Diagnostic) InitError!PkgConfig {
+ var list: std.ArrayList(Pkg) = .empty;
+ var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
+ var line_index: usize = 0;
+ while (line_it.next()) |line| : (line_index += 1) {
+ if (mem.trim(u8, line, " \t").len == 0) continue;
+ var tok_it = mem.tokenizeAny(u8, line, " \t");
+ try list.append(arena, .{
+ .name = tok_it.next() orelse {
+ if (diagnostic) |d| d.* = .{
+ .invalid_line_index = line_index,
+ .invalid_line = line,
+ };
+ return error.InvalidPkgConfigOutput;
+ },
+ .desc = tok_it.rest(),
+ });
+ }
+ try list.shrinkToLen(arena);
+ return .{ .all = list.toOwnedSliceAssert() };
+}
+
+// Maps the library name to pkg config name. Unfortunately, there are several
+// examples where this is not straightforward:
+// * -lSDL2 -> pkg-config sdl2
+// * -lgdk-3 -> pkg-config gdk-3.0
+// * -latk-1.0 -> pkg-config atk
+// * -lpulse -> pkg-config libpulse
+pub fn find(pc: *const PkgConfig, lib_name: []const u8) ?usize {
+ const all = pc.all;
+
+ // Exact match means instant winner.
+ for (all, 0..) |pkg, i| {
+ if (mem.eql(u8, pkg.name, lib_name))
+ return i;
+ }
+
+ // Next we'll try ignoring case.
+ for (all, 0..) |pkg, i| {
+ if (std.ascii.eqlIgnoreCase(pkg.name, lib_name))
+ return i;
+ }
+
+ // Prefixed "lib" or suffixed ".0".
+ for (all, 0..) |pkg, i| {
+ if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
+ const prefix = pkg.name[0..pos];
+ const suffix = pkg.name[pos + lib_name.len ..];
+ if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
+ if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
+ return i;
+ }
+ }
+
+ // Trimming "-1.0".
+ if (mem.cutSuffix(u8, lib_name, "-1.0")) |trimmed| {
+ for (all, 0..) |pkg, i| {
+ if (std.ascii.eqlIgnoreCase(pkg.name, trimmed)) {
+ return i;
+ }
+ }
+ }
+
+ return null;
+}
+
+pub fn exe(environ_map: *const std.process.Environ.Map) []const u8 {
+ return std.zig.EnvVar.PKG_CONFIG.get(environ_map) orelse "pkg-config";
+}
+
+pub const Parsed = struct {
+ cflags: []const []const u8,
+ libs: []const []const u8,
+ unknown_flags: []const []const u8,
+};
+
+pub const ParseError = Allocator.Error || error{InvalidPkgConfigOutput};
+
+/// Parses the output of `pkg-config [name] --cflags --libs`.
+pub fn parse(arena: Allocator, stdout: []const u8) ParseError!Parsed {
+ var zig_cflags: std.ArrayList([]const u8) = .empty;
+ var zig_libs: std.ArrayList([]const u8) = .empty;
+ var unknown_flags: std.ArrayList([]const u8) = .empty;
+ var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
+
+ while (arg_it.next()) |arg| {
+ if (mem.eql(u8, arg, "-I")) {
+ const dir = arg_it.next() orelse return error.InvalidPkgConfigOutput;
+ try zig_cflags.appendSlice(arena, &.{ "-I", dir });
+ } else if (mem.startsWith(u8, arg, "-I")) {
+ try zig_cflags.append(arena, arg);
+ } else if (mem.eql(u8, arg, "-L")) {
+ const dir = arg_it.next() orelse return error.InvalidPkgConfigOutput;
+ try zig_libs.appendSlice(arena, &.{ "-L", dir });
+ } else if (mem.startsWith(u8, arg, "-L")) {
+ try zig_libs.append(arena, arg);
+ } else if (mem.eql(u8, arg, "-l")) {
+ const lib = arg_it.next() orelse return error.InvalidPkgConfigOutput;
+ try zig_libs.appendSlice(arena, &.{ "-l", lib });
+ } else if (mem.startsWith(u8, arg, "-l")) {
+ try zig_libs.append(arena, arg);
+ } else if (mem.eql(u8, arg, "-D")) {
+ const macro = arg_it.next() orelse return error.InvalidPkgConfigOutput;
+ try zig_cflags.appendSlice(arena, &.{ "-D", macro });
+ } else if (mem.startsWith(u8, arg, "-D")) {
+ try zig_cflags.append(arena, arg);
+ } else if (mem.cutPrefix(u8, arg, "-Wl,-rpath,")) |rest| {
+ try zig_cflags.appendSlice(arena, &.{ "-rpath", rest });
+ } else {
+ try unknown_flags.append(arena, arg);
+ }
+ }
+
+ try zig_cflags.shrinkToLen(arena);
+ try zig_libs.shrinkToLen(arena);
+ try unknown_flags.shrinkToLen(arena);
+
+ return .{
+ .cflags = zig_cflags.toOwnedSliceAssert(),
+ .libs = zig_libs.toOwnedSliceAssert(),
+ .unknown_flags = unknown_flags.toOwnedSliceAssert(),
+ };
+}
diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig
@@ -28,6 +28,8 @@ pub const Executor = union(enum) {
};
pub const GetExternalExecutorOptions = struct {
+ host_cpu_arch: std.Target.Cpu.Arch,
+ host_os_tag: std.Target.Os.Tag,
allow_darling: bool = true,
allow_qemu: bool = true,
allow_rosetta: bool = true,
@@ -39,24 +41,21 @@ pub const GetExternalExecutorOptions = struct {
/// Return whether or not the given host is capable of running executables of
/// the other target.
-pub fn getExternalExecutor(
- io: Io,
- host: *const std.Target,
- candidate: *const std.Target,
- options: GetExternalExecutorOptions,
-) Executor {
- const os_match = host.os.tag == candidate.os.tag;
+pub fn getExternalExecutor(io: Io, candidate: *const std.Target, options: GetExternalExecutorOptions) Executor {
+ const host_os_tag = options.host_os_tag;
+ const host_cpu_arch = options.host_cpu_arch;
+ const os_match = host_os_tag == candidate.os.tag;
const cpu_ok = cpu_ok: {
- if (host.cpu.arch == candidate.cpu.arch)
+ if (host_cpu_arch == candidate.cpu.arch)
break :cpu_ok true;
- if (host.cpu.arch == .x86_64 and candidate.cpu.arch == .x86)
+ if (host_cpu_arch == .x86_64 and candidate.cpu.arch == .x86)
break :cpu_ok true;
- if (host.cpu.arch == .aarch64 and candidate.cpu.arch == .arm)
+ if (host_cpu_arch == .aarch64 and candidate.cpu.arch == .arm)
break :cpu_ok true;
- if (host.cpu.arch == .aarch64_be and candidate.cpu.arch == .armeb)
+ if (host_cpu_arch == .aarch64_be and candidate.cpu.arch == .armeb)
break :cpu_ok true;
// TODO additionally detect incompatible CPU features.
@@ -83,7 +82,7 @@ pub fn getExternalExecutor(
// If the OS match and OS is macOS and CPU is arm64, we can use Rosetta 2
// to emulate the foreign architecture.
if (options.allow_rosetta and os_match and
- (host.os.tag == .maccatalyst or host.os.tag == .macos) and host.cpu.arch == .aarch64)
+ (host_os_tag == .maccatalyst or host_os_tag == .macos) and host_cpu_arch == .aarch64)
{
switch (candidate.cpu.arch) {
.x86_64 => return .rosetta,
@@ -173,13 +172,13 @@ pub fn getExternalExecutor(
.windows => {
if (options.allow_wine) {
const wine_supported = switch (candidate.cpu.arch) {
- .thumb => switch (host.cpu.arch) {
+ .thumb => switch (host_cpu_arch) {
.arm, .thumb, .aarch64 => true,
else => false,
},
- .aarch64 => host.cpu.arch == .aarch64,
- .x86 => host.cpu.arch.isX86(),
- .x86_64 => host.cpu.arch == .x86_64,
+ .aarch64 => host_cpu_arch == .aarch64,
+ .x86 => host_cpu_arch.isX86(),
+ .x86_64 => host_cpu_arch == .x86_64,
else => false,
};
return if (wine_supported) .{ .wine = "wine" } else bad_result;
@@ -191,7 +190,7 @@ pub fn getExternalExecutor(
// This check can be loosened once darling adds a QEMU-based emulation
// layer for non-host architectures:
// https://github.com/darlinghq/darling/issues/863
- if (candidate.cpu.arch != host.cpu.arch) {
+ if (candidate.cpu.arch != host_cpu_arch) {
return bad_result;
}
return .{ .darling = "darling" };
diff --git a/lib/std/zon/Serializer.zig b/lib/std/zon/Serializer.zig
@@ -122,7 +122,7 @@ pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, dep
/// Serialize a value, similar to `serializeArbitraryDepth`.
pub fn valueArbitraryDepth(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
- comptime assert(canSerializeType(@TypeOf(val)));
+ comptime assertCanSerializeType(@TypeOf(val));
switch (@typeInfo(@TypeOf(val))) {
.int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| {
self.codePoint(c) catch |err| switch (err) {
@@ -321,7 +321,7 @@ pub fn tupleArbitraryDepth(
}
fn tupleImpl(self: *Serializer, val: anytype, options: ValueOptions) Error!void {
- comptime assert(canSerializeType(@TypeOf(val)));
+ comptime assertCanSerializeType(@TypeOf(val));
switch (@typeInfo(@TypeOf(val))) {
.@"struct" => {
var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } });
@@ -814,6 +814,10 @@ test checkValueDepth {
try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{ 1, 2, 3 }}));
}
+inline fn assertCanSerializeType(T: type) void {
+ if (!canSerializeType(T)) @compileError("cannot serialize: " ++ @typeName(T));
+}
+
inline fn canSerializeType(T: type) bool {
comptime return canSerializeTypeInner(T, &.{}, false);
}
diff --git a/src/Compilation.zig b/src/Compilation.zig
@@ -752,13 +752,10 @@ pub const Directories = struct {
else => []const u8,
},
environ_map: *const std.process.Environ.Map,
+ cwd: []const u8,
) Directories {
const wasi = builtin.target.os.tag == .wasi;
- const cwd = introspect.getResolvedCwd(io, arena) catch |err| {
- fatal("unable to get cwd: {t}", .{err});
- };
-
const zig_lib: Cache.Directory = d: {
if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib");
if (wasi) break :d getPreopen(preopens, "/lib");
@@ -1750,9 +1747,13 @@ pub const CreateOptions = struct {
.no => return null,
.yes_cache => {
assert(opts.cache_mode != .none);
+ const target = &opts.root_mod.resolved_target.result;
return try ea.cacheName(arena, .{
.root_name = opts.root_name,
- .target = &opts.root_mod.resolved_target.result,
+ .cpu_arch = target.cpu.arch,
+ .os_tag = target.os.tag,
+ .ofmt = target.ofmt,
+ .abi = target.abi,
.output_mode = opts.config.output_mode,
.link_mode = opts.config.link_mode,
.version = opts.version,
@@ -3236,9 +3237,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) UpdateE
}
// Failure here only means an unnecessary cache miss.
- man.writeManifest() catch |err| {
- log.warn("failed to write cache manifest: {s}", .{@errorName(err)});
- };
+ man.writeManifest() catch |err| log.warn("failed to write cache manifest: {t}", .{err});
assert(whole.lock == null);
whole.lock = man.toOwnedLock();
@@ -3528,14 +3527,7 @@ fn addNonIncrementalStuffToCacheManifest(
man.hash.addListOfBytes(opts.rpath_list);
man.hash.addListOfBytes(opts.symbol_wrap_set.keys());
if (comp.config.link_libc) {
- man.hash.add(comp.libc_installation != null);
- if (comp.libc_installation) |libc_installation| {
- man.hash.addOptionalBytes(libc_installation.crt_dir);
- if (target.abi == .msvc or target.abi == .itanium) {
- man.hash.addOptionalBytes(libc_installation.msvc_lib_dir);
- man.hash.addOptionalBytes(libc_installation.kernel32_lib_dir);
- }
- }
+ LibCInstallation.addToHash(comp.libc_installation, &man.hash, target.abi);
man.hash.addOptionalBytes(target.dynamic_linker.get());
}
man.hash.add(opts.repro);
@@ -7473,9 +7465,14 @@ pub fn build_crt_file(
defer arena_allocator.deinit();
const arena = arena_allocator.allocator();
+ const target = &comp.root_mod.resolved_target.result;
+
const basename = try std.zig.binNameAlloc(gpa, .{
.root_name = root_name,
- .target = &comp.root_mod.resolved_target.result,
+ .cpu_arch = target.cpu.arch,
+ .os_tag = target.os.tag,
+ .ofmt = target.ofmt,
+ .abi = target.abi,
.output_mode = output_mode,
});
diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig
@@ -782,7 +782,7 @@ fn runResource(
f.package_root = try ls.pkg_root.join(arena, computed_package_hash.toSlice());
renameTmpIntoCache(io, package_sub_path, f.package_root) catch |err| {
try eb.addRootErrorMessage(.{ .msg = try eb.printString(
- "unable to rename temporary directory {f} into package cache directory {f}: {t}",
+ "failed renaming temporary directory {f} into package cache directory {f}: {t}",
.{ package_sub_path, f.package_root, err },
) });
return error.FetchFailed;
@@ -802,7 +802,7 @@ fn runResource(
if (!package_sub_path.eql(tmp_directory_path)) {
tmp_directory_path.root_dir.handle.deleteDir(io, tmp_directory_path.sub_path) catch |err| switch (err) {
error.Canceled => |e| return e,
- else => |e| log.warn("failed to delete temporary directory {f}: {t}", .{ tmp_directory_path, e }),
+ else => |e| log.warn("failed deleting temporary directory {f}: {t}", .{ tmp_directory_path, e }),
};
}
diff --git a/src/libs/libtsan.zig b/src/libs/libtsan.zig
@@ -41,7 +41,10 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo
const output_mode = .Lib;
const basename = try std.zig.binNameAlloc(arena, .{
.root_name = root_name,
- .target = target,
+ .cpu_arch = target.cpu.arch,
+ .os_tag = target.os.tag,
+ .ofmt = target.ofmt,
+ .abi = target.abi,
.output_mode = output_mode,
.link_mode = link_mode,
});
diff --git a/src/main.zig b/src/main.zig
@@ -3166,6 +3166,8 @@ fn buildOutputType(
else => process.executablePathAlloc(io, arena) catch |err| fatal("unable to find zig self exe path: {t}", .{err}),
};
+ const cwd_path = try introspect.getResolvedCwd(io, arena);
+
// This `init` calls `fatal` on error.
var dirs: Compilation.Directories = .init(
arena,
@@ -3182,6 +3184,7 @@ fn buildOutputType(
preopens,
self_exe_path,
environ_map,
+ cwd_path,
);
defer dirs.deinit(io);
@@ -3377,7 +3380,10 @@ fn buildOutputType(
.pch => try std.fmt.allocPrint(arena, "{s}.pch", .{root_name}),
else => try std.zig.binNameAlloc(arena, .{
.root_name = root_name,
- .target = target,
+ .cpu_arch = target.cpu.arch,
+ .os_tag = target.os.tag,
+ .ofmt = target.ofmt,
+ .abi = target.abi,
.output_mode = create_module.resolved_options.output_mode,
.link_mode = create_module.resolved_options.link_mode,
.version = optional_version,
@@ -3675,26 +3681,16 @@ fn buildOutputType(
if (t.arch == target.cpu.arch and t.os == target.os.tag) {
// If there's a `glibc_min`, there's also an `os_ver`.
if (t.glibc_min) |glibc_min| {
- std.log.info("zig can provide libc for related target {s}-{s}.{f}-{s}.{d}.{d}", .{
- @tagName(t.arch),
- @tagName(t.os),
- t.os_ver.?,
- @tagName(t.abi),
- glibc_min.major,
- glibc_min.minor,
+ std.log.info("zig can provide libc for related target {t}-{t}.{f}-{t}.{d}.{d}", .{
+ t.arch, t.os, t.os_ver.?, t.abi, glibc_min.major, glibc_min.minor,
});
} else if (t.os_ver) |os_ver| {
- std.log.info("zig can provide libc for related target {s}-{s}.{f}-{s}", .{
- @tagName(t.arch),
- @tagName(t.os),
- os_ver,
- @tagName(t.abi),
+ std.log.info("zig can provide libc for related target {t}-{t}.{f}-{t}", .{
+ t.arch, t.os, os_ver, t.abi,
});
} else {
- std.log.info("zig can provide libc for related target {s}-{s}-{s}", .{
- @tagName(t.arch),
- @tagName(t.os),
- @tagName(t.abi),
+ std.log.info("zig can provide libc for related target {t}-{t}-{t}", .{
+ t.arch, t.os, t.abi,
});
}
}
@@ -3703,7 +3699,7 @@ fn buildOutputType(
},
else => fatal("{f}", .{create_diag}),
},
- else => fatal("failed to create compilation: {s}", .{@errorName(err)}),
+ else => fatal("failed to create compilation: {t}", .{err}),
};
var comp_destroyed = false;
defer if (!comp_destroyed) comp.destroy();
@@ -4936,16 +4932,25 @@ test sanitizeExampleName {
try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project"));
}
-fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, environ_map: *process.Environ.Map) !void {
- dev.check(.build_command);
-
+fn cmdBuild(
+ gpa: Allocator,
+ arena: Allocator,
+ io: Io,
+ args: []const []const u8,
+ environ_map: *process.Environ.Map,
+) !void {
var build_file: ?[]const u8 = null;
var override_lib_dir: ?[]const u8 = EnvVar.ZIG_LIB_DIR.get(environ_map);
var override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map);
var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(environ_map);
var override_pkg_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_PKG_DIR.get(environ_map);
- var override_build_runner: ?[]const u8 = EnvVar.ZIG_BUILD_RUNNER.get(environ_map);
- var child_argv: std.ArrayList([]const u8) = .empty;
+ var maker_optimize_mode: std.builtin.OptimizeMode = if (EnvVar.ZIG_DEBUG_CMD.isSet(environ_map))
+ .Debug
+ else
+ .ReleaseSafe;
+ var configure_argv: std.ArrayList([]const u8) = .empty;
+ var make_argv: std.ArrayList([]const u8) = .empty;
+ var cached_passthru_configure: std.ArrayList(u32) = .empty;
var forks: std.ArrayList(Fork) = .empty;
var reference_trace: ?u32 = null;
var debug_compile_errors = false;
@@ -4964,47 +4969,41 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,
var system_pkg_dir_path: ?[]const u8 = null;
var debug_target: ?[]const u8 = null;
var debug_libc_paths_file: ?[]const u8 = null;
-
- const argv_index_exe = child_argv.items.len;
- _ = try child_argv.addOne(arena);
+ var cache_poison: std.Build.Graph.CachePoison = .pure;
const self_exe_path = try process.executablePathAlloc(io, arena);
- try child_argv.append(arena, self_exe_path);
+ const default_seed = try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)});
- const argv_index_zig_lib_dir = child_argv.items.len;
- _ = try child_argv.addOne(arena);
+ try configure_argv.ensureUnusedCapacity(arena, 16);
+ try make_argv.ensureUnusedCapacity(arena, 16);
+ try cached_passthru_configure.ensureUnusedCapacity(arena, 16);
- const argv_index_build_file = child_argv.items.len;
- _ = try child_argv.addOne(arena);
+ _ = configure_argv.addOneAssumeCapacity(); // configurer executable
+ _ = make_argv.addOneAssumeCapacity(); // maker executable
- const argv_index_cache_dir = child_argv.items.len;
- _ = try child_argv.addOne(arena);
+ make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", self_exe_path };
+ configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", self_exe_path };
- const argv_index_global_cache_dir = child_argv.items.len;
- _ = try child_argv.addOne(arena);
+ make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig-lib-dir", undefined };
+ const make_argv_index_zig_lib_dir = make_argv.items.len - 1;
- try child_argv.appendSlice(arena, &.{
- "--seed",
- try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)}),
- });
- const argv_index_seed = child_argv.items.len - 1;
-
- // This parent process needs a way to obtain results from the configuration
- // phase of the child process. In the future, the make phase will be
- // executed in a separate process than the configure phase, and we can then
- // use stdout from the configuration phase for this purpose.
- //
- // However, currently, both phases are in the same process, and Run Step
- // provides API for making the runned subprocesses inherit stdout and stderr
- // which means these streams are not available for passing metadata back
- // to the parent.
- //
- // Until make and configure phases are separated into different processes,
- // the strategy is to choose a temporary file name ahead of time, and then
- // read this file in the parent to obtain the results, in the case the child
- // exits with code 3.
- const results_tmp_file_nonce = std.fmt.hex(randInt(io, u64));
- try child_argv.append(arena, "-Z" ++ results_tmp_file_nonce);
+ make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined };
+ const make_argv_index_build_root = make_argv.items.len - 1;
+
+ make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--local-cache", undefined };
+ const make_argv_index_cache_dir = make_argv.items.len - 1;
+
+ make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--global-cache", undefined };
+ const make_argv_index_global_cache_dir = make_argv.items.len - 1;
+
+ make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--configuration", undefined };
+ const argv_index_configuration_file = make_argv.items.len - 1;
+
+ make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--seed", default_seed };
+ const argv_index_seed = make_argv.items.len - 1;
+
+ configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined };
+ const conf_argv_index_build_root = configure_argv.items.len - 1;
var color: Color = .auto;
var n_jobs: ?u32 = null;
@@ -5014,20 +5013,65 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,
while (i < args.len) : (i += 1) {
const arg = args[i];
if (mem.startsWith(u8, arg, "-")) {
- if (mem.eql(u8, arg, "--build-file")) {
+ try configure_argv.ensureUnusedCapacity(arena, 2);
+
+ if (mem.startsWith(u8, arg, "-D") or
+ mem.startsWith(u8, arg, "-fsys=") or
+ mem.startsWith(u8, arg, "-fno-sys=") or
+ mem.startsWith(u8, arg, "--release=") or
+ mem.eql(u8, arg, "--release"))
+ {
+ try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
+ configure_argv.appendAssumeCapacity(arg);
+ continue;
+ } else if (mem.eql(u8, arg, "--system")) {
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
i += 1;
- build_file = args[i];
+ system_pkg_dir_path = args[i];
+
+ try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
+ configure_argv.appendAssumeCapacity(arg); // Intentionally "--system" only; not the path.
continue;
- } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
+ } else if (mem.cutPrefix(u8, arg, "--color=")) |rest| {
+ color = std.meta.stringToEnum(Color, rest) orelse
+ fatal("expected --color=[auto|on|off]; found: {s}", .{arg});
+
+ try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
+ configure_argv.appendAssumeCapacity(arg);
+ continue;
+ } else if (mem.eql(u8, arg, "--cache-poison")) {
+ cache_poison = .poisoned;
+ configure_argv.appendAssumeCapacity("--cache-poison=poisoned");
+ continue;
+ } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
+ // Allow the configurer process to report parse failure.
+ if (std.meta.stringToEnum(std.Build.Graph.CachePoison, rest)) |poison| {
+ cache_poison = poison;
+ }
+ configure_argv.appendAssumeCapacity(arg);
+ continue;
+ } else if (mem.eql(u8, arg, "--verbose")) {
+ // Intentionally is added both to make and configure but
+ // does not go into the cache hash.
+ configure_argv.appendAssumeCapacity(arg);
+ } else if (mem.eql(u8, arg, "--search-prefix")) {
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
i += 1;
- override_lib_dir = args[i];
+ // This argument is cache poisonous: it does not go into
+ // the cache and configurer must set the poison bit when
+ // choosing to observe it.
+ configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ arg, args[i] };
+ (try make_argv.addManyAsArray(arena, 2)).* = .{ arg, args[i] };
+ continue;
+ } else if (mem.eql(u8, arg, "--build-file")) {
+ if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
+ i += 1;
+ build_file = args[i];
continue;
- } else if (mem.eql(u8, arg, "--build-runner")) {
+ } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
i += 1;
- override_build_runner = args[i];
+ override_lib_dir = args[i];
continue;
} else if (mem.eql(u8, arg, "--cache-dir")) {
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
@@ -5051,37 +5095,27 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,
} else if (mem.cutPrefix(u8, arg, "--fetch=")) |sub_arg| {
fetch_only = true;
fetch_mode = std.meta.stringToEnum(Package.Fetch.JobQueue.Mode, sub_arg) orelse
- fatal("expected [needed|all] after '--fetch=', found '{s}'", .{
- sub_arg,
- });
+ fatal("expected [needed|all] after '--fetch=', found '{s}'", .{sub_arg});
} else if (mem.cutPrefix(u8, arg, "--fork=")) |sub_arg| {
- try forks.append(arena, .{
- .manifest_ast = undefined,
- .manifest = undefined,
- .error_bundle = undefined,
- .arena_allocator = undefined,
- .path = .{
- .root_dir = .cwd(),
- .sub_path = sub_arg,
- },
- .failed = false,
- });
+ try forks.append(arena, .init(sub_arg));
continue;
- } else if (mem.eql(u8, arg, "--system")) {
- if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
+ } else if (mem.eql(u8, arg, "--fork")) {
+ if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
i += 1;
- system_pkg_dir_path = args[i];
- try child_argv.append(arena, "--system");
+ try forks.append(arena, .init(args[i]));
continue;
} else if (mem.cutPrefix(u8, arg, "-freference-trace=")) |num| {
reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
- fatal("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
+ fatal("unable to parse reference_trace count '{s}': {t}", .{ num, err });
};
} else if (mem.eql(u8, arg, "-fno-reference-trace")) {
reference_trace = null;
+ } else if (mem.cutPrefix(u8, arg, "--maker-opt=")) |rest| {
+ maker_optimize_mode = parseOptimizeMode(rest);
+ continue;
} else if (mem.eql(u8, arg, "--debug-log")) {
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
- try child_argv.appendSlice(arena, args[i .. i + 2]);
+ try make_argv.appendSlice(arena, args[i .. i + 2]);
i += 1;
try addDebugLog(arena, args[i]);
continue;
@@ -5125,505 +5159,720 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,
verbose_llvm_bc = rest;
} else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
verbose_llvm_cpu_features = true;
- } else if (mem.eql(u8, arg, "--color")) {
- if (i + 1 >= args.len) fatal("expected [auto|on|off] after {s}", .{arg});
- i += 1;
- color = std.meta.stringToEnum(Color, args[i]) orelse {
- fatal("expected [auto|on|off] after {s}, found '{s}'", .{ arg, args[i] });
- };
- try child_argv.appendSlice(arena, &.{ arg, args[i] });
- continue;
} else if (mem.cutPrefix(u8, arg, "-j")) |str| {
- const num = std.fmt.parseUnsigned(u32, str, 10) catch |err| {
- fatal("unable to parse jobs count '{s}': {s}", .{
- str, @errorName(err),
- });
- };
+ const num = std.fmt.parseUnsigned(u32, str, 10) catch |err|
+ fatal("unable to parse jobs count {s}: {t}", .{ str, err });
if (num < 1) {
- fatal("number of jobs must be at least 1\n", .{});
+ fatal("number of jobs must be at least 1", .{});
}
n_jobs = num;
} else if (mem.eql(u8, arg, "--seed")) {
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
i += 1;
- child_argv.items[argv_index_seed] = args[i];
+ make_argv.items[argv_index_seed] = args[i];
continue;
} else if (mem.eql(u8, arg, "--")) {
- // The rest of the args are supposed to get passed onto
- // build runner's `build.args`
- try child_argv.appendSlice(arena, args[i..]);
+ try make_argv.appendSlice(arena, args[i..]);
break;
}
}
- try child_argv.append(arena, arg);
+ try make_argv.append(arena, arg);
}
}
const root_prog_node = std.Progress.start(io, .{
.disable_printing = (color == .off),
- .root_name = "Compile Build Script",
+ .root_name = "",
});
defer root_prog_node.end();
- // Normally the build runner is compiled for the host target but here is
- // some code to help when debugging edits to the build runner so that you
- // can make sure it compiles successfully on other targets.
- const resolved_target: Package.Module.ResolvedTarget = t: {
- if (build_options.enable_debug_extensions) {
- if (debug_target) |triple| {
- const target_query = try std.Target.Query.parse(.{
- .arch_os_abi = triple,
- });
- break :t .{
- .result = std.zig.resolveTargetQueryOrFatal(io, target_query),
- .is_native_os = false,
- .is_native_abi = false,
- .is_explicit_dynamic_linker = false,
- };
- }
- }
- break :t .{
- .result = std.zig.resolveTargetQueryOrFatal(io, .{}),
- .is_native_os = true,
- .is_native_abi = true,
- .is_explicit_dynamic_linker = false,
- };
- };
- // Likewise, `--debug-libc` allows overriding the libc installation.
- const libc_installation: ?*const LibCInstallation = lci: {
- const paths_file = debug_libc_paths_file orelse break :lci null;
- if (!build_options.enable_debug_extensions) unreachable;
- const lci = try arena.create(LibCInstallation);
- lci.* = try .parse(arena, io, paths_file, &resolved_target.result);
- break :lci lci;
- };
-
process.raiseFileDescriptorLimit();
- const cwd_path = try introspect.getResolvedCwd(io, arena);
+ const cwd_path = introspect.getResolvedCwd(io, arena) catch |err|
+ fatal("failed to get current directory path: {t}", .{err});
+
const build_root = try findBuildRoot(arena, io, .{
.cwd_path = cwd_path,
.build_file = build_file,
});
- // This `init` calls `fatal` on error.
- var dirs: Compilation.Directories = .init(
- arena,
- io,
- override_lib_dir,
- override_global_cache_dir,
- .{ .override = path: {
- if (override_local_cache_dir) |d| break :path d;
- break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename});
- } },
- .empty,
- self_exe_path,
- environ_map,
- );
- defer dirs.deinit(io);
+ {
+ // This `init` calls `fatal` on error.
+ var dirs: Compilation.Directories = .init(
+ arena,
+ io,
+ override_lib_dir,
+ override_global_cache_dir,
+ .{ .override = path: {
+ if (override_local_cache_dir) |d| break :path d;
+ break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename});
+ } },
+ .empty,
+ self_exe_path,
+ environ_map,
+ cwd_path,
+ );
+ defer dirs.deinit(io);
- child_argv.items[argv_index_zig_lib_dir] = dirs.zig_lib.path orelse cwd_path;
- child_argv.items[argv_index_build_file] = build_root.directory.path orelse cwd_path;
- child_argv.items[argv_index_global_cache_dir] = dirs.global_cache.path orelse cwd_path;
- child_argv.items[argv_index_cache_dir] = dirs.local_cache.path orelse cwd_path;
+ const thread_limit = @min(
+ @max(n_jobs orelse std.Thread.getCpuCount() catch 1, 1),
+ std.math.maxInt(Zcu.PerThread.IdBacking),
+ );
+ try setThreadLimit(arena, thread_limit);
+
+ // Cache lookup for configure options. If we get a match, we can skip
+ // execution of the configure script. If not, we get the file path to pass
+ // to the configure process.
+ var local_cache: Cache = .{
+ .gpa = gpa,
+ .io = io,
+ .manifest_dir = try dirs.local_cache.handle.createDirPathOpen(io, "h", .{}),
+ .cwd = cwd_path,
+ };
+ local_cache.addPrefix(.{ .path = null, .handle = Io.Dir.cwd() });
+ local_cache.addPrefix(dirs.zig_lib);
+ local_cache.addPrefix(dirs.local_cache);
+ local_cache.addPrefix(dirs.global_cache);
+ defer local_cache.manifest_dir.close(io);
+
+ var config_man = local_cache.obtain();
+ defer config_man.deinit();
+ config_man.hash.addBytes(build_options.version);
+
+ for (cached_passthru_configure.items) |i|
+ config_man.hash.addBytes(configure_argv.items[i]);
+
+ // Prevents a `zig build` from getting a false positive cache hit following
+ // a `zig build --cache-poison=ignored`.
+ config_man.hash.add(cache_poison == .ignored);
+
+ // Normally the build runner is compiled for the host target but here is
+ // some code to help when debugging edits to the build runner so that you
+ // can make sure it compiles successfully on other targets.
+ const resolved_target: Package.Module.ResolvedTarget = t: {
+ if (build_options.enable_debug_extensions) {
+ if (debug_target) |triple| {
+ const target_query = try std.Target.Query.parse(.{
+ .arch_os_abi = triple,
+ });
+ config_man.hash.addBytes(triple);
+ break :t .{
+ .result = std.zig.resolveTargetQueryOrFatal(io, target_query),
+ .is_native_os = false,
+ .is_native_abi = false,
+ .is_explicit_dynamic_linker = false,
+ };
+ }
+ }
+ break :t .{
+ .result = std.zig.resolveTargetQueryOrFatal(io, .{}),
+ .is_native_os = true,
+ .is_native_abi = true,
+ .is_explicit_dynamic_linker = false,
+ };
+ };
- const thread_limit = @min(
- @max(n_jobs orelse std.Thread.getCpuCount() catch 1, 1),
- std.math.maxInt(Zcu.PerThread.IdBacking),
- );
- try setThreadLimit(arena, thread_limit);
+ // Likewise, `--debug-libc` allows overriding the libc installation.
+ const libc_installation: ?*const LibCInstallation = lci: {
+ const paths_file = debug_libc_paths_file orelse break :lci null;
+ if (!build_options.enable_debug_extensions) unreachable;
+ const lci = try arena.create(LibCInstallation);
+ lci.* = try .parse(arena, io, paths_file, &resolved_target.result);
+ LibCInstallation.addToHash(lci, &config_man.hash, resolved_target.result.abi);
+ break :lci lci;
+ };
- // Dummy http client that is not actually used when fetch_command is unsupported.
- // Prevents bootstrap from depending on a bunch of unnecessary stuff.
- var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct {
- allocator: Allocator,
- io: Io,
- fn deinit(_: @This()) void {}
- } = .{ .allocator = gpa, .io = io };
- defer http_client.deinit();
+ // Kick off an optimized compilation of the make runner.
+ var make_runner_task = io.async(compileMakeRunner, .{ gpa, arena, io, .{
+ .dirs = .{
+ .cwd = dirs.cwd,
+ .zig_lib = dirs.zig_lib,
+ .global_cache = dirs.global_cache,
+ .local_cache = dirs.global_cache,
+ },
+ .environ_map = environ_map,
+ .parent_prog_node = root_prog_node,
+ .resolved_target = resolved_target,
+ .libc_installation = libc_installation,
+ .thread_limit = thread_limit,
+ .self_exe_path = self_exe_path,
+ .color = color,
+ .reference_trace = reference_trace,
+ .optimize_mode = maker_optimize_mode,
+ } });
+ defer _ = make_runner_task.cancel(io) catch {};
+
+ const pkg_root: Path = if (override_pkg_dir) |p|
+ .initCwd(p)
+ else if (system_pkg_dir_path) |p|
+ .initCwd(p)
+ else
+ .{
+ .root_dir = build_root.directory,
+ .sub_path = "zig-pkg",
+ };
- var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{};
- var fork_set: Package.Fetch.JobQueue.ForkSet = .{};
+ make_argv.items[make_argv_index_zig_lib_dir] = dirs.zig_lib.path orelse cwd_path;
+ make_argv.items[make_argv_index_build_root] = build_root.directory.path orelse cwd_path;
+ make_argv.items[make_argv_index_global_cache_dir] = dirs.global_cache.path orelse cwd_path;
+ make_argv.items[make_argv_index_cache_dir] = dirs.local_cache.path orelse cwd_path;
- {
- // Populate fork_set.
- var group: Io.Group = .init;
- defer group.cancel(io);
-
- for (forks.items) |*fork|
- group.async(io, Fork.load, .{ io, gpa, fork, color });
-
- try group.await(io);
-
- for (forks.items) |*fork| {
- if (fork.failed) process.exit(1);
- try fork_set.put(arena, .{
- .path = fork.path,
- .manifest_ast = fork.manifest_ast,
- .manifest = fork.manifest,
- .uses = 0,
- }, {});
- }
- }
- defer Fork.deinitList(forks.items);
+ configure_argv.items[conf_argv_index_build_root] = build_root.directory.path orelse cwd_path;
+
+ // Dummy http client that is not actually used when fetch_command is unsupported.
+ // Prevents bootstrap from depending on a bunch of unnecessary stuff.
+ var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct {
+ allocator: Allocator,
+ io: Io,
+ fn deinit(_: @This()) void {}
+ } = .{ .allocator = gpa, .io = io };
+ defer http_client.deinit();
+
+ var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{};
+ var fork_set: Package.Fetch.JobQueue.ForkSet = .{};
- // This loop is re-evaluated when the build script exits with an indication that it
- // could not continue due to missing lazy dependencies.
- while (true) {
- // We want to release all the locks before executing the child process, so we make a nice
- // big block here to ensure the cleanup gets run when we extract out our argv.
{
- const main_mod_paths: Package.Module.CreateOptions.Paths = if (override_build_runner) |runner| .{
- .root = try .fromUnresolved(arena, dirs, &.{fs.path.dirname(runner) orelse "."}),
- .root_src_path = fs.path.basename(runner),
- } else .{
- .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"),
- .root_src_path = "build_runner.zig",
- };
+ // Populate fork_set.
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+
+ for (forks.items) |*fork|
+ group.async(io, Fork.load, .{ io, gpa, fork, color });
+
+ try group.await(io);
+
+ for (forks.items) |*fork| {
+ if (fork.failed) process.exit(1);
+ try fork_set.put(arena, .{
+ .path = fork.path,
+ .manifest_ast = fork.manifest_ast,
+ .manifest = fork.manifest,
+ .uses = 0,
+ }, {});
+ }
+ }
+ defer Fork.deinitList(forks.items);
- const config = try Compilation.Config.resolve(.{
- .output_mode = .Exe,
- .resolved_target = resolved_target,
- .have_zcu = true,
- .emit_bin = true,
- .is_test = false,
- });
+ // This loop is re-evaluated when the build script exits with an indication that it
+ // could not continue due to missing lazy dependencies.
+ const configuration_path: Path, const poisoned: bool = cp: while (true) {
+ // We want to release all the locks before executing the child process, so we make a nice
+ // big block here to ensure the cleanup gets run when we extract out our argv.
+ {
+ const main_mod_paths: Package.Module.CreateOptions.Paths = .{
+ .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"),
+ .root_src_path = "configurer.zig",
+ };
- const root_mod = try Package.Module.create(arena, .{
- .paths = main_mod_paths,
- .fully_qualified_name = "root",
- .cc_argv = &.{},
- .inherited = .{
+ const config = try Compilation.Config.resolve(.{
+ .output_mode = .Exe,
.resolved_target = resolved_target,
- },
- .global = config,
- .parent = null,
- });
+ .have_zcu = true,
+ .emit_bin = true,
+ .is_test = false,
+ });
- const build_mod = try Package.Module.create(arena, .{
- .paths = .{
- .root = try .fromUnresolved(arena, dirs, &.{build_root.directory.path orelse "."}),
- .root_src_path = build_root.build_zig_basename,
- },
- .fully_qualified_name = "root.@build",
- .cc_argv = &.{},
- .inherited = .{},
- .global = config,
- .parent = root_mod,
- });
+ const root_mod = try Package.Module.create(arena, .{
+ .paths = main_mod_paths,
+ .fully_qualified_name = "root",
+ .cc_argv = &.{},
+ .inherited = .{
+ .resolved_target = resolved_target,
+ .single_threaded = true,
+ },
+ .global = config,
+ .parent = null,
+ });
- if (dev.env.supports(.fetch_command)) {
- const fetch_prog_node = root_prog_node.start("Fetch Packages", 0);
- defer fetch_prog_node.end();
-
- // Reset fork match counts.
- for (fork_set.keys()) |*fork| fork.uses = 0;
-
- var job_queue: Package.Fetch.JobQueue = .{
- .io = io,
- .http_client = &http_client,
- .global_cache = dirs.global_cache,
- .local_storage = &.{
- .cache_root = .{ .root_dir = dirs.local_cache, .sub_path = "" },
- .pkg_root = if (override_pkg_dir) |p|
- .initCwd(p)
- else if (system_pkg_dir_path) |p|
- .initCwd(p)
- else
- .{
- .root_dir = build_root.directory,
- .sub_path = "zig-pkg",
- },
+ const build_mod = try Package.Module.create(arena, .{
+ .paths = .{
+ .root = try .fromUnresolved(arena, dirs, &.{build_root.directory.path orelse "."}),
+ .root_src_path = build_root.build_zig_basename,
},
- .recursive = true,
- .debug_hash = false,
- .unlazy_set = unlazy_set,
- .fork_set = fork_set,
- .mode = fetch_mode,
- .prog_node = fetch_prog_node,
- .read_only = system_pkg_dir_path != null,
- };
- defer job_queue.deinit();
+ .fully_qualified_name = "root.@build",
+ .cc_argv = &.{},
+ .inherited = .{},
+ .global = config,
+ .parent = root_mod,
+ });
- if (system_pkg_dir_path == null) {
- try http_client.initDefaultProxies(arena, environ_map);
- }
+ if (dev.env.supports(.fetch_command)) {
+ const fetch_prog_node = root_prog_node.start("Fetch Packages", 0);
+ defer fetch_prog_node.end();
- try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
- try job_queue.table.ensureUnusedCapacity(gpa, 1);
-
- const phantom_package_root: Cache.Path = .{ .root_dir = build_root.directory };
-
- var fetch: Package.Fetch = .{
- .arena = std.heap.ArenaAllocator.init(gpa),
- .location = .{ .relative_path = phantom_package_root },
- .location_tok = 0,
- .hash_tok = .none,
- .name_tok = 0,
- .lazy_status = .eager,
- .remote_package_root = phantom_package_root,
- .parent_package_root = phantom_package_root,
- .parent_manifest_ast = null,
- .prog_node = fetch_prog_node,
- .job_queue = &job_queue,
- .omit_missing_hash_error = true,
- .allow_missing_paths_field = false,
- .use_latest_commit = false,
-
- .package_root = undefined,
- .error_bundle = undefined,
- .manifest = undefined,
- .manifest_ast = undefined,
- .have_manifest = false,
- .computed_hash = undefined,
- .has_build_zig = true,
- .oom_flag = false,
- .latest_commit = null,
-
- .module = build_mod,
- };
+ // Reset fork match counts.
+ for (fork_set.keys()) |*fork| fork.uses = 0;
+
+ var job_queue: Package.Fetch.JobQueue = .{
+ .io = io,
+ .http_client = &http_client,
+ .global_cache = dirs.global_cache,
+ .local_storage = &.{
+ .cache_root = .{ .root_dir = dirs.local_cache, .sub_path = "" },
+ .pkg_root = pkg_root,
+ },
+ .recursive = true,
+ .debug_hash = false,
+ .unlazy_set = unlazy_set,
+ .fork_set = fork_set,
+ .mode = fetch_mode,
+ .prog_node = fetch_prog_node,
+ .read_only = system_pkg_dir_path != null,
+ };
+ defer job_queue.deinit();
- job_queue.all_fetches.appendAssumeCapacity(&fetch);
+ if (system_pkg_dir_path == null) {
+ try http_client.initDefaultProxies(arena, environ_map);
+ }
- job_queue.table.putAssumeCapacityNoClobber(
- Package.Fetch.relativePathDigest(phantom_package_root, dirs.global_cache),
- &fetch,
- );
+ try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
+ try job_queue.table.ensureUnusedCapacity(gpa, 1);
+
+ const phantom_package_root: Cache.Path = .{ .root_dir = build_root.directory };
+
+ var fetch: Package.Fetch = .{
+ .arena = std.heap.ArenaAllocator.init(gpa),
+ .location = .{ .relative_path = phantom_package_root },
+ .location_tok = 0,
+ .hash_tok = .none,
+ .name_tok = 0,
+ .lazy_status = .eager,
+ .remote_package_root = phantom_package_root,
+ .parent_package_root = phantom_package_root,
+ .parent_manifest_ast = null,
+ .prog_node = fetch_prog_node,
+ .job_queue = &job_queue,
+ .omit_missing_hash_error = true,
+ .allow_missing_paths_field = false,
+ .use_latest_commit = false,
+
+ .package_root = undefined,
+ .error_bundle = undefined,
+ .manifest = undefined,
+ .manifest_ast = undefined,
+ .have_manifest = false,
+ .computed_hash = undefined,
+ .has_build_zig = true,
+ .oom_flag = false,
+ .latest_commit = null,
- job_queue.group.async(io, Package.Fetch.workerRun, .{ &fetch, "root" });
- try job_queue.group.await(io);
+ .module = build_mod,
+ };
- {
- // Ensure that forks were actually used. This is done
- // before printing manifest errors because using a fork can
- // prevent them.
- var any_unused = false;
- for (fork_set.keys()) |*fork| {
- if (fork.uses == 0) {
- std.log.err("fork {f} matched no {s} packages", .{
- fork.path, fork.manifest.name,
- });
- any_unused = true;
- } else {
- std.log.info("fork {f} matched {d} {s} packages", .{
- fork.path, fork.uses, fork.manifest.name,
- });
+ job_queue.all_fetches.appendAssumeCapacity(&fetch);
+
+ job_queue.table.putAssumeCapacityNoClobber(
+ Package.Fetch.relativePathDigest(phantom_package_root, dirs.global_cache),
+ &fetch,
+ );
+
+ job_queue.group.async(io, Package.Fetch.workerRun, .{ &fetch, "root" });
+ try job_queue.group.await(io);
+
+ {
+ // Ensure that forks were actually used. This is done
+ // before printing manifest errors because using a fork can
+ // prevent them.
+ var any_unused = false;
+ for (fork_set.keys()) |*fork| {
+ if (fork.uses == 0) {
+ std.log.err("fork {f} matched no {s} packages", .{
+ fork.path, fork.manifest.name,
+ });
+ any_unused = true;
+ } else {
+ std.log.info("fork {f} matched {d} {s} packages", .{
+ fork.path, fork.uses, fork.manifest.name,
+ });
+ }
}
+ if (any_unused) process.exit(1);
}
- if (any_unused) process.exit(1);
- }
- try job_queue.consolidateErrors();
+ try job_queue.consolidateErrors();
- if (fetch.error_bundle.root_list.items.len > 0) {
- var errors = try fetch.error_bundle.toOwnedBundle("");
- errors.renderToStderr(io, .{}, color) catch {};
- process.exit(1);
- }
+ if (fetch.error_bundle.root_list.items.len > 0) {
+ var errors = try fetch.error_bundle.toOwnedBundle("");
+ errors.renderToStderr(io, .{}, color) catch {};
+ process.exit(1);
+ }
+
+ if (fetch_only) return cleanExit(io);
+
+ var source_buf = std.array_list.Managed(u8).init(gpa);
+ defer source_buf.deinit();
+ try job_queue.createDependenciesSource(&source_buf);
+ const deps_mod = try createDependenciesModule(
+ arena,
+ io,
+ source_buf.items,
+ root_mod,
+ dirs,
+ config,
+ );
- if (fetch_only) return cleanExit(io);
+ {
+ // We need a Module for each package's build.zig.
+ const hashes = job_queue.table.keys();
+ const fetches = job_queue.table.values();
+ try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
+ for (hashes, fetches) |*hash, f| {
+ if (f == &fetch) {
+ // The first one is a dummy package for the current project.
+ continue;
+ }
+ if (!f.has_build_zig)
+ continue;
+ const hash_slice = hash.toSlice();
+ const mod_root_path = try f.package_root.toString(arena);
+ const m = try Package.Module.create(arena, .{
+ .paths = .{
+ .root = try .fromUnresolved(arena, dirs, &.{mod_root_path}),
+ .root_src_path = Package.build_zig_basename,
+ },
+ .fully_qualified_name = try std.fmt.allocPrint(
+ arena,
+ "root.@dependencies.{s}",
+ .{hash_slice},
+ ),
+ .cc_argv = &.{},
+ .inherited = .{},
+ .global = config,
+ .parent = root_mod,
+ });
+ const hash_cloned = try arena.dupe(u8, hash_slice);
+ deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
+ f.module = m;
+ }
- var source_buf = std.array_list.Managed(u8).init(gpa);
- defer source_buf.deinit();
- try job_queue.createDependenciesSource(&source_buf);
- const deps_mod = try createDependenciesModule(
+ // Each build.zig module needs access to each of its
+ // dependencies' build.zig modules by name.
+ for (fetches) |f| {
+ const mod = f.module orelse continue;
+ if (!f.have_manifest) continue;
+ const man = &f.manifest;
+ const dep_names = man.dependencies.keys();
+ try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
+ for (dep_names, man.dependencies.values()) |name, dep| {
+ const dep_digest = Package.Fetch.depDigest(
+ f.package_root,
+ dirs.global_cache,
+ dep,
+ ) orelse continue;
+ const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue;
+ const name_cloned = try arena.dupe(u8, name);
+ mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
+ }
+ }
+ }
+ } else try createEmptyDependenciesModule(
arena,
io,
- source_buf.items,
root_mod,
dirs,
config,
);
- {
- // We need a Module for each package's build.zig.
- const hashes = job_queue.table.keys();
- const fetches = job_queue.table.values();
- try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
- for (hashes, fetches) |*hash, f| {
- if (f == &fetch) {
- // The first one is a dummy package for the current project.
- continue;
- }
- if (!f.has_build_zig)
- continue;
- const hash_slice = hash.toSlice();
- const mod_root_path = try f.package_root.toString(arena);
- const m = try Package.Module.create(arena, .{
- .paths = .{
- .root = try .fromUnresolved(arena, dirs, &.{mod_root_path}),
- .root_src_path = Package.build_zig_basename,
- },
- .fully_qualified_name = try std.fmt.allocPrint(
- arena,
- "root.@dependencies.{s}",
- .{hash_slice},
- ),
- .cc_argv = &.{},
- .inherited = .{},
- .global = config,
- .parent = root_mod,
- });
- const hash_cloned = try arena.dupe(u8, hash_slice);
- deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
- f.module = m;
- }
+ const compile_prog_node = root_prog_node.start("Compile Configure Script", 0);
+ defer compile_prog_node.end();
+
+ try root_mod.deps.put(arena, "@build", build_mod);
+
+ var create_diag: Compilation.CreateDiagnostic = undefined;
+ const comp = Compilation.create(gpa, arena, io, &create_diag, .{
+ .libc_installation = libc_installation,
+ .dirs = dirs,
+ .root_name = "configure",
+ .config = config,
+ .root_mod = root_mod,
+ .main_mod = build_mod,
+ .emit_bin = .yes_cache,
+ .self_exe_path = self_exe_path,
+ .thread_limit = thread_limit,
+ .verbose_cc = verbose_cc,
+ .verbose_link = verbose_link,
+ .verbose_air = verbose_air,
+ .verbose_intern_pool = verbose_intern_pool,
+ .verbose_generic_instances = verbose_generic_instances,
+ .verbose_llvm_ir = verbose_llvm_ir,
+ .verbose_llvm_bc = verbose_llvm_bc,
+ .verbose_llvm_cpu_features = verbose_llvm_cpu_features,
+ .cache_mode = .whole,
+ .reference_trace = reference_trace,
+ .debug_compile_errors = debug_compile_errors,
+ .environ_map = environ_map,
+ }) catch |err| switch (err) {
+ error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
+ else => |e| fatal("failed to create compilation: {t}", .{e}),
+ };
+ defer comp.destroy();
- // Each build.zig module needs access to each of its
- // dependencies' build.zig modules by name.
- for (fetches) |f| {
- const mod = f.module orelse continue;
- if (!f.have_manifest) continue;
- const man = &f.manifest;
- const dep_names = man.dependencies.keys();
- try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
- for (dep_names, man.dependencies.values()) |name, dep| {
- const dep_digest = Package.Fetch.depDigest(
- f.package_root,
- dirs.global_cache,
- dep,
- ) orelse continue;
- const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue;
- const name_cloned = try arena.dupe(u8, name);
- mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
- }
- }
- }
- } else try createEmptyDependenciesModule(
- arena,
- io,
- root_mod,
- dirs,
- config,
- );
+ updateModule(comp, color, compile_prog_node) catch |err| switch (err) {
+ error.CompileErrorsReported => process.exit(2),
+ else => |e| return e,
+ };
- try root_mod.deps.put(arena, "@build", build_mod);
-
- var create_diag: Compilation.CreateDiagnostic = undefined;
- const comp = Compilation.create(gpa, arena, io, &create_diag, .{
- .libc_installation = libc_installation,
- .dirs = dirs,
- .root_name = "build",
- .config = config,
- .root_mod = root_mod,
- .main_mod = build_mod,
- .emit_bin = .yes_cache,
- .self_exe_path = self_exe_path,
- .thread_limit = thread_limit,
- .verbose_cc = verbose_cc,
- .verbose_link = verbose_link,
- .verbose_air = verbose_air,
- .verbose_intern_pool = verbose_intern_pool,
- .verbose_generic_instances = verbose_generic_instances,
- .verbose_llvm_ir = verbose_llvm_ir,
- .verbose_llvm_bc = verbose_llvm_bc,
- .verbose_llvm_cpu_features = verbose_llvm_cpu_features,
- .cache_mode = .whole,
- .reference_trace = reference_trace,
- .debug_compile_errors = debug_compile_errors,
- .environ_map = environ_map,
- }) catch |err| switch (err) {
- error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
- else => fatal("failed to create compilation: {t}", .{err}),
- };
- defer comp.destroy();
+ // Since incremental compilation isn't done yet, we use cache_mode = whole
+ // above, and thus the output file is already closed.
+ //try comp.makeBinFileExecutable();
+ const hex_digest: []const u8 = &Cache.binToHex(comp.digest.?);
+ const exe_path: Path = .{
+ .root_dir = dirs.local_cache,
+ .sub_path = try std.fmt.allocPrint(arena, "o/{s}/{s}", .{ hex_digest, comp.emit_bin.? }),
+ };
+ _ = try config_man.addFilePath(exe_path, null);
+ configure_argv.items[0] = try exe_path.toString(arena);
- updateModule(comp, color, root_prog_node) catch |err| switch (err) {
- error.CompileErrorsReported => process.exit(2),
- else => |e| return e,
- };
+ switch (cache_poison) {
+ .pure, .disallowed, .ignored => if (try config_man.hit()) {
+ const digest = config_man.final();
+ break :cp .{
+ .{
+ .root_dir = dirs.local_cache,
+ .sub_path = try std.fmt.allocPrint(arena, "c/{s}", .{&digest}),
+ },
+ false,
+ };
+ },
+ .poisoned => {}, // Don't bother checking for cache hit.
+ }
+ }
- // Since incremental compilation isn't done yet, we use cache_mode = whole
- // above, and thus the output file is already closed.
- //try comp.makeBinFileExecutable();
- child_argv.items[argv_index_exe] = try dirs.local_cache.join(arena, &.{
- "o",
- &Cache.binToHex(comp.digest.?),
- comp.emit_bin.?,
- });
- }
+ if (!process.can_spawn) {
+ const cmd = try std.mem.join(arena, " ", configure_argv.items);
+ fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ native_os, cmd });
+ }
- if (!process.can_spawn) {
- const cmd = try std.mem.join(arena, " ", child_argv.items);
- fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ native_os, cmd });
- }
- switch (term: {
- _ = try io.lockStderr(&.{}, .no_color);
- defer io.unlockStderr();
- var child = std.process.spawn(io, .{
- .argv = child_argv.items,
- }) catch |err| fatal("failed to spawn build runner {s}: {t}", .{ child_argv.items[0], err });
- defer child.kill(io);
- break :term child.wait(io) catch |err|
- fatal("failed to wait build runner {s}: {t}", .{ child_argv.items[0], err });
- }) {
- .exited => |code| {
- if (code == 0) return cleanExit(io);
- // Indicates that the build runner has reported compile errors
- // and this parent process does not need to report any further
- // diagnostics.
- if (code == 2) process.exit(2);
-
- if (code == 3) {
- if (!dev.env.supports(.fetch_command)) process.exit(3);
- // Indicates the configure phase failed due to missing lazy
- // dependencies and stdout contains the hashes of the ones
- // that are missing.
- const s = fs.path.sep_str;
- const tmp_sub_path = "tmp" ++ s ++ results_tmp_file_nonce;
- const stdout = dirs.local_cache.handle.readFileAlloc(io, tmp_sub_path, arena, .limited(50 * 1024 * 1024)) catch |err| {
- fatal("unable to read results of configure phase from '{f}{s}': {t}", .{
- dirs.local_cache, tmp_sub_path, err,
- });
- };
- dirs.local_cache.handle.deleteFile(io, tmp_sub_path) catch {};
-
- var it = mem.splitScalar(u8, stdout, '\n');
- var any_errors = false;
- while (it.next()) |hash| {
- if (hash.len == 0) continue;
- if (hash.len > Package.Hash.max_len) {
- std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{
- hash.len, hash,
- });
- any_errors = true;
- continue;
- }
- try unlazy_set.put(arena, .fromSlice(hash), {});
+ const rand_int = randInt(io, u64);
+ const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
+ const config_tmp_path: Path = .{
+ .root_dir = dirs.local_cache,
+ .sub_path = tmp_dir_sub_path,
+ };
+ const config_tmp_file: Io.File = try config_tmp_path.root_dir.handle.createFile(
+ io,
+ config_tmp_path.sub_path,
+ .{ .read = true, .exclusive = true },
+ );
+ defer config_tmp_file.close(io);
+
+ const term = term: {
+ const child_node = root_prog_node.start("Run Configure Script", 0);
+ defer child_node.end();
+ var child = std.process.spawn(io, .{
+ .argv = configure_argv.items,
+ .stdout = .{ .file = config_tmp_file },
+ .progress_node = child_node,
+ }) catch |err| fatal("failed to spawn configure script {s}: {t}", .{ configure_argv.items[0], err });
+ defer child.kill(io);
+ break :term child.wait(io) catch |err|
+ fatal("failed to wait configure script {s}: {t}", .{ configure_argv.items[0], err });
+ };
+ if (!term.success()) {
+ // Failure to produce the configuration file.
+ const cmd = try std.mem.join(arena, " ", configure_argv.items);
+ fatal("the following configure command {f}:\n{s}", .{ term, cmd });
+ }
+ // Even though the file is designed to be sent directly to make
+ // runner, we must load it now because:
+ // * If it contains additional file dependencies, we need to
+ // add them to `config_man` before obtaining the final digest.
+ // * If it contains a set of lazy packages that need to be
+ // fetched, we need to fetch those now and re-run configure.
+ var configuration = std.Build.Configuration.loadFile(arena, io, config_tmp_file) catch |err|
+ fatal("failed to load configuration file {f}: {t}", .{ config_tmp_path, err });
+
+ if (configuration.unlazy_deps.len != 0) {
+ if (!dev.env.supports(.fetch_command)) process.exit(1);
+ var any_errors = false;
+ for (configuration.unlazy_deps) |hash_string| {
+ const hash = hash_string.slice(&configuration);
+ assert(hash.len != 0);
+ if (hash.len > Package.Hash.max_len) {
+ std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{ hash.len, hash });
+ any_errors = true;
+ continue;
}
- if (any_errors) process.exit(3);
- if (system_pkg_dir_path) |p| {
- // In this mode, the system needs to provide these packages; they
- // cannot be fetched by Zig.
- for (unlazy_set.keys()) |*hash| {
- std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{
- p, hash.toSlice(),
- });
- }
- std.log.info("remote package fetching disabled due to --system mode", .{});
- std.log.info("dependencies might be avoidable depending on build configuration", .{});
- process.exit(3);
+ try unlazy_set.put(arena, .fromSlice(hash), {});
+ }
+ if (any_errors) process.exit(1);
+ if (system_pkg_dir_path) |p| {
+ // In this mode, the system needs to provide these packages; they
+ // cannot be fetched by Zig.
+ const s = fs.path.sep_str;
+ for (unlazy_set.keys()) |*hash| {
+ std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ p, hash.toSlice() });
}
- continue;
+ std.log.info("remote package fetching disabled due to --system mode", .{});
+ std.log.info("dependencies might be avoidable depending on build configuration", .{});
+ process.exit(1);
}
+ continue :cp;
+ }
- const cmd = try std.mem.join(arena, " ", child_argv.items);
- fatal("the following build command failed with exit code {d}:\n{s}", .{ code, cmd });
- },
- .signal => |sig| {
- const cmd = try std.mem.join(arena, " ", child_argv.items);
- fatal("the following build command terminated with signal {t}:\n{s}", .{ sig, cmd });
- },
- .stopped => |sig| {
- const cmd = try std.mem.join(arena, " ", child_argv.items);
- fatal("the following build command stopped with signal {t}:\n{s}", .{ sig, cmd });
- },
- .unknown => {
- const cmd = try std.mem.join(arena, " ", child_argv.items);
- fatal("the following build command crashed:\n{s}", .{cmd});
- },
+ for (configuration.path_deps_base, configuration.path_deps_sub) |base, sub| {
+ const conf_path: std.Build.Configuration.Path = .{ .base = base, .sub = sub };
+ try config_man.addPathPost(conf_path.toCachePath(&configuration, arena));
+ }
+
+ // If it is poisoned, there is no point in moving it to cached
+ // location. Just leave it in the tmp directory.
+ if (configuration.poisoned) {
+ break :cp .{ config_tmp_path, true };
+ } else {
+ const digest = config_man.final();
+ const final_path: Path = .{
+ .root_dir = dirs.local_cache,
+ .sub_path = try std.fmt.allocPrint(arena, "c/{s}", .{&digest}),
+ };
+ Io.Dir.rename(
+ config_tmp_path.root_dir.handle,
+ config_tmp_path.sub_path,
+ final_path.root_dir.handle,
+ final_path.sub_path,
+ io,
+ ) catch |err| retry: {
+ const e = switch (err) {
+ error.FileNotFound => e: {
+ const dir_path = final_path.dirname().?;
+ dir_path.root_dir.handle.createDirPath(io, dir_path.sub_path) catch |e|
+ fatal("failed to create directory {f}: {t}", .{ dir_path, e });
+ if (Io.Dir.rename(
+ config_tmp_path.root_dir.handle,
+ config_tmp_path.sub_path,
+ final_path.root_dir.handle,
+ final_path.sub_path,
+ io,
+ )) |_| break :retry else |e| break :e e;
+ },
+ else => |e| e,
+ };
+ fatal("failed to rename configuration file from {f} into {f}: {t}", .{
+ config_tmp_path, final_path, e,
+ });
+ };
+ config_man.writeManifest() catch |err| warn("failed to write cache manifest: {t}", .{err});
+ break :cp .{ final_path, false };
+ }
+ };
+
+ {
+ // Release all file system locks just before running the maker process.
+ var configuration_lock = if (!poisoned) config_man.toOwnedLock() else null;
+ defer if (configuration_lock) |*l| l.release(io);
+
+ const make_runner = make_runner_task.await(io) catch |err| fatal("failed compiling maker: {t}", .{err});
+
+ make_argv.items[0] = try make_runner.exe_path.toString(arena);
+ make_argv.items[argv_index_configuration_file] = try configuration_path.toString(arena);
}
}
+
+ if (!process.can_spawn) {
+ const cmd = try std.mem.join(arena, " ", make_argv.items);
+ fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{
+ native_os, cmd,
+ });
+ }
+
+ const term = term: {
+ _ = try io.lockStderr(&.{}, .no_color);
+ defer io.unlockStderr();
+ var child = std.process.spawn(io, .{
+ .argv = make_argv.items,
+ }) catch |err| fatal("failed spawning maker {s}: {t}", .{ make_argv.items[0], err });
+ defer child.kill(io);
+ break :term child.wait(io) catch |err|
+ fatal("failed waiting on maker {s}: {t}", .{ make_argv.items[0], err });
+ };
+ if (term.success()) return cleanExit(io);
+ const cmd = try std.mem.join(arena, " ", make_argv.items);
+ fatal("the following maker command {f}:\n{s}", .{ term, cmd });
+}
+
+const MakeRunner = struct {
+ exe_path: Path,
+
+ const Options = struct {
+ environ_map: *const process.Environ.Map,
+ dirs: Compilation.Directories,
+ parent_prog_node: std.Progress.Node,
+ resolved_target: Package.Module.ResolvedTarget,
+ libc_installation: ?*const LibCInstallation,
+ self_exe_path: []const u8,
+ thread_limit: usize,
+ color: Color,
+ reference_trace: ?u32,
+ optimize_mode: std.builtin.OptimizeMode,
+ };
+};
+
+fn compileMakeRunner(gpa: Allocator, arena: Allocator, io: Io, options: MakeRunner.Options) !MakeRunner {
+ const compile_prog_node = options.parent_prog_node.start("Compile Maker", 0);
+ defer compile_prog_node.end();
+
+ const strip = options.optimize_mode != .Debug;
+
+ const main_mod_paths: Package.Module.CreateOptions.Paths = .{
+ .root = try .fromRoot(arena, options.dirs, .zig_lib, "compiler"),
+ .root_src_path = "Maker.zig",
+ };
+
+ const config = try Compilation.Config.resolve(.{
+ .output_mode = .Exe,
+ .root_strip = strip,
+ .root_optimize_mode = options.optimize_mode,
+ .resolved_target = options.resolved_target,
+ .have_zcu = true,
+ .emit_bin = true,
+ .is_test = false,
+ });
+
+ const root_mod = try Package.Module.create(arena, .{
+ .paths = main_mod_paths,
+ .fully_qualified_name = "root",
+ .cc_argv = &.{},
+ .inherited = .{
+ .resolved_target = options.resolved_target,
+ .optimize_mode = options.optimize_mode,
+ .strip = strip,
+ },
+ .global = config,
+ .parent = null,
+ });
+
+ var create_diag: Compilation.CreateDiagnostic = undefined;
+ const comp = Compilation.create(gpa, arena, io, &create_diag, .{
+ .dirs = options.dirs,
+ .root_name = "maker",
+ .config = config,
+ .root_mod = root_mod,
+ .main_mod = root_mod,
+ .emit_bin = .yes_cache,
+ .self_exe_path = options.self_exe_path,
+ .thread_limit = options.thread_limit,
+ .cache_mode = .whole,
+ .environ_map = options.environ_map,
+ .reference_trace = options.reference_trace,
+ }) catch |err| switch (err) {
+ error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
+ error.Canceled => |e| return e,
+ else => |e| fatal("failed to create compilation: {t}", .{e}),
+ };
+ defer comp.destroy();
+
+ try updateModule(comp, options.color, compile_prog_node);
+
+ const exe_path: Path = .{
+ .root_dir = options.dirs.global_cache,
+ .sub_path = try std.fmt.allocPrint(arena, "o/{s}/{s}", .{
+ &Cache.binToHex(comp.digest.?), comp.emit_bin.?,
+ }),
+ };
+
+ return .{
+ .exe_path = exe_path,
+ };
}
const Fork = struct {
@@ -5634,6 +5883,20 @@ const Fork = struct {
failed: bool,
arena_allocator: std.heap.ArenaAllocator,
+ fn init(cwd_relative_path: []const u8) Fork {
+ return .{
+ .manifest_ast = undefined,
+ .manifest = undefined,
+ .error_bundle = undefined,
+ .arena_allocator = undefined,
+ .path = .{
+ .root_dir = .cwd(),
+ .sub_path = cwd_relative_path,
+ },
+ .failed = false,
+ };
+ }
+
fn load(io: Io, gpa: Allocator, fork: *Fork, color: Color) Io.Cancelable!void {
loadFallible(io, gpa, fork, color) catch |err| switch (err) {
error.Canceled => |e| return e,
@@ -5749,6 +6012,8 @@ fn jitCmdInner(
const override_lib_dir: ?[]const u8 = EnvVar.ZIG_LIB_DIR.get(environ_map);
const override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map);
+ const cwd_path = try introspect.getResolvedCwd(io, arena);
+
// This `init` calls `fatal` on error.
var dirs: Compilation.Directories = .init(
arena,
@@ -5759,6 +6024,7 @@ fn jitCmdInner(
preopens,
self_exe_path,
environ_map,
+ cwd_path,
);
defer dirs.deinit(io);
@@ -5829,7 +6095,7 @@ fn jitCmdInner(
.environ_map = environ_map,
}) catch |err| switch (err) {
error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
- else => fatal("failed to create compilation: {s}", .{@errorName(err)}),
+ else => fatal("failed to create compilation: {t}", .{err}),
};
defer comp.destroy();
@@ -6582,7 +6848,11 @@ fn warnAboutForeignBinaries(
const host_query: std.Target.Query = .{};
const host_target = std.zig.resolveTargetQueryOrFatal(io, host_query);
- switch (std.zig.system.getExternalExecutor(io, &host_target, target, .{ .link_libc = link_libc })) {
+ switch (std.zig.system.getExternalExecutor(io, target, .{
+ .host_cpu_arch = host_target.cpu.arch,
+ .host_os_tag = host_target.os.tag,
+ .link_libc = link_libc,
+ })) {
.native => return,
.rosetta => {
const host_name = try host_target.zigTriple(arena);
diff --git a/src/print_env.zig b/src/print_env.zig
@@ -8,6 +8,7 @@ const fatal = std.process.fatal;
const build_options = @import("build_options");
const Compilation = @import("Compilation.zig");
+const introspect = @import("introspect.zig");
pub fn cmdEnv(
arena: Allocator,
@@ -28,6 +29,8 @@ pub fn cmdEnv(
},
};
+ const cwd_path = try introspect.getResolvedCwd(io, arena);
+
var dirs: Compilation.Directories = .init(
arena,
io,
@@ -37,6 +40,7 @@ pub fn cmdEnv(
preopens,
if (builtin.target.os.tag != .wasi) self_exe_path,
environ_map,
+ cwd_path,
);
defer dirs.deinit(io);
diff --git a/test/src/Cases.zig b/test/src/Cases.zig
@@ -470,8 +470,9 @@ pub fn lowerToBuildSteps(
options: CaseTestOptions,
) void {
const io = self.io;
+ const graph = b.graph;
+ const arena = graph.arena;
const host = b.resolveTargetQuery(.{});
- const cases_dir_path = b.build_root.join(b.allocator, &.{ "test", "cases" }) catch @panic("OOM");
for (self.cases.items) |case| {
for (options.test_filters) |test_filter| {
@@ -504,7 +505,7 @@ pub fn lowerToBuildSteps(
);
if (options.skip_llvm and would_use_llvm) continue;
- const triple_txt = case.target.query.zigTriple(b.allocator) catch @panic("OOM");
+ const triple_txt = case.target.query.zigTriple(arena) catch @panic("OOM");
if (options.test_target_filters.len > 0) {
for (options.test_target_filters) |filter| {
@@ -516,7 +517,7 @@ pub fn lowerToBuildSteps(
continue;
const writefiles = b.addWriteFiles();
- var file_sources = std.StringHashMap(std.Build.LazyPath).init(b.allocator);
+ var file_sources = std.StringHashMap(std.Build.LazyPath).init(arena);
defer file_sources.deinit();
const first_file = case.files.items[0];
const root_source_file = writefiles.add(first_file.path, first_file.src);
@@ -526,12 +527,15 @@ pub fn lowerToBuildSteps(
}
for (case.imports) |import_rel| {
- const import_abs = std.fs.path.join(b.allocator, &.{
- cases_dir_path,
- case.import_path orelse @panic("import_path not set"),
- import_rel,
- }) catch @panic("OOM");
- _ = writefiles.addCopyFile(.{ .cwd_relative = import_abs }, import_rel);
+ _ = writefiles.addCopyFile(.{ .src_path = .{
+ .owner = b,
+ .sub_path = b.pathJoin(&.{
+ "test",
+ "cases",
+ case.import_path orelse @panic("import_path not set"),
+ import_rel,
+ }),
+ } }, import_rel);
}
const mod = b.createModule(.{
@@ -605,7 +609,11 @@ pub fn lowerToBuildSteps(
},
.Execution => |expected_stdout| no_exec: {
const run = if (case.target.result.ofmt == .c) run_step: {
- if (getExternalExecutor(io, &host.result, &case.target.result, .{ .link_libc = true }) != .native) {
+ if (getExternalExecutor(io, &case.target.result, .{
+ .host_cpu_arch = host.result.cpu.arch,
+ .host_os_tag = host.result.os.tag,
+ .link_libc = true,
+ }) != .native) {
// We wouldn't be able to run the compiled C code.
break :no_exec;
}
diff --git a/test/src/Libc.zig b/test/src/Libc.zig
@@ -31,9 +31,11 @@ pub fn addLibcTestCase(
supports_wasi_libc: bool,
options: LibcTestCaseOption,
) void {
- const name = libc.b.dupe(path[0 .. path.len - std.fs.path.extension(path).len]);
+ const graph = libc.b.graph;
+ const arena = graph.arena;
+ const name = arena.dupe(u8, path[0 .. path.len - std.fs.path.extension(path).len]) catch @panic("OOM");
std.mem.replaceScalar(u8, name, '/', '.');
- libc.test_cases.append(libc.b.allocator, .{
+ libc.test_cases.append(arena, .{
.name = name,
.src_file = libc.libc_test_src_path.path(libc.b, path),
.additional_src_file = if (options.additional_src_file) |additional_src_file| libc.libc_test_src_path.path(libc.b, additional_src_file) else null,
@@ -112,6 +114,7 @@ pub fn addTarget(libc: *const Libc, target: std.Build.ResolvedTarget) void {
const run = libc.b.addRunArtifact(exe);
run.setName(annotated_case_name);
run.skip_foreign_checks = true;
+ run.disable_zig_progress = true; // can interfere with fd count assumptions
run.expectStdErrEqual("");
run.expectStdOutEqual("");
run.expectExitCode(0);
diff --git a/test/standalone/build.zig.zon b/test/standalone/build.zig.zon
@@ -153,9 +153,6 @@
.compiler_rt_panic = .{
.path = "compiler_rt_panic",
},
- .ios = .{
- .path = "ios",
- },
.depend_on_main_mod = .{
.path = "depend_on_main_mod",
},
@@ -171,9 +168,6 @@
.run_output_paths = .{
.path = "run_output_paths",
},
- .run_output_caching = .{
- .path = "run_output_caching",
- },
.empty_global_error_set = .{
.path = "empty_global_error_set",
},
diff --git a/test/standalone/cmakedefine/build.zig b/test/standalone/cmakedefine/build.zig
@@ -48,7 +48,6 @@ pub fn build(b: *std.Build) void {
.include_path = "stack.h",
},
.{
- .AT = "@",
.UNDERSCORE = "_",
.NEST_UNDERSCORE_PROXY = "UNDERSCORE",
.NEST_PROXY = "NEST_UNDERSCORE_PROXY",
diff --git a/test/standalone/dependency_options/build.zig b/test/standalone/dependency_options/build.zig
@@ -10,7 +10,7 @@ pub fn build(b: *std.Build) !void {
const none_specified_mod = none_specified.module("dummy");
if (!none_specified_mod.resolved_target.?.query.eql(b.graph.host.query)) return error.TestFailed;
- const expected_optimize: std.builtin.OptimizeMode = switch (b.release_mode) {
+ const expected_optimize: std.builtin.OptimizeMode = switch (b.graph.release_mode) {
.off => .Debug,
.any => unreachable,
.fast => .ReleaseFast,
diff --git a/test/standalone/dirname/build.zig b/test/standalone/dirname/build.zig
@@ -27,41 +27,16 @@ pub fn build(b: *std.Build) void {
}),
});
- const has_basename = b.addExecutable(.{
- .name = "has_basename",
- .root_module = b.createModule(.{
- .root_source_file = b.path("has_basename.zig"),
- .optimize = .Debug,
- .target = target,
- }),
- });
-
- // Known path:
- addTestRun(test_step, exists_in, touch_src.dirname(), &.{"touch.zig"});
-
- // Generated file:
- addTestRun(test_step, exists_in, generated.dirname(), &.{"generated.txt"});
-
- // Generated file multiple levels:
- addTestRun(test_step, exists_in, generated.dirname().dirname(), &.{
+ addTestRun(test_step, exists_in, "run exists_in (known path)", touch_src.dirname(), &.{"touch.zig"});
+ addTestRun(test_step, exists_in, "run exists_in (generated file)", generated.dirname(), &.{"generated.txt"});
+ addTestRun(test_step, exists_in, "run exists_in (generated file multi level)", generated.dirname().dirname(), &.{
"subdir" ++ std.fs.path.sep_str ++ "generated.txt",
});
- // Cache root:
- const cache_dir = b.cache_root.path orelse
- (b.cache_root.join(b.allocator, &.{"."}) catch @panic("OOM"));
- addTestRun(
- test_step,
- has_basename,
- generated.dirname().dirname().dirname().dirname(),
- &.{std.fs.path.basename(cache_dir)},
- );
-
- // Absolute path:
const write_files = b.addWriteFiles();
_ = write_files.add("foo.txt", "");
const abs_path = write_files.getDirectory();
- addTestRun(test_step, exists_in, abs_path, &.{"foo.txt"});
+ addTestRun(test_step, exists_in, "run exists_in (absolute path)", abs_path, &.{"foo.txt"});
}
// Runs exe with the parameters [dirname, args...].
@@ -69,10 +44,12 @@ pub fn build(b: *std.Build) void {
fn addTestRun(
test_step: *std.Build.Step,
exe: *std.Build.Step.Compile,
+ step_name: []const u8,
dirname: std.Build.LazyPath,
args: []const []const u8,
) void {
const run = test_step.owner.addRunArtifact(exe);
+ run.setName(step_name);
run.addDirectoryArg(dirname);
run.addArgs(args);
run.expectExitCode(0);
diff --git a/test/standalone/dirname/touch.zig b/test/standalone/dirname/touch.zig
@@ -7,27 +7,26 @@
//! Path must be absolute.
const std = @import("std");
+const Io = std.Io;
pub fn main(init: std.process.Init) !void {
+ const io = init.io;
+
var args = try init.minimal.args.iterateAllocator(init.gpa);
defer args.deinit();
- _ = args.next() orelse unreachable; // skip binary name
+ _ = args.next().?; // skip binary name
const path = args.next() orelse {
std.log.err("missing <path> argument", .{});
return error.BadUsage;
};
- const dir_path = std.Io.Dir.path.dirname(path) orelse unreachable;
- const basename = std.Io.Dir.path.basename(path);
-
- const io = std.Io.Threaded.global_single_threaded.io();
+ const dir_path = Io.Dir.path.dirname(path).?;
+ const basename = Io.Dir.path.basename(path);
- var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{});
+ var dir = try Io.Dir.cwd().openDir(io, dir_path, .{});
defer dir.close(io);
- _ = dir.statFile(io, basename, .{}) catch {
- var file = try dir.createFile(io, basename, .{});
- file.close(io);
- };
+ var file = try dir.createFile(io, basename, .{ .truncate = false });
+ file.close(io);
}
diff --git a/test/standalone/install_headers/build.zig b/test/standalone/install_headers/build.zig
@@ -106,7 +106,7 @@ pub fn build(b: *std.Build) void {
"custom/include/foo/config.h",
"custom/include/bar.h",
});
- run_check_exists.setCwd(.{ .cwd_relative = b.getInstallPath(.prefix, "") });
+ run_check_exists.setCwd(.{ .relative = .{ .base = .install_prefix } });
run_check_exists.expectExitCode(0);
run_check_exists.step.dependOn(&install_libfoo.step);
test_step.dependOn(&run_check_exists.step);
diff --git a/test/standalone/ios/build.zig b/test/standalone/ios/build.zig
@@ -1,40 +0,0 @@
-const std = @import("std");
-
-pub const requires_symlinks = true;
-pub const requires_ios_sdk = true;
-
-pub fn build(b: *std.Build) void {
- const test_step = b.step("test", "Test it");
- b.default_step = test_step;
-
- const optimize: std.builtin.OptimizeMode = .Debug;
- const target = b.resolveTargetQuery(.{
- .cpu_arch = .aarch64,
- .os_tag = .ios,
- });
-
- const exe = b.addExecutable(.{
- .name = "main",
- .root_module = b.createModule(.{
- .root_source_file = null,
- .optimize = optimize,
- .target = target,
- .link_libc = true,
- }),
- });
-
- const io = b.graph.io;
-
- if (std.zig.system.darwin.getSdk(b.allocator, io, &target.result)) |sdk| {
- b.sysroot = sdk;
- exe.root_module.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/include" }) });
- exe.root_module.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/System/Library/Frameworks" }) });
- exe.root_module.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/lib" }) });
- } else {
- exe.step.dependOn(&b.addFail("no iOS SDK found").step);
- }
-
- exe.root_module.addCSourceFile(.{ .file = b.path("main.m"), .flags = &.{} });
- exe.root_module.linkFramework("Foundation", .{});
- exe.root_module.linkFramework("UIKit", .{});
-}
diff --git a/test/standalone/ios/main.m b/test/standalone/ios/main.m
@@ -1,34 +0,0 @@
-#import <UIKit/UIKit.h>
-
-@interface AppDelegate : UIResponder <UIApplicationDelegate>
-@property (strong, nonatomic) UIWindow *window;
-@end
-
-int main() {
- @autoreleasepool {
- return UIApplicationMain(0, nil, nil, NSStringFromClass([AppDelegate class]));
- }
-}
-
-@implementation AppDelegate
-
-- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(id)options {
- CGRect mainScreenBounds = [[UIScreen mainScreen] bounds];
- self.window = [[UIWindow alloc] initWithFrame:mainScreenBounds];
- UIViewController *viewController = [[UIViewController alloc] init];
- viewController.view.frame = mainScreenBounds;
-
- NSString* msg = @"Hello world";
-
- UILabel *label = [[UILabel alloc] initWithFrame:mainScreenBounds];
- [label setText:msg];
- [viewController.view addSubview: label];
-
- self.window.rootViewController = viewController;
-
- [self.window makeKeyAndVisible];
-
- return YES;
-}
-
-@end
diff --git a/test/standalone/libfuzzer/build.zig b/test/standalone/libfuzzer/build.zig
@@ -24,6 +24,6 @@ pub fn build(b: *std.Build) void {
b.default_step = run_step;
const run_artifact = b.addRunArtifact(exe);
- run_artifact.addArg(b.cache_root.path orelse "");
+ run_artifact.addFileArg(.cache_root);
run_step.dependOn(&run_artifact.step);
}
diff --git a/test/standalone/run_output_caching/build.zig b/test/standalone/run_output_caching/build.zig
@@ -1,140 +0,0 @@
-const builtin = @import("builtin");
-const std = @import("std");
-
-pub fn build(b: *std.Build) void {
- const test_step = b.step("test", "Test it");
- b.default_step = test_step;
-
- if (builtin.os.tag == .windows) return; // https://codeberg.org/ziglang/zig/issues/31564
-
- const target = b.standardTargetOptions(.{});
- const optimize = b.standardOptimizeOption(.{});
-
- const exe = b.addExecutable(.{
- .name = "create-file",
- .root_module = b.createModule(.{
- .root_source_file = b.path("main.zig"),
- .target = target,
- .optimize = optimize,
- }),
- });
-
- {
- const run_random_with_sideeffects_first = b.addRunArtifact(exe);
- run_random_with_sideeffects_first.setName("run with side-effects (first)");
- run_random_with_sideeffects_first.has_side_effects = true;
-
- const run_random_with_sideeffects_second = b.addRunArtifact(exe);
- run_random_with_sideeffects_second.setName("run with side-effects (second)");
- run_random_with_sideeffects_second.has_side_effects = true;
-
- // ensure that "second" runs after "first"
- run_random_with_sideeffects_second.step.dependOn(&run_random_with_sideeffects_first.step);
-
- const first_output = run_random_with_sideeffects_first.addOutputFileArg("a.txt");
- const second_output = run_random_with_sideeffects_second.addOutputFileArg("a.txt");
-
- const expect_uncached_dependencies = CheckOutputCaching.init(b, false, &.{ first_output, second_output });
- test_step.dependOn(&expect_uncached_dependencies.step);
-
- const expect_unequal_output = CheckPathEquality.init(b, true, &.{ first_output, second_output });
- test_step.dependOn(&expect_unequal_output.step);
-
- const check_first_output = b.addCheckFile(first_output, .{ .expected_matches = &.{"a.txt"} });
- test_step.dependOn(&check_first_output.step);
- const check_second_output = b.addCheckFile(second_output, .{ .expected_matches = &.{"a.txt"} });
- test_step.dependOn(&check_second_output.step);
- }
-
- {
- const run_random_without_sideeffects_1 = b.addRunArtifact(exe);
- run_random_without_sideeffects_1.setName("run without side-effects (A)");
-
- const run_random_without_sideeffects_2 = b.addRunArtifact(exe);
- run_random_without_sideeffects_2.setName("run without side-effects (B)");
-
- run_random_without_sideeffects_2.step.dependOn(&run_random_without_sideeffects_1.step);
-
- const first_output = run_random_without_sideeffects_1.addOutputFileArg("a.txt");
- const second_output = run_random_without_sideeffects_2.addOutputFileArg("a.txt");
-
- const expect_cached_dependencies = CheckOutputCaching.init(b, true, &.{second_output});
- test_step.dependOn(&expect_cached_dependencies.step);
-
- const expect_equal_output = CheckPathEquality.init(b, true, &.{ first_output, second_output });
- test_step.dependOn(&expect_equal_output.step);
-
- const check_first_output = b.addCheckFile(first_output, .{ .expected_matches = &.{"a.txt"} });
- test_step.dependOn(&check_first_output.step);
- const check_second_output = b.addCheckFile(second_output, .{ .expected_matches = &.{"a.txt"} });
- test_step.dependOn(&check_second_output.step);
- }
-}
-
-const CheckOutputCaching = struct {
- step: std.Build.Step,
- expect_caching: bool,
-
- pub fn init(owner: *std.Build, expect_caching: bool, output_paths: []const std.Build.LazyPath) *CheckOutputCaching {
- const check = owner.allocator.create(CheckOutputCaching) catch @panic("OOM");
- check.* = .{
- .step = std.Build.Step.init(.{
- .id = .custom,
- .name = "check output caching",
- .owner = owner,
- .makeFn = make,
- }),
- .expect_caching = expect_caching,
- };
- for (output_paths) |output_path| {
- output_path.addStepDependencies(&check.step);
- }
- return check;
- }
-
- fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
- const check: *CheckOutputCaching = @fieldParentPtr("step", step);
-
- for (step.dependencies.items) |dependency| {
- if (check.expect_caching) {
- if (dependency.result_cached) continue;
- return step.fail("expected '{s}' step to be cached, but it was not", .{dependency.name});
- } else {
- if (!dependency.result_cached) continue;
- return step.fail("expected '{s}' step to not be cached, but it was", .{dependency.name});
- }
- }
- }
-};
-
-const CheckPathEquality = struct {
- step: std.Build.Step,
- expected_equality: bool,
- output_paths: []const std.Build.LazyPath,
-
- pub fn init(owner: *std.Build, expected_equality: bool, output_paths: []const std.Build.LazyPath) *CheckPathEquality {
- const check = owner.allocator.create(CheckPathEquality) catch @panic("OOM");
- check.* = .{
- .step = std.Build.Step.init(.{
- .id = .custom,
- .name = "check output path equality",
- .owner = owner,
- .makeFn = make,
- }),
- .expected_equality = expected_equality,
- .output_paths = owner.allocator.dupe(std.Build.LazyPath, output_paths) catch @panic("OOM"),
- };
- for (output_paths) |output_path| {
- output_path.addStepDependencies(&check.step);
- }
- return check;
- }
-
- fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
- const check: *CheckPathEquality = @fieldParentPtr("step", step);
- std.debug.assert(check.output_paths.len != 0);
- for (check.output_paths[0 .. check.output_paths.len - 1], check.output_paths[1..]) |a, b| {
- try std.testing.expectEqual(check.expected_equality, std.mem.eql(u8, a.getPath(step.owner), b.getPath(step.owner)));
- }
- }
-};
diff --git a/test/standalone/run_output_caching/main.zig b/test/standalone/run_output_caching/main.zig
@@ -1,11 +0,0 @@
-const std = @import("std");
-
-pub fn main(init: std.process.Init) !void {
- const io = init.io;
- var args = try init.minimal.args.iterateAllocator(init.arena.allocator());
- _ = args.skip();
- const filename = args.next().?;
- const file = try std.Io.Dir.cwd().createFile(io, filename, .{});
- defer file.close(io);
- try file.writeStreamingAll(io, filename);
-}
diff --git a/test/standalone/windows_resources/build.zig b/test/standalone/windows_resources/build.zig
@@ -38,7 +38,7 @@ fn add(
.file = b.path("res/zig.rc"),
.flags = &.{"/c65001"}, // UTF-8 code page
.include_paths = &.{
- .{ .generated = .{ .file = &generated_h_step.generated_directory } },
+ .{ .generated = .{ .index = generated_h_step.generated_directory } },
},
});
exe.rc_includes = switch (rc_includes) {
diff --git a/test/tests.zig b/test/tests.zig
@@ -2433,8 +2433,10 @@ pub fn addCliTests(b: *std.Build) *Step {
});
run_test.addArg("--build-file");
run_test.addFileArg(b.path("test/cli/options/build.zig"));
+
run_test.addArg("--cache-dir");
- run_test.addFileArg(.{ .cwd_relative = b.cache_root.join(b.allocator, &.{}) catch @panic("OOM") });
+ run_test.addFileArg(.cache_root);
+
run_test.setName("test build options");
step.dependOn(&run_test.step);
@@ -2890,7 +2892,7 @@ pub fn addCases(
var cases = @import("src/Cases.zig").init(gpa, arena, io);
- var dir = try b.build_root.handle.openDir(io, "test/cases", .{ .iterate = true });
+ var dir = try b.root.openDir(io, "test/cases", .{ .iterate = true });
defer dir.close(io);
cases.addFromDir(dir, b);
@@ -2948,7 +2950,7 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons
}),
});
- var dir = try b.build_root.handle.openDir(io, "test/incremental", .{ .iterate = true });
+ var dir = try b.root.openDir(io, "test/incremental", .{ .iterate = true });
defer dir.close(io);
var it = try dir.walk(b.graph.arena);
@@ -2966,10 +2968,11 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons
run.addArg(b.graph.zig_exe);
run.addFileArg(b.path("test/incremental/").path(b, entry.path));
- run.addArgs(&.{
- "--zig-lib-dir", b.graph.zig_lib_directory.path orelse ".",
- "--target", target_str,
- });
+
+ run.addArg("--zig-lib-dir");
+ run.addDirectoryArg(.zig_lib);
+
+ run.addArgs(&.{ "--target", target_str });
run.addArg("--quiet"); // don't fill stderr telling us about skipped tests etc
diff --git a/tools/docgen.zig b/tools/docgen.zig
@@ -3,6 +3,7 @@ const builtin = @import("builtin");
const std = @import("std");
const Io = std.Io;
const Dir = std.Io.Dir;
+const Path = std.Build.Cache.Path;
const process = std.process;
const Progress = std.Progress;
const print = std.debug.print;
@@ -73,8 +74,13 @@ pub fn main(init: std.process.Init) !void {
var out_file_buffer: [4096]u8 = undefined;
var out_file_writer = out_file.writer(io, &out_file_buffer);
- var code_dir = try Dir.cwd().openDir(io, code_dir_path, .{});
- defer code_dir.close(io);
+ var code_dir: Path = .{
+ .root_dir = .{
+ .handle = try Dir.cwd().openDir(io, code_dir_path, .{}),
+ .path = code_dir_path,
+ },
+ };
+ defer code_dir.root_dir.handle.close(io);
var in_file_reader = in_file.reader(io, &.{});
const input_file_bytes = try in_file_reader.interface.allocRemaining(arena, .limited(max_doc_file_size));
@@ -988,7 +994,7 @@ fn genHtml(
io: Io,
tokenizer: *Tokenizer,
toc: *Toc,
- code_dir: Dir,
+ code_dir: Path,
out: *Writer,
) !void {
for (toc.nodes) |node| {
@@ -1044,8 +1050,13 @@ fn genHtml(
});
defer allocator.free(out_basename);
- const contents = code_dir.readFileAlloc(io, out_basename, allocator, .limited(std.math.maxInt(u32))) catch |err| {
- return parseError(tokenizer, code.token, "unable to open '{s}': {t}", .{ out_basename, err });
+ const out_path: Path = .{
+ .root_dir = code_dir.root_dir,
+ .sub_path = out_basename,
+ };
+
+ const contents = out_path.root_dir.handle.readFileAlloc(io, out_path.sub_path, allocator, .unlimited) catch |err| {
+ return parseError(tokenizer, code.token, "failed opening {f}: {t}", .{ out_path, err });
};
defer allocator.free(contents);
diff --git a/tools/doctest.zig b/tools/doctest.zig
@@ -311,7 +311,9 @@ fn printOutput(
.arch_os_abi = triple,
});
const target = try std.zig.system.resolveTargetQuery(io, target_query);
- switch (getExternalExecutor(io, &host, &target, .{
+ switch (getExternalExecutor(io, &target, .{
+ .host_cpu_arch = host.cpu.arch,
+ .host_os_tag = host.os.tag,
.link_libc = code.link_libc,
})) {
.native => {},
@@ -526,7 +528,10 @@ fn printOutput(
.lib => {
const bin_basename = try std.zig.binNameAlloc(arena, .{
.root_name = code_name,
- .target = &builtin.target,
+ .cpu_arch = builtin.target.cpu.arch,
+ .os_tag = builtin.target.os.tag,
+ .ofmt = builtin.target.ofmt,
+ .abi = builtin.target.abi,
.output_mode = .Lib,
});
diff --git a/tools/incr-check.zig b/tools/incr-check.zig
@@ -360,7 +360,10 @@ const Eval = struct {
const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{
.root_name = "root", // corresponds to the module name "root"
- .target = &eval.target,
+ .cpu_arch = eval.target.cpu.arch,
+ .os_tag = eval.target.os.tag,
+ .ofmt = eval.target.ofmt,
+ .abi = eval.target.abi,
.output_mode = .Exe,
});
const bin_path = try Dir.path.join(arena, &.{ result_dir, bin_name });
@@ -487,9 +490,12 @@ const Eval = struct {
var argv_buf: [2][]const u8 = undefined;
const argv: []const []const u8, const is_foreign: bool = sw: switch (std.zig.system.getExternalExecutor(
io,
- &eval.host,
&eval.target,
- .{ .link_libc = eval.backend == .cbe },
+ .{
+ .link_libc = eval.backend == .cbe,
+ .host_cpu_arch = eval.host.cpu.arch,
+ .host_os_tag = eval.host.os.tag,
+ },
)) {
.bad_dl, .bad_os_or_cpu => {
// This binary cannot be executed on this host.