authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-02-15 16:03:37-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 18:54:33-07:00
log2e88ac8842a6a5bbd9fe654292ab416cadfaf8bf
tree55716dcecb4e6c749261dbbcf19b0f1ea8f3ac7a
parente10cbf08eeed53561b5efe87a8cae0d7827f7301

zig build: configure runner basics implemented


37 files changed, 9743 insertions(+), 9540 deletions(-)

lib/compiler/build_runner.zig deleted-1857
......@@ -1,1857 +0,0 @@
1const runner = @This();
2const builtin = @import("builtin");
3
4const std = @import("std");
5const Io = std.Io;
6const assert = std.debug.assert;
7const fmt = std.fmt;
8const mem = std.mem;
9const process = std.process;
10const File = std.Io.File;
11const Step = std.Build.Step;
12const Watch = std.Build.Watch;
13const WebServer = std.Build.WebServer;
14const Allocator = std.mem.Allocator;
15const fatal = std.process.fatal;
16const Writer = std.Io.Writer;
17
18pub const root = @import("@build");
19pub const dependencies = @import("@dependencies");
20
21pub const std_options: std.Options = .{
22 .side_channels_mitigations = .none,
23 .http_disable_tls = true,
24};
25
26pub fn main(init: process.Init.Minimal) !void {
27 // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not
28 // always the case. So, we do need a true gpa for some things.
29 var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{});
30 defer _ = safe_gpa_state.deinit();
31 const gpa = safe_gpa_state.allocator();
32
33 var threaded: std.Io.Threaded = .init(gpa, .{
34 .environ = init.environ,
35 .argv0 = .init(init.args),
36 });
37 defer threaded.deinit();
38 const io = threaded.io();
39
40 // ...but we'll back our arena by `std.heap.page_allocator` for efficiency.
41 var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
42 defer arena_instance.deinit();
43 const arena = arena_instance.allocator();
44
45 const args = try init.args.toSlice(arena);
46
47 // skip my own exe name
48 var arg_idx: usize = 1;
49
50 const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{});
51 const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{});
52 const build_root = nextArg(args, &arg_idx) orelse fatal("missing build root directory path", .{});
53 const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{});
54 const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{});
55
56 const cwd: Io.Dir = .cwd();
57
58 const zig_lib_directory: std.Build.Cache.Directory = .{
59 .path = zig_lib_dir,
60 .handle = try cwd.openDir(io, zig_lib_dir, .{}),
61 };
62
63 const build_root_directory: std.Build.Cache.Directory = .{
64 .path = build_root,
65 .handle = try cwd.openDir(io, build_root, .{}),
66 };
67
68 const local_cache_directory: std.Build.Cache.Directory = .{
69 .path = cache_root,
70 .handle = try cwd.createDirPathOpen(io, cache_root, .{}),
71 };
72
73 const global_cache_directory: std.Build.Cache.Directory = .{
74 .path = global_cache_root,
75 .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}),
76 };
77
78 var graph: std.Build.Graph = .{
79 .io = io,
80 .arena = arena,
81 .cache = .{
82 .io = io,
83 .gpa = gpa,
84 .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}),
85 .cwd = try process.currentPathAlloc(io, arena),
86 },
87 .zig_exe = zig_exe,
88 .environ_map = try init.environ.createMap(arena),
89 .global_cache_root = global_cache_directory,
90 .zig_lib_directory = zig_lib_directory,
91 .host = .{
92 .query = .{},
93 .result = try std.zig.system.resolveTargetQuery(io, .{}),
94 },
95 .time_report = false,
96 };
97
98 graph.cache.addPrefix(.{ .path = null, .handle = cwd });
99 graph.cache.addPrefix(build_root_directory);
100 graph.cache.addPrefix(local_cache_directory);
101 graph.cache.addPrefix(global_cache_directory);
102 graph.cache.hash.addBytes(builtin.zig_version_string);
103
104 const builder = try std.Build.create(
105 &graph,
106 build_root_directory,
107 local_cache_directory,
108 dependencies.root_deps,
109 );
110
111 var targets = std.array_list.Managed([]const u8).init(arena);
112 var debug_log_scopes = std.array_list.Managed([]const u8).init(arena);
113
114 var install_prefix: ?[]const u8 = null;
115 var dir_list = std.Build.DirList{};
116 var error_style: ErrorStyle = .verbose;
117 var multiline_errors: MultilineErrors = .indent;
118 var summary: ?Summary = null;
119 var max_rss: u64 = 0;
120 var skip_oom_steps = false;
121 var test_timeout_ns: ?u64 = null;
122 var color: Color = .auto;
123 var help_menu = false;
124 var steps_menu = false;
125 var output_tmp_nonce: ?[16]u8 = null;
126 var watch = false;
127 var fuzz: ?std.Build.Fuzz.Mode = null;
128 var debounce_interval_ms: u16 = 50;
129 var webui_listen: ?Io.net.IpAddress = null;
130
131 if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| {
132 if (std.meta.stringToEnum(ErrorStyle, str)) |style| {
133 error_style = style;
134 }
135 }
136
137 if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| {
138 if (std.meta.stringToEnum(MultilineErrors, str)) |style| {
139 multiline_errors = style;
140 }
141 }
142
143 while (nextArg(args, &arg_idx)) |arg| {
144 if (mem.startsWith(u8, arg, "-Z")) {
145 if (arg.len != 18) fatalWithHint("bad argument: '{s}'", .{arg});
146 output_tmp_nonce = arg[2..18].*;
147 } else if (mem.startsWith(u8, arg, "-D")) {
148 const option_contents = arg[2..];
149 if (option_contents.len == 0)
150 fatalWithHint("expected option name after '-D'", .{});
151 if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| {
152 const option_name = option_contents[0..name_end];
153 const option_value = option_contents[name_end + 1 ..];
154 if (try builder.addUserInputOption(option_name, option_value))
155 fatal(" access the help menu with 'zig build -h'", .{});
156 } else {
157 if (try builder.addUserInputFlag(option_contents))
158 fatal(" access the help menu with 'zig build -h'", .{});
159 }
160 } else if (mem.startsWith(u8, arg, "-")) {
161 if (mem.eql(u8, arg, "--verbose")) {
162 builder.verbose = true;
163 } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
164 help_menu = true;
165 } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
166 install_prefix = nextArgOrFatal(args, &arg_idx);
167 } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
168 steps_menu = true;
169 } else if (mem.startsWith(u8, arg, "-fsys=")) {
170 const name = arg["-fsys=".len..];
171 graph.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM");
172 } else if (mem.startsWith(u8, arg, "-fno-sys=")) {
173 const name = arg["-fno-sys=".len..];
174 graph.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM");
175 } else if (mem.eql(u8, arg, "--release")) {
176 builder.release_mode = .any;
177 } else if (mem.startsWith(u8, arg, "--release=")) {
178 const text = arg["--release=".len..];
179 builder.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, text) orelse {
180 fatalWithHint("expected [off|any|fast|safe|small] in '{s}', found '{s}'", .{
181 arg, text,
182 });
183 };
184 } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
185 dir_list.lib_dir = nextArgOrFatal(args, &arg_idx);
186 } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
187 dir_list.exe_dir = nextArgOrFatal(args, &arg_idx);
188 } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
189 dir_list.include_dir = nextArgOrFatal(args, &arg_idx);
190 } else if (mem.eql(u8, arg, "--sysroot")) {
191 builder.sysroot = nextArgOrFatal(args, &arg_idx);
192 } else if (mem.eql(u8, arg, "--maxrss")) {
193 const max_rss_text = nextArgOrFatal(args, &arg_idx);
194 max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| {
195 std.debug.print("invalid byte size: '{s}': {s}\n", .{
196 max_rss_text, @errorName(err),
197 });
198 process.exit(1);
199 };
200 } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
201 skip_oom_steps = true;
202 } else if (mem.eql(u8, arg, "--test-timeout")) {
203 const units: []const struct { []const u8, u64 } = &.{
204 .{ "ns", 1 },
205 .{ "nanosecond", 1 },
206 .{ "us", std.time.ns_per_us },
207 .{ "microsecond", std.time.ns_per_us },
208 .{ "ms", std.time.ns_per_ms },
209 .{ "millisecond", std.time.ns_per_ms },
210 .{ "s", std.time.ns_per_s },
211 .{ "second", std.time.ns_per_s },
212 .{ "m", std.time.ns_per_min },
213 .{ "minute", std.time.ns_per_min },
214 .{ "h", std.time.ns_per_hour },
215 .{ "hour", std.time.ns_per_hour },
216 };
217 const timeout_str = nextArgOrFatal(args, &arg_idx);
218 const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal(
219 "invalid timeout '{s}': expected unit (ns, us, ms, s, m, h)",
220 .{timeout_str},
221 );
222 const num_str = timeout_str[0 .. num_end_idx + 1];
223 const unit_str = timeout_str[num_end_idx + 1 ..];
224 const unit_factor: f64 = for (units) |unit_and_factor| {
225 if (std.mem.eql(u8, unit_str, unit_and_factor[0])) {
226 break @floatFromInt(unit_and_factor[1]);
227 }
228 } else fatal(
229 "invalid timeout '{s}': invalid unit '{s}' (expected ns, us, ms, s, m, h)",
230 .{ timeout_str, unit_str },
231 );
232 const num_parsed = std.fmt.parseFloat(f64, num_str) catch |err| fatal(
233 "invalid timeout '{s}': invalid number '{s}' ({t})",
234 .{ timeout_str, num_str, err },
235 );
236 test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed);
237 } else if (mem.eql(u8, arg, "--search-prefix")) {
238 const search_prefix = nextArgOrFatal(args, &arg_idx);
239 builder.addSearchPrefix(search_prefix);
240 } else if (mem.eql(u8, arg, "--libc")) {
241 builder.libc_file = nextArgOrFatal(args, &arg_idx);
242 } else if (mem.eql(u8, arg, "--color")) {
243 const next_arg = nextArg(args, &arg_idx) orelse
244 fatalWithHint("expected [auto|on|off] after '{s}'", .{arg});
245 color = std.meta.stringToEnum(Color, next_arg) orelse {
246 fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{
247 arg, next_arg,
248 });
249 };
250 } else if (mem.eql(u8, arg, "--error-style")) {
251 const next_arg = nextArg(args, &arg_idx) orelse
252 fatalWithHint("expected style after '{s}'", .{arg});
253 error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse {
254 fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg });
255 };
256 } else if (mem.eql(u8, arg, "--multiline-errors")) {
257 const next_arg = nextArg(args, &arg_idx) orelse
258 fatalWithHint("expected style after '{s}'", .{arg});
259 multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse {
260 fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg });
261 };
262 } else if (mem.eql(u8, arg, "--summary")) {
263 const next_arg = nextArg(args, &arg_idx) orelse
264 fatalWithHint("expected [all|new|failures|line|none] after '{s}'", .{arg});
265 summary = std.meta.stringToEnum(Summary, next_arg) orelse {
266 fatalWithHint("expected [all|new|failures|line|none] after '{s}', found '{s}'", .{
267 arg, next_arg,
268 });
269 };
270 } else if (mem.eql(u8, arg, "--seed")) {
271 const next_arg = nextArg(args, &arg_idx) orelse
272 fatalWithHint("expected u32 after '{s}'", .{arg});
273 graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
274 fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {s}\n", .{
275 next_arg, @errorName(err),
276 });
277 };
278 } else if (mem.eql(u8, arg, "--build-id")) {
279 builder.build_id = .fast;
280 } else if (mem.startsWith(u8, arg, "--build-id=")) {
281 const style = arg["--build-id=".len..];
282 builder.build_id = std.zig.BuildId.parse(style) catch |err| {
283 fatal("unable to parse --build-id style '{s}': {s}", .{
284 style, @errorName(err),
285 });
286 };
287 } else if (mem.eql(u8, arg, "--debounce")) {
288 const next_arg = nextArg(args, &arg_idx) orelse
289 fatalWithHint("expected u16 after '{s}'", .{arg});
290 debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
291 fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {t}\n", .{
292 next_arg, err,
293 });
294 };
295 } else if (mem.eql(u8, arg, "--webui")) {
296 if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
297 } else if (mem.startsWith(u8, arg, "--webui=")) {
298 const addr_str = arg["--webui=".len..];
299 if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{});
300 webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| {
301 fatal("invalid web UI address '{s}': {s}", .{ addr_str, @errorName(err) });
302 };
303 } else if (mem.eql(u8, arg, "--debug-log")) {
304 const next_arg = nextArgOrFatal(args, &arg_idx);
305 try debug_log_scopes.append(next_arg);
306 } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
307 builder.debug_pkg_config = true;
308 } else if (mem.eql(u8, arg, "--debug-rt")) {
309 graph.debug_compiler_runtime_libs = .Debug;
310 } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| {
311 graph.debug_compiler_runtime_libs =
312 std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse
313 fatal("unrecognized optimization mode: '{s}'", .{rest});
314 } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
315 builder.debug_compile_errors = true;
316 } else if (mem.eql(u8, arg, "--debug-incremental")) {
317 builder.debug_incremental = true;
318 } else if (mem.eql(u8, arg, "--system")) {
319 // The usage text shows another argument after this parameter
320 // but it is handled by the parent process. The build runner
321 // only sees this flag.
322 graph.system_package_mode = true;
323 } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) {
324 // --glibc-runtimes was the old name of the flag; kept for compatibility for now.
325 builder.libc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
326 } else if (mem.eql(u8, arg, "--verbose-link")) {
327 builder.verbose_link = true;
328 } else if (mem.eql(u8, arg, "--verbose-air")) {
329 builder.verbose_air = true;
330 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
331 builder.verbose_llvm_ir = "-";
332 } else if (mem.startsWith(u8, arg, "--verbose-llvm-ir=")) {
333 builder.verbose_llvm_ir = arg["--verbose-llvm-ir=".len..];
334 } else if (mem.startsWith(u8, arg, "--verbose-llvm-bc=")) {
335 builder.verbose_llvm_bc = arg["--verbose-llvm-bc=".len..];
336 } else if (mem.eql(u8, arg, "--verbose-cc")) {
337 builder.verbose_cc = true;
338 } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
339 builder.verbose_llvm_cpu_features = true;
340 } else if (mem.eql(u8, arg, "--watch")) {
341 watch = true;
342 } else if (mem.eql(u8, arg, "--time-report")) {
343 graph.time_report = true;
344 if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
345 } else if (mem.eql(u8, arg, "--fuzz")) {
346 fuzz = .{ .forever = undefined };
347 if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
348 } else if (mem.startsWith(u8, arg, "--fuzz=")) {
349 const value = arg["--fuzz=".len..];
350 if (value.len == 0) fatal("missing argument to --fuzz", .{});
351
352 const unit: u8 = value[value.len - 1];
353 const digits = switch (unit) {
354 '0'...'9' => value,
355 'K', 'M', 'G' => value[0 .. value.len - 1],
356 else => fatal(
357 "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
358 .{},
359 ),
360 };
361
362 const amount = std.fmt.parseInt(u64, digits, 10) catch {
363 fatal(
364 "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
365 .{},
366 );
367 };
368
369 const normalized_amount = std.math.mul(u64, amount, switch (unit) {
370 else => unreachable,
371 '0'...'9' => 1,
372 'K' => 1000,
373 'M' => 1_000_000,
374 'G' => 1_000_000_000,
375 }) catch fatal("fuzzing limit amount overflows u64", .{});
376
377 fuzz = .{
378 .limit = .{
379 .amount = normalized_amount,
380 },
381 };
382 } else if (mem.eql(u8, arg, "-fincremental")) {
383 graph.incremental = true;
384 } else if (mem.eql(u8, arg, "-fno-incremental")) {
385 graph.incremental = false;
386 } else if (mem.eql(u8, arg, "-fwine")) {
387 builder.enable_wine = true;
388 } else if (mem.eql(u8, arg, "-fno-wine")) {
389 builder.enable_wine = false;
390 } else if (mem.eql(u8, arg, "-fqemu")) {
391 builder.enable_qemu = true;
392 } else if (mem.eql(u8, arg, "-fno-qemu")) {
393 builder.enable_qemu = false;
394 } else if (mem.eql(u8, arg, "-fwasmtime")) {
395 builder.enable_wasmtime = true;
396 } else if (mem.eql(u8, arg, "-fno-wasmtime")) {
397 builder.enable_wasmtime = false;
398 } else if (mem.eql(u8, arg, "-frosetta")) {
399 builder.enable_rosetta = true;
400 } else if (mem.eql(u8, arg, "-fno-rosetta")) {
401 builder.enable_rosetta = false;
402 } else if (mem.eql(u8, arg, "-fdarling")) {
403 builder.enable_darling = true;
404 } else if (mem.eql(u8, arg, "-fno-darling")) {
405 builder.enable_darling = false;
406 } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
407 graph.allow_so_scripts = true;
408 } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
409 graph.allow_so_scripts = false;
410 } else if (mem.eql(u8, arg, "-freference-trace")) {
411 builder.reference_trace = 256;
412 } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
413 const num = arg["-freference-trace=".len..];
414 builder.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
415 std.debug.print("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
416 process.exit(1);
417 };
418 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
419 builder.reference_trace = null;
420 } else if (mem.cutPrefix(u8, arg, "-j")) |text| {
421 const n = std.fmt.parseUnsigned(u32, text, 10) catch |err|
422 fatal("unable to parse jobs count '{s}': {t}", .{ text, err });
423 if (n < 1) fatal("number of jobs must be at least 1", .{});
424 threaded.setAsyncLimit(.limited(n));
425 graph.max_jobs = n;
426 } else if (mem.eql(u8, arg, "--")) {
427 builder.args = argsRest(args, arg_idx);
428 break;
429 } else {
430 fatalWithHint("unrecognized argument: '{s}'", .{arg});
431 }
432 } else {
433 try targets.append(arg);
434 }
435 }
436
437 const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map);
438 const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
439
440 graph.stderr_mode = switch (color) {
441 .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE),
442 .on => .escape_codes,
443 .off => .no_color,
444 };
445
446 if (webui_listen != null) {
447 if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{});
448 if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{});
449 }
450
451 const main_progress_node = std.Progress.start(io, .{
452 .disable_printing = (color == .off),
453 });
454 defer main_progress_node.end();
455
456 builder.debug_log_scopes = debug_log_scopes.items;
457 builder.resolveInstallPrefix(install_prefix, dir_list);
458 {
459 var prog_node = main_progress_node.start("Configure", 0);
460 defer prog_node.end();
461 try builder.runBuild(root);
462 createModuleDependencies(builder) catch @panic("OOM");
463 }
464
465 if (graph.needed_lazy_dependencies.entries.len != 0) {
466 var buffer: std.ArrayList(u8) = .empty;
467 for (graph.needed_lazy_dependencies.keys()) |k| {
468 try buffer.appendSlice(arena, k);
469 try buffer.append(arena, '\n');
470 }
471 const s = std.fs.path.sep_str;
472 const tmp_sub_path = "tmp" ++ s ++ (output_tmp_nonce orelse fatal("missing -Z arg", .{}));
473 local_cache_directory.handle.writeFile(io, .{
474 .sub_path = tmp_sub_path,
475 .data = buffer.items,
476 .flags = .{ .exclusive = true },
477 }) catch |err| {
478 fatal("unable to write configuration results to '{f}{s}': {s}", .{
479 local_cache_directory, tmp_sub_path, @errorName(err),
480 });
481 };
482 process.exit(3); // Indicate configure phase failed with meaningful stdout.
483 }
484
485 if (builder.validateUserInputDidItFail()) {
486 fatal(" access the help menu with 'zig build -h'", .{});
487 }
488
489 validateSystemLibraryOptions(builder);
490
491 if (help_menu) {
492 var w = initStdoutWriter(io);
493 printUsage(builder, w) catch return stdout_writer_allocation.err.?;
494 w.flush() catch return stdout_writer_allocation.err.?;
495 return;
496 }
497
498 if (steps_menu) {
499 var w = initStdoutWriter(io);
500 printSteps(builder, w) catch return stdout_writer_allocation.err.?;
501 w.flush() catch return stdout_writer_allocation.err.?;
502 return;
503 }
504
505 var run: Run = .{
506 .gpa = gpa,
507
508 .available_rss = max_rss,
509 .max_rss_is_default = false,
510 .max_rss_mutex = .init,
511 .skip_oom_steps = skip_oom_steps,
512 .unit_test_timeout_ns = test_timeout_ns,
513
514 .watch = watch,
515 .web_server = undefined, // set after `prepare`
516 .memory_blocked_steps = .empty,
517 .step_stack = .empty,
518
519 .error_style = error_style,
520 .multiline_errors = multiline_errors,
521 .summary = summary orelse if (watch or webui_listen != null) .line else .failures,
522 };
523 defer {
524 run.memory_blocked_steps.deinit(gpa);
525 run.step_stack.deinit(gpa);
526 }
527
528 if (run.available_rss == 0) {
529 run.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
530 run.max_rss_is_default = true;
531 }
532
533 prepare(arena, builder, targets.items, &run, graph.random_seed) catch |err| switch (err) {
534 error.DependencyLoopDetected, error.InsufficientMemory => {
535 // Perhaps in the future there could be an Advanced Options flag
536 // such as --debug-build-runner-leaks which would make this code
537 // return instead of calling exit.
538 _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
539 process.exit(1);
540 },
541 else => |e| return e,
542 };
543
544 var w: Watch = w: {
545 if (!watch) break :w undefined;
546 if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag});
547 break :w try .init(graph.cache.cwd);
548 };
549
550 const now = Io.Clock.Timestamp.now(io, .awake);
551
552 run.web_server = if (webui_listen) |listen_address| ws: {
553 if (builtin.single_threaded) unreachable; // `fatal` above
554 break :ws .init(.{
555 .gpa = gpa,
556 .graph = &graph,
557 .all_steps = run.step_stack.keys(),
558 .root_prog_node = main_progress_node,
559 .watch = watch,
560 .listen_address = listen_address,
561 .base_timestamp = now,
562 });
563 } else null;
564
565 if (run.web_server) |*ws| {
566 ws.start() catch |err| fatal("failed to start web server: {t}", .{err});
567 }
568
569 rebuild: while (true) : (if (run.error_style.clearOnUpdate()) {
570 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
571 defer io.unlockStderr();
572 try stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H");
573 }) {
574 if (run.web_server) |*ws| ws.startBuild();
575
576 try runStepNames(
577 builder,
578 targets.items,
579 main_progress_node,
580 &run,
581 fuzz,
582 );
583
584 if (run.web_server) |*web_server| {
585 if (fuzz) |mode| if (mode != .forever) fatal(
586 "error: limited fuzzing is not implemented yet for --webui",
587 .{},
588 );
589
590 web_server.finishBuild(.{ .fuzz = fuzz != null });
591 }
592
593 if (run.web_server) |*ws| {
594 assert(!watch); // fatal error after CLI parsing
595 while (true) switch (try ws.wait()) {
596 .rebuild => {
597 for (run.step_stack.keys()) |step| {
598 step.state = .precheck_done;
599 step.pending_deps = @intCast(step.dependencies.items.len);
600 step.reset(gpa);
601 }
602 continue :rebuild;
603 },
604 };
605 }
606
607 // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
608 if (!Watch.have_impl) unreachable;
609
610 try w.update(gpa, run.step_stack.keys());
611
612 // Wait until a file system notification arrives. Read all such events
613 // until the buffer is empty. Then wait for a debounce interval, resetting
614 // if any more events come in. After the debounce interval has passed,
615 // trigger a rebuild on all steps with modified inputs, as well as their
616 // recursive dependants.
617 var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
618 const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
619 w.dir_count, countSubProcesses(run.step_stack.keys()),
620 }) catch &caption_buf;
621 var debouncing_node = main_progress_node.start(caption, 0);
622 var in_debounce = false;
623 while (true) switch (try w.wait(gpa, io, if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) {
624 .timeout => {
625 assert(in_debounce);
626 debouncing_node.end();
627 markFailedStepsDirty(gpa, run.step_stack.keys());
628 continue :rebuild;
629 },
630 .dirty => if (!in_debounce) {
631 in_debounce = true;
632 debouncing_node.end();
633 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
634 },
635 .clean => {},
636 };
637 }
638}
639
640fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
641 for (all_steps) |step| switch (step.state) {
642 .dependency_failure, .failure, .skipped => _ = step.invalidateResult(gpa),
643 else => continue,
644 };
645 // Now that all dirty steps have been found, the remaining steps that
646 // succeeded from last run shall be marked "cached".
647 for (all_steps) |step| switch (step.state) {
648 .success => step.result_cached = true,
649 else => continue,
650 };
651}
652
653fn countSubProcesses(all_steps: []const *Step) usize {
654 var count: usize = 0;
655 for (all_steps) |s| {
656 count += @intFromBool(s.getZigProcess() != null);
657 }
658 return count;
659}
660
661const Run = struct {
662 gpa: Allocator,
663
664 available_rss: usize,
665 max_rss_is_default: bool,
666 max_rss_mutex: Io.Mutex,
667 skip_oom_steps: bool,
668 unit_test_timeout_ns: ?u64,
669 watch: bool,
670 web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn,
671 /// Allocated into `gpa`.
672 memory_blocked_steps: std.ArrayList(*Step),
673 /// Allocated into `gpa`.
674 step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
675
676 error_style: ErrorStyle,
677 multiline_errors: MultilineErrors,
678 summary: Summary,
679};
680
681fn prepare(
682 arena: Allocator,
683 b: *std.Build,
684 step_names: []const []const u8,
685 run: *Run,
686 seed: u32,
687) !void {
688 const gpa = run.gpa;
689 const step_stack = &run.step_stack;
690
691 if (step_names.len == 0) {
692 try step_stack.put(gpa, b.default_step, {});
693 } else {
694 try step_stack.ensureUnusedCapacity(gpa, step_names.len);
695 for (0..step_names.len) |i| {
696 const step_name = step_names[step_names.len - i - 1];
697 const s = b.top_level_steps.get(step_name) orelse {
698 std.log.info("access the help menu with \"zig build -h\"", .{});
699 fatal("no step named '{s}'", .{step_name});
700 };
701 step_stack.putAssumeCapacity(&s.step, {});
702 }
703 }
704
705 const starting_steps = try arena.dupe(*Step, step_stack.keys());
706
707 var rng = std.Random.DefaultPrng.init(seed);
708 const rand = rng.random();
709 rand.shuffle(*Step, starting_steps);
710
711 for (starting_steps) |s| {
712 try constructGraphAndCheckForDependencyLoop(gpa, b, s, &run.step_stack, rand);
713 }
714
715 {
716 // Check that we have enough memory to complete the build.
717 var any_problems = false;
718 var max_needed: usize = 0;
719 for (step_stack.keys()) |s| {
720 if (s.max_rss == 0) continue;
721 max_needed = @max(max_needed, s.max_rss);
722 if (s.max_rss > run.available_rss) {
723 if (run.skip_oom_steps) {
724 s.state = .skipped_oom;
725 for (s.dependants.items) |dependant| {
726 dependant.pending_deps -= 1;
727 }
728 } else {
729 std.log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{
730 s.owner.dep_prefix, s.name, s.max_rss, run.available_rss,
731 });
732 any_problems = true;
733 }
734 }
735 }
736 if (any_problems) {
737 if (run.max_rss_is_default) {
738 std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{
739 max_needed,
740 });
741 }
742 return error.InsufficientMemory;
743 }
744 }
745}
746
747fn runStepNames(
748 b: *std.Build,
749 step_names: []const []const u8,
750 parent_prog_node: std.Progress.Node,
751 run: *Run,
752 fuzz: ?std.Build.Fuzz.Mode,
753) !void {
754 const gpa = run.gpa;
755 const graph = b.graph;
756 const io = graph.io;
757 const step_stack = &run.step_stack;
758
759 {
760 // Collect the initial set of tasks (those with no outstanding dependencies) into a buffer,
761 // then spawn them. The buffer is so that we don't race with `makeStep` and end up thinking
762 // a step is initial when it actually became ready due to an earlier initial step.
763 var initial_set: std.ArrayList(*Step) = .empty;
764 defer initial_set.deinit(gpa);
765 try initial_set.ensureUnusedCapacity(gpa, step_stack.count());
766 for (step_stack.keys()) |s| {
767 if (s.state == .precheck_done and s.pending_deps == 0) {
768 initial_set.appendAssumeCapacity(s);
769 }
770 }
771
772 const step_prog = parent_prog_node.start("steps", step_stack.count());
773 defer step_prog.end();
774
775 var group: Io.Group = .init;
776 defer group.cancel(io);
777 // Start working on all of the initial steps...
778 for (initial_set.items) |s| try stepReady(&group, b, s, step_prog, run);
779 // ...and `makeStep` will trigger every other step when their last dependency finishes.
780 try group.await(io);
781 }
782
783 assert(run.memory_blocked_steps.items.len == 0);
784
785 var test_pass_count: usize = 0;
786 var test_skip_count: usize = 0;
787 var test_fail_count: usize = 0;
788 var test_crash_count: usize = 0;
789 var test_timeout_count: usize = 0;
790
791 var test_count: usize = 0;
792
793 var success_count: usize = 0;
794 var skipped_count: usize = 0;
795 var failure_count: usize = 0;
796 var pending_count: usize = 0;
797 var total_compile_errors: usize = 0;
798
799 var cleanup_task = io.async(cleanTmpFiles, .{ io, step_stack.keys() });
800 defer cleanup_task.await(io);
801
802 for (step_stack.keys()) |s| {
803 test_pass_count += s.test_results.passCount();
804 test_skip_count += s.test_results.skip_count;
805 test_fail_count += s.test_results.fail_count;
806 test_crash_count += s.test_results.crash_count;
807 test_timeout_count += s.test_results.timeout_count;
808
809 test_count += s.test_results.test_count;
810
811 switch (s.state) {
812 .precheck_unstarted => unreachable,
813 .precheck_started => unreachable,
814 .precheck_done => unreachable,
815 .dependency_failure => pending_count += 1,
816 .success => success_count += 1,
817 .skipped, .skipped_oom => skipped_count += 1,
818 .failure => {
819 failure_count += 1;
820 const compile_errors_len = s.result_error_bundle.errorMessageCount();
821 if (compile_errors_len > 0) {
822 total_compile_errors += compile_errors_len;
823 }
824 },
825 }
826 }
827
828 if (fuzz) |mode| blk: {
829 switch (builtin.os.tag) {
830 // Current implementation depends on two things that need to be ported to Windows:
831 // * Memory-mapping to share data between the fuzzer and build runner.
832 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
833 // many addresses to source locations).
834 .windows => fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
835 else => {},
836 }
837 if (@bitSizeOf(usize) != 64) {
838 // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
839 // being compatible with file system's u64 return value. This is not the case
840 // on 32-bit platforms.
841 // Affects or affected by issues #5185, #22523, and #22464.
842 fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
843 }
844
845 switch (mode) {
846 .forever => break :blk,
847 .limit => {},
848 }
849
850 assert(mode == .limit);
851 var f = std.Build.Fuzz.init(
852 gpa,
853 io,
854 step_stack.keys(),
855 parent_prog_node,
856 mode,
857 ) catch |err| fatal("failed to start fuzzer: {t}", .{err});
858 defer f.deinit();
859
860 f.start();
861 try f.waitAndPrintReport();
862 }
863
864 // Every test has a state
865 assert(test_pass_count + test_skip_count + test_fail_count + test_crash_count + test_timeout_count == test_count);
866
867 if (failure_count == 0) {
868 std.Progress.setStatus(.success);
869 } else {
870 std.Progress.setStatus(.failure);
871 }
872
873 summary: {
874 switch (run.summary) {
875 .all, .new, .line => {},
876 .failures => if (failure_count == 0) break :summary,
877 .none => break :summary,
878 }
879
880 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
881 defer io.unlockStderr();
882 const t = stderr.terminal();
883 const w = &stderr.file_writer.interface;
884
885 const total_count = success_count + failure_count + pending_count + skipped_count;
886 t.setColor(.cyan) catch {};
887 t.setColor(.bold) catch {};
888 w.writeAll("Build Summary: ") catch {};
889 t.setColor(.reset) catch {};
890 w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
891 {
892 t.setColor(.dim) catch {};
893 var first = true;
894 if (skipped_count > 0) {
895 w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {};
896 first = false;
897 }
898 if (failure_count > 0) {
899 w.print("{s}{d} failed", .{ if (first) " (" else ", ", failure_count }) catch {};
900 first = false;
901 }
902 if (!first) w.writeByte(')') catch {};
903 t.setColor(.reset) catch {};
904 }
905
906 if (test_count > 0) {
907 w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {};
908 t.setColor(.dim) catch {};
909 var first = true;
910 if (test_skip_count > 0) {
911 w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {};
912 first = false;
913 }
914 if (test_fail_count > 0) {
915 w.print("{s}{d} failed", .{ if (first) " (" else ", ", test_fail_count }) catch {};
916 first = false;
917 }
918 if (test_crash_count > 0) {
919 w.print("{s}{d} crashed", .{ if (first) " (" else ", ", test_crash_count }) catch {};
920 first = false;
921 }
922 if (test_timeout_count > 0) {
923 w.print("{s}{d} timed out", .{ if (first) " (" else ", ", test_timeout_count }) catch {};
924 first = false;
925 }
926 if (!first) w.writeByte(')') catch {};
927 t.setColor(.reset) catch {};
928 }
929
930 w.writeAll("\n") catch {};
931
932 if (run.summary == .line) break :summary;
933
934 // Print a fancy tree with build results.
935 var step_stack_copy = try step_stack.clone(gpa);
936 defer step_stack_copy.deinit(gpa);
937
938 var print_node: PrintNode = .{ .parent = null };
939 if (step_names.len == 0) {
940 print_node.last = true;
941 printTreeStep(b, b.default_step, run, t, &print_node, &step_stack_copy) catch {};
942 } else {
943 const last_index = if (run.summary == .all) b.top_level_steps.count() else blk: {
944 var i: usize = step_names.len;
945 while (i > 0) {
946 i -= 1;
947 const step = b.top_level_steps.get(step_names[i]).?.step;
948 const found = switch (run.summary) {
949 .all, .line, .none => unreachable,
950 .failures => step.state != .success,
951 .new => !step.result_cached,
952 };
953 if (found) break :blk i;
954 }
955 break :blk b.top_level_steps.count();
956 };
957 for (step_names, 0..) |step_name, i| {
958 const tls = b.top_level_steps.get(step_name).?;
959 print_node.last = i + 1 == last_index;
960 printTreeStep(b, &tls.step, run, t, &print_node, &step_stack_copy) catch {};
961 }
962 }
963 w.writeByte('\n') catch {};
964 }
965
966 if (run.watch or run.web_server != null) return;
967
968 // Perhaps in the future there could be an Advanced Options flag such as
969 // --debug-build-runner-leaks which would make this code return instead of
970 // calling exit.
971
972 const code: u8 = code: {
973 if (failure_count == 0) break :code 0; // success
974 if (run.error_style.verboseContext()) break :code 1; // failure; print build command
975 break :code 2; // failure; do not print build command
976 };
977 _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
978 process.exit(code);
979}
980
981const PrintNode = struct {
982 parent: ?*PrintNode,
983 last: bool = false,
984};
985
986fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void {
987 const parent = node.parent orelse return;
988 const writer = stderr.writer;
989 if (parent.parent == null) return;
990 try printPrefix(parent, stderr);
991 if (parent.last) {
992 try writer.writeAll(" ");
993 } else {
994 try writer.writeAll(switch (stderr.mode) {
995 .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │
996 else => "| ",
997 });
998 }
999}
1000
1001fn printChildNodePrefix(stderr: Io.Terminal) !void {
1002 try stderr.writer.writeAll(switch (stderr.mode) {
1003 .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─
1004 else => "+- ",
1005 });
1006}
1007
1008fn printStepStatus(s: *Step, stderr: Io.Terminal, run: *const Run) !void {
1009 const writer = stderr.writer;
1010 switch (s.state) {
1011 .precheck_unstarted => unreachable,
1012 .precheck_started => unreachable,
1013 .precheck_done => unreachable,
1014
1015 .dependency_failure => {
1016 try stderr.setColor(.dim);
1017 try writer.writeAll(" transitive failure\n");
1018 try stderr.setColor(.reset);
1019 },
1020
1021 .success => {
1022 try stderr.setColor(.green);
1023 if (s.result_cached) {
1024 try writer.writeAll(" cached");
1025 } else if (s.test_results.test_count > 0) {
1026 const pass_count = s.test_results.passCount();
1027 assert(s.test_results.test_count == pass_count + s.test_results.skip_count);
1028 try writer.print(" {d} pass", .{pass_count});
1029 if (s.test_results.skip_count > 0) {
1030 try stderr.setColor(.reset);
1031 try writer.writeAll(", ");
1032 try stderr.setColor(.yellow);
1033 try writer.print("{d} skip", .{s.test_results.skip_count});
1034 }
1035 try stderr.setColor(.reset);
1036 try writer.print(" ({d} total)", .{s.test_results.test_count});
1037 } else {
1038 try writer.writeAll(" success");
1039 }
1040 try stderr.setColor(.reset);
1041 if (s.result_duration_ns) |ns| {
1042 try stderr.setColor(.dim);
1043 if (ns >= std.time.ns_per_min) {
1044 try writer.print(" {d}m", .{ns / std.time.ns_per_min});
1045 } else if (ns >= std.time.ns_per_s) {
1046 try writer.print(" {d}s", .{ns / std.time.ns_per_s});
1047 } else if (ns >= std.time.ns_per_ms) {
1048 try writer.print(" {d}ms", .{ns / std.time.ns_per_ms});
1049 } else if (ns >= std.time.ns_per_us) {
1050 try writer.print(" {d}us", .{ns / std.time.ns_per_us});
1051 } else {
1052 try writer.print(" {d}ns", .{ns});
1053 }
1054 try stderr.setColor(.reset);
1055 }
1056 if (s.result_peak_rss != 0) {
1057 const rss = s.result_peak_rss;
1058 try stderr.setColor(.dim);
1059 if (rss >= 1000_000_000) {
1060 try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000});
1061 } else if (rss >= 1000_000) {
1062 try writer.print(" MaxRSS:{d}M", .{rss / 1000_000});
1063 } else if (rss >= 1000) {
1064 try writer.print(" MaxRSS:{d}K", .{rss / 1000});
1065 } else {
1066 try writer.print(" MaxRSS:{d}B", .{rss});
1067 }
1068 try stderr.setColor(.reset);
1069 }
1070 try writer.writeAll("\n");
1071 },
1072 .skipped => {
1073 try stderr.setColor(.yellow);
1074 try writer.writeAll(" skipped\n");
1075 try stderr.setColor(.reset);
1076 },
1077 .skipped_oom => {
1078 try stderr.setColor(.yellow);
1079 try writer.writeAll(" skipped (not enough memory)");
1080 try stderr.setColor(.dim);
1081 try writer.print(" upper bound of {d} exceeded runner limit ({d})\n", .{ s.max_rss, run.available_rss });
1082 try stderr.setColor(.reset);
1083 },
1084 .failure => {
1085 try printStepFailure(s, stderr, false);
1086 try stderr.setColor(.reset);
1087 },
1088 }
1089}
1090
1091fn printStepFailure(s: *Step, stderr: Io.Terminal, dim: bool) !void {
1092 const w = stderr.writer;
1093 if (s.result_error_bundle.errorMessageCount() > 0) {
1094 try stderr.setColor(.red);
1095 try w.print(" {d} errors\n", .{
1096 s.result_error_bundle.errorMessageCount(),
1097 });
1098 } else if (!s.test_results.isSuccess()) {
1099 // These first values include all of the test "statuses". Every test is either passsed,
1100 // skipped, failed, crashed, or timed out.
1101 try stderr.setColor(.green);
1102 try w.print(" {d} pass", .{s.test_results.passCount()});
1103 try stderr.setColor(.reset);
1104 if (dim) try stderr.setColor(.dim);
1105 if (s.test_results.skip_count > 0) {
1106 try w.writeAll(", ");
1107 try stderr.setColor(.yellow);
1108 try w.print("{d} skip", .{s.test_results.skip_count});
1109 try stderr.setColor(.reset);
1110 if (dim) try stderr.setColor(.dim);
1111 }
1112 if (s.test_results.fail_count > 0) {
1113 try w.writeAll(", ");
1114 try stderr.setColor(.red);
1115 try w.print("{d} fail", .{s.test_results.fail_count});
1116 try stderr.setColor(.reset);
1117 if (dim) try stderr.setColor(.dim);
1118 }
1119 if (s.test_results.crash_count > 0) {
1120 try w.writeAll(", ");
1121 try stderr.setColor(.red);
1122 try w.print("{d} crash", .{s.test_results.crash_count});
1123 try stderr.setColor(.reset);
1124 if (dim) try stderr.setColor(.dim);
1125 }
1126 if (s.test_results.timeout_count > 0) {
1127 try w.writeAll(", ");
1128 try stderr.setColor(.red);
1129 try w.print("{d} timeout", .{s.test_results.timeout_count});
1130 try stderr.setColor(.reset);
1131 if (dim) try stderr.setColor(.dim);
1132 }
1133 try w.print(" ({d} total)", .{s.test_results.test_count});
1134
1135 // Memory leaks are intentionally written after the total, because is isn't a test *status*,
1136 // but just a flag that any tests -- even passed ones -- can have. We also use a different
1137 // separator, so it looks like:
1138 // 2 pass, 1 skip, 2 fail (5 total); 2 leaks
1139 if (s.test_results.leak_count > 0) {
1140 try w.writeAll("; ");
1141 try stderr.setColor(.red);
1142 try w.print("{d} leaks", .{s.test_results.leak_count});
1143 try stderr.setColor(.reset);
1144 if (dim) try stderr.setColor(.dim);
1145 }
1146
1147 // It's usually not helpful to know how many error logs there were because they tend to
1148 // just come with other errors (e.g. crashes and leaks print stack traces, and clean
1149 // failures print error traces). So only mention them if they're the only thing causing
1150 // the failure.
1151 const show_err_logs: bool = show: {
1152 var alt_results = s.test_results;
1153 alt_results.log_err_count = 0;
1154 break :show alt_results.isSuccess();
1155 };
1156 if (show_err_logs) {
1157 try w.writeAll("; ");
1158 try stderr.setColor(.red);
1159 try w.print("{d} error logs", .{s.test_results.log_err_count});
1160 try stderr.setColor(.reset);
1161 if (dim) try stderr.setColor(.dim);
1162 }
1163
1164 try w.writeAll("\n");
1165 } else if (s.result_error_msgs.items.len > 0) {
1166 try stderr.setColor(.red);
1167 try w.writeAll(" failure\n");
1168 } else {
1169 assert(s.result_stderr.len > 0);
1170 try stderr.setColor(.red);
1171 try w.writeAll(" w\n");
1172 }
1173}
1174
1175fn printTreeStep(
1176 b: *std.Build,
1177 s: *Step,
1178 run: *const Run,
1179 stderr: Io.Terminal,
1180 parent_node: *PrintNode,
1181 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
1182) !void {
1183 const writer = stderr.writer;
1184 const first = step_stack.swapRemove(s);
1185 const summary = run.summary;
1186 const skip = switch (summary) {
1187 .none, .line => unreachable,
1188 .all => false,
1189 .new => s.result_cached,
1190 .failures => s.state == .success,
1191 };
1192 if (skip) return;
1193 try printPrefix(parent_node, stderr);
1194
1195 if (parent_node.parent != null) {
1196 if (parent_node.last) {
1197 try printChildNodePrefix(stderr);
1198 } else {
1199 try writer.writeAll(switch (stderr.mode) {
1200 .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─
1201 else => "+- ",
1202 });
1203 }
1204 }
1205
1206 if (!first) try stderr.setColor(.dim);
1207
1208 // dep_prefix omitted here because it is redundant with the tree.
1209 try writer.writeAll(s.name);
1210
1211 if (first) {
1212 try printStepStatus(s, stderr, run);
1213
1214 const last_index = if (summary == .all) s.dependencies.items.len -| 1 else blk: {
1215 var i: usize = s.dependencies.items.len;
1216 while (i > 0) {
1217 i -= 1;
1218
1219 const step = s.dependencies.items[i];
1220 const found = switch (summary) {
1221 .all, .line, .none => unreachable,
1222 .failures => step.state != .success,
1223 .new => !step.result_cached,
1224 };
1225 if (found) break :blk i;
1226 }
1227 break :blk s.dependencies.items.len -| 1;
1228 };
1229 for (s.dependencies.items, 0..) |dep, i| {
1230 var print_node: PrintNode = .{
1231 .parent = parent_node,
1232 .last = i == last_index,
1233 };
1234 try printTreeStep(b, dep, run, stderr, &print_node, step_stack);
1235 }
1236 } else {
1237 if (s.dependencies.items.len == 0) {
1238 try writer.writeAll(" (reused)\n");
1239 } else {
1240 try writer.print(" (+{d} more reused dependencies)\n", .{
1241 s.dependencies.items.len,
1242 });
1243 }
1244 try stderr.setColor(.reset);
1245 }
1246}
1247
1248/// Traverse the dependency graph depth-first and make it undirected by having
1249/// steps know their dependants (they only know dependencies at start).
1250/// Along the way, check that there is no dependency loop, and record the steps
1251/// in traversal order in `step_stack`.
1252/// Each step has its dependencies traversed in random order, this accomplishes
1253/// two things:
1254/// - `step_stack` will be in randomized-depth-first order, so the build runner
1255/// spawns initial steps in a random order
1256/// - each step's `dependants` list is also filled in a random order, so that
1257/// when it finishes executing in `makeStep`, it spawns next steps to run in
1258/// random order
1259fn constructGraphAndCheckForDependencyLoop(
1260 gpa: Allocator,
1261 b: *std.Build,
1262 s: *Step,
1263 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
1264 rand: std.Random,
1265) !void {
1266 switch (s.state) {
1267 .precheck_started => {
1268 std.debug.print("dependency loop detected:\n {s}\n", .{s.name});
1269 return error.DependencyLoopDetected;
1270 },
1271 .precheck_unstarted => {
1272 s.state = .precheck_started;
1273
1274 try step_stack.ensureUnusedCapacity(gpa, s.dependencies.items.len);
1275
1276 // We dupe to avoid shuffling the steps in the summary, it depends
1277 // on s.dependencies' order.
1278 const deps = gpa.dupe(*Step, s.dependencies.items) catch @panic("OOM");
1279 defer gpa.free(deps);
1280
1281 rand.shuffle(*Step, deps);
1282
1283 for (deps) |dep| {
1284 try step_stack.put(gpa, dep, {});
1285 try dep.dependants.append(b.allocator, s);
1286 constructGraphAndCheckForDependencyLoop(gpa, b, dep, step_stack, rand) catch |err| {
1287 if (err == error.DependencyLoopDetected) {
1288 std.debug.print(" {s}\n", .{s.name});
1289 }
1290 return err;
1291 };
1292 }
1293
1294 s.state = .precheck_done;
1295 s.pending_deps = @intCast(s.dependencies.items.len);
1296 },
1297 .precheck_done => {},
1298
1299 // These don't happen until we actually run the step graph.
1300 .dependency_failure => unreachable,
1301 .success => unreachable,
1302 .failure => unreachable,
1303 .skipped => unreachable,
1304 .skipped_oom => unreachable,
1305 }
1306}
1307
1308/// Runs the "make" function of the single step `s`, updates its state, and then spawns newly-ready
1309/// dependant steps in `group`. If `s` makes an RSS claim (i.e. `s.max_rss != 0`), the caller must
1310/// have already subtracted this value from `run.available_rss`. This function will release the RSS
1311/// claim (i.e. add `s.max_rss` back into `run.available_rss`) and queue any viable memory-blocked
1312/// steps after "make" completes for `s`.
1313fn makeStep(
1314 group: *Io.Group,
1315 b: *std.Build,
1316 s: *Step,
1317 root_prog_node: std.Progress.Node,
1318 run: *Run,
1319) Io.Cancelable!void {
1320 const graph = b.graph;
1321 const io = graph.io;
1322 const gpa = run.gpa;
1323
1324 {
1325 const step_prog_node = root_prog_node.start(s.name, 0);
1326 defer step_prog_node.end();
1327
1328 if (run.web_server) |*ws| ws.updateStepStatus(s, .wip);
1329
1330 const new_state: Step.State = for (s.dependencies.items) |dep| {
1331 switch (@atomicLoad(Step.State, &dep.state, .monotonic)) {
1332 .precheck_unstarted => unreachable,
1333 .precheck_started => unreachable,
1334 .precheck_done => unreachable,
1335
1336 .failure,
1337 .dependency_failure,
1338 .skipped_oom,
1339 => break .dependency_failure,
1340
1341 .success, .skipped => {},
1342 }
1343 } else if (s.make(.{
1344 .progress_node = step_prog_node,
1345 .watch = run.watch,
1346 .web_server = if (run.web_server) |*ws| ws else null,
1347 .unit_test_timeout_ns = run.unit_test_timeout_ns,
1348 .gpa = gpa,
1349 })) state: {
1350 break :state .success;
1351 } else |err| switch (err) {
1352 error.MakeFailed => .failure,
1353 error.MakeSkipped => .skipped,
1354 };
1355
1356 @atomicStore(Step.State, &s.state, new_state, .monotonic);
1357
1358 switch (new_state) {
1359 .precheck_unstarted => unreachable,
1360 .precheck_started => unreachable,
1361 .precheck_done => unreachable,
1362
1363 .failure,
1364 .dependency_failure,
1365 .skipped_oom,
1366 => {
1367 if (run.web_server) |*ws| ws.updateStepStatus(s, .failure);
1368 std.Progress.setStatus(.failure_working);
1369 },
1370
1371 .success,
1372 .skipped,
1373 => {
1374 if (run.web_server) |*ws| ws.updateStepStatus(s, .success);
1375 },
1376 }
1377 }
1378
1379 // No matter the result, we want to display error/warning messages.
1380 if (s.result_error_bundle.errorMessageCount() > 0 or
1381 s.result_error_msgs.items.len > 0 or
1382 s.result_stderr.len > 0)
1383 {
1384 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
1385 defer io.unlockStderr();
1386 printErrorMessages(gpa, s, .{}, stderr.terminal(), run.error_style, run.multiline_errors) catch {};
1387 }
1388
1389 if (s.max_rss != 0) {
1390 var dispatch_set: std.ArrayList(*Step) = .empty;
1391 defer dispatch_set.deinit(gpa);
1392
1393 // Release our RSS claim and kick off some blocked steps if possible. We use `dispatch_set`
1394 // as a staging buffer to avoid recursing into `makeStep` while `run.max_rss_mutex` is held.
1395 {
1396 try run.max_rss_mutex.lock(io);
1397 defer run.max_rss_mutex.unlock(io);
1398 run.available_rss += s.max_rss;
1399 dispatch_set.ensureUnusedCapacity(gpa, run.memory_blocked_steps.items.len) catch @panic("OOM");
1400 while (run.memory_blocked_steps.getLast()) |candidate| {
1401 if (run.available_rss < candidate.max_rss) break;
1402 assert(run.memory_blocked_steps.pop() == candidate);
1403 dispatch_set.appendAssumeCapacity(candidate);
1404 }
1405 }
1406 for (dispatch_set.items) |candidate| {
1407 group.async(io, makeStep, .{ group, b, candidate, root_prog_node, run });
1408 }
1409 }
1410
1411 for (s.dependants.items) |dependant| {
1412 // `.acq_rel` synchronizes with itself to ensure all dependencies' final states are visible when this hits 0.
1413 if (@atomicRmw(u32, &dependant.pending_deps, .Sub, 1, .acq_rel) == 1) {
1414 try stepReady(group, b, dependant, root_prog_node, run);
1415 }
1416 }
1417}
1418
1419fn stepReady(
1420 group: *Io.Group,
1421 b: *std.Build,
1422 s: *Step,
1423 root_prog_node: std.Progress.Node,
1424 run: *Run,
1425) !void {
1426 const io = b.graph.io;
1427 if (s.max_rss != 0) {
1428 try run.max_rss_mutex.lock(io);
1429 defer run.max_rss_mutex.unlock(io);
1430 if (run.available_rss < s.max_rss) {
1431 // Running this step right now could possibly exceed the allotted RSS.
1432 run.memory_blocked_steps.append(run.gpa, s) catch @panic("OOM");
1433 return;
1434 }
1435 run.available_rss -= s.max_rss;
1436 }
1437 group.async(io, makeStep, .{ group, b, s, root_prog_node, run });
1438}
1439
1440pub fn printErrorMessages(
1441 gpa: Allocator,
1442 failing_step: *Step,
1443 options: std.zig.ErrorBundle.RenderOptions,
1444 stderr: Io.Terminal,
1445 error_style: ErrorStyle,
1446 multiline_errors: MultilineErrors,
1447) !void {
1448 const writer = stderr.writer;
1449 if (error_style.verboseContext()) {
1450 // Provide context for where these error messages are coming from by
1451 // printing the corresponding Step subtree.
1452 var step_stack: std.ArrayList(*Step) = .empty;
1453 defer step_stack.deinit(gpa);
1454 try step_stack.append(gpa, failing_step);
1455 while (step_stack.items[step_stack.items.len - 1].dependants.items.len != 0) {
1456 try step_stack.append(gpa, step_stack.items[step_stack.items.len - 1].dependants.items[0]);
1457 }
1458
1459 // Now, `step_stack` has the subtree that we want to print, in reverse order.
1460 try stderr.setColor(.dim);
1461 var indent: usize = 0;
1462 while (step_stack.pop()) |s| : (indent += 1) {
1463 if (indent > 0) {
1464 try writer.splatByteAll(' ', (indent - 1) * 3);
1465 try printChildNodePrefix(stderr);
1466 }
1467
1468 try writer.writeAll(s.name);
1469
1470 if (s == failing_step) {
1471 try printStepFailure(s, stderr, true);
1472 } else {
1473 try writer.writeAll("\n");
1474 }
1475 }
1476 try stderr.setColor(.reset);
1477 } else {
1478 // Just print the failing step itself.
1479 try stderr.setColor(.dim);
1480 try writer.writeAll(failing_step.name);
1481 try printStepFailure(failing_step, stderr, true);
1482 try stderr.setColor(.reset);
1483 }
1484
1485 if (failing_step.result_stderr.len > 0) {
1486 try writer.writeAll(failing_step.result_stderr);
1487 if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) {
1488 try writer.writeAll("\n");
1489 }
1490 }
1491
1492 try failing_step.result_error_bundle.renderToTerminal(options, stderr);
1493
1494 for (failing_step.result_error_msgs.items) |msg| {
1495 try stderr.setColor(.red);
1496 try writer.writeAll("error:");
1497 try stderr.setColor(.reset);
1498 if (std.mem.indexOfScalar(u8, msg, '\n') == null) {
1499 try writer.print(" {s}\n", .{msg});
1500 } else switch (multiline_errors) {
1501 .indent => {
1502 var it = std.mem.splitScalar(u8, msg, '\n');
1503 try writer.print(" {s}\n", .{it.first()});
1504 while (it.next()) |line| {
1505 try writer.print(" {s}\n", .{line});
1506 }
1507 },
1508 .newline => try writer.print("\n{s}\n", .{msg}),
1509 .none => try writer.print(" {s}\n", .{msg}),
1510 }
1511 }
1512
1513 if (error_style.verboseContext()) {
1514 if (failing_step.result_failed_command) |cmd_str| {
1515 try stderr.setColor(.red);
1516 try writer.writeAll("failed command: ");
1517 try stderr.setColor(.reset);
1518 try writer.writeAll(cmd_str);
1519 try writer.writeByte('\n');
1520 }
1521 }
1522
1523 try writer.writeByte('\n');
1524}
1525
1526fn printSteps(builder: *std.Build, w: *Writer) !void {
1527 const arena = builder.graph.arena;
1528 for (builder.top_level_steps.values()) |top_level_step| {
1529 const name = if (&top_level_step.step == builder.default_step)
1530 try fmt.allocPrint(arena, "{s} (default)", .{top_level_step.step.name})
1531 else
1532 top_level_step.step.name;
1533 try w.print(" {s:<28} {s}\n", .{ name, top_level_step.description });
1534 }
1535}
1536
1537fn printUsage(b: *std.Build, w: *Writer) !void {
1538 const arena = b.graph.arena;
1539
1540 try w.print(
1541 \\Usage: {s} build [steps] [options]
1542 \\
1543 \\Steps:
1544 \\
1545 , .{b.graph.zig_exe});
1546 try printSteps(b, w);
1547 try w.writeAll(
1548 \\
1549 \\Project-Specific Options:
1550 \\
1551 );
1552
1553 if (b.available_options_list.items.len == 0) {
1554 try w.print(" (none)\n", .{});
1555 } else {
1556 for (b.available_options_list.items) |option| {
1557 const name = try fmt.allocPrint(arena, " -D{s}=[{t}]", .{ option.name, option.type_id });
1558 try w.print("{s:<30} {s}\n", .{ name, option.description });
1559 if (option.enum_options) |enum_options| {
1560 const padding: [33]u8 = @splat(' ');
1561 try w.writeAll(padding ++ "Supported Values:\n");
1562 for (enum_options) |enum_option| {
1563 try w.print(padding ++ " {s}\n", .{enum_option});
1564 }
1565 }
1566 }
1567 }
1568
1569 try w.writeAll(
1570 \\
1571 \\System Integration Options:
1572 \\ --search-prefix [path] Add a path to look for binaries, libraries, headers
1573 \\ --sysroot [path] Set the system root directory (usually /)
1574 \\ --libc [file] Provide a file which specifies libc paths
1575 \\
1576 \\ --system [pkgdir] Disable package fetching; enable all integrations
1577 \\ -fsys=[name] Enable a system integration
1578 \\ -fno-sys=[name] Disable a system integration
1579 \\
1580 \\ -fdarling, -fno-darling Integration with system-installed Darling to
1581 \\ execute macOS programs on Linux hosts
1582 \\ (default: no)
1583 \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute
1584 \\ foreign-architecture programs on Linux hosts
1585 \\ (default: no)
1586 \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc
1587 \\ (e.g. glibc or musl) built for multiple foreign
1588 \\ architectures, allowing execution of non-native
1589 \\ programs that link with libc.
1590 \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
1591 \\ ARM64 macOS hosts. (default: no)
1592 \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
1593 \\ execute WASI binaries. (default: no)
1594 \\ -fwine, -fno-wine Integration with system-installed Wine to execute
1595 \\ Windows programs on Linux hosts. (default: no)
1596 \\
1597 \\ Available System Integrations: Enabled:
1598 \\
1599 );
1600 if (b.graph.system_library_options.entries.len == 0) {
1601 try w.writeAll(" (none) -\n");
1602 } else {
1603 for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
1604 const status = switch (v) {
1605 .declared_enabled => "yes",
1606 .declared_disabled => "no",
1607 .user_enabled, .user_disabled => unreachable, // already emitted error
1608 };
1609 try w.print(" {s:<43} {s}\n", .{ k, status });
1610 }
1611 }
1612
1613 try w.writeAll(
1614 \\
1615 \\General Options:
1616 \\ -h, --help Print this help and exit
1617 \\ -l, --list-steps Print available steps
1618 \\
1619 \\ -p, --prefix [path] Where to install files (default: zig-out)
1620 \\ --prefix-lib-dir [path] Where to install libraries
1621 \\ --prefix-exe-dir [path] Where to install executables
1622 \\ --prefix-include-dir [path] Where to install C header files
1623 \\ --release[=mode] Request release mode, optionally specifying a
1624 \\ preferred optimization mode: fast, safe, small
1625 \\
1626 \\ --verbose Print commands before executing them
1627 \\ --color [auto|off|on] Enable or disable colored error messages
1628 \\ --error-style [style] Control how build errors are printed
1629 \\ verbose (Default) Report errors with full context
1630 \\ minimal Report errors after summary, excluding context like command lines
1631 \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update
1632 \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update
1633 \\ --multiline-errors [style] Control how multi-line error messages are printed
1634 \\ indent (Default) Indent non-initial lines to align with initial line
1635 \\ newline Include a leading newline so that the error message is on its own lines
1636 \\ none Print as usual so the first line is misaligned
1637 \\ --summary [mode] Control the printing of the build summary
1638 \\ all Print the build summary in its entirety
1639 \\ new Omit cached steps
1640 \\ failures (Default if short-lived) Only print failed steps
1641 \\ line (Default if long-lived) Only print the single-line summary
1642 \\ none Do not print the build summary
1643 \\ -j<N> Limit concurrent jobs (default is to use all CPU cores)
1644 \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
1645 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
1646 \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded.
1647 \\ The timeout must include a unit: ns, us, ms, s, m, h
1648 \\ --watch Continuously rebuild when source files are modified
1649 \\ --debounce <ms> Delay before rebuilding after changed file detected
1650 \\ --webui[=ip] Enable the web interface on the given IP address
1651 \\ --fuzz[=limit] Continuously search for unit test failures with an optional
1652 \\ limit to the max number of iterations. The argument supports
1653 \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
1654 \\ '--webui' when no limit is specified.
1655 \\ --time-report Force full rebuild and provide detailed information on
1656 \\ compilation time of Zig source code (implies '--webui')
1657 \\ -fincremental Enable incremental compilation
1658 \\ -fno-incremental Disable incremental compilation
1659 \\
1660 \\Package Management Options:
1661 \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit
1662 \\ needed (Default) Lazy dependencies are fetched as needed
1663 \\ all Lazy dependencies are always fetched
1664 \\ --fork=[path] Override one or more projects from dependency tree
1665 \\
1666 \\Advanced Options:
1667 \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error
1668 \\ -fno-reference-trace Disable reference trace
1669 \\ -fallow-so-scripts Allows .so files to be GNU ld scripts
1670 \\ -fno-allow-so-scripts (default) .so files must be ELF files
1671 \\ --build-file [file] Override path to build.zig
1672 \\ --cache-dir [path] Override path to local Zig cache directory
1673 \\ --global-cache-dir [path] Override path to global Zig cache directory
1674 \\ --zig-lib-dir [arg] Override path to Zig lib directory
1675 \\ --build-runner [file] Override path to build runner
1676 \\ --seed [integer] For shuffling dependency traversal order (default: random)
1677 \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries
1678 \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM)
1679 \\ sha1, tree 20-byte cryptographic hash (ELF, WASM)
1680 \\ md5 16-byte cryptographic hash (ELF)
1681 \\ uuid 16-byte random UUID (ELF, WASM)
1682 \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM)
1683 \\ none (default) No build ID
1684 \\ --debug-log [scope] Enable debugging the compiler
1685 \\ --debug-pkg-config Fail if unknown pkg-config flags encountered
1686 \\ --debug-rt Debug compiler runtime libraries
1687 \\ --verbose-link Enable compiler debug output for linking
1688 \\ --verbose-air Enable compiler debug output for Zig AIR
1689 \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
1690 \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
1691 \\ --verbose-cimport Enable compiler debug output for C imports
1692 \\ --verbose-cc Enable compiler debug output for C compilation
1693 \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
1694 \\
1695 );
1696}
1697
1698fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
1699 if (idx.* >= args.len) return null;
1700 defer idx.* += 1;
1701 return args[idx.*];
1702}
1703
1704fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
1705 return nextArg(args, idx) orelse {
1706 std.debug.print("expected argument after '{s}'\n access the help menu with 'zig build -h'\n", .{args[idx.* - 1]});
1707 process.exit(1);
1708 };
1709}
1710
1711fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
1712 if (idx >= args.len) return null;
1713 return args[idx..];
1714}
1715
1716const Color = std.zig.Color;
1717const ErrorStyle = enum {
1718 verbose,
1719 minimal,
1720 verbose_clear,
1721 minimal_clear,
1722 fn verboseContext(s: ErrorStyle) bool {
1723 return switch (s) {
1724 .verbose, .verbose_clear => true,
1725 .minimal, .minimal_clear => false,
1726 };
1727 }
1728 fn clearOnUpdate(s: ErrorStyle) bool {
1729 return switch (s) {
1730 .verbose, .minimal => false,
1731 .verbose_clear, .minimal_clear => true,
1732 };
1733 }
1734};
1735const MultilineErrors = enum { indent, newline, none };
1736const Summary = enum { all, new, failures, line, none };
1737
1738fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
1739 std.debug.print(f ++ "\n access the help menu with 'zig build -h'\n", args);
1740 process.exit(1);
1741}
1742
1743fn validateSystemLibraryOptions(b: *std.Build) void {
1744 var bad = false;
1745 for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
1746 switch (v) {
1747 .user_disabled, .user_enabled => {
1748 // The user tried to enable or disable a system library integration, but
1749 // the build script did not recognize that option.
1750 std.debug.print("system library name not recognized by build script: '{s}'\n", .{k});
1751 bad = true;
1752 },
1753 .declared_disabled, .declared_enabled => {},
1754 }
1755 }
1756 if (bad) {
1757 std.debug.print(" access the help menu with 'zig build -h'\n", .{});
1758 process.exit(1);
1759 }
1760}
1761
1762/// Starting from all top-level steps in `b`, traverses the entire step graph
1763/// and adds all step dependencies implied by module graphs.
1764fn createModuleDependencies(b: *std.Build) Allocator.Error!void {
1765 const arena = b.graph.arena;
1766
1767 var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty;
1768 var next_step_idx: usize = 0;
1769
1770 try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count());
1771 for (b.top_level_steps.values()) |tls| {
1772 all_steps.putAssumeCapacityNoClobber(&tls.step, {});
1773 }
1774
1775 while (next_step_idx < all_steps.count()) {
1776 const step = all_steps.keys()[next_step_idx];
1777 next_step_idx += 1;
1778
1779 // Set up any implied dependencies for this step. It's important that we do this first, so
1780 // that the loop below discovers steps implied by the module graph.
1781 try createModuleDependenciesForStep(step);
1782
1783 try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len);
1784 for (step.dependencies.items) |other_step| {
1785 all_steps.putAssumeCapacity(other_step, {});
1786 }
1787 }
1788}
1789
1790/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
1791/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
1792fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
1793 const root_module = if (step.cast(Step.Compile)) |cs| root: {
1794 break :root cs.root_module;
1795 } else return; // not a compile step so no module dependencies
1796
1797 // Starting from `root_module`, discover all modules in this graph.
1798 const modules = root_module.getGraph().modules;
1799
1800 // For each of those modules, set up the implied step dependencies.
1801 for (modules) |mod| {
1802 if (mod.root_source_file) |lp| lp.addStepDependencies(step);
1803 for (mod.include_dirs.items) |include_dir| switch (include_dir) {
1804 .path,
1805 .path_system,
1806 .path_after,
1807 .framework_path,
1808 .framework_path_system,
1809 .embed_path,
1810 => |lp| lp.addStepDependencies(step),
1811
1812 .other_step => |other| {
1813 other.getEmittedIncludeTree().addStepDependencies(step);
1814 step.dependOn(&other.step);
1815 },
1816
1817 .config_header_step => |other| step.dependOn(&other.step),
1818 };
1819 for (mod.lib_paths.items) |lp| lp.addStepDependencies(step);
1820 for (mod.rpaths.items) |rpath| switch (rpath) {
1821 .lazy_path => |lp| lp.addStepDependencies(step),
1822 .special => {},
1823 };
1824 for (mod.link_objects.items) |link_object| switch (link_object) {
1825 .static_path,
1826 .assembly_file,
1827 => |lp| lp.addStepDependencies(step),
1828 .other_step => |other| step.dependOn(&other.step),
1829 .system_lib => {},
1830 .c_source_file => |source| source.file.addStepDependencies(step),
1831 .c_source_files => |source_files| source_files.root.addStepDependencies(step),
1832 .win32_resource_file => |rc_source| {
1833 rc_source.file.addStepDependencies(step);
1834 for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
1835 },
1836 };
1837 }
1838}
1839
1840var stdio_buffer_allocation: [256]u8 = undefined;
1841var stdout_writer_allocation: Io.File.Writer = undefined;
1842
1843fn initStdoutWriter(io: Io) *Writer {
1844 stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation);
1845 return &stdout_writer_allocation.interface;
1846}
1847
1848fn cleanTmpFiles(io: Io, steps: []const *Step) void {
1849 for (steps) |step| {
1850 const wf = step.cast(std.Build.Step.WriteFile) orelse continue;
1851 if (wf.mode != .tmp) continue;
1852 const path = wf.generated_directory.path orelse continue;
1853 Io.Dir.cwd().deleteTree(io, path) catch |err| {
1854 std.log.warn("failed to delete {s}: {t}", .{ path, err });
1855 };
1856 }
1857}
lib/compiler/configure_runner.zig+168-88
......@@ -8,12 +8,11 @@ const mem = std.mem;
88const process = std.process;
99const File = std.Io.File;
1010const Step = std.Build.Step;
11const Watch = std.Build.Watch;
12const WebServer = std.Build.WebServer;
1311const Allocator = std.mem.Allocator;
1412const fatal = std.process.fatal;
1513const Writer = std.Io.Writer;
1614const Color = std.zig.Color;
15const Configuration = std.Build.Configuration;
1716
1817pub const root = @import("@build");
1918pub const dependencies = @import("@dependencies");
......@@ -26,7 +25,11 @@ pub const std_options: std.Options = .{
2625pub fn main(init: process.Init.Minimal) !void {
2726 // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not
2827 // always the case. So, we do need a true gpa for some things.
29 var debug_gpa_state: std.heap.DebugAllocator(.{}) = .init;
28 var debug_gpa_state: std.heap.DebugAllocator(.{
29 // We'd rather have `zig build` run faster than catch harmless leaks in
30 // the user's build.zig script.
31 .stack_trace_frames = 0,
32 }) = .init;
3033 defer _ = debug_gpa_state.deinit();
3134 const gpa = debug_gpa_state.allocator();
3235
......@@ -47,11 +50,11 @@ pub fn main(init: process.Init.Minimal) !void {
4750 // skip my own exe name
4851 var arg_idx: usize = 1;
4952
50 const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{});
51 const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{});
52 const build_root = nextArg(args, &arg_idx) orelse fatal("missing build root directory path", .{});
53 const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{});
54 const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{});
53 const zig_exe = expectArgOrFatal(args, &arg_idx, "--zig");
54 const zig_lib_dir = expectArgOrFatal(args, &arg_idx, "--zig-lib-dir");
55 const build_root = expectArgOrFatal(args, &arg_idx, "--build-root");
56 const local_cache_root = expectArgOrFatal(args, &arg_idx, "--local-cache");
57 const global_cache_root = expectArgOrFatal(args, &arg_idx, "--global-cache");
5558
5659 const cwd: Io.Dir = .cwd();
5760
......@@ -66,8 +69,8 @@ pub fn main(init: process.Init.Minimal) !void {
6669 };
6770
6871 const local_cache_directory: std.Build.Cache.Directory = .{
69 .path = cache_root,
70 .handle = try cwd.createDirPathOpen(io, cache_root, .{}),
72 .path = local_cache_root,
73 .handle = try cwd.createDirPathOpen(io, local_cache_root, .{}),
7174 };
7275
7376 const global_cache_directory: std.Build.Cache.Directory = .{
......@@ -137,8 +140,6 @@ pub fn main(init: process.Init.Minimal) !void {
137140 if (try builder.addUserInputFlag(option_contents))
138141 fatal(" access the help menu with 'zig build -h'", .{});
139142 }
140 } else if (mem.eql(u8, arg, "--verbose")) {
141 builder.verbose = true;
142143 } else if (mem.startsWith(u8, arg, "-fsys=")) {
143144 const name = arg["-fsys=".len..];
144145 graph.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM");
......@@ -146,19 +147,14 @@ pub fn main(init: process.Init.Minimal) !void {
146147 const name = arg["-fno-sys=".len..];
147148 graph.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM");
148149 } else if (mem.eql(u8, arg, "--release")) {
149 builder.release_mode = .any;
150 graph.release_mode = .any;
150151 } else if (mem.startsWith(u8, arg, "--release=")) {
151152 const text = arg["--release=".len..];
152 builder.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, text) orelse {
153 graph.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, text) orelse {
153154 fatalWithHint("expected [off|any|fast|safe|small] in '{s}', found '{s}'", .{
154155 arg, text,
155156 });
156157 };
157 } else if (mem.eql(u8, arg, "--search-prefix")) {
158 const search_prefix = nextArgOrFatal(args, &arg_idx);
159 builder.addSearchPrefix(search_prefix);
160 } else if (mem.eql(u8, arg, "--libc")) {
161 builder.libc_file = nextArgOrFatal(args, &arg_idx);
162158 } else if (mem.eql(u8, arg, "--color")) {
163159 const next_arg = nextArg(args, &arg_idx) orelse
164160 fatalWithHint("expected [auto|on|off] after '{s}'", .{arg});
......@@ -196,8 +192,6 @@ pub fn main(init: process.Init.Minimal) !void {
196192 style, @errorName(err),
197193 });
198194 };
199 } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
200 builder.debug_pkg_config = true;
201195 } else if (mem.eql(u8, arg, "--debug-rt")) {
202196 graph.debug_compiler_runtime_libs = true;
203197 } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
......@@ -209,71 +203,11 @@ pub fn main(init: process.Init.Minimal) !void {
209203 // but it is handled by the parent process. The build runner
210204 // only sees this flag.
211205 graph.system_package_mode = true;
212 } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) {
213 // --glibc-runtimes was the old name of the flag; kept for compatibility for now.
214 builder.libc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
215 } else if (mem.eql(u8, arg, "--verbose-link")) {
216 builder.verbose_link = true;
217 } else if (mem.eql(u8, arg, "--verbose-air")) {
218 builder.verbose_air = true;
219 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
220 builder.verbose_llvm_ir = "-";
221 } else if (mem.startsWith(u8, arg, "--verbose-llvm-ir=")) {
222 builder.verbose_llvm_ir = arg["--verbose-llvm-ir=".len..];
223 } else if (mem.startsWith(u8, arg, "--verbose-llvm-bc=")) {
224 builder.verbose_llvm_bc = arg["--verbose-llvm-bc=".len..];
225 } else if (mem.eql(u8, arg, "--verbose-cimport")) {
226 builder.verbose_cimport = true;
227 } else if (mem.eql(u8, arg, "--verbose-cc")) {
228 builder.verbose_cc = true;
229 } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
230 builder.verbose_llvm_cpu_features = true;
231 } else if (mem.eql(u8, arg, "-fincremental")) {
232 graph.incremental = true;
233 } else if (mem.eql(u8, arg, "-fno-incremental")) {
234 graph.incremental = false;
235 } else if (mem.eql(u8, arg, "-fwine")) {
236 builder.enable_wine = true;
237 } else if (mem.eql(u8, arg, "-fno-wine")) {
238 builder.enable_wine = false;
239 } else if (mem.eql(u8, arg, "-fqemu")) {
240 builder.enable_qemu = true;
241 } else if (mem.eql(u8, arg, "-fno-qemu")) {
242 builder.enable_qemu = false;
243 } else if (mem.eql(u8, arg, "-fwasmtime")) {
244 builder.enable_wasmtime = true;
245 } else if (mem.eql(u8, arg, "-fno-wasmtime")) {
246 builder.enable_wasmtime = false;
247 } else if (mem.eql(u8, arg, "-frosetta")) {
248 builder.enable_rosetta = true;
249 } else if (mem.eql(u8, arg, "-fno-rosetta")) {
250 builder.enable_rosetta = false;
251 } else if (mem.eql(u8, arg, "-fdarling")) {
252 builder.enable_darling = true;
253 } else if (mem.eql(u8, arg, "-fno-darling")) {
254 builder.enable_darling = false;
255 } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
256 graph.allow_so_scripts = true;
257 } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
258 graph.allow_so_scripts = false;
259 } else if (mem.eql(u8, arg, "-freference-trace")) {
260 builder.reference_trace = 256;
261 } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
262 const num = arg["-freference-trace=".len..];
263 builder.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
264 std.debug.print("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
265 process.exit(1);
266 };
267 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
268 builder.reference_trace = null;
269206 } else if (mem.cutPrefix(u8, arg, "-j")) |text| {
270207 const n = std.fmt.parseUnsigned(u32, text, 10) catch |err|
271208 fatal("unable to parse jobs count '{s}': {t}", .{ text, err });
272209 if (n < 1) fatal("number of jobs must be at least 1", .{});
273210 threaded.setAsyncLimit(.limited(n));
274 } else if (mem.eql(u8, arg, "--")) {
275 builder.args = argsRest(args, arg_idx);
276 break;
277211 } else {
278212 fatalWithHint("unrecognized argument: '{s}'", .{arg});
279213 }
......@@ -289,6 +223,150 @@ pub fn main(init: process.Init.Minimal) !void {
289223 };
290224
291225 try builder.runBuild(root);
226
227 var wc: Configuration.Wip = .init(gpa);
228 defer wc.deinit();
229
230 var stdout_buffer: [1024]u8 = undefined;
231 var file_writer = Io.File.stdout().writerStreaming(io, &stdout_buffer);
232 serialize(builder, &wc, &file_writer.interface) catch |err| switch (err) {
233 error.WriteFailed => fatal("failed to write configuration output: {t}", .{file_writer.err.?}),
234 error.OutOfMemory => |e| return e,
235 };
236
237 // This executable is short-lived and run in Debug mode, so we'd rather
238 // have `zig build` run faster than catch resource leaks in the user's
239 // build.zig script (or, frankly, this configure runner), therefore we call
240 // exit directly here rather than cleanExit.
241 process.exit(0);
242}
243
244fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
245 const graph = b.graph;
246 const arena = graph.arena;
247 const gpa = wc.gpa;
248
249 // Starting from all top-level steps in `b`, traverse the entire step graph
250 // and add all step dependencies implied by module graphs.
251 const top_level_steps = b.top_level_steps.values();
252 // Index corresponds to `Configuration.steps` index.
253 var step_map: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty;
254 try step_map.ensureUnusedCapacity(arena, top_level_steps.len);
255 for (top_level_steps) |tls| {
256 step_map.putAssumeCapacityNoClobber(&tls.step, {});
257 }
258 {
259 while (wc.steps.items.len < step_map.count()) {
260 const step = step_map.keys()[wc.steps.items.len];
261
262 // Set up any implied dependencies for this step. It's important that we do this first, so
263 // that the loop below discovers steps implied by the module graph.
264 try createModuleDependenciesForStep(step);
265
266 try step_map.ensureUnusedCapacity(arena, step.dependencies.items.len);
267 for (step.dependencies.items) |other_step| {
268 step_map.putAssumeCapacity(other_step, {});
269 }
270
271 // Add and then de-duplicate dependencies.
272 const deps = d: {
273 const deps: Configuration.Deps = @enumFromInt(wc.extra.items.len);
274 for (try wc.prepareDeps(step.dependencies.items.len), step.dependencies.items) |*dep, dep_step|
275 dep.* = @intCast(step_map.getIndex(dep_step).?);
276 break :d try wc.dedupeDeps(deps);
277 };
278
279 try wc.steps.ensureTotalCapacity(gpa, step_map.entries.capacity);
280 wc.steps.appendAssumeCapacity(.{
281 .name = try wc.addString(step.name),
282 .flags = .{ .tag = step.tag },
283 .deps = deps,
284 .extra_index = switch (step.tag) {
285 .top_level => e: {
286 const top_level: *Step.TopLevel = @fieldParentPtr("step", step);
287 break :e try wc.addExtra(@as(Configuration.Step.TopLevel, .{
288 .description = try wc.addString(top_level.description),
289 }));
290 },
291 .compile => @panic("TODO"),
292 .install_artifact => @panic("TODO"),
293 .install_file => @panic("TODO"),
294 .install_dir => @panic("TODO"),
295 .remove_dir => @panic("TODO"),
296 .fail => @panic("TODO"),
297 .fmt => @panic("TODO"),
298 .translate_c => @panic("TODO"),
299 .write_file => @panic("TODO"),
300 .update_source_files => @panic("TODO"),
301 .run => @panic("TODO"),
302 .check_file => @panic("TODO"),
303 .check_object => @panic("TODO"),
304 .config_header => @panic("TODO"),
305 .objcopy => @panic("TODO"),
306 .options => @panic("TODO"),
307 },
308 });
309 }
310 }
311
312 try wc.unlazy_deps.ensureUnusedCapacity(gpa, graph.needed_lazy_dependencies.keys().len);
313 for (graph.needed_lazy_dependencies.keys()) |k| {
314 wc.unlazy_deps.appendAssumeCapacity(try wc.addString(k));
315 }
316
317 try wc.write(writer, .{
318 .default_step = @intCast(step_map.getIndex(b.default_step).?),
319 });
320}
321
322/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
323/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
324fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
325 const root_module = if (step.cast(Step.Compile)) |cs| root: {
326 break :root cs.root_module;
327 } else return; // not a compile step so no module dependencies
328
329 // Starting from `root_module`, discover all modules in this graph.
330 const modules = root_module.getGraph().modules;
331
332 // For each of those modules, set up the implied step dependencies.
333 for (modules) |mod| {
334 if (mod.root_source_file) |lp| lp.addStepDependencies(step);
335 for (mod.include_dirs.items) |include_dir| switch (include_dir) {
336 .path,
337 .path_system,
338 .path_after,
339 .framework_path,
340 .framework_path_system,
341 .embed_path,
342 => |lp| lp.addStepDependencies(step),
343
344 .other_step => |other| {
345 other.getEmittedIncludeTree().addStepDependencies(step);
346 step.dependOn(&other.step);
347 },
348
349 .config_header_step => |other| step.dependOn(&other.step),
350 };
351 for (mod.lib_paths.items) |lp| lp.addStepDependencies(step);
352 for (mod.rpaths.items) |rpath| switch (rpath) {
353 .lazy_path => |lp| lp.addStepDependencies(step),
354 .special => {},
355 };
356 for (mod.link_objects.items) |link_object| switch (link_object) {
357 .static_path,
358 .assembly_file,
359 => |lp| lp.addStepDependencies(step),
360 .other_step => |other| step.dependOn(&other.step),
361 .system_lib => {},
362 .c_source_file => |source| source.file.addStepDependencies(step),
363 .c_source_files => |source_files| source_files.root.addStepDependencies(step),
364 .win32_resource_file => |rc_source| {
365 rc_source.file.addStepDependencies(step);
366 for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
367 },
368 };
369 }
292370}
293371
294372fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
......@@ -299,14 +377,17 @@ fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
299377
300378fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
301379 return nextArg(args, idx) orelse {
302 std.debug.print("expected argument after '{s}'\n access the help menu with 'zig build -h'\n", .{args[idx.* - 1]});
303 process.exit(1);
380 fatal("expected argument after {q}\n access the help menu with \"zig build -h\"", .{
381 args[idx.* - 1],
382 });
304383 };
305384}
306385
307fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
308 if (idx >= args.len) return null;
309 return args[idx..];
386fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {
387 const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});
388 if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg });
389 const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first});
390 return arg;
310391}
311392
312393const ErrorStyle = enum {
......@@ -331,6 +412,5 @@ const MultilineErrors = enum { indent, newline, none };
331412const Summary = enum { all, new, failures, line, none };
332413
333414fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
334 std.debug.print(f ++ "\n access the help menu with 'zig build -h'\n", args);
335 process.exit(1);
415 fatal(f ++ "\n access the help menu with \"zig build -h\"", args);
336416}
lib/compiler/maker.zig created+1717
......@@ -0,0 +1,1717 @@
1const builtin = @import("builtin");
2
3const std = @import("std");
4const Io = std.Io;
5const assert = std.debug.assert;
6const fmt = std.fmt;
7const mem = std.mem;
8const process = std.process;
9const File = std.Io.File;
10const Allocator = std.mem.Allocator;
11const fatal = std.process.fatal;
12const Writer = std.Io.Writer;
13const Cache = std.Build.Cache;
14const Configuration = std.Build.Configuration;
15
16const Fuzz = @import("maker/Fuzz.zig");
17const Graph = @import("maker/Graph.zig");
18const Step = @import("maker/Step.zig");
19const Watch = @import("maker/Watch.zig");
20const WebServer = @import("maker/WebServer.zig");
21
22pub const std_options: std.Options = .{
23 .side_channels_mitigations = .none,
24 .http_disable_tls = true,
25};
26
27pub fn main(init: process.Init.Minimal) !void {
28 // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not
29 // always the case. So, we do need a true gpa for some things.
30 var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{});
31 defer _ = safe_gpa_state.deinit();
32 const gpa = safe_gpa_state.allocator();
33
34 var threaded: std.Io.Threaded = .init(gpa, .{
35 .environ = init.environ,
36 .argv0 = .init(init.args),
37 });
38 defer threaded.deinit();
39 const io = threaded.io();
40
41 // ...but we'll back our arena by `std.heap.page_allocator` for efficiency.
42 var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
43 defer arena_instance.deinit();
44 const arena = arena_instance.allocator();
45
46 const args = try init.args.toSlice(arena);
47
48 // skip my own exe name
49 var arg_idx: usize = 1;
50
51 const zig_exe = cutArgPrefixOrFatal(args, &arg_idx, "--zig=");
52 const zig_lib_dir = cutArgPrefixOrFatal(args, &arg_idx, "--lib=");
53 const build_root = cutArgPrefixOrFatal(args, &arg_idx, "--build-root=");
54 const local_cache_root = cutArgPrefixOrFatal(args, &arg_idx, "--local-cache=");
55 const global_cache_root = cutArgPrefixOrFatal(args, &arg_idx, "--global-cache=");
56 const configure_path = cutArgPrefixOrFatal(args, &arg_idx, "--configure=");
57
58 const cwd: Io.Dir = .cwd();
59
60 const zig_lib_directory: Cache.Directory = .{
61 .path = zig_lib_dir,
62 .handle = try cwd.openDir(io, zig_lib_dir, .{}),
63 };
64
65 const build_root_directory: Cache.Directory = .{
66 .path = build_root,
67 .handle = try cwd.openDir(io, build_root, .{}),
68 };
69
70 const local_cache_directory: Cache.Directory = .{
71 .path = local_cache_root,
72 .handle = try cwd.createDirPathOpen(io, local_cache_root, .{}),
73 };
74
75 const global_cache_directory: Cache.Directory = .{
76 .path = global_cache_root,
77 .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}),
78 };
79
80 var graph: Graph = .{
81 .io = io,
82 .arena = arena,
83 .cache = .{
84 .io = io,
85 .gpa = gpa,
86 .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}),
87 .cwd = try process.currentPathAlloc(io, arena),
88 },
89 .zig_exe = zig_exe,
90 .environ_map = try init.environ.createMap(arena),
91 .global_cache_root = global_cache_directory,
92 .zig_lib_directory = zig_lib_directory,
93 .host = .{
94 .query = .{},
95 .result = try std.zig.system.resolveTargetQuery(io, .{}),
96 },
97 .time_report = false,
98 };
99
100 graph.cache.addPrefix(.{ .path = null, .handle = cwd });
101 graph.cache.addPrefix(build_root_directory);
102 graph.cache.addPrefix(local_cache_directory);
103 graph.cache.addPrefix(global_cache_directory);
104 graph.cache.hash.addBytes(builtin.zig_version_string);
105
106 var targets = std.array_list.Managed([]const u8).init(arena);
107 var debug_log_scopes = std.array_list.Managed([]const u8).init(arena);
108
109 var install_prefix: ?[]const u8 = null;
110 var dir_list: std.Build.DirList = .{};
111 var error_style: ErrorStyle = .verbose;
112 var multiline_errors: MultilineErrors = .indent;
113 var summary: ?Summary = null;
114 var max_rss: u64 = 0;
115 var skip_oom_steps = false;
116 var test_timeout_ns: ?u64 = null;
117 var color: Color = .auto;
118 var help_menu = false;
119 var steps_menu = false;
120 var watch = false;
121 var fuzz: ?Fuzz.Mode = null;
122 var debounce_interval_ms: u16 = 50;
123 var webui_listen: ?Io.net.IpAddress = null;
124 var verbose = false;
125 var sysroot: ?[]const u8 = null;
126 var search_prefixes: std.ArrayList([]const u8) = .empty;
127 var libc_file: ?[]const u8 = null;
128 var debug_pkg_config: bool = false;
129 // After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md,
130 // this will be the directory $glibc-build-dir/install/glibcs
131 // Given the example of the aarch64 target, this is the directory
132 // that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`.
133 // Also works for dynamic musl.
134 var libc_runtimes_dir: ?[]const u8 = null;
135 var enable_wine = false;
136 var enable_qemu = false;
137 var enable_wasmtime = false;
138 var enable_darling = false;
139 var enable_rosetta = false;
140 var reference_trace: ?u32 = null;
141 var run_args: ?[]const []const u8 = null;
142
143 if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| {
144 if (std.meta.stringToEnum(ErrorStyle, str)) |style| {
145 error_style = style;
146 }
147 }
148
149 if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| {
150 if (std.meta.stringToEnum(MultilineErrors, str)) |style| {
151 multiline_errors = style;
152 }
153 }
154
155 var configuration: Configuration = undefined;
156 {
157 var file = cwd.openFile(io, configure_path, .{}) catch |err|
158 fatal("failed to open configuration file {f}: {t}", .{ configure_path, err });
159 defer file.close(io);
160 configuration = Configuration.load(arena, io, file) catch |err|
161 fatal("failed to load configuration file {f}: {t}", .{ configure_path, err });
162 }
163 graph.configuration = &configuration;
164 graph.scanConfiguration();
165
166 std.log.err("TODO handle user -D options", .{});
167
168 while (nextArg(args, &arg_idx)) |arg| {
169 if (mem.startsWith(u8, arg, "-")) {
170 if (mem.eql(u8, arg, "--verbose")) {
171 verbose = true;
172 } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
173 help_menu = true;
174 } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
175 install_prefix = nextArgOrFatal(args, &arg_idx);
176 } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
177 steps_menu = true;
178 } else if (mem.startsWith(u8, arg, "-fsys=")) {
179 const name = arg["-fsys=".len..];
180 graph.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM");
181 } else if (mem.startsWith(u8, arg, "-fno-sys=")) {
182 const name = arg["-fno-sys=".len..];
183 graph.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM");
184 } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
185 dir_list.lib_dir = nextArgOrFatal(args, &arg_idx);
186 } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
187 dir_list.exe_dir = nextArgOrFatal(args, &arg_idx);
188 } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
189 dir_list.include_dir = nextArgOrFatal(args, &arg_idx);
190 } else if (mem.eql(u8, arg, "--sysroot")) {
191 sysroot = nextArgOrFatal(args, &arg_idx);
192 } else if (mem.eql(u8, arg, "--maxrss")) {
193 const max_rss_text = nextArgOrFatal(args, &arg_idx);
194 max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| {
195 std.debug.print("invalid byte size: '{s}': {s}\n", .{
196 max_rss_text, @errorName(err),
197 });
198 process.exit(1);
199 };
200 } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
201 skip_oom_steps = true;
202 } else if (mem.eql(u8, arg, "--test-timeout")) {
203 const units: []const struct { []const u8, u64 } = &.{
204 .{ "ns", 1 },
205 .{ "nanosecond", 1 },
206 .{ "us", std.time.ns_per_us },
207 .{ "microsecond", std.time.ns_per_us },
208 .{ "ms", std.time.ns_per_ms },
209 .{ "millisecond", std.time.ns_per_ms },
210 .{ "s", std.time.ns_per_s },
211 .{ "second", std.time.ns_per_s },
212 .{ "m", std.time.ns_per_min },
213 .{ "minute", std.time.ns_per_min },
214 .{ "h", std.time.ns_per_hour },
215 .{ "hour", std.time.ns_per_hour },
216 };
217 const timeout_str = nextArgOrFatal(args, &arg_idx);
218 const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal(
219 "invalid timeout '{s}': expected unit (ns, us, ms, s, m, h)",
220 .{timeout_str},
221 );
222 const num_str = timeout_str[0 .. num_end_idx + 1];
223 const unit_str = timeout_str[num_end_idx + 1 ..];
224 const unit_factor: f64 = for (units) |unit_and_factor| {
225 if (std.mem.eql(u8, unit_str, unit_and_factor[0])) {
226 break @floatFromInt(unit_and_factor[1]);
227 }
228 } else fatal(
229 "invalid timeout '{s}': invalid unit '{s}' (expected ns, us, ms, s, m, h)",
230 .{ timeout_str, unit_str },
231 );
232 const num_parsed = std.fmt.parseFloat(f64, num_str) catch |err| fatal(
233 "invalid timeout '{s}': invalid number '{s}' ({t})",
234 .{ timeout_str, num_str, err },
235 );
236 test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed);
237 } else if (mem.eql(u8, arg, "--search-prefix")) {
238 try search_prefixes.append(arena, nextArgOrFatal(args, &arg_idx));
239 } else if (mem.eql(u8, arg, "--libc")) {
240 libc_file = nextArgOrFatal(args, &arg_idx);
241 } else if (mem.eql(u8, arg, "--color")) {
242 const next_arg = nextArg(args, &arg_idx) orelse
243 fatalWithHint("expected [auto|on|off] after '{s}'", .{arg});
244 color = std.meta.stringToEnum(Color, next_arg) orelse {
245 fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{
246 arg, next_arg,
247 });
248 };
249 } else if (mem.eql(u8, arg, "--error-style")) {
250 const next_arg = nextArg(args, &arg_idx) orelse
251 fatalWithHint("expected style after '{s}'", .{arg});
252 error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse {
253 fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg });
254 };
255 } else if (mem.eql(u8, arg, "--multiline-errors")) {
256 const next_arg = nextArg(args, &arg_idx) orelse
257 fatalWithHint("expected style after '{s}'", .{arg});
258 multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse {
259 fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg });
260 };
261 } else if (mem.eql(u8, arg, "--summary")) {
262 const next_arg = nextArg(args, &arg_idx) orelse
263 fatalWithHint("expected [all|new|failures|line|none] after '{s}'", .{arg});
264 summary = std.meta.stringToEnum(Summary, next_arg) orelse {
265 fatalWithHint("expected [all|new|failures|line|none] after '{s}', found '{s}'", .{
266 arg, next_arg,
267 });
268 };
269 } else if (mem.eql(u8, arg, "--seed")) {
270 const next_arg = nextArg(args, &arg_idx) orelse
271 fatalWithHint("expected u32 after '{s}'", .{arg});
272 graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
273 fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {s}\n", .{
274 next_arg, @errorName(err),
275 });
276 };
277 } else if (mem.eql(u8, arg, "--debounce")) {
278 const next_arg = nextArg(args, &arg_idx) orelse
279 fatalWithHint("expected u16 after '{s}'", .{arg});
280 debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
281 fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {t}\n", .{
282 next_arg, err,
283 });
284 };
285 } else if (mem.eql(u8, arg, "--webui")) {
286 if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
287 } else if (mem.startsWith(u8, arg, "--webui=")) {
288 const addr_str = arg["--webui=".len..];
289 if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{});
290 webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| {
291 fatal("invalid web UI address '{s}': {s}", .{ addr_str, @errorName(err) });
292 };
293 } else if (mem.eql(u8, arg, "--debug-log")) {
294 const next_arg = nextArgOrFatal(args, &arg_idx);
295 try debug_log_scopes.append(next_arg);
296 } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
297 debug_pkg_config = true;
298 } else if (mem.eql(u8, arg, "--debug-rt")) {
299 graph.debug_compiler_runtime_libs = .Debug;
300 } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| {
301 graph.debug_compiler_runtime_libs = std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse
302 fatal("unrecognized optimization mode: {s}", .{rest});
303 } else if (mem.eql(u8, arg, "--system")) {
304 // The usage text shows another argument after this parameter
305 // but it is handled by the parent process. The build runner
306 // only sees this flag.
307 graph.system_package_mode = true;
308 } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) {
309 // --glibc-runtimes was the old name of the flag; kept for compatibility for now.
310 libc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
311 } else if (mem.eql(u8, arg, "--watch")) {
312 watch = true;
313 } else if (mem.eql(u8, arg, "--time-report")) {
314 graph.time_report = true;
315 if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
316 } else if (mem.eql(u8, arg, "--fuzz")) {
317 fuzz = .{ .forever = undefined };
318 if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
319 } else if (mem.startsWith(u8, arg, "--fuzz=")) {
320 const value = arg["--fuzz=".len..];
321 if (value.len == 0) fatal("missing argument to --fuzz", .{});
322
323 const unit: u8 = value[value.len - 1];
324 const digits = switch (unit) {
325 '0'...'9' => value,
326 'K', 'M', 'G' => value[0 .. value.len - 1],
327 else => fatal(
328 "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
329 .{},
330 ),
331 };
332
333 const amount = std.fmt.parseInt(u64, digits, 10) catch {
334 fatal(
335 "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
336 .{},
337 );
338 };
339
340 const normalized_amount = std.math.mul(u64, amount, switch (unit) {
341 else => unreachable,
342 '0'...'9' => 1,
343 'K' => 1000,
344 'M' => 1_000_000,
345 'G' => 1_000_000_000,
346 }) catch fatal("fuzzing limit amount overflows u64", .{});
347
348 fuzz = .{
349 .limit = .{
350 .amount = normalized_amount,
351 },
352 };
353 } else if (mem.eql(u8, arg, "-fincremental")) {
354 graph.incremental = true;
355 } else if (mem.eql(u8, arg, "-fno-incremental")) {
356 graph.incremental = false;
357 } else if (mem.eql(u8, arg, "-fwine")) {
358 enable_wine = true;
359 } else if (mem.eql(u8, arg, "-fno-wine")) {
360 enable_wine = false;
361 } else if (mem.eql(u8, arg, "-fqemu")) {
362 enable_qemu = true;
363 } else if (mem.eql(u8, arg, "-fno-qemu")) {
364 enable_qemu = false;
365 } else if (mem.eql(u8, arg, "-fwasmtime")) {
366 enable_wasmtime = true;
367 } else if (mem.eql(u8, arg, "-fno-wasmtime")) {
368 enable_wasmtime = false;
369 } else if (mem.eql(u8, arg, "-frosetta")) {
370 enable_rosetta = true;
371 } else if (mem.eql(u8, arg, "-fno-rosetta")) {
372 enable_rosetta = false;
373 } else if (mem.eql(u8, arg, "-fdarling")) {
374 enable_darling = true;
375 } else if (mem.eql(u8, arg, "-fno-darling")) {
376 enable_darling = false;
377 } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
378 graph.allow_so_scripts = true;
379 } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
380 graph.allow_so_scripts = false;
381 } else if (mem.eql(u8, arg, "-freference-trace")) {
382 reference_trace = 256;
383 } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
384 const num = arg["-freference-trace=".len..];
385 reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
386 std.debug.print("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
387 process.exit(1);
388 };
389 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
390 reference_trace = null;
391 } else if (mem.cutPrefix(u8, arg, "-j")) |text| {
392 const n = std.fmt.parseUnsigned(u32, text, 10) catch |err|
393 fatal("unable to parse jobs count '{s}': {t}", .{ text, err });
394 if (n < 1) fatal("number of jobs must be at least 1", .{});
395 threaded.setAsyncLimit(.limited(n));
396 graph.max_jobs = n;
397 } else if (mem.eql(u8, arg, "--")) {
398 run_args = argsRest(args, arg_idx);
399 break;
400 } else {
401 fatalWithHint("unrecognized argument: '{s}'", .{arg});
402 }
403 } else {
404 try targets.append(arg);
405 }
406 }
407
408 const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map);
409 const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
410
411 graph.stderr_mode = switch (color) {
412 .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE),
413 .on => .escape_codes,
414 .off => .no_color,
415 };
416
417 if (webui_listen != null) {
418 if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{});
419 if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{});
420 }
421
422 const main_progress_node = std.Progress.start(io, .{
423 .disable_printing = (color == .off),
424 });
425 defer main_progress_node.end();
426
427 graph.resolveInstallPrefix(install_prefix, dir_list);
428
429 if (graph.validateUserInputDidItFail()) {
430 fatal(" access the help menu with 'zig build -h'", .{});
431 }
432
433 validateSystemLibraryOptions(&graph);
434
435 if (help_menu) {
436 var w = initStdoutWriter(io);
437 printUsage(&graph, w) catch return stdout_writer_allocation.err.?;
438 w.flush() catch return stdout_writer_allocation.err.?;
439 return;
440 }
441
442 if (steps_menu) {
443 var w = initStdoutWriter(io);
444 printSteps(&graph, w) catch return stdout_writer_allocation.err.?;
445 w.flush() catch return stdout_writer_allocation.err.?;
446 return;
447 }
448
449 var run: Run = .{
450 .gpa = gpa,
451
452 .available_rss = max_rss,
453 .max_rss_is_default = false,
454 .max_rss_mutex = .init,
455 .skip_oom_steps = skip_oom_steps,
456 .unit_test_timeout_ns = test_timeout_ns,
457
458 .watch = watch,
459 .web_server = undefined, // set after `prepare`
460 .memory_blocked_steps = .empty,
461 .step_stack = .empty,
462
463 .error_style = error_style,
464 .multiline_errors = multiline_errors,
465 .summary = summary orelse if (watch or webui_listen != null) .line else .failures,
466 };
467 defer {
468 run.memory_blocked_steps.deinit(gpa);
469 run.step_stack.deinit(gpa);
470 }
471
472 if (run.available_rss == 0) {
473 run.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
474 run.max_rss_is_default = true;
475 }
476
477 prepare(arena, &graph, targets.items, &run) catch |err| switch (err) {
478 error.DependencyLoopDetected, error.InsufficientMemory => {
479 // Perhaps in the future there could be an Advanced Options flag
480 // such as --debug-build-runner-leaks which would make this code
481 // return instead of calling exit.
482 _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
483 process.exit(1);
484 },
485 else => |e| return e,
486 };
487
488 var w: Watch = w: {
489 if (!watch) break :w undefined;
490 if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag});
491 break :w try .init(graph.cache.cwd);
492 };
493
494 const now = Io.Clock.Timestamp.now(io, .awake);
495
496 run.web_server = if (webui_listen) |listen_address| ws: {
497 if (builtin.single_threaded) unreachable; // `fatal` above
498 break :ws .init(.{
499 .gpa = gpa,
500 .graph = &graph,
501 .all_steps = run.step_stack.keys(),
502 .root_prog_node = main_progress_node,
503 .watch = watch,
504 .listen_address = listen_address,
505 .base_timestamp = now,
506 });
507 } else null;
508
509 if (run.web_server) |*ws| {
510 ws.start() catch |err| fatal("failed to start web server: {t}", .{err});
511 }
512
513 rebuild: while (true) : (if (run.error_style.clearOnUpdate()) {
514 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
515 defer io.unlockStderr();
516 try stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H");
517 }) {
518 if (run.web_server) |*ws| ws.startBuild();
519
520 try runStepNames(graph, targets.items, main_progress_node, &run, fuzz);
521
522 if (run.web_server) |*web_server| {
523 if (fuzz) |mode| if (mode != .forever) fatal(
524 "error: limited fuzzing is not implemented yet for --webui",
525 .{},
526 );
527
528 web_server.finishBuild(.{ .fuzz = fuzz != null });
529 }
530
531 if (run.web_server) |*ws| {
532 assert(!watch); // fatal error after CLI parsing
533 while (true) switch (try ws.wait()) {
534 .rebuild => {
535 for (run.step_stack.keys()) |step| {
536 step.state = .precheck_done;
537 step.pending_deps = @intCast(step.dependencies.items.len);
538 step.reset(gpa);
539 }
540 continue :rebuild;
541 },
542 };
543 }
544
545 // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
546 if (!Watch.have_impl) unreachable;
547
548 try w.update(gpa, run.step_stack.keys());
549
550 // Wait until a file system notification arrives. Read all such events
551 // until the buffer is empty. Then wait for a debounce interval, resetting
552 // if any more events come in. After the debounce interval has passed,
553 // trigger a rebuild on all steps with modified inputs, as well as their
554 // recursive dependants.
555 var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
556 const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
557 w.dir_count, countSubProcesses(run.step_stack.keys()),
558 }) catch &caption_buf;
559 var debouncing_node = main_progress_node.start(caption, 0);
560 var in_debounce = false;
561 while (true) switch (try w.wait(gpa, io, if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) {
562 .timeout => {
563 assert(in_debounce);
564 debouncing_node.end();
565 markFailedStepsDirty(gpa, run.step_stack.keys());
566 continue :rebuild;
567 },
568 .dirty => if (!in_debounce) {
569 in_debounce = true;
570 debouncing_node.end();
571 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
572 },
573 .clean => {},
574 };
575 }
576}
577
578fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
579 for (all_steps) |step| switch (step.state) {
580 .dependency_failure, .failure, .skipped => _ = step.invalidateResult(gpa),
581 else => continue,
582 };
583 // Now that all dirty steps have been found, the remaining steps that
584 // succeeded from last run shall be marked "cached".
585 for (all_steps) |step| switch (step.state) {
586 .success => step.result_cached = true,
587 else => continue,
588 };
589}
590
591fn countSubProcesses(all_steps: []const *Step) usize {
592 var count: usize = 0;
593 for (all_steps) |s| {
594 count += @intFromBool(s.getZigProcess() != null);
595 }
596 return count;
597}
598
599const Run = struct {
600 gpa: Allocator,
601
602 available_rss: usize,
603 max_rss_is_default: bool,
604 max_rss_mutex: Io.Mutex,
605 skip_oom_steps: bool,
606 unit_test_timeout_ns: ?u64,
607 watch: bool,
608 web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn,
609 /// Allocated into `gpa`.
610 memory_blocked_steps: std.ArrayList(*Step),
611 /// Allocated into `gpa`.
612 step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
613
614 error_style: ErrorStyle,
615 multiline_errors: MultilineErrors,
616 summary: Summary,
617};
618
619fn prepare(graph: *Graph, step_names: []const []const u8, run: *Run) !void {
620 const arena = graph.arena;
621 const seed: u32 = graph.random_seed;
622 const gpa = run.gpa;
623 const step_stack = &run.step_stack;
624
625 if (step_names.len == 0) {
626 try step_stack.put(gpa, graph.configuration.default_step, {});
627 } else {
628 try step_stack.ensureUnusedCapacity(gpa, step_names.len);
629 for (0..step_names.len) |i| {
630 const step_name = step_names[step_names.len - i - 1];
631 const s = graph.top_level_steps.get(step_name) orelse {
632 std.log.info("access the help menu with \"zig build -h\"", .{});
633 fatal("no step named '{s}'", .{step_name});
634 };
635 step_stack.putAssumeCapacity(&s.step, {});
636 }
637 }
638
639 const starting_steps = try arena.dupe(*Step, step_stack.keys());
640
641 var rng = std.Random.DefaultPrng.init(seed);
642 const rand = rng.random();
643 rand.shuffle(*Step, starting_steps);
644
645 for (starting_steps) |s| {
646 try constructGraphAndCheckForDependencyLoop(gpa, s, &run.step_stack, rand);
647 }
648
649 {
650 // Check that we have enough memory to complete the build.
651 var any_problems = false;
652 var max_needed: usize = 0;
653 for (step_stack.keys()) |s| {
654 if (s.max_rss == 0) continue;
655 max_needed = @max(max_needed, s.max_rss);
656 if (s.max_rss > run.available_rss) {
657 if (run.skip_oom_steps) {
658 s.state = .skipped_oom;
659 for (s.dependants.items) |dependant| {
660 dependant.pending_deps -= 1;
661 }
662 } else {
663 std.log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{
664 s.owner.dep_prefix, s.name, s.max_rss, run.available_rss,
665 });
666 any_problems = true;
667 }
668 }
669 }
670 if (any_problems) {
671 if (run.max_rss_is_default) {
672 std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{
673 max_needed,
674 });
675 }
676 return error.InsufficientMemory;
677 }
678 }
679}
680
681fn runStepNames(
682 graph: *Graph,
683 step_names: []const []const u8,
684 parent_prog_node: std.Progress.Node,
685 run: *Run,
686 fuzz: ?Fuzz.Mode,
687) !void {
688 const gpa = run.gpa;
689 const io = graph.io;
690 const step_stack = &run.step_stack;
691
692 {
693 // Collect the initial set of tasks (those with no outstanding dependencies) into a buffer,
694 // then spawn them. The buffer is so that we don't race with `makeStep` and end up thinking
695 // a step is initial when it actually became ready due to an earlier initial step.
696 var initial_set: std.ArrayList(*Step) = .empty;
697 defer initial_set.deinit(gpa);
698 try initial_set.ensureUnusedCapacity(gpa, step_stack.count());
699 for (step_stack.keys()) |s| {
700 if (s.state == .precheck_done and s.pending_deps == 0) {
701 initial_set.appendAssumeCapacity(s);
702 }
703 }
704
705 const step_prog = parent_prog_node.start("steps", step_stack.count());
706 defer step_prog.end();
707
708 var group: Io.Group = .init;
709 defer group.cancel(io);
710 // Start working on all of the initial steps...
711 for (initial_set.items) |s| try stepReady(&group, s, step_prog, run);
712 // ...and `makeStep` will trigger every other step when their last dependency finishes.
713 try group.await(io);
714 }
715
716 assert(run.memory_blocked_steps.items.len == 0);
717
718 var test_pass_count: usize = 0;
719 var test_skip_count: usize = 0;
720 var test_fail_count: usize = 0;
721 var test_crash_count: usize = 0;
722 var test_timeout_count: usize = 0;
723
724 var test_count: usize = 0;
725
726 var success_count: usize = 0;
727 var skipped_count: usize = 0;
728 var failure_count: usize = 0;
729 var pending_count: usize = 0;
730 var total_compile_errors: usize = 0;
731
732 var cleanup_task = io.async(cleanTmpFiles, .{ io, step_stack.keys() });
733 defer cleanup_task.await(io);
734
735 for (step_stack.keys()) |s| {
736 test_pass_count += s.test_results.passCount();
737 test_skip_count += s.test_results.skip_count;
738 test_fail_count += s.test_results.fail_count;
739 test_crash_count += s.test_results.crash_count;
740 test_timeout_count += s.test_results.timeout_count;
741
742 test_count += s.test_results.test_count;
743
744 switch (s.state) {
745 .precheck_unstarted => unreachable,
746 .precheck_started => unreachable,
747 .precheck_done => unreachable,
748 .dependency_failure => pending_count += 1,
749 .success => success_count += 1,
750 .skipped, .skipped_oom => skipped_count += 1,
751 .failure => {
752 failure_count += 1;
753 const compile_errors_len = s.result_error_bundle.errorMessageCount();
754 if (compile_errors_len > 0) {
755 total_compile_errors += compile_errors_len;
756 }
757 },
758 }
759 }
760
761 if (fuzz) |mode| blk: {
762 switch (builtin.os.tag) {
763 // Current implementation depends on two things that need to be ported to Windows:
764 // * Memory-mapping to share data between the fuzzer and build runner.
765 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
766 // many addresses to source locations).
767 .windows => fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
768 else => {},
769 }
770 if (@bitSizeOf(usize) != 64) {
771 // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
772 // being compatible with file system's u64 return value. This is not the case
773 // on 32-bit platforms.
774 // Affects or affected by issues #5185, #22523, and #22464.
775 fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
776 }
777
778 switch (mode) {
779 .forever => break :blk,
780 .limit => {},
781 }
782
783 assert(mode == .limit);
784 var f = Fuzz.init(
785 gpa,
786 io,
787 step_stack.keys(),
788 parent_prog_node,
789 mode,
790 ) catch |err| fatal("failed to start fuzzer: {t}", .{err});
791 defer f.deinit();
792
793 f.start();
794 try f.waitAndPrintReport();
795 }
796
797 // Every test has a state
798 assert(test_pass_count + test_skip_count + test_fail_count + test_crash_count + test_timeout_count == test_count);
799
800 if (failure_count == 0) {
801 std.Progress.setStatus(.success);
802 } else {
803 std.Progress.setStatus(.failure);
804 }
805
806 summary: {
807 switch (run.summary) {
808 .all, .new, .line => {},
809 .failures => if (failure_count == 0) break :summary,
810 .none => break :summary,
811 }
812
813 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
814 defer io.unlockStderr();
815 const t = stderr.terminal();
816 const w = &stderr.file_writer.interface;
817
818 const total_count = success_count + failure_count + pending_count + skipped_count;
819 t.setColor(.cyan) catch {};
820 t.setColor(.bold) catch {};
821 w.writeAll("Build Summary: ") catch {};
822 t.setColor(.reset) catch {};
823 w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
824 {
825 t.setColor(.dim) catch {};
826 var first = true;
827 if (skipped_count > 0) {
828 w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {};
829 first = false;
830 }
831 if (failure_count > 0) {
832 w.print("{s}{d} failed", .{ if (first) " (" else ", ", failure_count }) catch {};
833 first = false;
834 }
835 if (!first) w.writeByte(')') catch {};
836 t.setColor(.reset) catch {};
837 }
838
839 if (test_count > 0) {
840 w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {};
841 t.setColor(.dim) catch {};
842 var first = true;
843 if (test_skip_count > 0) {
844 w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {};
845 first = false;
846 }
847 if (test_fail_count > 0) {
848 w.print("{s}{d} failed", .{ if (first) " (" else ", ", test_fail_count }) catch {};
849 first = false;
850 }
851 if (test_crash_count > 0) {
852 w.print("{s}{d} crashed", .{ if (first) " (" else ", ", test_crash_count }) catch {};
853 first = false;
854 }
855 if (test_timeout_count > 0) {
856 w.print("{s}{d} timed out", .{ if (first) " (" else ", ", test_timeout_count }) catch {};
857 first = false;
858 }
859 if (!first) w.writeByte(')') catch {};
860 t.setColor(.reset) catch {};
861 }
862
863 w.writeAll("\n") catch {};
864
865 if (run.summary == .line) break :summary;
866
867 // Print a fancy tree with build results.
868 var step_stack_copy = try step_stack.clone(gpa);
869 defer step_stack_copy.deinit(gpa);
870
871 var print_node: PrintNode = .{ .parent = null };
872 if (step_names.len == 0) {
873 print_node.last = true;
874 printTreeStep(graph, graph.default_step, run, t, &print_node, &step_stack_copy) catch {};
875 } else {
876 const last_index = if (run.summary == .all) graph.top_level_steps.count() else blk: {
877 var i: usize = step_names.len;
878 while (i > 0) {
879 i -= 1;
880 const step = graph.top_level_steps.get(step_names[i]).?.step;
881 const found = switch (run.summary) {
882 .all, .line, .none => unreachable,
883 .failures => step.state != .success,
884 .new => !step.result_cached,
885 };
886 if (found) break :blk i;
887 }
888 break :blk graph.top_level_steps.count();
889 };
890 for (step_names, 0..) |step_name, i| {
891 const tls = graph.top_level_steps.get(step_name).?;
892 print_node.last = i + 1 == last_index;
893 printTreeStep(graph, &tls.step, run, t, &print_node, &step_stack_copy) catch {};
894 }
895 }
896 w.writeByte('\n') catch {};
897 }
898
899 if (run.watch or run.web_server != null) return;
900
901 // Perhaps in the future there could be an Advanced Options flag such as
902 // --debug-build-runner-leaks which would make this code return instead of
903 // calling exit.
904
905 const code: u8 = code: {
906 if (failure_count == 0) break :code 0; // success
907 if (run.error_style.verboseContext()) break :code 1; // failure; print build command
908 break :code 2; // failure; do not print build command
909 };
910 _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
911 process.exit(code);
912}
913
914const PrintNode = struct {
915 parent: ?*PrintNode,
916 last: bool = false,
917};
918
919fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void {
920 const parent = node.parent orelse return;
921 const writer = stderr.writer;
922 if (parent.parent == null) return;
923 try printPrefix(parent, stderr);
924 if (parent.last) {
925 try writer.writeAll(" ");
926 } else {
927 try writer.writeAll(switch (stderr.mode) {
928 .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │
929 else => "| ",
930 });
931 }
932}
933
934fn printChildNodePrefix(stderr: Io.Terminal) !void {
935 try stderr.writer.writeAll(switch (stderr.mode) {
936 .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─
937 else => "+- ",
938 });
939}
940
941fn printStepStatus(s: *Step, stderr: Io.Terminal, run: *const Run) !void {
942 const writer = stderr.writer;
943 switch (s.state) {
944 .precheck_unstarted => unreachable,
945 .precheck_started => unreachable,
946 .precheck_done => unreachable,
947
948 .dependency_failure => {
949 try stderr.setColor(.dim);
950 try writer.writeAll(" transitive failure\n");
951 try stderr.setColor(.reset);
952 },
953
954 .success => {
955 try stderr.setColor(.green);
956 if (s.result_cached) {
957 try writer.writeAll(" cached");
958 } else if (s.test_results.test_count > 0) {
959 const pass_count = s.test_results.passCount();
960 assert(s.test_results.test_count == pass_count + s.test_results.skip_count);
961 try writer.print(" {d} pass", .{pass_count});
962 if (s.test_results.skip_count > 0) {
963 try stderr.setColor(.reset);
964 try writer.writeAll(", ");
965 try stderr.setColor(.yellow);
966 try writer.print("{d} skip", .{s.test_results.skip_count});
967 }
968 try stderr.setColor(.reset);
969 try writer.print(" ({d} total)", .{s.test_results.test_count});
970 } else {
971 try writer.writeAll(" success");
972 }
973 try stderr.setColor(.reset);
974 if (s.result_duration_ns) |ns| {
975 try stderr.setColor(.dim);
976 if (ns >= std.time.ns_per_min) {
977 try writer.print(" {d}m", .{ns / std.time.ns_per_min});
978 } else if (ns >= std.time.ns_per_s) {
979 try writer.print(" {d}s", .{ns / std.time.ns_per_s});
980 } else if (ns >= std.time.ns_per_ms) {
981 try writer.print(" {d}ms", .{ns / std.time.ns_per_ms});
982 } else if (ns >= std.time.ns_per_us) {
983 try writer.print(" {d}us", .{ns / std.time.ns_per_us});
984 } else {
985 try writer.print(" {d}ns", .{ns});
986 }
987 try stderr.setColor(.reset);
988 }
989 if (s.result_peak_rss != 0) {
990 const rss = s.result_peak_rss;
991 try stderr.setColor(.dim);
992 if (rss >= 1000_000_000) {
993 try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000});
994 } else if (rss >= 1000_000) {
995 try writer.print(" MaxRSS:{d}M", .{rss / 1000_000});
996 } else if (rss >= 1000) {
997 try writer.print(" MaxRSS:{d}K", .{rss / 1000});
998 } else {
999 try writer.print(" MaxRSS:{d}B", .{rss});
1000 }
1001 try stderr.setColor(.reset);
1002 }
1003 try writer.writeAll("\n");
1004 },
1005 .skipped => {
1006 try stderr.setColor(.yellow);
1007 try writer.writeAll(" skipped\n");
1008 try stderr.setColor(.reset);
1009 },
1010 .skipped_oom => {
1011 try stderr.setColor(.yellow);
1012 try writer.writeAll(" skipped (not enough memory)");
1013 try stderr.setColor(.dim);
1014 try writer.print(" upper bound of {d} exceeded runner limit ({d})\n", .{ s.max_rss, run.available_rss });
1015 try stderr.setColor(.reset);
1016 },
1017 .failure => {
1018 try printStepFailure(s, stderr, false);
1019 try stderr.setColor(.reset);
1020 },
1021 }
1022}
1023
1024fn printStepFailure(s: *Step, stderr: Io.Terminal, dim: bool) !void {
1025 const w = stderr.writer;
1026 if (s.result_error_bundle.errorMessageCount() > 0) {
1027 try stderr.setColor(.red);
1028 try w.print(" {d} errors\n", .{
1029 s.result_error_bundle.errorMessageCount(),
1030 });
1031 } else if (!s.test_results.isSuccess()) {
1032 // These first values include all of the test "statuses". Every test is either passsed,
1033 // skipped, failed, crashed, or timed out.
1034 try stderr.setColor(.green);
1035 try w.print(" {d} pass", .{s.test_results.passCount()});
1036 try stderr.setColor(.reset);
1037 if (dim) try stderr.setColor(.dim);
1038 if (s.test_results.skip_count > 0) {
1039 try w.writeAll(", ");
1040 try stderr.setColor(.yellow);
1041 try w.print("{d} skip", .{s.test_results.skip_count});
1042 try stderr.setColor(.reset);
1043 if (dim) try stderr.setColor(.dim);
1044 }
1045 if (s.test_results.fail_count > 0) {
1046 try w.writeAll(", ");
1047 try stderr.setColor(.red);
1048 try w.print("{d} fail", .{s.test_results.fail_count});
1049 try stderr.setColor(.reset);
1050 if (dim) try stderr.setColor(.dim);
1051 }
1052 if (s.test_results.crash_count > 0) {
1053 try w.writeAll(", ");
1054 try stderr.setColor(.red);
1055 try w.print("{d} crash", .{s.test_results.crash_count});
1056 try stderr.setColor(.reset);
1057 if (dim) try stderr.setColor(.dim);
1058 }
1059 if (s.test_results.timeout_count > 0) {
1060 try w.writeAll(", ");
1061 try stderr.setColor(.red);
1062 try w.print("{d} timeout", .{s.test_results.timeout_count});
1063 try stderr.setColor(.reset);
1064 if (dim) try stderr.setColor(.dim);
1065 }
1066 try w.print(" ({d} total)", .{s.test_results.test_count});
1067
1068 // Memory leaks are intentionally written after the total, because is isn't a test *status*,
1069 // but just a flag that any tests -- even passed ones -- can have. We also use a different
1070 // separator, so it looks like:
1071 // 2 pass, 1 skip, 2 fail (5 total); 2 leaks
1072 if (s.test_results.leak_count > 0) {
1073 try w.writeAll("; ");
1074 try stderr.setColor(.red);
1075 try w.print("{d} leaks", .{s.test_results.leak_count});
1076 try stderr.setColor(.reset);
1077 if (dim) try stderr.setColor(.dim);
1078 }
1079
1080 // It's usually not helpful to know how many error logs there were because they tend to
1081 // just come with other errors (e.g. crashes and leaks print stack traces, and clean
1082 // failures print error traces). So only mention them if they're the only thing causing
1083 // the failure.
1084 const show_err_logs: bool = show: {
1085 var alt_results = s.test_results;
1086 alt_results.log_err_count = 0;
1087 break :show alt_results.isSuccess();
1088 };
1089 if (show_err_logs) {
1090 try w.writeAll("; ");
1091 try stderr.setColor(.red);
1092 try w.print("{d} error logs", .{s.test_results.log_err_count});
1093 try stderr.setColor(.reset);
1094 if (dim) try stderr.setColor(.dim);
1095 }
1096
1097 try w.writeAll("\n");
1098 } else if (s.result_error_msgs.items.len > 0) {
1099 try stderr.setColor(.red);
1100 try w.writeAll(" failure\n");
1101 } else {
1102 assert(s.result_stderr.len > 0);
1103 try stderr.setColor(.red);
1104 try w.writeAll(" w\n");
1105 }
1106}
1107
1108fn printTreeStep(
1109 graph: *Graph,
1110 s: *Step,
1111 run: *const Run,
1112 stderr: Io.Terminal,
1113 parent_node: *PrintNode,
1114 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
1115) !void {
1116 const writer = stderr.writer;
1117 const first = step_stack.swapRemove(s);
1118 const summary = run.summary;
1119 const skip = switch (summary) {
1120 .none, .line => unreachable,
1121 .all => false,
1122 .new => s.result_cached,
1123 .failures => s.state == .success,
1124 };
1125 if (skip) return;
1126 try printPrefix(parent_node, stderr);
1127
1128 if (parent_node.parent != null) {
1129 if (parent_node.last) {
1130 try printChildNodePrefix(stderr);
1131 } else {
1132 try writer.writeAll(switch (stderr.mode) {
1133 .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─
1134 else => "+- ",
1135 });
1136 }
1137 }
1138
1139 if (!first) try stderr.setColor(.dim);
1140
1141 // dep_prefix omitted here because it is redundant with the tree.
1142 try writer.writeAll(s.name);
1143
1144 if (first) {
1145 try printStepStatus(s, stderr, run);
1146
1147 const last_index = if (summary == .all) s.dependencies.items.len -| 1 else blk: {
1148 var i: usize = s.dependencies.items.len;
1149 while (i > 0) {
1150 i -= 1;
1151
1152 const step = s.dependencies.items[i];
1153 const found = switch (summary) {
1154 .all, .line, .none => unreachable,
1155 .failures => step.state != .success,
1156 .new => !step.result_cached,
1157 };
1158 if (found) break :blk i;
1159 }
1160 break :blk s.dependencies.items.len -| 1;
1161 };
1162 for (s.dependencies.items, 0..) |dep, i| {
1163 var print_node: PrintNode = .{
1164 .parent = parent_node,
1165 .last = i == last_index,
1166 };
1167 try printTreeStep(graph, dep, run, stderr, &print_node, step_stack);
1168 }
1169 } else {
1170 if (s.dependencies.items.len == 0) {
1171 try writer.writeAll(" (reused)\n");
1172 } else {
1173 try writer.print(" (+{d} more reused dependencies)\n", .{
1174 s.dependencies.items.len,
1175 });
1176 }
1177 try stderr.setColor(.reset);
1178 }
1179}
1180
1181/// Traverse the dependency graph depth-first and make it undirected by having
1182/// steps know their dependants (they only know dependencies at start).
1183/// Along the way, check that there is no dependency loop, and record the steps
1184/// in traversal order in `step_stack`.
1185/// Each step has its dependencies traversed in random order, this accomplishes
1186/// two things:
1187/// - `step_stack` will be in randomized-depth-first order, so the build runner
1188/// spawns initial steps in a random order
1189/// - each step's `dependants` list is also filled in a random order, so that
1190/// when it finishes executing in `makeStep`, it spawns next steps to run in
1191/// random order
1192fn constructGraphAndCheckForDependencyLoop(
1193 gpa: Allocator,
1194 s: *Step,
1195 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
1196 rand: std.Random,
1197) !void {
1198 switch (s.state) {
1199 .precheck_started => {
1200 std.debug.print("dependency loop detected:\n {s}\n", .{s.name});
1201 return error.DependencyLoopDetected;
1202 },
1203 .precheck_unstarted => {
1204 s.state = .precheck_started;
1205
1206 try step_stack.ensureUnusedCapacity(gpa, s.dependencies.items.len);
1207
1208 // We dupe to avoid shuffling the steps in the summary, it depends
1209 // on s.dependencies' order.
1210 const deps = gpa.dupe(*Step, s.dependencies.items) catch @panic("OOM");
1211 defer gpa.free(deps);
1212
1213 rand.shuffle(*Step, deps);
1214
1215 for (deps) |dep| {
1216 try step_stack.put(gpa, dep, {});
1217 try dep.dependants.append(gpa, s);
1218 constructGraphAndCheckForDependencyLoop(gpa, dep, step_stack, rand) catch |err| {
1219 if (err == error.DependencyLoopDetected) {
1220 std.debug.print(" {s}\n", .{s.name});
1221 }
1222 return err;
1223 };
1224 }
1225
1226 s.state = .precheck_done;
1227 s.pending_deps = @intCast(s.dependencies.items.len);
1228 },
1229 .precheck_done => {},
1230
1231 // These don't happen until we actually run the step graph.
1232 .dependency_failure => unreachable,
1233 .success => unreachable,
1234 .failure => unreachable,
1235 .skipped => unreachable,
1236 .skipped_oom => unreachable,
1237 }
1238}
1239
1240/// Runs the "make" function of the single step `s`, updates its state, and then spawns newly-ready
1241/// dependant steps in `group`. If `s` makes an RSS claim (i.e. `s.max_rss != 0`), the caller must
1242/// have already subtracted this value from `run.available_rss`. This function will release the RSS
1243/// claim (i.e. add `s.max_rss` back into `run.available_rss`) and queue any viable memory-blocked
1244/// steps after "make" completes for `s`.
1245fn makeStep(
1246 graph: *Graph,
1247 group: *Io.Group,
1248 s: *Step,
1249 root_prog_node: std.Progress.Node,
1250 run: *Run,
1251) Io.Cancelable!void {
1252 const io = graph.io;
1253 const gpa = run.gpa;
1254
1255 {
1256 const step_prog_node = root_prog_node.start(s.name, 0);
1257 defer step_prog_node.end();
1258
1259 if (run.web_server) |*ws| ws.updateStepStatus(s, .wip);
1260
1261 const new_state: Step.State = for (s.dependencies.items) |dep| {
1262 switch (@atomicLoad(Step.State, &dep.state, .monotonic)) {
1263 .precheck_unstarted => unreachable,
1264 .precheck_started => unreachable,
1265 .precheck_done => unreachable,
1266
1267 .failure,
1268 .dependency_failure,
1269 .skipped_oom,
1270 => break .dependency_failure,
1271
1272 .success, .skipped => {},
1273 }
1274 } else if (s.make(.{
1275 .progress_node = step_prog_node,
1276 .watch = run.watch,
1277 .web_server = if (run.web_server) |*ws| ws else null,
1278 .unit_test_timeout_ns = run.unit_test_timeout_ns,
1279 .gpa = gpa,
1280 })) state: {
1281 break :state .success;
1282 } else |err| switch (err) {
1283 error.MakeFailed => .failure,
1284 error.MakeSkipped => .skipped,
1285 };
1286
1287 @atomicStore(Step.State, &s.state, new_state, .monotonic);
1288
1289 switch (new_state) {
1290 .precheck_unstarted => unreachable,
1291 .precheck_started => unreachable,
1292 .precheck_done => unreachable,
1293
1294 .failure,
1295 .dependency_failure,
1296 .skipped_oom,
1297 => {
1298 if (run.web_server) |*ws| ws.updateStepStatus(s, .failure);
1299 std.Progress.setStatus(.failure_working);
1300 },
1301
1302 .success,
1303 .skipped,
1304 => {
1305 if (run.web_server) |*ws| ws.updateStepStatus(s, .success);
1306 },
1307 }
1308 }
1309
1310 // No matter the result, we want to display error/warning messages.
1311 if (s.result_error_bundle.errorMessageCount() > 0 or
1312 s.result_error_msgs.items.len > 0 or
1313 s.result_stderr.len > 0)
1314 {
1315 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
1316 defer io.unlockStderr();
1317 printErrorMessages(gpa, s, .{}, stderr.terminal(), run.error_style, run.multiline_errors) catch {};
1318 }
1319
1320 if (s.max_rss != 0) {
1321 var dispatch_set: std.ArrayList(*Step) = .empty;
1322 defer dispatch_set.deinit(gpa);
1323
1324 // Release our RSS claim and kick off some blocked steps if possible. We use `dispatch_set`
1325 // as a staging buffer to avoid recursing into `makeStep` while `run.max_rss_mutex` is held.
1326 {
1327 try run.max_rss_mutex.lock(io);
1328 defer run.max_rss_mutex.unlock(io);
1329 run.available_rss += s.max_rss;
1330 dispatch_set.ensureUnusedCapacity(gpa, run.memory_blocked_steps.items.len) catch @panic("OOM");
1331 while (run.memory_blocked_steps.getLast()) |candidate| {
1332 if (run.available_rss < candidate.max_rss) break;
1333 assert(run.memory_blocked_steps.pop() == candidate);
1334 dispatch_set.appendAssumeCapacity(candidate);
1335 }
1336 }
1337 for (dispatch_set.items) |candidate| {
1338 group.async(io, makeStep, .{ graph, group, candidate, root_prog_node, run });
1339 }
1340 }
1341
1342 for (s.dependants.items) |dependant| {
1343 // `.acq_rel` synchronizes with itself to ensure all dependencies' final states are visible when this hits 0.
1344 if (@atomicRmw(u32, &dependant.pending_deps, .Sub, 1, .acq_rel) == 1) {
1345 try stepReady(graph, group, dependant, root_prog_node, run);
1346 }
1347 }
1348}
1349
1350fn stepReady(
1351 graph: *Graph,
1352 group: *Io.Group,
1353 s: *Step,
1354 root_prog_node: std.Progress.Node,
1355 run: *Run,
1356) !void {
1357 const io = graph.io;
1358 if (s.max_rss != 0) {
1359 try run.max_rss_mutex.lock(io);
1360 defer run.max_rss_mutex.unlock(io);
1361 if (run.available_rss < s.max_rss) {
1362 // Running this step right now could possibly exceed the allotted RSS.
1363 run.memory_blocked_steps.append(run.gpa, s) catch @panic("OOM");
1364 return;
1365 }
1366 run.available_rss -= s.max_rss;
1367 }
1368 group.async(io, makeStep, .{ graph, group, s, root_prog_node, run });
1369}
1370
1371pub fn printErrorMessages(
1372 gpa: Allocator,
1373 failing_step: *Step,
1374 options: std.zig.ErrorBundle.RenderOptions,
1375 stderr: Io.Terminal,
1376 error_style: ErrorStyle,
1377 multiline_errors: MultilineErrors,
1378) !void {
1379 const writer = stderr.writer;
1380 if (error_style.verboseContext()) {
1381 // Provide context for where these error messages are coming from by
1382 // printing the corresponding Step subtree.
1383 var step_stack: std.ArrayList(*Step) = .empty;
1384 defer step_stack.deinit(gpa);
1385 try step_stack.append(gpa, failing_step);
1386 while (step_stack.items[step_stack.items.len - 1].dependants.items.len != 0) {
1387 try step_stack.append(gpa, step_stack.items[step_stack.items.len - 1].dependants.items[0]);
1388 }
1389
1390 // Now, `step_stack` has the subtree that we want to print, in reverse order.
1391 try stderr.setColor(.dim);
1392 var indent: usize = 0;
1393 while (step_stack.pop()) |s| : (indent += 1) {
1394 if (indent > 0) {
1395 try writer.splatByteAll(' ', (indent - 1) * 3);
1396 try printChildNodePrefix(stderr);
1397 }
1398
1399 try writer.writeAll(s.name);
1400
1401 if (s == failing_step) {
1402 try printStepFailure(s, stderr, true);
1403 } else {
1404 try writer.writeAll("\n");
1405 }
1406 }
1407 try stderr.setColor(.reset);
1408 } else {
1409 // Just print the failing step itself.
1410 try stderr.setColor(.dim);
1411 try writer.writeAll(failing_step.name);
1412 try printStepFailure(failing_step, stderr, true);
1413 try stderr.setColor(.reset);
1414 }
1415
1416 if (failing_step.result_stderr.len > 0) {
1417 try writer.writeAll(failing_step.result_stderr);
1418 if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) {
1419 try writer.writeAll("\n");
1420 }
1421 }
1422
1423 try failing_step.result_error_bundle.renderToTerminal(options, stderr);
1424
1425 for (failing_step.result_error_msgs.items) |msg| {
1426 try stderr.setColor(.red);
1427 try writer.writeAll("error:");
1428 try stderr.setColor(.reset);
1429 if (std.mem.indexOfScalar(u8, msg, '\n') == null) {
1430 try writer.print(" {s}\n", .{msg});
1431 } else switch (multiline_errors) {
1432 .indent => {
1433 var it = std.mem.splitScalar(u8, msg, '\n');
1434 try writer.print(" {s}\n", .{it.first()});
1435 while (it.next()) |line| {
1436 try writer.print(" {s}\n", .{line});
1437 }
1438 },
1439 .newline => try writer.print("\n{s}\n", .{msg}),
1440 .none => try writer.print(" {s}\n", .{msg}),
1441 }
1442 }
1443
1444 if (error_style.verboseContext()) {
1445 if (failing_step.result_failed_command) |cmd_str| {
1446 try stderr.setColor(.red);
1447 try writer.writeAll("failed command: ");
1448 try stderr.setColor(.reset);
1449 try writer.writeAll(cmd_str);
1450 try writer.writeByte('\n');
1451 }
1452 }
1453
1454 try writer.writeByte('\n');
1455}
1456
1457fn printSteps(graph: *Graph, w: *Writer) !void {
1458 const arena = graph.arena;
1459 for (graph.top_level_steps.values()) |top_level_step| {
1460 const name = if (&top_level_step.step == graph.default_step)
1461 try fmt.allocPrint(arena, "{s} (default)", .{top_level_step.step.name})
1462 else
1463 top_level_step.step.name;
1464 try w.print(" {s:<28} {s}\n", .{ name, top_level_step.description });
1465 }
1466}
1467
1468fn printUsage(graph: *Graph, w: *Writer) !void {
1469 const arena = graph.arena;
1470
1471 try w.print(
1472 \\Usage: {s} build [steps] [options]
1473 \\
1474 \\Steps:
1475 \\
1476 , .{graph.zig_exe});
1477 try printSteps(graph, w);
1478 try w.writeAll(
1479 \\
1480 \\Project-Specific Options:
1481 \\
1482 );
1483
1484 if (graph.available_options_list.items.len == 0) {
1485 try w.print(" (none)\n", .{});
1486 } else {
1487 for (graph.available_options_list.items) |option| {
1488 const name = try fmt.allocPrint(arena, " -D{s}=[{t}]", .{ option.name, option.type_id });
1489 try w.print("{s:<30} {s}\n", .{ name, option.description });
1490 if (option.enum_options) |enum_options| {
1491 const padding: [33]u8 = @splat(' ');
1492 try w.writeAll(padding ++ "Supported Values:\n");
1493 for (enum_options) |enum_option| {
1494 try w.print(padding ++ " {s}\n", .{enum_option});
1495 }
1496 }
1497 }
1498 }
1499
1500 try w.writeAll(
1501 \\
1502 \\System Integration Options:
1503 \\ --search-prefix [path] Add a path to look for binaries, libraries, headers
1504 \\ --sysroot [path] Set the system root directory (usually /)
1505 \\ --libc [file] Provide a file which specifies libc paths
1506 \\
1507 \\ --system [pkgdir] Disable package fetching; enable all integrations
1508 \\ -fsys=[name] Enable a system integration
1509 \\ -fno-sys=[name] Disable a system integration
1510 \\
1511 \\ -fdarling, -fno-darling Integration with system-installed Darling to
1512 \\ execute macOS programs on Linux hosts
1513 \\ (default: no)
1514 \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute
1515 \\ foreign-architecture programs on Linux hosts
1516 \\ (default: no)
1517 \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc
1518 \\ (e.g. glibc or musl) built for multiple foreign
1519 \\ architectures, allowing execution of non-native
1520 \\ programs that link with libc.
1521 \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
1522 \\ ARM64 macOS hosts. (default: no)
1523 \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
1524 \\ execute WASI binaries. (default: no)
1525 \\ -fwine, -fno-wine Integration with system-installed Wine to execute
1526 \\ Windows programs on Linux hosts. (default: no)
1527 \\
1528 \\ Available System Integrations: Enabled:
1529 \\
1530 );
1531 if (graph.system_library_options.entries.len == 0) {
1532 try w.writeAll(" (none) -\n");
1533 } else {
1534 for (graph.system_library_options.keys(), graph.system_library_options.values()) |k, v| {
1535 const status = switch (v) {
1536 .declared_enabled => "yes",
1537 .declared_disabled => "no",
1538 .user_enabled, .user_disabled => unreachable, // already emitted error
1539 };
1540 try w.print(" {s:<43} {s}\n", .{ k, status });
1541 }
1542 }
1543
1544 try w.writeAll(
1545 \\
1546 \\General Options:
1547 \\ -h, --help Print this help and exit
1548 \\ -l, --list-steps Print available steps
1549 \\
1550 \\ -p, --prefix [path] Where to install files (default: zig-out)
1551 \\ --prefix-lib-dir [path] Where to install libraries
1552 \\ --prefix-exe-dir [path] Where to install executables
1553 \\ --prefix-include-dir [path] Where to install C header files
1554 \\ --release[=mode] Request release mode, optionally specifying a
1555 \\ preferred optimization mode: fast, safe, small
1556 \\
1557 \\ --verbose Print commands before executing them
1558 \\ --color [auto|off|on] Enable or disable colored error messages
1559 \\ --error-style [style] Control how build errors are printed
1560 \\ verbose (Default) Report errors with full context
1561 \\ minimal Report errors after summary, excluding context like command lines
1562 \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update
1563 \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update
1564 \\ --multiline-errors [style] Control how multi-line error messages are printed
1565 \\ indent (Default) Indent non-initial lines to align with initial line
1566 \\ newline Include a leading newline so that the error message is on its own lines
1567 \\ none Print as usual so the first line is misaligned
1568 \\ --summary [mode] Control the printing of the build summary
1569 \\ all Print the build summary in its entirety
1570 \\ new Omit cached steps
1571 \\ failures (Default if short-lived) Only print failed steps
1572 \\ line (Default if long-lived) Only print the single-line summary
1573 \\ none Do not print the build summary
1574 \\ -j<N> Limit concurrent jobs (default is to use all CPU cores)
1575 \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
1576 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
1577 \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded.
1578 \\ The timeout must include a unit: ns, us, ms, s, m, h
1579 \\ --watch Continuously rebuild when source files are modified
1580 \\ --debounce <ms> Delay before rebuilding after changed file detected
1581 \\ --webui[=ip] Enable the web interface on the given IP address
1582 \\ --fuzz[=limit] Continuously search for unit test failures with an optional
1583 \\ limit to the max number of iterations. The argument supports
1584 \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
1585 \\ '--webui' when no limit is specified.
1586 \\ --time-report Force full rebuild and provide detailed information on
1587 \\ compilation time of Zig source code (implies '--webui')
1588 \\ -fincremental Enable incremental compilation
1589 \\ -fno-incremental Disable incremental compilation
1590 \\
1591 \\Package Management Options:
1592 \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit
1593 \\ needed (Default) Lazy dependencies are fetched as needed
1594 \\ all Lazy dependencies are always fetched
1595 \\ --fork=[path] Override one or more projects from dependency tree
1596 \\
1597 \\Advanced Options:
1598 \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error
1599 \\ -fno-reference-trace Disable reference trace
1600 \\ -fallow-so-scripts Allows .so files to be GNU ld scripts
1601 \\ -fno-allow-so-scripts (default) .so files must be ELF files
1602 \\ --build-file [file] Override path to build.zig
1603 \\ --cache-dir [path] Override path to local Zig cache directory
1604 \\ --global-cache-dir [path] Override path to global Zig cache directory
1605 \\ --zig-lib-dir [arg] Override path to Zig lib directory
1606 \\ --build-runner [file] Override path to build runner
1607 \\ --seed [integer] For shuffling dependency traversal order (default: random)
1608 \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries
1609 \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM)
1610 \\ sha1, tree 20-byte cryptographic hash (ELF, WASM)
1611 \\ md5 16-byte cryptographic hash (ELF)
1612 \\ uuid 16-byte random UUID (ELF, WASM)
1613 \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM)
1614 \\ none (default) No build ID
1615 \\ --debug-log [scope] Enable debugging the compiler
1616 \\ --debug-pkg-config Fail if unknown pkg-config flags encountered
1617 \\ --debug-rt Debug compiler runtime libraries
1618 \\ --verbose-link Enable compiler debug output for linking
1619 \\ --verbose-air Enable compiler debug output for Zig AIR
1620 \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
1621 \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
1622 \\ --verbose-cimport Enable compiler debug output for C imports
1623 \\ --verbose-cc Enable compiler debug output for C compilation
1624 \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
1625 \\
1626 );
1627}
1628
1629fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
1630 if (idx.* >= args.len) return null;
1631 defer idx.* += 1;
1632 return args[idx.*];
1633}
1634
1635fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
1636 return nextArg(args, idx) orelse
1637 fatal("expected argument after {q}\n access the help menu with \"zig build -h\"", .{args[idx.* - 1]});
1638}
1639
1640fn cutArgPrefixOrFatal(args: []const [:0]const u8, idx: *usize, prefix: []const u8) []const u8 {
1641 if (nextArg(args, idx)) |next_arg| {
1642 if (mem.cutPrefix(u8, next_arg, prefix)) |arg| {
1643 return arg;
1644 }
1645 }
1646 fatal("expected argument after {q} to start with {q}", .{ args[idx.* - 1], prefix });
1647}
1648
1649fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
1650 if (idx >= args.len) return null;
1651 return args[idx..];
1652}
1653
1654const Color = std.zig.Color;
1655const ErrorStyle = enum {
1656 verbose,
1657 minimal,
1658 verbose_clear,
1659 minimal_clear,
1660 fn verboseContext(s: ErrorStyle) bool {
1661 return switch (s) {
1662 .verbose, .verbose_clear => true,
1663 .minimal, .minimal_clear => false,
1664 };
1665 }
1666 fn clearOnUpdate(s: ErrorStyle) bool {
1667 return switch (s) {
1668 .verbose, .minimal => false,
1669 .verbose_clear, .minimal_clear => true,
1670 };
1671 }
1672};
1673const MultilineErrors = enum { indent, newline, none };
1674const Summary = enum { all, new, failures, line, none };
1675
1676fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
1677 std.debug.print(f ++ "\n access the help menu with 'zig build -h'\n", args);
1678 process.exit(1);
1679}
1680
1681fn validateSystemLibraryOptions(graph: *Graph) void {
1682 var bad = false;
1683 for (graph.system_library_options.keys(), graph.system_library_options.values()) |k, v| {
1684 switch (v) {
1685 .user_disabled, .user_enabled => {
1686 // The user tried to enable or disable a system library integration, but
1687 // the build script did not recognize that option.
1688 std.debug.print("system library name not recognized by build script: '{s}'\n", .{k});
1689 bad = true;
1690 },
1691 .declared_disabled, .declared_enabled => {},
1692 }
1693 }
1694 if (bad) {
1695 std.debug.print(" access the help menu with 'zig build -h'\n", .{});
1696 process.exit(1);
1697 }
1698}
1699
1700var stdio_buffer_allocation: [256]u8 = undefined;
1701var stdout_writer_allocation: Io.File.Writer = undefined;
1702
1703fn initStdoutWriter(io: Io) *Writer {
1704 stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation);
1705 return &stdout_writer_allocation.interface;
1706}
1707
1708fn cleanTmpFiles(io: Io, steps: []const *Step) void {
1709 for (steps) |step| {
1710 const wf = step.cast(Step.WriteFile) orelse continue;
1711 if (wf.mode != .tmp) continue;
1712 const path = wf.generated_directory.path orelse continue;
1713 Io.Dir.cwd().deleteTree(io, path) catch |err| {
1714 std.log.warn("failed to delete {s}: {t}", .{ path, err });
1715 };
1716 }
1717}
lib/compiler/maker/Fuzz.zig created+597
......@@ -0,0 +1,597 @@
1const std = @import("Std");
2const Io = std.Io;
3const Build = std.Build;
4const Cache = Build.Cache;
5const Step = std.Build.Step;
6const assert = std.debug.assert;
7const fatal = std.process.fatal;
8const Allocator = std.mem.Allocator;
9const log = std.log;
10const Coverage = std.debug.Coverage;
11const abi = Build.abi.fuzz;
12
13const Fuzz = @This();
14const build_runner = @import("root");
15
16gpa: Allocator,
17io: Io,
18mode: Mode,
19
20/// Allocated into `gpa`.
21run_steps: []const *Step.Run,
22
23group: Io.Group,
24root_prog_node: std.Progress.Node,
25prog_node: std.Progress.Node,
26
27/// Protects `coverage_files`.
28coverage_mutex: Io.Mutex,
29coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
30
31queue_mutex: Io.Mutex,
32queue_cond: Io.Condition,
33msg_queue: std.ArrayList(Msg),
34
35pub const Mode = union(enum) {
36 forever: struct { ws: *Build.WebServer },
37 limit: Limited,
38
39 pub const Limited = struct {
40 amount: u64,
41 };
42};
43
44const Msg = union(enum) {
45 coverage: struct {
46 id: u64,
47 cumulative: struct {
48 runs: u64,
49 unique: u64,
50 coverage: u64,
51 },
52 run: *Step.Run,
53 },
54 entry_point: struct {
55 coverage_id: u64,
56 addr: u64,
57 },
58};
59
60const CoverageMap = struct {
61 mapped_memory: []align(std.heap.page_size_min) const u8,
62 coverage: Coverage,
63 source_locations: []Coverage.SourceLocation,
64 /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
65 entry_points: std.ArrayList(u32),
66 start_timestamp: i64,
67 start_n_runs: u64,
68
69 fn deinit(cm: *CoverageMap, gpa: Allocator) void {
70 std.posix.munmap(cm.mapped_memory);
71 cm.coverage.deinit(gpa);
72 cm.* = undefined;
73 }
74};
75
76pub fn init(
77 gpa: Allocator,
78 io: Io,
79 all_steps: []const *Build.Step,
80 root_prog_node: std.Progress.Node,
81 mode: Mode,
82) error{ OutOfMemory, Canceled }!Fuzz {
83 const run_steps: []const *Step.Run = steps: {
84 var steps: std.ArrayList(*Step.Run) = .empty;
85 defer steps.deinit(gpa);
86 const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0);
87 defer rebuild_node.end();
88 var rebuild_group: Io.Group = .init;
89 defer rebuild_group.cancel(io);
90
91 for (all_steps) |step| {
92 const run = step.cast(Step.Run) orelse continue;
93 if (run.producer == null) continue;
94 if (run.fuzz_tests.items.len == 0) continue;
95 try steps.append(gpa, run);
96 rebuild_group.async(io, rebuildTestsWorkerRun, .{ run, gpa, rebuild_node });
97 }
98
99 if (steps.items.len == 0) fatal("no fuzz tests found", .{});
100 rebuild_node.setEstimatedTotalItems(steps.items.len);
101 const run_steps = try gpa.dupe(*Step.Run, steps.items);
102 try rebuild_group.await(io);
103 break :steps run_steps;
104 };
105 errdefer gpa.free(run_steps);
106
107 for (run_steps) |run| {
108 assert(run.fuzz_tests.items.len > 0);
109 if (run.rebuilt_executable == null)
110 fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
111 }
112
113 return .{
114 .gpa = gpa,
115 .io = io,
116 .mode = mode,
117 .run_steps = run_steps,
118 .group = .init,
119 .root_prog_node = root_prog_node,
120 .prog_node = .none,
121 .coverage_files = .empty,
122 .coverage_mutex = .init,
123 .queue_mutex = .init,
124 .queue_cond = .init,
125 .msg_queue = .empty,
126 };
127}
128
129pub fn start(fuzz: *Fuzz) void {
130 const io = fuzz.io;
131 fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0);
132
133 if (fuzz.mode == .forever) {
134 // For polling messages and sending updates to subscribers.
135 fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err|
136 fatal("unable to spawn coverage task: {t}", .{err});
137 }
138
139 for (fuzz.run_steps) |run| {
140 assert(run.rebuilt_executable != null);
141 fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run });
142 }
143}
144
145pub fn deinit(fuzz: *Fuzz) void {
146 const io = fuzz.io;
147 fuzz.group.cancel(io);
148 fuzz.prog_node.end();
149 fuzz.gpa.free(fuzz.run_steps);
150}
151
152fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) void {
153 rebuildTestsWorkerRunFallible(run, gpa, parent_prog_node) catch |err| {
154 const compile = run.producer.?;
155 log.err("step '{s}': failed to rebuild in fuzz mode: {t}", .{ compile.step.name, err });
156 };
157}
158
159fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) !void {
160 const graph = run.step.owner.graph;
161 const io = graph.io;
162 const compile = run.producer.?;
163 const prog_node = parent_prog_node.start(compile.step.name, 0);
164 defer prog_node.end();
165
166 const result = compile.rebuildInFuzzMode(gpa, prog_node);
167
168 const show_compile_errors = compile.step.result_error_bundle.errorMessageCount() > 0;
169 const show_error_msgs = compile.step.result_error_msgs.items.len > 0;
170 const show_stderr = compile.step.result_stderr.len > 0;
171
172 if (show_error_msgs or show_compile_errors or show_stderr) {
173 var buf: [256]u8 = undefined;
174 const stderr = try io.lockStderr(&buf, graph.stderr_mode);
175 defer io.unlockStderr();
176 build_runner.printErrorMessages(gpa, &compile.step, .{}, stderr.terminal(), .verbose, .indent) catch {};
177 }
178
179 const rebuilt_bin_path = result catch |err| switch (err) {
180 error.MakeFailed => return,
181 else => |other| return other,
182 };
183 run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename);
184}
185
186fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run) void {
187 const owner = run.step.owner;
188 const gpa = owner.allocator;
189 const graph = owner.graph;
190 const io = graph.io;
191
192 run.rerunInFuzzMode(fuzz, fuzz.prog_node) catch |err| switch (err) {
193 error.MakeFailed => {
194 var buf: [256]u8 = undefined;
195 const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) {
196 error.Canceled => return,
197 };
198 defer io.unlockStderr();
199 build_runner.printErrorMessages(gpa, &run.step, .{}, stderr.terminal(), .verbose, .indent) catch {};
200 return;
201 },
202 else => {
203 log.err("step '{s}': failed to rerun in fuzz mode: {t}", .{ run.step.name, err });
204 return;
205 },
206 };
207}
208
209pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void {
210 assert(fuzz.mode == .forever);
211
212 var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa);
213 defer arena_state.deinit();
214 const arena = arena_state.allocator();
215
216 const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
217 var dedup_table: DedupTable = .empty;
218 defer dedup_table.deinit(fuzz.gpa);
219
220 for (fuzz.run_steps) |run_step| {
221 const compile_inputs = run_step.producer.?.step.inputs.table;
222 for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| {
223 try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len);
224 for (file_list.items) |sub_path| {
225 if (!std.mem.endsWith(u8, sub_path, ".zig")) continue;
226 const joined_path = try dir_path.join(arena, sub_path);
227 dedup_table.putAssumeCapacity(joined_path, {});
228 }
229 }
230 }
231
232 const deduped_paths = dedup_table.keys();
233 const SortContext = struct {
234 pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
235 _ = this;
236 return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
237 .lt => true,
238 .gt => false,
239 .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
240 };
241 }
242 };
243 std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
244 return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths);
245}
246
247pub const Previous = struct {
248 unique_runs: usize,
249 entry_points: usize,
250 sent_source_index: bool,
251 pub const init: Previous = .{
252 .unique_runs = 0,
253 .entry_points = 0,
254 .sent_source_index = false,
255 };
256};
257pub fn sendUpdate(
258 fuzz: *Fuzz,
259 socket: *std.http.Server.WebSocket,
260 prev: *Previous,
261) !void {
262 const io = fuzz.io;
263
264 try fuzz.coverage_mutex.lock(io);
265 defer fuzz.coverage_mutex.unlock(io);
266
267 const coverage_maps = fuzz.coverage_files.values();
268 if (coverage_maps.len == 0) return;
269 // TODO: handle multiple fuzz steps in the WebSocket packets
270 const coverage_map = &coverage_maps[0];
271 const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
272 // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the
273 // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the
274 // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass
275 // this data straight to the socket with sendfile...
276 const seen_pcs = cov_header.seenBits();
277 const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
278 const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
279 {
280 if (!prev.sent_source_index) {
281 prev.sent_source_index = true;
282 // We need to send initial context.
283 const header: abi.SourceIndexHeader = .{
284 .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
285 .files_len = @intCast(coverage_map.coverage.files.entries.len),
286 .source_locations_len = @intCast(coverage_map.source_locations.len),
287 .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
288 .start_timestamp = coverage_map.start_timestamp,
289 .start_n_runs = coverage_map.start_n_runs,
290 };
291 var iovecs: [5][]const u8 = .{
292 @ptrCast(&header),
293 @ptrCast(coverage_map.coverage.directories.keys()),
294 @ptrCast(coverage_map.coverage.files.keys()),
295 @ptrCast(coverage_map.source_locations),
296 coverage_map.coverage.string_bytes.items,
297 };
298 try socket.writeMessageVec(&iovecs, .binary);
299 }
300
301 const header: abi.CoverageUpdateHeader = .{
302 .n_runs = n_runs,
303 .unique_runs = unique_runs,
304 };
305 var iovecs: [2][]const u8 = .{
306 @ptrCast(&header),
307 @ptrCast(seen_pcs),
308 };
309 try socket.writeMessageVec(&iovecs, .binary);
310
311 prev.unique_runs = unique_runs;
312 }
313
314 if (prev.entry_points != coverage_map.entry_points.items.len) {
315 const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len));
316 var iovecs: [2][]const u8 = .{
317 @ptrCast(&header),
318 @ptrCast(coverage_map.entry_points.items),
319 };
320 try socket.writeMessageVec(&iovecs, .binary);
321
322 prev.entry_points = coverage_map.entry_points.items.len;
323 }
324}
325
326fn coverageRun(fuzz: *Fuzz) void {
327 coverageRunCancelable(fuzz) catch |err| switch (err) {
328 error.Canceled => return,
329 };
330}
331
332fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void {
333 const io = fuzz.io;
334
335 try fuzz.queue_mutex.lock(io);
336 defer fuzz.queue_mutex.unlock(io);
337
338 while (true) {
339 try fuzz.queue_cond.wait(io, &fuzz.queue_mutex);
340 for (fuzz.msg_queue.items) |msg| switch (msg) {
341 .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) {
342 error.AlreadyReported => continue,
343 error.Canceled => return,
344 else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
345 },
346 .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
347 error.AlreadyReported => continue,
348 error.Canceled => return,
349 else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
350 },
351 };
352 fuzz.msg_queue.clearRetainingCapacity();
353 }
354}
355fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void {
356 assert(fuzz.mode == .forever);
357 const ws = fuzz.mode.forever.ws;
358 const gpa = fuzz.gpa;
359 const io = fuzz.io;
360
361 try fuzz.coverage_mutex.lock(io);
362 defer fuzz.coverage_mutex.unlock(io);
363
364 const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id);
365 if (gop.found_existing) {
366 // We are fuzzing the same executable with multiple threads.
367 // Perhaps the same unit test; perhaps a different one. In any
368 // case, since the coverage file is the same, we only have to
369 // notice changes to that one file in order to learn coverage for
370 // this particular executable.
371 return;
372 }
373 errdefer _ = fuzz.coverage_files.pop();
374
375 gop.value_ptr.* = .{
376 .coverage = std.debug.Coverage.init,
377 .mapped_memory = undefined, // populated below
378 .source_locations = undefined, // populated below
379 .entry_points = .empty,
380 .start_timestamp = ws.now(),
381 .start_n_runs = undefined, // populated below
382 };
383 errdefer gop.value_ptr.coverage.deinit(gpa);
384
385 const rebuilt_exe_path = run_step.rebuilt_executable.?;
386 const target = run_step.producer.?.rootModuleTarget();
387 var debug_info = std.debug.Info.load(
388 gpa,
389 io,
390 rebuilt_exe_path,
391 &gop.value_ptr.coverage,
392 target.ofmt,
393 target.cpu.arch,
394 ) catch |err| {
395 log.err("step '{s}': failed to load debug information for '{f}': {t}", .{
396 run_step.step.name, rebuilt_exe_path, err,
397 });
398 return error.AlreadyReported;
399 };
400 defer debug_info.deinit(gpa);
401
402 const coverage_file_path: Build.Cache.Path = .{
403 .root_dir = run_step.step.owner.cache_root,
404 .sub_path = "v/" ++ std.fmt.hex(coverage_id),
405 };
406 var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
407 log.err("step '{s}': failed to load coverage file '{f}': {t}", .{
408 run_step.step.name, coverage_file_path, err,
409 });
410 return error.AlreadyReported;
411 };
412 defer coverage_file.close(io);
413
414 const file_size = coverage_file.length(io) catch |err| {
415 log.err("unable to check len of coverage file '{f}': {t}", .{ coverage_file_path, err });
416 return error.AlreadyReported;
417 };
418
419 const mapped_memory = std.posix.mmap(
420 null,
421 file_size,
422 .{ .READ = true },
423 .{ .TYPE = .SHARED },
424 coverage_file.handle,
425 0,
426 ) catch |err| {
427 log.err("failed to map coverage file '{f}': {t}", .{ coverage_file_path, err });
428 return error.AlreadyReported;
429 };
430 gop.value_ptr.mapped_memory = mapped_memory;
431
432 const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
433 const pcs = header.pcAddrs();
434 const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
435 errdefer gpa.free(source_locations);
436
437 // Unfortunately the PCs array that LLVM gives us from the 8-bit PC
438 // counters feature is not sorted.
439 var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty;
440 defer sorted_pcs.deinit(gpa);
441 try sorted_pcs.resize(gpa, pcs.len);
442 @memcpy(sorted_pcs.items(.pc), pcs);
443 for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i);
444 sorted_pcs.sortUnstable(struct {
445 addrs: []const u64,
446
447 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
448 return ctx.addrs[a_index] < ctx.addrs[b_index];
449 }
450 }{ .addrs = sorted_pcs.items(.pc) });
451
452 debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
453 log.err("failed to resolve addresses to source locations: {t}", .{err});
454 return error.AlreadyReported;
455 };
456
457 for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl;
458 gop.value_ptr.source_locations = source_locations;
459 gop.value_ptr.start_n_runs = header.n_runs;
460
461 ws.notifyUpdate();
462}
463
464fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void {
465 const io = fuzz.io;
466
467 try fuzz.coverage_mutex.lock(io);
468 defer fuzz.coverage_mutex.unlock(io);
469
470 const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?;
471 const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
472 const pcs = header.pcAddrs();
473
474 // Since this pcs list is unsorted, we must linear scan for the best index.
475 const index = i: {
476 var best: usize = 0;
477 for (pcs[1..], 1..) |elem_addr, i| {
478 if (elem_addr == addr) break :i i;
479 if (elem_addr > addr) continue;
480 if (elem_addr > pcs[best]) best = i;
481 }
482 break :i best;
483 };
484 if (index >= pcs.len) {
485 log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
486 addr, pcs[0], pcs[pcs.len - 1],
487 });
488 return error.AlreadyReported;
489 }
490 if (false) {
491 const sl = coverage_map.source_locations[index];
492 const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
493 if (pcs.len == 1) {
494 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{
495 addr, file_name, sl.line, sl.column,
496 });
497 } else if (index == 0) {
498 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{
499 addr, file_name, sl.line, sl.column, pcs[index + 1],
500 });
501 } else if (index == pcs.len - 1) {
502 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{
503 addr, file_name, sl.line, sl.column, index, pcs[index - 1],
504 });
505 } else {
506 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
507 addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
508 });
509 }
510 }
511 try coverage_map.entry_points.append(fuzz.gpa, @intCast(index));
512}
513
514pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
515 assert(fuzz.mode == .limit);
516 const io = fuzz.io;
517
518 try fuzz.group.await(io);
519 fuzz.group = .init;
520
521 std.debug.print("======= FUZZING REPORT =======\n", .{});
522 for (fuzz.msg_queue.items) |msg| {
523 if (msg != .coverage) continue;
524
525 const cov = msg.coverage;
526 const coverage_file_path: std.Build.Cache.Path = .{
527 .root_dir = cov.run.step.owner.cache_root,
528 .sub_path = "v/" ++ std.fmt.hex(cov.id),
529 };
530 var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
531 fatal("step '{s}': failed to load coverage file '{f}': {t}", .{
532 cov.run.step.name, coverage_file_path, err,
533 });
534 };
535 defer coverage_file.close(io);
536
537 const fuzz_abi = std.Build.abi.fuzz;
538 var rbuf: [0x1000]u8 = undefined;
539 var r = coverage_file.reader(io, &rbuf);
540
541 var header: fuzz_abi.SeenPcsHeader = undefined;
542 r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
543 fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{
544 cov.run.step.name, coverage_file_path, err,
545 });
546 };
547
548 if (header.pcs_len == 0) {
549 fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{
550 cov.run.step.name, coverage_file_path,
551 });
552 }
553
554 var seen_count: usize = 0;
555 const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
556 for (0..chunk_count) |_| {
557 const seen = r.interface.takeInt(usize, .little) catch |err| {
558 fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{
559 cov.run.step.name, coverage_file_path, err,
560 });
561 };
562 seen_count += @popCount(seen);
563 }
564
565 const seen_f: f64 = @floatFromInt(seen_count);
566 const total_f: f64 = @floatFromInt(header.pcs_len);
567 const ratio = seen_f / total_f;
568 std.debug.print(
569 \\Step: {s}
570 \\Fuzz test: "{s}" ({x})
571 \\Runs: {} -> {}
572 \\Unique runs: {} -> {}
573 \\Coverage: {}/{} -> {}/{} ({:.02}%)
574 \\
575 , .{
576 cov.run.step.name,
577 cov.run.fuzz_tests.items[0],
578 cov.id,
579 cov.cumulative.runs,
580 header.n_runs,
581 cov.cumulative.unique,
582 header.unique_runs,
583 cov.cumulative.coverage,
584 header.pcs_len,
585 seen_count,
586 header.pcs_len,
587 ratio * 100,
588 });
589
590 std.debug.print("------------------------------\n", .{});
591 }
592 std.debug.print(
593 \\Values are accumulated across multiple runs when preserving the cache.
594 \\==============================
595 \\
596 , .{});
597}
lib/compiler/maker/Graph.zig created+92
......@@ -0,0 +1,92 @@
1//! Shared maker state among all steps.
2const Graph = @This();
3
4const std = @import("std");
5const Io = std.Io;
6const Allocator = std.mem.Allocator;
7const Configuration = std.Build.Configuration;
8
9const Step = @import("Step.zig");
10const Package = @import("Package.zig");
11
12io: Io,
13/// Process lifetime.
14arena: Allocator,
15system_library_options: std.StringArrayHashMapUnmanaged(std.Build.SystemLibraryMode),
16system_package_mode: bool,
17debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null,
18cache: std.Build.Cache,
19zig_exe: [:0]const u8,
20environ_map: std.process.Environ.Map,
21global_cache_root: std.Build.Cache.Directory,
22zig_lib_directory: std.Build.Cache.Directory,
23incremental: ?bool,
24random_seed: u32,
25allow_so_scripts: ?bool,
26time_report: bool,
27/// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also
28/// respects the '--color' flag.
29stderr_mode: ?Io.Terminal.Mode,
30
31configuration: *const Configuration,
32top_level_steps: std.AutoArrayHashMapUnmanaged(Configuration.String, Configuration.Step.Index),
33
34pub const DirList = struct {
35 lib_dir: ?[]const u8 = null,
36 exe_dir: ?[]const u8 = null,
37 include_dir: ?[]const u8 = null,
38};
39
40/// This function is intended to be called by lib/build_runner.zig, not a build.zig file.
41pub fn resolveInstallPrefix(graph: *Graph, p: *Package, install_prefix: ?[]const u8, dir_list: DirList) !void {
42 if (p.dest_dir) |dest_dir| {
43 p.install_prefix = install_prefix orelse "/usr";
44 p.install_path = b.pathJoin(&.{ dest_dir, p.install_prefix });
45 } else {
46 p.install_prefix = install_prefix orelse
47 (p.build_root.join(b.allocator, &.{"zig-out"}) catch @panic("unhandled error"));
48 b.install_path = b.install_prefix;
49 }
50
51 var lib_list = [_][]const u8{ b.install_path, "lib" };
52 var exe_list = [_][]const u8{ b.install_path, "bin" };
53 var h_list = [_][]const u8{ b.install_path, "include" };
54
55 if (dir_list.lib_dir) |dir| {
56 if (fs.path.isAbsolute(dir)) lib_list[0] = b.dest_dir orelse "";
57 lib_list[1] = dir;
58 }
59
60 if (dir_list.exe_dir) |dir| {
61 if (fs.path.isAbsolute(dir)) exe_list[0] = b.dest_dir orelse "";
62 exe_list[1] = dir;
63 }
64
65 if (dir_list.include_dir) |dir| {
66 if (fs.path.isAbsolute(dir)) h_list[0] = b.dest_dir orelse "";
67 h_list[1] = dir;
68 }
69
70 b.lib_dir = b.pathJoin(&lib_list);
71 b.exe_dir = b.pathJoin(&exe_list);
72 b.h_dir = b.pathJoin(&h_list);
73}
74
75fn determineAndApplyInstallPrefix(b: *Build) error{OutOfMemory}!void {
76 // Create an installation directory local to this package. This will be used when
77 // dependant packages require a standard prefix, such as include directories for C headers.
78 var hash = b.graph.cache.hash;
79 // Random bytes to make unique. Refresh this with new random bytes when
80 // implementation is modified in a non-backwards-compatible way.
81 hash.add(@as(u32, 0xd8cb0056));
82 hash.addBytes(b.dep_prefix);
83
84 var wyhash = std.hash.Wyhash.init(0);
85 hashUserInputOptionsMap(b.allocator, b.user_input_options, &wyhash);
86 hash.add(wyhash.final());
87
88 const digest = hash.final();
89 const install_prefix = try b.cache_root.join(b.allocator, &.{ "i", &digest });
90 b.resolveInstallPrefix(install_prefix, .{});
91}
92
lib/compiler/maker/Package.zig created+12
......@@ -0,0 +1,12 @@
1const Package = @This();
2
3const std = @import("std");
4
5install_prefix: []const u8,
6install_path: []const u8,
7dest_dir: ?[]const u8,
8lib_dir: []const u8,
9exe_dir: []const u8,
10h_dir: []const u8,
11/// Path to the directory containing build.zig.
12build_root: std.Build.Cache.Path,
lib/compiler/maker/Step.zig created+850
......@@ -0,0 +1,850 @@
1const Step = @This();
2
3const std = @import("std");
4const Io = std.Io;
5const Allocator = std.mem.Allocator;
6const Cache = std.Build.Cache;
7const assert = std.debug.assert;
8
9const WebServer = @import("WebServer.zig");
10
11pub const Compile = @import("Step/Compile.zig");
12pub const Run = @import("Step/Run.zig");
13
14state: State,
15makeFn: MakeFn,
16dependants: std.ArrayList(*Step),
17/// Collects the set of files that retrigger this step to run.
18///
19/// This is used by the build system's implementation of `--watch` but it can
20/// also be potentially useful for IDEs to know what effects editing a
21/// particular file has.
22///
23/// Populated within `make`. Implementation may choose to clear and repopulate,
24/// retain previous value, or update.
25inputs: Inputs = .init,
26pending_deps: u32,
27
28result_error_msgs: std.ArrayList([]const u8),
29result_error_bundle: std.zig.ErrorBundle,
30result_stderr: []const u8,
31result_cached: bool,
32result_duration_ns: ?u64,
33/// 0 means unavailable or not reported.
34result_peak_rss: usize,
35/// If the step is failed and this field is populated, this is the command which failed.
36/// This field may be populated even if the step succeeded.
37result_failed_command: ?[]const u8,
38test_results: TestResults,
39
40
41pub const State = enum {
42 precheck_unstarted,
43 precheck_started,
44 /// This is also used to indicate "dirty" steps that have been modified
45 /// after a previous build completed, in which case, the step may or may
46 /// not have been completed before. Either way, one or more of its direct
47 /// file system inputs have been modified, meaning that the step needs to
48 /// be re-evaluated.
49 precheck_done,
50 dependency_failure,
51 success,
52 failure,
53 /// This state indicates that the step did not complete, however, it also did not fail,
54 /// and it is safe to continue executing its dependencies.
55 skipped,
56 /// This step was skipped because it specified a max_rss that exceeded the runner's maximum.
57 /// It is not safe to run its dependencies.
58 skipped_oom,
59};
60
61pub const Inputs = struct {
62 table: Table,
63
64 pub const init: Inputs = .{
65 .table = .{},
66 };
67
68 pub const Table = std.ArrayHashMapUnmanaged(Cache.Path, Files, Cache.Path.TableAdapter, false);
69 /// The special file name "." means any changes inside the directory.
70 pub const Files = std.ArrayList([]const u8);
71
72 pub fn populated(inputs: *Inputs) bool {
73 return inputs.table.count() != 0;
74 }
75
76 pub fn clear(inputs: *Inputs, gpa: Allocator) void {
77 for (inputs.table.values()) |*files| files.deinit(gpa);
78 inputs.table.clearRetainingCapacity();
79 }
80};
81
82pub const TestResults = struct {
83 /// The total number of tests in the step. Every test has a "status" from the following:
84 /// * passed
85 /// * skipped
86 /// * failed cleanly
87 /// * crashed
88 /// * timed out
89 test_count: u32 = 0,
90
91 /// The number of tests which were skipped (`error.SkipZigTest`).
92 skip_count: u32 = 0,
93 /// The number of tests which failed cleanly.
94 fail_count: u32 = 0,
95 /// The number of tests which terminated unexpectedly, i.e. crashed.
96 crash_count: u32 = 0,
97 /// The number of tests which timed out.
98 timeout_count: u32 = 0,
99
100 /// The number of detected memory leaks. The associated test may still have passed; indeed, *all*
101 /// individual tests may have passed. However, the step as a whole fails if any test has leaks.
102 leak_count: u32 = 0,
103 /// The number of detected error logs. The associated test may still have passed; indeed, *all*
104 /// individual tests may have passed. However, the step as a whole fails if any test logs errors.
105 log_err_count: u32 = 0,
106
107 pub fn isSuccess(tr: TestResults) bool {
108 // all steps are success or skip
109 return tr.fail_count == 0 and
110 tr.crash_count == 0 and
111 tr.timeout_count == 0 and
112 // no (otherwise successful) step leaked memory or logged errors
113 tr.leak_count == 0 and
114 tr.log_err_count == 0;
115 }
116
117 /// Computes the number of tests which passed from the other values.
118 pub fn passCount(tr: TestResults) u32 {
119 return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count;
120 }
121};
122
123pub const MakeOptions = struct {
124 progress_node: std.Progress.Node,
125 watch: bool,
126 web_server: ?*WebServer,
127 /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds.
128 unit_test_timeout_ns: ?u64,
129 /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`.
130 gpa: Allocator,
131};
132
133pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void;
134
135/// If the Step's `make` function reports `error.MakeFailed`, it indicates they
136/// have already reported the error. Otherwise, we add a simple error report
137/// here.
138pub fn make(s: *Step, options: MakeOptions) error{ MakeFailed, MakeSkipped }!void {
139 const arena = s.owner.allocator;
140 const graph = s.owner.graph;
141 const io = graph.io;
142
143 var start_ts: ?Io.Timestamp = t: {
144 if (!graph.time_report) break :t null;
145 if (s.id == .compile) break :t null;
146 if (s.id == .run and s.cast(Run).?.stdio == .zig_test) break :t null;
147 break :t Io.Clock.awake.now(io);
148 };
149 const make_result = s.makeFn(s, options);
150 if (start_ts) |*ts| {
151 const duration = ts.untilNow(io, .awake);
152 options.web_server.?.updateTimeReportGeneric(s, duration);
153 }
154
155 make_result catch |err| switch (err) {
156 error.MakeFailed, error.MakeSkipped => |e| return e,
157 else => {
158 s.result_error_msgs.append(arena, @errorName(err)) catch @panic("OOM");
159 return error.MakeFailed;
160 },
161 };
162
163 if (!s.test_results.isSuccess()) {
164 return error.MakeFailed;
165 }
166
167 if (s.max_rss != 0 and s.result_peak_rss > s.max_rss) {
168 const msg = std.fmt.allocPrint(arena, "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", .{
169 s.result_peak_rss, s.max_rss,
170 }) catch @panic("OOM");
171 s.result_error_msgs.append(arena, msg) catch @panic("OOM");
172 }
173}
174
175fn makeNoOp(step: *Step, options: MakeOptions) anyerror!void {
176 _ = options;
177
178 var all_cached = true;
179
180 for (step.dependencies.items) |dep| {
181 all_cached = all_cached and dep.result_cached;
182 }
183
184 step.result_cached = all_cached;
185}
186
187/// Implementation detail of file watching. Prepares the step for being re-evaluated.
188/// Returns `true` if the step was newly invalidated, `false` if it was already invalidated.
189pub fn invalidateResult(step: *Step, gpa: Allocator) bool {
190 if (step.state == .precheck_done) return false;
191 assert(step.pending_deps == 0);
192 step.state = .precheck_done;
193 step.reset(gpa);
194 for (step.dependants.items) |dependant| {
195 _ = dependant.invalidateResult(gpa);
196 dependant.pending_deps += 1;
197 }
198 return true;
199}
200
201/// Implementation detail of file watching and forced rebuilds. Prepares the step for being re-evaluated.
202pub fn reset(step: *Step, gpa: Allocator) void {
203 assert(step.state == .precheck_done);
204
205 if (step.result_failed_command) |cmd| gpa.free(cmd);
206
207 step.result_error_msgs.clearRetainingCapacity();
208 step.result_stderr = "";
209 step.result_cached = false;
210 step.result_duration_ns = null;
211 step.result_peak_rss = 0;
212 step.result_failed_command = null;
213 step.test_results = .{};
214 step.clearWatchInputs();
215
216 step.result_error_bundle.deinit(gpa);
217 step.result_error_bundle = std.zig.ErrorBundle.empty;
218}
219
220/// Populates `s.result_failed_command`.
221pub fn captureChildProcess(
222 s: *Step,
223 gpa: Allocator,
224 progress_node: std.Progress.Node,
225 argv: []const []const u8,
226) !std.process.RunResult {
227 const graph = s.owner.graph;
228 const arena = graph.arena;
229 const io = graph.io;
230
231 // If an error occurs, it's happened in this command:
232 assert(s.result_failed_command == null);
233 s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv);
234
235 try handleChildProcUnsupported(s);
236 try handleVerbose(s.owner, .inherit, argv);
237
238 const result = std.process.run(arena, io, .{
239 .argv = argv,
240 .environ_map = &graph.environ_map,
241 .progress_node = progress_node,
242 }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err });
243
244 if (result.stderr.len > 0) {
245 try s.result_error_msgs.append(arena, result.stderr);
246 }
247
248 return result;
249}
250
251pub fn fail(step: *Step, comptime fmt: []const u8, args: anytype) error{ OutOfMemory, MakeFailed } {
252 try step.addError(fmt, args);
253 return error.MakeFailed;
254}
255
256pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void {
257 const arena = step.owner.allocator;
258 const msg = try std.fmt.allocPrint(arena, fmt, args);
259 try step.result_error_msgs.append(arena, msg);
260}
261
262pub const ZigProcess = struct {
263 child: std.process.Child,
264 multi_reader_buffer: Io.File.MultiReader.Buffer(2),
265 multi_reader: Io.File.MultiReader,
266 progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn,
267
268 pub const StreamEnum = enum { stdout, stderr };
269
270 pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void {
271 zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null;
272 }
273
274 pub fn deinit(zp: *ZigProcess, io: Io) void {
275 zp.child.kill(io);
276 zp.multi_reader.deinit();
277 zp.* = undefined;
278 }
279};
280
281/// Assumes that argv contains `--listen=-` and that the process being spawned
282/// is the zig compiler - the same version that compiled the build runner.
283/// Populates `s.result_failed_command`.
284pub fn evalZigProcess(
285 s: *Step,
286 argv: []const []const u8,
287 prog_node: std.Progress.Node,
288 watch: bool,
289 web_server: ?*WebServer,
290 gpa: Allocator,
291) !?Cache.Path {
292 const b = s.owner;
293 const io = b.graph.io;
294
295 // If an error occurs, it's happened in this command:
296 assert(s.result_failed_command == null);
297 s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv);
298
299 if (s.getZigProcess()) |zp| update: {
300 assert(watch);
301 if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index);
302 zp.progress_ipc_index = null;
303 var exited = false;
304 defer if (exited) {
305 s.cast(Compile).?.zig_process = null;
306 zp.deinit(io);
307 gpa.destroy(zp);
308 } else zp.saveState(prog_node);
309 const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) {
310 error.BrokenPipe, error.EndOfStream => |reason| {
311 std.log.info("{s} restart required: {t}", .{ argv[0], reason });
312 // Process restart required.
313 const term = zp.child.wait(io) catch |e| {
314 return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
315 };
316 _ = term;
317 exited = true;
318 break :update;
319 },
320 else => |e| return e,
321 };
322
323 if (s.result_error_bundle.errorMessageCount() > 0) {
324 return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
325 }
326
327 if (s.result_error_msgs.items.len > 0 and result == null) {
328 // Crash detected.
329 const term = zp.child.wait(io) catch |e| {
330 return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
331 };
332 s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
333 exited = true;
334 try handleChildProcessTerm(s, term);
335 return error.MakeFailed;
336 }
337
338 return result;
339 }
340 assert(argv.len != 0);
341
342 try handleChildProcUnsupported(s);
343 try handleVerbose(s.owner, .inherit, argv);
344
345 const zp = try gpa.create(ZigProcess);
346 defer if (!watch) gpa.destroy(zp);
347
348 zp.child = std.process.spawn(io, .{
349 .argv = argv,
350 .environ_map = &b.graph.environ_map,
351 .stdin = .pipe,
352 .stdout = .pipe,
353 .stderr = .pipe,
354 .request_resource_usage_statistics = true,
355 .progress_node = prog_node,
356 }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
357
358 zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{
359 zp.child.stdout.?, zp.child.stderr.?,
360 });
361 if (watch) s.cast(Compile).?.zig_process = zp;
362 defer if (!watch) zp.deinit(io);
363
364 const result = result: {
365 defer if (watch) zp.saveState(prog_node);
366 break :result try zigProcessUpdate(s, zp, watch, web_server, gpa);
367 };
368
369 if (!watch) {
370 // Send EOF to stdin.
371 zp.child.stdin.?.close(io);
372 zp.child.stdin = null;
373
374 const term = zp.child.wait(io) catch |err| {
375 return s.fail("unable to wait for {s}: {t}", .{ argv[0], err });
376 };
377 s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
378
379 // Special handling for Compile step that is expecting compile errors.
380 if (s.cast(Compile)) |compile| switch (term) {
381 .exited => {
382 // Note that the exit code may be 0 in this case due to the
383 // compiler server protocol.
384 if (compile.expect_errors != null) {
385 return error.NeedCompileErrorCheck;
386 }
387 },
388 else => {},
389 };
390
391 try handleChildProcessTerm(s, term);
392 }
393
394 if (s.result_error_bundle.errorMessageCount() > 0) {
395 return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
396 }
397
398 return result;
399}
400
401/// Wrapper around `Io.Dir.updateFile` that handles verbose and error output.
402pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus {
403 const b = s.owner;
404 const io = b.graph.io;
405 const src_path = src_lazy_path.getPath3(b, s);
406 try handleVerbose(b, .inherit, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path });
407 return Io.Dir.updateFile(src_path.root_dir.handle, io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err|
408 return s.fail("unable to update file from '{f}' to '{s}': {t}", .{ src_path, dest_path, err });
409}
410
411/// Wrapper around `Io.Dir.createDirPathStatus` that handles verbose and error output.
412pub fn installDir(s: *Step, dest_path: []const u8) !Io.Dir.CreatePathStatus {
413 const b = s.owner;
414 const io = b.graph.io;
415 try handleVerbose(b, .inherit, &.{ "install", "-d", dest_path });
416 return Io.Dir.cwd().createDirPathStatus(io, dest_path, .default_dir) catch |err|
417 return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err });
418}
419
420fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*WebServer, gpa: Allocator) !?Path {
421 const b = s.owner;
422 const arena = b.allocator;
423 const io = b.graph.io;
424
425 const start_ts = Io.Clock.awake.now(io);
426
427 try sendMessage(io, zp.child.stdin.?, .update);
428 if (!watch) try sendMessage(io, zp.child.stdin.?, .exit);
429
430 var result: ?Path = null;
431 var eos_err: error{EndOfStream}!void = {};
432
433 const stdout = zp.multi_reader.fileReader(0);
434
435 while (true) {
436 const Header = std.zig.Server.Message.Header;
437 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
438 error.EndOfStream => break,
439 error.ReadFailed => return stdout.err.?,
440 };
441 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
442 error.EndOfStream => |e| {
443 // Better to report the crash with stderr below, but we set
444 // this in case the child exits successfully while violating
445 // this protocol.
446 eos_err = e;
447 break;
448 },
449 error.ReadFailed => return stdout.err.?,
450 };
451 switch (header.tag) {
452 .zig_version => {
453 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
454 return s.fail(
455 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
456 .{ builtin.zig_version_string, body },
457 );
458 }
459 },
460 .error_bundle => {
461 s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
462 // This message indicates the end of the update.
463 if (watch) break;
464 },
465 .emit_digest => {
466 const EmitDigest = std.zig.Server.Message.EmitDigest;
467 const emit_digest: *align(1) const EmitDigest = @ptrCast(body);
468 s.result_cached = emit_digest.flags.cache_hit;
469 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
470 result = .{
471 .root_dir = b.cache_root,
472 .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
473 };
474 },
475 .file_system_inputs => {
476 s.clearWatchInputs();
477 var it = std.mem.splitScalar(u8, body, 0);
478 while (it.next()) |prefixed_path| {
479 const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1);
480 const sub_path = try arena.dupe(u8, prefixed_path[1..]);
481 const sub_path_dirname = std.fs.path.dirname(sub_path) orelse "";
482 switch (prefix_index) {
483 .cwd => {
484 const path: Cache.Path = .{
485 .root_dir = Cache.Directory.cwd(),
486 .sub_path = sub_path_dirname,
487 };
488 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
489 },
490 .zig_lib => zl: {
491 if (s.cast(Step.Compile)) |compile| {
492 if (compile.zig_lib_dir) |zig_lib_dir| {
493 const lp = try zig_lib_dir.join(arena, sub_path);
494 try addWatchInput(s, lp);
495 break :zl;
496 }
497 }
498 const path: Cache.Path = .{
499 .root_dir = s.owner.graph.zig_lib_directory,
500 .sub_path = sub_path_dirname,
501 };
502 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
503 },
504 .local_cache => {
505 const path: Cache.Path = .{
506 .root_dir = b.cache_root,
507 .sub_path = sub_path_dirname,
508 };
509 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
510 },
511 .global_cache => {
512 const path: Cache.Path = .{
513 .root_dir = s.owner.graph.global_cache_root,
514 .sub_path = sub_path_dirname,
515 };
516 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
517 },
518 }
519 }
520 },
521 .time_report => if (web_server) |ws| {
522 const TimeReport = std.zig.Server.Message.TimeReport;
523 const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]);
524 ws.updateTimeReportCompile(.{
525 .compile = s.cast(Step.Compile).?,
526 .use_llvm = tr.flags.use_llvm,
527 .stats = tr.stats,
528 .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()),
529 .llvm_pass_timings_len = tr.llvm_pass_timings_len,
530 .files_len = tr.files_len,
531 .decls_len = tr.decls_len,
532 .trailing = body[@sizeOf(TimeReport)..],
533 });
534 },
535 else => {}, // ignore other messages
536 }
537 }
538
539 s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds());
540
541 const stderr_contents = zp.multi_reader.reader(1).buffered();
542 if (stderr_contents.len > 0) {
543 try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents));
544 }
545
546 try eos_err;
547
548 return result;
549}
550
551pub fn getZigProcess(s: *Step) ?*ZigProcess {
552 return switch (s.id) {
553 .compile => s.cast(Compile).?.zig_process,
554 else => null,
555 };
556}
557
558fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
559 const header: std.zig.Client.Message.Header = .{
560 .tag = tag,
561 .bytes_len = 0,
562 };
563 var w = file.writer(io, &.{});
564 w.interface.writeStruct(header, .little) catch |err| switch (err) {
565 error.WriteFailed => return w.err.?,
566 };
567}
568
569pub fn handleVerbose(
570 b: *Build,
571 cwd: std.process.Child.Cwd,
572 argv: []const []const u8,
573) error{OutOfMemory}!void {
574 return handleVerbose2(b, cwd, null, argv);
575}
576
577pub fn handleVerbose2(
578 b: *Build,
579 cwd: std.process.Child.Cwd,
580 opt_env: ?*const std.process.Environ.Map,
581 argv: []const []const u8,
582) error{OutOfMemory}!void {
583 if (b.verbose) {
584 const graph = b.graph;
585 // Intention of verbose is to print all sub-process command lines to
586 // stderr before spawning them.
587 const text = try allocPrintCmd(b.allocator, cwd, if (opt_env) |env| .{
588 .child = env,
589 .parent = &graph.environ_map,
590 } else null, argv);
591 std.debug.print("{s}\n", .{text});
592 }
593}
594
595/// Asserts that the caller has already populated `s.result_failed_command`.
596pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void {
597 if (!std.process.can_spawn) {
598 return s.fail("unable to spawn process: host cannot spawn child processes", .{});
599 }
600}
601
602/// Asserts that the caller has already populated `s.result_failed_command`.
603pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void {
604 assert(s.result_failed_command != null);
605 return switch (term) {
606 .exited => |code| if (code != 0) s.fail("process exited with error code {d}", .{code}),
607 .signal => |sig| s.fail("process terminated with signal {t}", .{sig}),
608 .stopped => |sig| s.fail("process stopped with signal {t}", .{sig}),
609 .unknown => s.fail("process terminated unexpectedly", .{}),
610 };
611}
612
613/// Prefer `cacheHitAndWatch` unless you already added watch inputs
614/// separately from using the cache system.
615pub fn cacheHit(s: *Step, man: *Cache.Manifest) !bool {
616 s.result_cached = man.hit() catch |err| return failWithCacheError(s, man, err);
617 return s.result_cached;
618}
619
620/// Clears previous watch inputs, if any, and then populates watch inputs from
621/// the full set of files picked up by the cache manifest.
622///
623/// Must be accompanied with `writeManifestAndWatch`.
624pub fn cacheHitAndWatch(s: *Step, man: *Cache.Manifest) !bool {
625 const is_hit = man.hit() catch |err| return failWithCacheError(s, man, err);
626 s.result_cached = is_hit;
627 // The above call to hit() populates the manifest with files, so in case of
628 // a hit, we need to populate watch inputs.
629 if (is_hit) try setWatchInputsFromManifest(s, man);
630 return is_hit;
631}
632
633fn failWithCacheError(
634 s: *Step,
635 man: *const Cache.Manifest,
636 err: Cache.Manifest.HitError,
637) error{ OutOfMemory, Canceled, MakeFailed } {
638 switch (err) {
639 error.CacheCheckFailed => switch (man.diagnostic) {
640 .none => unreachable,
641 .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {t} {t}", .{
642 man.diagnostic, e,
643 }),
644 .file_open, .file_stat, .file_read, .file_hash => |op| {
645 const pp = man.files.keys()[op.file_index].prefixed_path;
646 const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
647 return s.fail("failed to check cache: '{s}{c}{s}' {t} {t}", .{
648 prefix, std.fs.path.sep, pp.sub_path, man.diagnostic, op.err,
649 });
650 },
651 },
652 error.OutOfMemory, error.Canceled => |e| return e,
653 error.InvalidFormat => return s.fail("failed to check cache: invalid manifest file format", .{}),
654 }
655}
656
657/// Prefer `writeManifestAndWatch` unless you already added watch inputs
658/// separately from using the cache system.
659pub fn writeManifest(s: *Step, man: *Cache.Manifest) !void {
660 if (s.test_results.isSuccess()) {
661 man.writeManifest() catch |err| {
662 try s.addError("unable to write cache manifest: {t}", .{err});
663 };
664 }
665}
666
667/// Clears previous watch inputs, if any, and then populates watch inputs from
668/// the full set of files picked up by the cache manifest.
669///
670/// Must be accompanied with `cacheHitAndWatch`.
671pub fn writeManifestAndWatch(s: *Step, man: *Cache.Manifest) !void {
672 try writeManifest(s, man);
673 try setWatchInputsFromManifest(s, man);
674}
675
676fn setWatchInputsFromManifest(s: *Step, man: *Cache.Manifest) !void {
677 const arena = s.owner.allocator;
678 const prefixes = man.cache.prefixes();
679 clearWatchInputs(s);
680 for (man.files.keys()) |file| {
681 // The file path data is freed when the cache manifest is cleaned up at the end of `make`.
682 const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path);
683 try addWatchInputFromPath(s, .{
684 .root_dir = prefixes[file.prefixed_path.prefix],
685 .sub_path = std.fs.path.dirname(sub_path) orelse "",
686 }, std.fs.path.basename(sub_path));
687 }
688}
689
690/// For steps that have a single input that never changes when re-running `make`.
691pub fn singleUnchangingWatchInput(step: *Step, lazy_path: Build.LazyPath) Allocator.Error!void {
692 if (!step.inputs.populated()) try step.addWatchInput(lazy_path);
693}
694
695pub fn clearWatchInputs(step: *Step) void {
696 const gpa = step.owner.allocator;
697 step.inputs.clear(gpa);
698}
699
700/// Places a *file* dependency on the path.
701pub fn addWatchInput(step: *Step, lazy_file: Build.LazyPath) Allocator.Error!void {
702 switch (lazy_file) {
703 .src_path => |src_path| try addWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
704 .dependency => |d| try addWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
705 .cwd_relative => |path_string| {
706 try addWatchInputFromPath(step, .{
707 .root_dir = .{
708 .path = null,
709 .handle = Io.Dir.cwd(),
710 },
711 .sub_path = std.fs.path.dirname(path_string) orelse "",
712 }, std.fs.path.basename(path_string));
713 },
714 // Nothing to watch because this dependency edge is modeled instead via `dependants`.
715 .generated => {},
716 }
717}
718
719/// Any changes inside the directory will trigger invalidation.
720///
721/// See also `addDirectoryWatchInputFromPath` which takes a `Cache.Path` instead.
722///
723/// Paths derived from this directory should also be manually added via
724/// `addDirectoryWatchInputFromPath` if and only if this function returns
725/// `true`.
726pub fn addDirectoryWatchInput(step: *Step, lazy_directory: Build.LazyPath) Allocator.Error!bool {
727 switch (lazy_directory) {
728 .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
729 .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
730 .cwd_relative => |path_string| {
731 try addDirectoryWatchInputFromPath(step, .{
732 .root_dir = .{
733 .path = null,
734 .handle = Io.Dir.cwd(),
735 },
736 .sub_path = path_string,
737 });
738 },
739 // Nothing to watch because this dependency edge is modeled instead via `dependants`.
740 .generated => return false,
741 }
742 return true;
743}
744
745/// Any changes inside the directory will trigger invalidation.
746///
747/// See also `addDirectoryWatchInput` which takes a `Build.LazyPath` instead.
748///
749/// This function should only be called when it has been verified that the
750/// dependency on `path` is not already accounted for by a `Step` dependency.
751/// In other words, before calling this function, first check that the
752/// `Build.LazyPath` which this `path` is derived from is not `generated`.
753pub fn addDirectoryWatchInputFromPath(step: *Step, path: Cache.Path) !void {
754 return addWatchInputFromPath(step, path, ".");
755}
756
757fn addWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
758 return addWatchInputFromPath(step, .{
759 .root_dir = builder.build_root,
760 .sub_path = std.fs.path.dirname(sub_path) orelse "",
761 }, std.fs.path.basename(sub_path));
762}
763
764fn addDirectoryWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
765 return addDirectoryWatchInputFromPath(step, .{
766 .root_dir = builder.build_root,
767 .sub_path = sub_path,
768 });
769}
770
771fn addWatchInputFromPath(step: *Step, path: Cache.Path, basename: []const u8) !void {
772 const gpa = step.owner.allocator;
773 const gop = try step.inputs.table.getOrPut(gpa, path);
774 if (!gop.found_existing) gop.value_ptr.* = .empty;
775 try gop.value_ptr.append(gpa, basename);
776}
777
778pub fn allocPrintCmd(
779 gpa: Allocator,
780 cwd: std.process.Child.Cwd,
781 opt_env: ?struct {
782 child: *const std.process.Environ.Map,
783 parent: *const std.process.Environ.Map,
784 },
785 argv: []const []const u8,
786) Allocator.Error![]u8 {
787 const shell = struct {
788 fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
789 for (string) |c| {
790 if (switch (c) {
791 else => true,
792 '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
793 '=' => is_argv0,
794 }) break;
795 } else return writer.writeAll(string);
796
797 try writer.writeByte('"');
798 for (string) |c| {
799 if (switch (c) {
800 std.ascii.control_code.nul => break,
801 '!', '"', '$', '\\', '`' => true,
802 else => !std.ascii.isPrint(c),
803 }) try writer.writeByte('\\');
804 switch (c) {
805 std.ascii.control_code.nul => unreachable,
806 std.ascii.control_code.bel => try writer.writeByte('a'),
807 std.ascii.control_code.bs => try writer.writeByte('b'),
808 std.ascii.control_code.ht => try writer.writeByte('t'),
809 std.ascii.control_code.lf => try writer.writeByte('n'),
810 std.ascii.control_code.vt => try writer.writeByte('v'),
811 std.ascii.control_code.ff => try writer.writeByte('f'),
812 std.ascii.control_code.cr => try writer.writeByte('r'),
813 std.ascii.control_code.esc => try writer.writeByte('E'),
814 ' '...'~' => try writer.writeByte(c),
815 else => try writer.print("{o:0>3}", .{c}),
816 }
817 }
818 try writer.writeByte('"');
819 }
820 };
821
822 var aw: Io.Writer.Allocating = .init(gpa);
823 defer aw.deinit();
824 const writer = &aw.writer;
825 switch (cwd) {
826 .inherit => {},
827 .path => |path| writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory,
828 .dir => @panic("TODO"),
829 }
830 if (opt_env) |env| {
831 var it = env.child.iterator();
832 while (it.next()) |entry| {
833 const key = entry.key_ptr.*;
834 const value = entry.value_ptr.*;
835 if (env.parent.get(key)) |process_value| {
836 if (std.mem.eql(u8, value, process_value)) continue;
837 }
838 writer.print("{s}=", .{key}) catch return error.OutOfMemory;
839 shell.escape(writer, value, false) catch return error.OutOfMemory;
840 writer.writeByte(' ') catch return error.OutOfMemory;
841 }
842 }
843 shell.escape(writer, argv[0], true) catch return error.OutOfMemory;
844 for (argv[1..]) |arg| {
845 writer.writeByte(' ') catch return error.OutOfMemory;
846 shell.escape(writer, arg, false) catch return error.OutOfMemory;
847 }
848 return aw.toOwnedSlice();
849}
850
lib/compiler/maker/Step/Compile.zig created+1074
......@@ -0,0 +1,1074 @@
1/// Populated during the make phase when there is a long-lived compiler process.
2/// Managed by the build runner, not user build script.
3zig_process: ?*Step.ZigProcess,
4
5fn make(step: *Step, options: Step.MakeOptions) !void {
6 const b = step.owner;
7 const compile: *Compile = @fieldParentPtr("step", step);
8
9 const zig_args = try getZigArgs(compile, false);
10
11 const maybe_output_dir = step.evalZigProcess(
12 zig_args,
13 options.progress_node,
14 (b.graph.incremental == true) and (options.watch or options.web_server != null),
15 options.web_server,
16 options.gpa,
17 ) catch |err| switch (err) {
18 error.NeedCompileErrorCheck => {
19 assert(compile.expect_errors != null);
20 try checkCompileErrors(compile);
21 return;
22 },
23 else => |e| return e,
24 };
25
26 // Update generated files
27 if (maybe_output_dir) |output_dir| {
28 if (compile.emit_directory) |lp| {
29 lp.path = b.fmt("{f}", .{output_dir});
30 }
31
32 // zig fmt: off
33 if (compile.generated_bin) |lp| lp.path = compile.outputPath(output_dir, .bin);
34 if (compile.generated_pdb) |lp| lp.path = compile.outputPath(output_dir, .pdb);
35 // hack for stage2_x86_64 + coff
36 if (compile.generated_compiler_rt_dyn_lib) |lp| lp.path = compile.outputPath(output_dir, .compiler_rt_dyn_lib);
37 if (compile.generated_implib) |lp| lp.path = compile.outputPath(output_dir, .implib);
38 if (compile.generated_h) |lp| lp.path = compile.outputPath(output_dir, .h);
39 if (compile.generated_docs) |lp| lp.path = compile.outputPath(output_dir, .docs);
40 if (compile.generated_asm) |lp| lp.path = compile.outputPath(output_dir, .@"asm");
41 if (compile.generated_llvm_ir) |lp| lp.path = compile.outputPath(output_dir, .llvm_ir);
42 if (compile.generated_llvm_bc) |lp| lp.path = compile.outputPath(output_dir, .llvm_bc);
43 // zig fmt: on
44 }
45
46 if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic and
47 compile.version != null and compile.generated_bin != null and
48 std.Build.wantSharedLibSymLinks(compile.rootModuleTarget()))
49 {
50 try doAtomicSymLinks(
51 step,
52 compile.getEmittedBin().getPath2(b, step),
53 compile.major_only_filename.?,
54 compile.name_only_filename.?,
55 );
56 }
57}
58
59fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
60 const step = &compile.step;
61 const b = step.owner;
62 const arena = b.allocator;
63
64 var zig_args = std.array_list.Managed([]const u8).init(arena);
65 defer zig_args.deinit();
66
67 try zig_args.append(b.graph.zig_exe);
68
69 const cmd = switch (compile.kind) {
70 .lib => "build-lib",
71 .exe => "build-exe",
72 .obj => "build-obj",
73 .@"test" => "test",
74 .test_obj => "test-obj",
75 };
76 try zig_args.append(cmd);
77
78 if (b.reference_trace) |some| {
79 try zig_args.append(try std.fmt.allocPrint(arena, "-freference-trace={d}", .{some}));
80 }
81 try addFlag(&zig_args, "allow-so-scripts", compile.allow_so_scripts orelse b.graph.allow_so_scripts);
82
83 try addFlag(&zig_args, "llvm", compile.use_llvm);
84 try addFlag(&zig_args, "lld", compile.use_lld);
85 try addFlag(&zig_args, "new-linker", compile.use_new_linker);
86
87 if (compile.root_module.resolved_target.?.query.ofmt) |ofmt| {
88 try zig_args.append(try std.fmt.allocPrint(arena, "-ofmt={s}", .{@tagName(ofmt)}));
89 }
90
91 switch (compile.entry) {
92 .default => {},
93 .disabled => try zig_args.append("-fno-entry"),
94 .enabled => try zig_args.append("-fentry"),
95 .symbol_name => |entry_name| {
96 try zig_args.append(try std.fmt.allocPrint(arena, "-fentry={s}", .{entry_name}));
97 },
98 }
99
100 {
101 var symbol_it = compile.force_undefined_symbols.keyIterator();
102 while (symbol_it.next()) |symbol_name| {
103 try zig_args.append("--force_undefined");
104 try zig_args.append(symbol_name.*);
105 }
106 }
107
108 if (compile.stack_size) |stack_size| {
109 try zig_args.append("--stack");
110 try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size}));
111 }
112
113 if (fuzz) {
114 try zig_args.append("-ffuzz");
115 }
116
117 {
118 // Stores system libraries that have already been seen for at least one
119 // module, along with any arguments that need to be passed to the
120 // compiler for each module individually.
121 var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty;
122 var frameworks: std.StringArrayHashMapUnmanaged(Module.LinkFrameworkOptions) = .empty;
123
124 var prev_has_cflags = false;
125 var prev_has_rcflags = false;
126 var prev_search_strategy: Module.SystemLib.SearchStrategy = .paths_first;
127 var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic;
128 // Track the number of positional arguments so that a nice error can be
129 // emitted if there is nothing to link.
130 var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null);
131
132 // Fully recursive iteration including dynamic libraries to detect
133 // libc and libc++ linkage.
134 for (compile.getCompileDependencies(true)) |some_compile| {
135 for (some_compile.root_module.getGraph().modules) |mod| {
136 if (mod.link_libc == true) compile.is_linking_libc = true;
137 if (mod.link_libcpp == true) compile.is_linking_libcpp = true;
138 }
139 }
140
141 var cli_named_modules = try CliNamedModules.init(arena, compile.root_module);
142
143 // For this loop, don't chase dynamic libraries because their link
144 // objects are already linked.
145 for (compile.getCompileDependencies(false)) |dep_compile| {
146 for (dep_compile.root_module.getGraph().modules) |mod| {
147 // While walking transitive dependencies, if a given link object is
148 // already included in a library, it should not redundantly be
149 // placed on the linker line of the dependee.
150 const my_responsibility = dep_compile == compile;
151 const already_linked = !my_responsibility and dep_compile.isDynamicLibrary();
152
153 // Inherit dependencies on darwin frameworks.
154 if (!already_linked) {
155 for (mod.frameworks.keys(), mod.frameworks.values()) |name, info| {
156 try frameworks.put(arena, name, info);
157 }
158 }
159
160 // Inherit dependencies on system libraries and static libraries.
161 for (mod.link_objects.items) |link_object| {
162 switch (link_object) {
163 .static_path => |static_path| {
164 if (my_responsibility) {
165 try zig_args.append(static_path.getPath2(mod.owner, step));
166 total_linker_objects += 1;
167 }
168 },
169 .system_lib => |system_lib| {
170 const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
171 if (system_lib_gop.found_existing) {
172 try zig_args.appendSlice(system_lib_gop.value_ptr.*);
173 continue;
174 } else {
175 system_lib_gop.value_ptr.* = &.{};
176 }
177
178 if (already_linked)
179 continue;
180
181 if ((system_lib.search_strategy != prev_search_strategy or
182 system_lib.preferred_link_mode != prev_preferred_link_mode) and
183 compile.linkage != .static)
184 {
185 switch (system_lib.search_strategy) {
186 .no_fallback => switch (system_lib.preferred_link_mode) {
187 .dynamic => try zig_args.append("-search_dylibs_only"),
188 .static => try zig_args.append("-search_static_only"),
189 },
190 .paths_first => switch (system_lib.preferred_link_mode) {
191 .dynamic => try zig_args.append("-search_paths_first"),
192 .static => try zig_args.append("-search_paths_first_static"),
193 },
194 .mode_first => switch (system_lib.preferred_link_mode) {
195 .dynamic => try zig_args.append("-search_dylibs_first"),
196 .static => try zig_args.append("-search_static_first"),
197 },
198 }
199 prev_search_strategy = system_lib.search_strategy;
200 prev_preferred_link_mode = system_lib.preferred_link_mode;
201 }
202
203 const prefix: []const u8 = prefix: {
204 if (system_lib.needed) break :prefix "-needed-l";
205 if (system_lib.weak) break :prefix "-weak-l";
206 break :prefix "-l";
207 };
208 switch (system_lib.use_pkg_config) {
209 .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })),
210 .yes, .force => {
211 if (compile.runPkgConfig(system_lib.name)) |result| {
212 try zig_args.appendSlice(result.cflags);
213 try zig_args.appendSlice(result.libs);
214 try seen_system_libs.put(arena, system_lib.name, result.cflags);
215 } else |err| switch (err) {
216 error.PkgConfigInvalidOutput,
217 error.PkgConfigCrashed,
218 error.PkgConfigFailed,
219 error.PkgConfigNotInstalled,
220 error.PackageNotFound,
221 => switch (system_lib.use_pkg_config) {
222 .yes => {
223 // pkg-config failed, so fall back to linking the library
224 // by name directly.
225 try zig_args.append(b.fmt("{s}{s}", .{
226 prefix,
227 system_lib.name,
228 }));
229 },
230 .force => {
231 panic("pkg-config failed for library {s}", .{system_lib.name});
232 },
233 .no => unreachable,
234 },
235
236 else => |e| return e,
237 }
238 },
239 }
240 },
241 .other_step => |other| {
242 switch (other.kind) {
243 .exe => return step.fail("cannot link with an executable build artifact", .{}),
244 .@"test" => return step.fail("cannot link with a test", .{}),
245 .obj, .test_obj => {
246 const included_in_lib_or_obj = !my_responsibility and
247 (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj);
248 if (!already_linked and !included_in_lib_or_obj) {
249 try zig_args.append(other.getEmittedBin().getPath2(b, step));
250 total_linker_objects += 1;
251 }
252 },
253 .lib => l: {
254 const other_produces_implib = other.producesImplib();
255 const other_is_static = other_produces_implib or other.isStaticLibrary();
256
257 if (compile.isStaticLibrary() and other_is_static) {
258 // Avoid putting a static library inside a static library.
259 break :l;
260 }
261
262 // For DLLs, we must link against the implib.
263 // For everything else, we directly link
264 // against the library file.
265 const full_path_lib = if (other_produces_implib)
266 try other.getGeneratedFilePath("generated_implib", &compile.step)
267 else
268 try other.getGeneratedFilePath("generated_bin", &compile.step);
269
270 try zig_args.append(full_path_lib);
271 total_linker_objects += 1;
272
273 if (other.linkage == .dynamic and
274 compile.rootModuleTarget().os.tag != .windows)
275 {
276 if (fs.path.dirname(full_path_lib)) |dirname| {
277 try zig_args.append("-rpath");
278 try zig_args.append(dirname);
279 }
280 }
281 },
282 }
283 },
284 .assembly_file => |asm_file| l: {
285 if (!my_responsibility) break :l;
286
287 if (prev_has_cflags) {
288 try zig_args.append("-cflags");
289 try zig_args.append("--");
290 prev_has_cflags = false;
291 }
292 try zig_args.append(asm_file.getPath2(mod.owner, step));
293 total_linker_objects += 1;
294 },
295
296 .c_source_file => |c_source_file| l: {
297 if (!my_responsibility) break :l;
298
299 if (prev_has_cflags or c_source_file.flags.len != 0) {
300 try zig_args.append("-cflags");
301 for (c_source_file.flags) |arg| {
302 try zig_args.append(arg);
303 }
304 try zig_args.append("--");
305 }
306 prev_has_cflags = (c_source_file.flags.len != 0);
307
308 if (c_source_file.language) |lang| {
309 try zig_args.append("-x");
310 try zig_args.append(lang.internalIdentifier());
311 }
312
313 try zig_args.append(c_source_file.file.getPath2(mod.owner, step));
314
315 if (c_source_file.language != null) {
316 try zig_args.append("-x");
317 try zig_args.append("none");
318 }
319 total_linker_objects += 1;
320 },
321
322 .c_source_files => |c_source_files| l: {
323 if (!my_responsibility) break :l;
324
325 if (prev_has_cflags or c_source_files.flags.len != 0) {
326 try zig_args.append("-cflags");
327 for (c_source_files.flags) |arg| {
328 try zig_args.append(arg);
329 }
330 try zig_args.append("--");
331 }
332 prev_has_cflags = (c_source_files.flags.len != 0);
333
334 if (c_source_files.language) |lang| {
335 try zig_args.append("-x");
336 try zig_args.append(lang.internalIdentifier());
337 }
338
339 const root_path = c_source_files.root.getPath2(mod.owner, step);
340 for (c_source_files.files) |file| {
341 try zig_args.append(b.pathJoin(&.{ root_path, file }));
342 }
343
344 if (c_source_files.language != null) {
345 try zig_args.append("-x");
346 try zig_args.append("none");
347 }
348
349 total_linker_objects += c_source_files.files.len;
350 },
351
352 .win32_resource_file => |rc_source_file| l: {
353 if (!my_responsibility) break :l;
354
355 if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) {
356 if (prev_has_rcflags) {
357 try zig_args.append("-rcflags");
358 try zig_args.append("--");
359 prev_has_rcflags = false;
360 }
361 } else {
362 try zig_args.append("-rcflags");
363 for (rc_source_file.flags) |arg| {
364 try zig_args.append(arg);
365 }
366 for (rc_source_file.include_paths) |include_path| {
367 try zig_args.append("/I");
368 try zig_args.append(include_path.getPath2(mod.owner, step));
369 }
370 try zig_args.append("--");
371 prev_has_rcflags = true;
372 }
373 try zig_args.append(rc_source_file.file.getPath2(mod.owner, step));
374 total_linker_objects += 1;
375 },
376 }
377 }
378
379 // We need to emit the --mod argument here so that the above link objects
380 // have the correct parent module, but only if the module is part of
381 // this compilation.
382 if (!my_responsibility) continue;
383 if (cli_named_modules.modules.getIndex(mod)) |module_cli_index| {
384 const module_cli_name = cli_named_modules.names.keys()[module_cli_index];
385 try mod.appendZigProcessFlags(&zig_args, step);
386
387 // --dep arguments
388 try zig_args.ensureUnusedCapacity(mod.import_table.count() * 2);
389 for (mod.import_table.keys(), mod.import_table.values()) |name, import| {
390 const import_index = cli_named_modules.modules.getIndex(import).?;
391 const import_cli_name = cli_named_modules.names.keys()[import_index];
392 zig_args.appendAssumeCapacity("--dep");
393 if (std.mem.eql(u8, import_cli_name, name)) {
394 zig_args.appendAssumeCapacity(import_cli_name);
395 } else {
396 zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name }));
397 }
398 }
399
400 // When the CLI sees a -M argument, it determines whether it
401 // implies the existence of a Zig compilation unit based on
402 // whether there is a root source file. If there is no root
403 // source file, then this is not a zig compilation unit - it is
404 // perhaps a set of linker objects, or C source files instead.
405 // Linker objects are added to the CLI globally, while C source
406 // files must have a module parent.
407 if (mod.root_source_file) |lp| {
408 const src = lp.getPath2(mod.owner, step);
409 try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src }));
410 } else if (moduleNeedsCliArg(mod)) {
411 try zig_args.append(b.fmt("-M{s}", .{module_cli_name}));
412 }
413 }
414 }
415 }
416
417 if (total_linker_objects == 0) {
418 return step.fail("the linker needs one or more objects to link", .{});
419 }
420
421 for (frameworks.keys(), frameworks.values()) |name, info| {
422 if (info.needed) {
423 try zig_args.append("-needed_framework");
424 } else if (info.weak) {
425 try zig_args.append("-weak_framework");
426 } else {
427 try zig_args.append("-framework");
428 }
429 try zig_args.append(name);
430 }
431
432 if (compile.is_linking_libcpp) {
433 try zig_args.append("-lc++");
434 }
435
436 if (compile.is_linking_libc) {
437 try zig_args.append("-lc");
438 }
439 }
440
441 if (compile.win32_manifest) |manifest_file| {
442 try zig_args.append(manifest_file.getPath2(b, step));
443 }
444
445 if (compile.win32_module_definition) |module_file| {
446 try zig_args.append(module_file.getPath2(b, step));
447 }
448
449 if (compile.image_base) |image_base| {
450 try zig_args.append("--image-base");
451 try zig_args.append(b.fmt("0x{x}", .{image_base}));
452 }
453
454 for (compile.filters) |filter| {
455 try zig_args.append("--test-filter");
456 try zig_args.append(filter);
457 }
458
459 if (compile.test_runner) |test_runner| {
460 try zig_args.append("--test-runner");
461 try zig_args.append(test_runner.path.getPath2(b, step));
462 }
463
464 for (b.debug_log_scopes) |log_scope| {
465 try zig_args.append("--debug-log");
466 try zig_args.append(log_scope);
467 }
468
469 if (b.debug_compile_errors) {
470 try zig_args.append("--debug-compile-errors");
471 }
472
473 if (b.debug_incremental) {
474 try zig_args.append("--debug-incremental");
475 }
476
477 if (b.verbose_air) try zig_args.append("--verbose-air");
478 if (b.verbose_llvm_ir) |path| try zig_args.append(b.fmt("--verbose-llvm-ir={s}", .{path}));
479 if (b.verbose_llvm_bc) |path| try zig_args.append(b.fmt("--verbose-llvm-bc={s}", .{path}));
480 if (b.verbose_link or compile.verbose_link) try zig_args.append("--verbose-link");
481 if (b.verbose_cc or compile.verbose_cc) try zig_args.append("--verbose-cc");
482 if (b.verbose_llvm_cpu_features) try zig_args.append("--verbose-llvm-cpu-features");
483 if (b.graph.time_report) try zig_args.append("--time-report");
484
485 if (compile.generated_asm != null) try zig_args.append("-femit-asm");
486 if (compile.generated_bin == null) try zig_args.append("-fno-emit-bin");
487 if (compile.generated_docs != null) try zig_args.append("-femit-docs");
488 if (compile.generated_implib != null) try zig_args.append("-femit-implib");
489 if (compile.generated_llvm_bc != null) try zig_args.append("-femit-llvm-bc");
490 if (compile.generated_llvm_ir != null) try zig_args.append("-femit-llvm-ir");
491 if (compile.generated_h != null) try zig_args.append("-femit-h");
492
493 try addFlag(&zig_args, "formatted-panics", compile.formatted_panics);
494
495 switch (compile.compress_debug_sections) {
496 .none => {},
497 .zlib => try zig_args.append("--compress-debug-sections=zlib"),
498 .zstd => try zig_args.append("--compress-debug-sections=zstd"),
499 }
500
501 if (compile.link_eh_frame_hdr) {
502 try zig_args.append("--eh-frame-hdr");
503 }
504 if (compile.link_emit_relocs) {
505 try zig_args.append("--emit-relocs");
506 }
507 if (compile.link_function_sections) {
508 try zig_args.append("-ffunction-sections");
509 }
510 if (compile.link_data_sections) {
511 try zig_args.append("-fdata-sections");
512 }
513 if (compile.link_gc_sections) |x| {
514 try zig_args.append(if (x) "--gc-sections" else "--no-gc-sections");
515 }
516 if (!compile.linker_dynamicbase) {
517 try zig_args.append("--no-dynamicbase");
518 }
519 if (compile.linker_allow_shlib_undefined) |x| {
520 try zig_args.append(if (x) "-fallow-shlib-undefined" else "-fno-allow-shlib-undefined");
521 }
522 if (compile.link_z_notext) {
523 try zig_args.append("-z");
524 try zig_args.append("notext");
525 }
526 if (!compile.link_z_relro) {
527 try zig_args.append("-z");
528 try zig_args.append("norelro");
529 }
530 if (compile.link_z_lazy) {
531 try zig_args.append("-z");
532 try zig_args.append("lazy");
533 }
534 if (compile.link_z_common_page_size) |size| {
535 try zig_args.append("-z");
536 try zig_args.append(b.fmt("common-page-size={d}", .{size}));
537 }
538 if (compile.link_z_max_page_size) |size| {
539 try zig_args.append("-z");
540 try zig_args.append(b.fmt("max-page-size={d}", .{size}));
541 }
542 if (compile.link_z_defs) {
543 try zig_args.append("-z");
544 try zig_args.append("defs");
545 }
546
547 if (compile.libc_file) |libc_file| {
548 try zig_args.append("--libc");
549 try zig_args.append(libc_file.getPath2(b, step));
550 } else if (b.libc_file) |libc_file| {
551 try zig_args.append("--libc");
552 try zig_args.append(libc_file);
553 }
554
555 try zig_args.append("--cache-dir");
556 try zig_args.append(b.cache_root.path orelse ".");
557
558 try zig_args.append("--global-cache-dir");
559 try zig_args.append(b.graph.global_cache_root.path orelse ".");
560
561 if (b.graph.debug_compiler_runtime_libs) |mode|
562 try zig_args.append(b.fmt("--debug-rt={t}", .{mode}));
563
564 try zig_args.append("--name");
565 try zig_args.append(compile.name);
566
567 if (compile.linkage) |some| switch (some) {
568 .dynamic => try zig_args.append("-dynamic"),
569 .static => try zig_args.append("-static"),
570 };
571 if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) {
572 if (compile.version) |version| {
573 try zig_args.append("--version");
574 try zig_args.append(b.fmt("{f}", .{version}));
575 }
576
577 if (compile.rootModuleTarget().os.tag.isDarwin()) {
578 const install_name = compile.install_name orelse b.fmt("@rpath/{s}{s}{s}", .{
579 compile.rootModuleTarget().libPrefix(),
580 compile.name,
581 compile.rootModuleTarget().dynamicLibSuffix(),
582 });
583 try zig_args.append("-install_name");
584 try zig_args.append(install_name);
585 }
586 }
587
588 if (compile.entitlements) |entitlements| {
589 try zig_args.appendSlice(&[_][]const u8{ "--entitlements", entitlements });
590 }
591 if (compile.pagezero_size) |pagezero_size| {
592 const size = try std.fmt.allocPrint(arena, "{x}", .{pagezero_size});
593 try zig_args.appendSlice(&[_][]const u8{ "-pagezero_size", size });
594 }
595 if (compile.headerpad_size) |headerpad_size| {
596 const size = try std.fmt.allocPrint(arena, "{x}", .{headerpad_size});
597 try zig_args.appendSlice(&[_][]const u8{ "-headerpad", size });
598 }
599 if (compile.headerpad_max_install_names) {
600 try zig_args.append("-headerpad_max_install_names");
601 }
602 if (compile.dead_strip_dylibs) {
603 try zig_args.append("-dead_strip_dylibs");
604 }
605 if (compile.force_load_objc) {
606 try zig_args.append("-ObjC");
607 }
608 if (compile.discard_local_symbols) {
609 try zig_args.append("--discard-all");
610 }
611
612 try addFlag(&zig_args, "compiler-rt", compile.bundle_compiler_rt);
613 try addFlag(&zig_args, "ubsan-rt", compile.bundle_ubsan_rt);
614 try addFlag(&zig_args, "dll-export-fns", compile.dll_export_fns);
615 if (compile.rdynamic) {
616 try zig_args.append("-rdynamic");
617 }
618 if (compile.import_memory) {
619 try zig_args.append("--import-memory");
620 }
621 if (compile.export_memory) {
622 try zig_args.append("--export-memory");
623 }
624 if (compile.import_symbols) {
625 try zig_args.append("--import-symbols");
626 }
627 if (compile.import_table) {
628 try zig_args.append("--import-table");
629 }
630 if (compile.export_table) {
631 try zig_args.append("--export-table");
632 }
633 if (compile.initial_memory) |initial_memory| {
634 try zig_args.append(b.fmt("--initial-memory={d}", .{initial_memory}));
635 }
636 if (compile.max_memory) |max_memory| {
637 try zig_args.append(b.fmt("--max-memory={d}", .{max_memory}));
638 }
639 if (compile.shared_memory) {
640 try zig_args.append("--shared-memory");
641 }
642 if (compile.global_base) |global_base| {
643 try zig_args.append(b.fmt("--global-base={d}", .{global_base}));
644 }
645
646 if (compile.wasi_exec_model) |model| {
647 try zig_args.append(b.fmt("-mexec-model={s}", .{@tagName(model)}));
648 }
649 if (compile.linker_script) |linker_script| {
650 try zig_args.append("--script");
651 try zig_args.append(linker_script.getPath2(b, step));
652 }
653
654 if (compile.version_script) |version_script| {
655 try zig_args.append("--version-script");
656 try zig_args.append(version_script.getPath2(b, step));
657 }
658 if (compile.linker_allow_undefined_version) |x| {
659 try zig_args.append(if (x) "--undefined-version" else "--no-undefined-version");
660 }
661
662 if (compile.linker_enable_new_dtags) |enabled| {
663 try zig_args.append(if (enabled) "--enable-new-dtags" else "--disable-new-dtags");
664 }
665
666 if (compile.kind == .@"test") {
667 if (compile.exec_cmd_args) |exec_cmd_args| {
668 for (exec_cmd_args) |cmd_arg| {
669 if (cmd_arg) |arg| {
670 try zig_args.append("--test-cmd");
671 try zig_args.append(arg);
672 } else {
673 try zig_args.append("--test-cmd-bin");
674 }
675 }
676 }
677 }
678
679 if (b.sysroot) |sysroot| {
680 try zig_args.appendSlice(&[_][]const u8{ "--sysroot", sysroot });
681 }
682
683 // -I and -L arguments that appear after the last --mod argument apply to all modules.
684 const cwd: Io.Dir = .cwd();
685 const io = b.graph.io;
686
687 for (b.search_prefixes.items) |search_prefix| {
688 var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| {
689 return step.fail("unable to open prefix directory '{s}': {s}", .{
690 search_prefix, @errorName(err),
691 });
692 };
693 defer prefix_dir.close(io);
694
695 // Avoid passing -L and -I flags for nonexistent directories.
696 // This prevents a warning, that should probably be upgraded to an error in Zig's
697 // CLI parsing code, when the linker sees an -L directory that does not exist.
698
699 if (prefix_dir.access(io, "lib", .{})) |_| {
700 try zig_args.appendSlice(&.{
701 "-L", b.pathJoin(&.{ search_prefix, "lib" }),
702 });
703 } else |err| switch (err) {
704 error.FileNotFound => {},
705 else => |e| return step.fail("unable to access '{s}/lib' directory: {s}", .{
706 search_prefix, @errorName(e),
707 }),
708 }
709
710 if (prefix_dir.access(io, "include", .{})) |_| {
711 try zig_args.appendSlice(&.{
712 "-I", b.pathJoin(&.{ search_prefix, "include" }),
713 });
714 } else |err| switch (err) {
715 error.FileNotFound => {},
716 else => |e| return step.fail("unable to access '{s}/include' directory: {s}", .{
717 search_prefix, @errorName(e),
718 }),
719 }
720 }
721
722 if (compile.rc_includes != .any) {
723 try zig_args.append("-rcincludes");
724 try zig_args.append(@tagName(compile.rc_includes));
725 }
726
727 try addFlag(&zig_args, "each-lib-rpath", compile.each_lib_rpath);
728
729 if (compile.build_id orelse b.build_id) |build_id| {
730 try zig_args.append(switch (build_id) {
731 .hexstring => |hs| b.fmt("--build-id=0x{x}", .{hs.toSlice()}),
732 .none, .fast, .uuid, .sha1, .md5 => b.fmt("--build-id={s}", .{@tagName(build_id)}),
733 });
734 }
735
736 const opt_zig_lib_dir = if (compile.zig_lib_dir) |dir|
737 dir.getPath2(b, step)
738 else if (b.graph.zig_lib_directory.path) |_|
739 b.fmt("{f}", .{b.graph.zig_lib_directory})
740 else
741 null;
742
743 if (opt_zig_lib_dir) |zig_lib_dir| {
744 try zig_args.append("--zig-lib-dir");
745 try zig_args.append(zig_lib_dir);
746 }
747
748 try addFlag(&zig_args, "PIE", compile.pie);
749
750 if (compile.lto) |lto| {
751 try zig_args.append(switch (lto) {
752 .full => "-flto=full",
753 .thin => "-flto=thin",
754 .none => "-fno-lto",
755 });
756 }
757
758 try addFlag(&zig_args, "sanitize-coverage-trace-pc-guard", compile.sanitize_coverage_trace_pc_guard);
759
760 if (compile.subsystem) |subsystem| {
761 try zig_args.append("--subsystem");
762 try zig_args.append(@tagName(subsystem));
763 }
764
765 if (compile.mingw_unicode_entry_point) {
766 try zig_args.append("-municode");
767 }
768
769 if (compile.error_limit) |err_limit| try zig_args.appendSlice(&.{
770 "--error-limit", b.fmt("{d}", .{err_limit}),
771 });
772
773 try addFlag(&zig_args, "incremental", b.graph.incremental);
774
775 try zig_args.append("--listen=-");
776
777 // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux
778 // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and
779 // pass that to zig, e.g. via 'zig build-lib @args.rsp'
780 // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html
781 var args_length: usize = 0;
782 for (zig_args.items) |arg| {
783 args_length += arg.len + 1; // +1 to account for null terminator
784 }
785 if (args_length >= 30 * 1024) {
786 try b.cache_root.handle.createDirPath(io, "args");
787
788 const args_to_escape = zig_args.items[2..];
789 var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len);
790 arg_blk: for (args_to_escape) |arg| {
791 for (arg, 0..) |c, arg_idx| {
792 if (c == '\\' or c == '"') {
793 // Slow path for arguments that need to be escaped. We'll need to allocate and copy
794 var escaped: std.ArrayList(u8) = .empty;
795 try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1);
796 try escaped.appendSlice(arena, arg[0..arg_idx]);
797 for (arg[arg_idx..]) |to_escape| {
798 if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\');
799 try escaped.append(arena, to_escape);
800 }
801 escaped_args.appendAssumeCapacity(escaped.items);
802 continue :arg_blk;
803 }
804 }
805 escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument
806 }
807
808 // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with
809 // other zig build commands running in parallel.
810 const partially_quoted = try std.mem.join(arena, "\" \"", escaped_args.items);
811 const args = try std.mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" });
812
813 var args_hash: [Sha256.digest_length]u8 = undefined;
814 Sha256.hash(args, &args_hash, .{});
815 var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined;
816 _ = try std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash});
817
818 const args_file = "args" ++ fs.path.sep_str ++ args_hex_hash;
819 if (b.cache_root.handle.access(io, args_file, .{})) |_| {
820 // The args file is already present from a previous run.
821 } else |err| switch (err) {
822 error.FileNotFound => {
823 var af = b.cache_root.handle.createFileAtomic(io, args_file, .{
824 .replace = false,
825 .make_path = true,
826 }) catch |e| return step.fail("failed creating tmp args file {f}{s}: {t}", .{
827 b.cache_root, args_file, e,
828 });
829 defer af.deinit(io);
830
831 af.file.writeStreamingAll(io, args) catch |e| {
832 return step.fail("failed writing args data to tmp file {f}{s}: {t}", .{
833 b.cache_root, args_file, e,
834 });
835 };
836 // Note we can't clean up this file, not even after build
837 // success, because that might interfere with another build
838 // process that needs the same file.
839 af.link(io) catch |e| switch (e) {
840 error.PathAlreadyExists => {
841 // The args file was created by another concurrent build process.
842 },
843 else => |other_err| return step.fail("failed linking tmp file {f}{s}: {t}", .{
844 b.cache_root, args_file, other_err,
845 }),
846 };
847 },
848 else => |other_err| return other_err,
849 }
850
851 const resolved_args_file = try mem.concat(arena, u8, &.{
852 "@",
853 try b.cache_root.join(arena, &.{args_file}),
854 });
855
856 zig_args.shrinkRetainingCapacity(2);
857 try zig_args.append(resolved_args_file);
858 }
859
860 return try zig_args.toOwnedSlice();
861}
862
863pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progress.Node) !Path {
864 c.step.result_error_msgs.clearRetainingCapacity();
865 c.step.result_stderr = "";
866
867 c.step.result_error_bundle.deinit(gpa);
868 c.step.result_error_bundle = std.zig.ErrorBundle.empty;
869
870 if (c.step.result_failed_command) |cmd| {
871 gpa.free(cmd);
872 c.step.result_failed_command = null;
873 }
874
875 const zig_args = try getZigArgs(c, true);
876 const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa);
877 return maybe_output_bin_path.?;
878}
879
880pub fn doAtomicSymLinks(
881 step: *Step,
882 output_path: []const u8,
883 filename_major_only: []const u8,
884 filename_name_only: []const u8,
885) !void {
886 const b = step.owner;
887 const io = b.graph.io;
888 const out_dir = fs.path.dirname(output_path) orelse ".";
889 const out_basename = fs.path.basename(output_path);
890 // sym link for libfoo.so.1 to libfoo.so.1.2.3
891 const major_only_path = b.pathJoin(&.{ out_dir, filename_major_only });
892 const cwd: Io.Dir = .cwd();
893 cwd.symLinkAtomic(io, out_basename, major_only_path, .{}) catch |err| {
894 return step.fail("unable to symlink {s} -> {s}: {s}", .{
895 major_only_path, out_basename, @errorName(err),
896 });
897 };
898 // sym link for libfoo.so to libfoo.so.1
899 const name_only_path = b.pathJoin(&.{ out_dir, filename_name_only });
900 cwd.symLinkAtomic(io, filename_major_only, name_only_path, .{}) catch |err| {
901 return step.fail("Unable to symlink {s} -> {s}: {s}", .{
902 name_only_path, filename_major_only, @errorName(err),
903 });
904 };
905}
906
907fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
908 const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
909 const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore);
910 var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator);
911 errdefer list.deinit();
912 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
913 while (line_it.next()) |line| {
914 if (mem.trim(u8, line, " \t").len == 0) continue;
915 var tok_it = mem.tokenizeAny(u8, line, " \t");
916 try list.append(PkgConfigPkg{
917 .name = tok_it.next() orelse return error.PkgConfigInvalidOutput,
918 .desc = tok_it.rest(),
919 });
920 }
921 return list.toOwnedSlice();
922}
923
924fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
925 if (b.pkg_config_pkg_list) |res| {
926 return res;
927 }
928 var code: u8 = undefined;
929 if (execPkgConfigList(b, &code)) |list| {
930 b.pkg_config_pkg_list = list;
931 return list;
932 } else |err| {
933 const result = switch (err) {
934 error.ProcessTerminated => error.PkgConfigCrashed,
935 error.ExecNotSupported => error.PkgConfigFailed,
936 error.ExitCodeFailure => error.PkgConfigFailed,
937 error.FileNotFound => error.PkgConfigNotInstalled,
938 error.InvalidName => error.PkgConfigNotInstalled,
939 error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
940 else => return err,
941 };
942 b.pkg_config_pkg_list = result;
943 return result;
944 }
945}
946
947fn addFlag(args: *std.array_list.Managed([]const u8), comptime name: []const u8, opt: ?bool) !void {
948 const cond = opt orelse return;
949 try args.ensureUnusedCapacity(1);
950 if (cond) {
951 args.appendAssumeCapacity("-f" ++ name);
952 } else {
953 args.appendAssumeCapacity("-fno-" ++ name);
954 }
955}
956
957const PkgConfigResult = struct {
958 cflags: []const []const u8,
959 libs: []const []const u8,
960};
961
962/// Run pkg-config for the given library name and parse the output, returning the arguments
963/// that should be passed to zig to link the given library.
964fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult {
965 const wl_rpath_prefix = "-Wl,-rpath,";
966
967 const b = compile.step.owner;
968 const arena = b.allocator;
969 const pkg_name = match: {
970 // First we have to map the library name to pkg config name. Unfortunately,
971 // there are several examples where this is not straightforward:
972 // -lSDL2 -> pkg-config sdl2
973 // -lgdk-3 -> pkg-config gdk-3.0
974 // -latk-1.0 -> pkg-config atk
975 // -lpulse -> pkg-config libpulse
976 const pkgs = try getPkgConfigList(b);
977
978 // Exact match means instant winner.
979 for (pkgs) |pkg| {
980 if (mem.eql(u8, pkg.name, lib_name)) {
981 break :match pkg.name;
982 }
983 }
984
985 // Next we'll try ignoring case.
986 for (pkgs) |pkg| {
987 if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
988 break :match pkg.name;
989 }
990 }
991
992 // Prefixed "lib" or suffixed ".0".
993 for (pkgs) |pkg| {
994 if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
995 const prefix = pkg.name[0..pos];
996 const suffix = pkg.name[pos + lib_name.len ..];
997 if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
998 if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
999 break :match pkg.name;
1000 }
1001 }
1002
1003 // Trimming "-1.0".
1004 if (mem.endsWith(u8, lib_name, "-1.0")) {
1005 const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
1006 for (pkgs) |pkg| {
1007 if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
1008 break :match pkg.name;
1009 }
1010 }
1011 }
1012
1013 return error.PackageNotFound;
1014 };
1015
1016 var code: u8 = undefined;
1017 const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
1018 const stdout = if (b.runAllowFail(&[_][]const u8{
1019 pkg_config_exe,
1020 pkg_name,
1021 "--cflags",
1022 "--libs",
1023 }, &code, .ignore)) |stdout| stdout else |err| switch (err) {
1024 error.ProcessTerminated => return error.PkgConfigCrashed,
1025 error.ExecNotSupported => return error.PkgConfigFailed,
1026 error.ExitCodeFailure => return error.PkgConfigFailed,
1027 error.FileNotFound => return error.PkgConfigNotInstalled,
1028 else => return err,
1029 };
1030
1031 var zig_cflags: std.ArrayList([]const u8) = .empty;
1032 defer zig_cflags.deinit(arena);
1033 var zig_libs: std.ArrayList([]const u8) = .empty;
1034 defer zig_libs.deinit(arena);
1035
1036 var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
1037 while (arg_it.next()) |arg| {
1038 if (mem.eql(u8, arg, "-I")) {
1039 const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1040 try zig_cflags.appendSlice(arena, &.{ "-I", dir });
1041 } else if (mem.startsWith(u8, arg, "-I")) {
1042 try zig_cflags.append(arena, arg);
1043 } else if (mem.eql(u8, arg, "-L")) {
1044 const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1045 try zig_libs.appendSlice(arena, &.{ "-L", dir });
1046 } else if (mem.startsWith(u8, arg, "-L")) {
1047 try zig_libs.append(arena, arg);
1048 } else if (mem.eql(u8, arg, "-l")) {
1049 const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1050 try zig_libs.appendSlice(arena, &.{ "-l", lib });
1051 } else if (mem.startsWith(u8, arg, "-l")) {
1052 try zig_libs.append(arena, arg);
1053 } else if (mem.eql(u8, arg, "-D")) {
1054 const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput;
1055 try zig_cflags.appendSlice(arena, &.{ "-D", macro });
1056 } else if (mem.startsWith(u8, arg, "-D")) {
1057 try zig_cflags.append(arena, arg);
1058 } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
1059 try zig_cflags.appendSlice(arena, &.{ "-rpath", arg[wl_rpath_prefix.len..] });
1060 } else if (b.debug_pkg_config) {
1061 return compile.step.fail("unknown pkg-config flag '{s}'", .{arg});
1062 }
1063 }
1064
1065 try zig_cflags.shrinkToLen(arena);
1066 try zig_libs.shrinkToLen(arena);
1067
1068 return .{
1069 .cflags = zig_cflags.toOwnedSliceAssert(),
1070 .libs = zig_libs.toOwnedSliceAssert(),
1071 };
1072}
1073
1074
lib/compiler/maker/Step/InstallArtifact.zig created+96
......@@ -0,0 +1,96 @@
1
2fn make(step: *Step, options: Step.MakeOptions) !void {
3 _ = options;
4 const install_artifact: *InstallArtifact = @fieldParentPtr("step", step);
5 const b = step.owner;
6 const io = b.graph.io;
7
8 var all_cached = true;
9
10 if (install_artifact.dest_dir) |dest_dir| {
11 const full_dest_path = b.getInstallPath(dest_dir, install_artifact.dest_sub_path);
12 const p = try step.installFile(install_artifact.emitted_bin.?, full_dest_path);
13 all_cached = all_cached and p == .fresh;
14
15 if (install_artifact.dylib_symlinks) |dls| {
16 try Step.Compile.doAtomicSymLinks(step, full_dest_path, dls.major_only_filename, dls.name_only_filename);
17 }
18
19 install_artifact.artifact.installed_path = full_dest_path;
20 }
21
22 if (install_artifact.compiler_rt_dyn_lib_dir) |compiler_rt_dir| {
23 const full_compiler_rt_path = b.getInstallPath(compiler_rt_dir, install_artifact.emitted_compiler_rt_dyn_lib.?.basename(b, step));
24 const p = try step.installFile(install_artifact.emitted_compiler_rt_dyn_lib.?, full_compiler_rt_path);
25 all_cached = all_cached and p == .fresh;
26 }
27
28 if (install_artifact.implib_dir) |implib_dir| {
29 const full_implib_path = b.getInstallPath(implib_dir, install_artifact.emitted_implib.?.basename(b, step));
30 const p = try step.installFile(install_artifact.emitted_implib.?, full_implib_path);
31 all_cached = all_cached and p == .fresh;
32 }
33
34 if (install_artifact.pdb_dir) |pdb_dir| {
35 const full_pdb_path = b.getInstallPath(pdb_dir, install_artifact.emitted_pdb.?.basename(b, step));
36 const p = try step.installFile(install_artifact.emitted_pdb.?, full_pdb_path);
37 all_cached = all_cached and p == .fresh;
38 }
39
40 if (install_artifact.h_dir) |h_dir| {
41 if (install_artifact.emitted_h) |emitted_h| {
42 const full_h_path = b.getInstallPath(h_dir, emitted_h.basename(b, step));
43 const p = try step.installFile(emitted_h, full_h_path);
44 all_cached = all_cached and p == .fresh;
45 }
46
47 for (install_artifact.artifact.installed_headers.items) |installation| switch (installation) {
48 .file => |file| {
49 const full_h_path = b.getInstallPath(h_dir, file.dest_rel_path);
50 const p = try step.installFile(file.source, full_h_path);
51 all_cached = all_cached and p == .fresh;
52 },
53 .directory => |dir| {
54 const src_dir_path = dir.source.getPath3(b, step);
55 const full_h_prefix = b.getInstallPath(h_dir, dir.dest_rel_path);
56
57 var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
58 return step.fail("unable to open source directory '{f}': {s}", .{
59 src_dir_path, @errorName(err),
60 });
61 };
62 defer src_dir.close(io);
63
64 var it = try src_dir.walk(b.allocator);
65 next_entry: while (try it.next(io)) |entry| {
66 for (dir.options.exclude_extensions) |ext| {
67 if (std.mem.endsWith(u8, entry.path, ext)) continue :next_entry;
68 }
69 if (dir.options.include_extensions) |incs| {
70 for (incs) |inc| {
71 if (std.mem.endsWith(u8, entry.path, inc)) break;
72 } else {
73 continue :next_entry;
74 }
75 }
76
77 const full_dest_path = b.pathJoin(&.{ full_h_prefix, entry.path });
78 switch (entry.kind) {
79 .directory => {
80 try Step.handleVerbose(b, .inherit, &.{ "install", "-d", full_dest_path });
81 const p = try step.installDir(full_dest_path);
82 all_cached = all_cached and p == .existed;
83 },
84 .file => {
85 const p = try step.installFile(try dir.source.join(b.allocator, entry.path), full_dest_path);
86 all_cached = all_cached and p == .fresh;
87 },
88 else => continue,
89 }
90 }
91 },
92 };
93 }
94
95 step.result_cached = all_cached;
96}
lib/compiler/maker/Step/Run.zig created+2127
......@@ -0,0 +1,2127 @@
1const Run = @This();
2
3const builtin = @import("builtin");
4
5const std = @import("std");
6const Io = std.Io;
7const Dir = std.Io.Dir;
8const mem = std.mem;
9const process = std.process;
10const EnvMap = std.process.Environ.Map;
11const assert = std.debug.assert;
12const Cache = std.Build.Cache;
13const Path = std.Build.Cache.Path;
14
15const Step = @import("../Step.zig");
16
17/// If this is a Zig unit test binary, this tracks the names of the unit
18/// tests that are also fuzz tests. Indexes cannot be used as they may
19/// change between reruns.
20fuzz_tests: std.ArrayList([]const u8),
21cached_test_metadata: ?CachedTestMetadata = null,
22
23
24fn make(step: *Step, options: Step.MakeOptions) !void {
25 const b = step.owner;
26 const io = b.graph.io;
27 const arena = b.allocator;
28 const run: *Run = @fieldParentPtr("step", step);
29 const has_side_effects = run.hasSideEffects();
30
31 var argv_list = std.array_list.Managed([]const u8).init(arena);
32 var output_placeholders = std.array_list.Managed(IndexedOutput).init(arena);
33
34 var man = b.graph.cache.obtain();
35 defer man.deinit();
36
37 if (run.environ_map) |environ_map| {
38 for (environ_map.keys(), environ_map.values()) |key, value| {
39 man.hash.addBytes(key);
40 man.hash.addBytes(value);
41 }
42 }
43
44 man.hash.add(run.color);
45 man.hash.add(run.disable_zig_progress);
46
47 for (run.argv.items) |arg| {
48 switch (arg) {
49 .bytes => |bytes| {
50 try argv_list.append(bytes);
51 man.hash.addBytes(bytes);
52 },
53 .lazy_path => |file| {
54 const file_path = file.lazy_path.getPath3(b, step);
55 try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
56 man.hash.addBytes(file.prefix);
57 _ = try man.addFilePath(file_path, null);
58 },
59 .decorated_directory => |dd| {
60 const file_path = dd.lazy_path.getPath3(b, step);
61 const resolved_arg = b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix });
62 try argv_list.append(resolved_arg);
63 man.hash.addBytes(resolved_arg);
64 },
65 .file_content => |file_plp| {
66 const file_path = file_plp.lazy_path.getPath3(b, step);
67
68 var result: std.Io.Writer.Allocating = .init(arena);
69 errdefer result.deinit();
70 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
71
72 const file = file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}) catch |err| {
73 return step.fail(
74 "unable to open input file '{f}': {t}",
75 .{ file_path, err },
76 );
77 };
78 defer file.close(io);
79
80 var buf: [1024]u8 = undefined;
81 var file_reader = file.reader(io, &buf);
82 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
83 error.ReadFailed => return step.fail(
84 "failed to read from '{f}': {t}",
85 .{ file_path, file_reader.err.? },
86 ),
87 error.WriteFailed => return error.OutOfMemory,
88 };
89
90 try argv_list.append(result.written());
91 man.hash.addBytes(file_plp.prefix);
92 _ = try man.addFilePath(file_path, null);
93 },
94 .artifact => |pa| {
95 const artifact = pa.artifact;
96
97 if (artifact.rootModuleTarget().os.tag == .windows) {
98 // On Windows we don't have rpaths so we have to add .dll search paths to PATH
99 run.addPathForDynLibs(artifact);
100 }
101 const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?;
102
103 try argv_list.append(b.fmt("{s}{s}", .{
104 pa.prefix,
105 run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
106 }));
107
108 _ = try man.addFile(file_path, null);
109 },
110 .output_file, .output_directory => |output| {
111 man.hash.addBytes(output.prefix);
112 man.hash.addBytes(output.basename);
113 // Add a placeholder into the argument list because we need the
114 // manifest hash to be updated with all arguments before the
115 // object directory is computed.
116 try output_placeholders.append(.{
117 .index = argv_list.items.len,
118 .tag = arg,
119 .output = output,
120 });
121 _ = try argv_list.addOne();
122 },
123 }
124 }
125
126 switch (run.stdin) {
127 .bytes => |bytes| {
128 man.hash.addBytes(bytes);
129 },
130 .lazy_path => |lazy_path| {
131 const file_path = lazy_path.getPath2(b, step);
132 _ = try man.addFile(file_path, null);
133 },
134 .none => {},
135 }
136
137 if (run.captured_stdout) |captured| {
138 man.hash.addBytes(captured.output.basename);
139 man.hash.add(captured.trim_whitespace);
140 }
141
142 if (run.captured_stderr) |captured| {
143 man.hash.addBytes(captured.output.basename);
144 man.hash.add(captured.trim_whitespace);
145 }
146
147 hashStdIo(&man.hash, run.stdio);
148
149 for (run.file_inputs.items) |lazy_path| {
150 _ = try man.addFile(lazy_path.getPath2(b, step), null);
151 }
152
153 if (run.cwd) |cwd| {
154 const cwd_path = cwd.getPath3(b, step);
155 _ = man.hash.addBytes(try cwd_path.toString(arena));
156 }
157
158 if (!has_side_effects and try step.cacheHitAndWatch(&man)) {
159 // cache hit, skip running command
160 const digest = man.final();
161
162 try populateGeneratedPaths(
163 arena,
164 output_placeholders.items,
165 run.captured_stdout,
166 run.captured_stderr,
167 b.cache_root,
168 &digest,
169 );
170
171 step.result_cached = true;
172 return;
173 }
174
175 const dep_output_file = run.dep_output_file orelse {
176 // We already know the final output paths, use them directly.
177 const digest = if (has_side_effects)
178 man.hash.final()
179 else
180 man.final();
181
182 try populateGeneratedPaths(
183 arena,
184 output_placeholders.items,
185 run.captured_stdout,
186 run.captured_stderr,
187 b.cache_root,
188 &digest,
189 );
190
191 const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest;
192 for (output_placeholders.items) |placeholder| {
193 const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename });
194 const output_sub_dir_path = switch (placeholder.tag) {
195 .output_file => Dir.path.dirname(output_sub_path).?,
196 .output_directory => output_sub_path,
197 else => unreachable,
198 };
199 b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| {
200 return step.fail("unable to make path '{f}{s}': {s}", .{
201 b.cache_root, output_sub_dir_path, @errorName(err),
202 });
203 };
204 const arg_output_path = run.convertPathArg(.{
205 .root_dir = .cwd(),
206 .sub_path = placeholder.output.generated_file.getPath(),
207 });
208 argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0)
209 arg_output_path
210 else
211 b.fmt("{s}{s}", .{ placeholder.output.prefix, arg_output_path });
212 }
213
214 try runCommand(run, argv_list.items, has_side_effects, output_dir_path, options, null);
215 if (!has_side_effects) try step.writeManifestAndWatch(&man);
216 return;
217 };
218
219 // We do not know the final output paths yet, use temp paths to run the command.
220 var rand_int: u64 = undefined;
221 io.random(@ptrCast(&rand_int));
222 const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
223
224 for (output_placeholders.items) |placeholder| {
225 const output_components = .{ tmp_dir_path, placeholder.output.basename };
226 const output_sub_path = b.pathJoin(&output_components);
227 const output_sub_dir_path = switch (placeholder.tag) {
228 .output_file => Dir.path.dirname(output_sub_path).?,
229 .output_directory => output_sub_path,
230 else => unreachable,
231 };
232 b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| {
233 return step.fail("unable to make path '{f}{s}': {s}", .{
234 b.cache_root, output_sub_dir_path, @errorName(err),
235 });
236 };
237 const raw_output_path: Cache.Path = .{
238 .root_dir = b.cache_root,
239 .sub_path = b.pathJoin(&output_components),
240 };
241 placeholder.output.generated_file.path = raw_output_path.toString(b.graph.arena) catch @panic("OOM");
242 argv_list.items[placeholder.index] = b.fmt("{s}{s}", .{
243 placeholder.output.prefix,
244 run.convertPathArg(raw_output_path),
245 });
246 }
247
248 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, options, null);
249
250 const dep_file_dir = Dir.cwd();
251 const dep_file_basename = dep_output_file.generated_file.getPath2(b, step);
252 if (has_side_effects)
253 try man.addDepFile(dep_file_dir, dep_file_basename)
254 else
255 try man.addDepFilePost(dep_file_dir, dep_file_basename);
256
257 const digest = if (has_side_effects)
258 man.hash.final()
259 else
260 man.final();
261
262 const any_output = output_placeholders.items.len > 0 or
263 run.captured_stdout != null or run.captured_stderr != null;
264
265 // Rename into place
266 if (any_output) {
267 const o_sub_path = "o" ++ Dir.path.sep_str ++ &digest;
268
269 b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |err| switch (err) {
270 Dir.RenameError.DirNotEmpty => {
271 b.cache_root.handle.deleteTree(io, o_sub_path) catch |del_err| {
272 return step.fail("unable to remove dir '{f}'{s}: {t}", .{
273 b.cache_root, tmp_dir_path, del_err,
274 });
275 };
276 b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |retry_err| {
277 return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{
278 b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, retry_err,
279 });
280 };
281 },
282 else => return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{
283 b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, err,
284 }),
285 };
286 }
287
288 if (!has_side_effects) try step.writeManifestAndWatch(&man);
289
290 try populateGeneratedPaths(
291 arena,
292 output_placeholders.items,
293 run.captured_stdout,
294 run.captured_stderr,
295 b.cache_root,
296 &digest,
297 );
298}
299
300/// Reads stdout of a Zig test process until a termination condition is reached:
301/// * A write fails, indicating the child unexpectedly closed stdin
302/// * A test (or a response from the test runner) times out
303/// * The wait fails, indicating the child closed stdout and stderr
304fn waitZigTest(
305 run: *Run,
306 child: *process.Child,
307 options: Step.MakeOptions,
308 multi_reader: *Io.File.MultiReader,
309 opt_metadata: *?TestMetadata,
310 results: *Step.TestResults,
311) !union(enum) {
312 write_failed: anyerror,
313 no_poll: struct {
314 active_test_index: ?u32,
315 ns_elapsed: u64,
316 },
317 timeout: struct {
318 active_test_index: ?u32,
319 ns_elapsed: u64,
320 },
321} {
322 const gpa = run.step.owner.allocator;
323 const arena = run.step.owner.allocator;
324 const io = run.step.owner.graph.io;
325
326 var sub_prog_node: ?std.Progress.Node = null;
327 defer if (sub_prog_node) |n| n.end();
328
329 if (opt_metadata.*) |*md| {
330 // Previous unit test process died or was killed; we're continuing where it left off
331 requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
332 } else {
333 // Running unit tests normally
334 run.fuzz_tests.clearRetainingCapacity();
335 sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err };
336 }
337
338 var active_test_index: ?u32 = null;
339
340 var last_update: Io.Clock.Timestamp = .now(io, .awake);
341
342 // This timeout is used when we're waiting on the test runner itself rather than a user-specified
343 // test. For instance, if the test runner leaves this much time between us requesting a test to
344 // start and it acknowledging the test starting, we terminate the child and raise an error. This
345 // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
346 const response_timeout: Io.Clock.Duration = t: {
347 if (fuzz_context != null) break :t null; // don't timeout fuzz tests
348 const ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
349 break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) };
350 };
351 const test_timeout: ?Io.Clock.Duration = if (options.unit_test_timeout_ns) |ns| .{
352 .clock = .awake,
353 .raw = .fromNanoseconds(ns),
354 } else null;
355
356 const stdout = multi_reader.reader(0);
357 const stderr = multi_reader.reader(1);
358 const Header = std.zig.Server.Message.Header;
359
360 while (true) {
361 const timeout: Io.Timeout = t: {
362 const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
363 const duration = opt_duration orelse break :t .none;
364 break :t .{ .deadline = last_update.addDuration(duration) };
365 };
366
367 // This block is exited when `stdout` contains enough bytes for a `Header`.
368 header_ready: {
369 if (stdout.buffered().len >= @sizeOf(Header)) {
370 // We already have one, no need to poll!
371 break :header_ready;
372 }
373
374 multi_reader.fill(64, timeout) catch |err| switch (err) {
375 error.Timeout => return .{ .timeout = .{
376 .active_test_index = active_test_index,
377 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
378 } },
379 error.EndOfStream => return .{ .no_poll = .{
380 .active_test_index = active_test_index,
381 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
382 } },
383 else => |e| return e,
384 };
385
386 continue;
387 }
388 // There is definitely a header available now -- read it.
389 const header = stdout.takeStruct(Header, .little) catch unreachable;
390
391 while (stdout.buffered().len < header.bytes_len) {
392 multi_reader.fill(64, timeout) catch |err| switch (err) {
393 error.Timeout => return .{ .timeout = .{
394 .active_test_index = active_test_index,
395 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
396 } },
397 error.EndOfStream => return .{ .no_poll = .{
398 .active_test_index = active_test_index,
399 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
400 } },
401 else => |e| return e,
402 };
403 }
404
405 const body = stdout.take(header.bytes_len) catch unreachable;
406 var body_r: std.Io.Reader = .fixed(body);
407 switch (header.tag) {
408 .zig_version => {
409 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return run.step.fail(
410 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
411 .{ builtin.zig_version_string, body },
412 );
413 },
414 .test_metadata => {
415 // `metadata` would only be populated if we'd already seen a `test_metadata`, but we
416 // only request it once (and importantly, we don't re-request it if we kill and
417 // restart the test runner).
418 assert(opt_metadata.* == null);
419
420 const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable;
421 results.test_count = tm_hdr.tests_len;
422
423 const names = try arena.alloc(u32, results.test_count);
424 for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
425
426 const expected_panic_msgs = try arena.alloc(u32, results.test_count);
427 for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
428
429 const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable;
430
431 options.progress_node.setEstimatedTotalItems(names.len);
432 opt_metadata.* = .{
433 .string_bytes = try arena.dupe(u8, string_bytes),
434 .ns_per_test = try arena.alloc(u64, results.test_count),
435 .names = names,
436 .expected_panic_msgs = expected_panic_msgs,
437 .next_index = 0,
438 .prog_node = options.progress_node,
439 };
440 @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
441
442 active_test_index = null;
443 last_update = .now(io, .awake);
444
445 requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
446 },
447 .test_started => {
448 active_test_index = opt_metadata.*.?.next_index - 1;
449 last_update = .now(io, .awake);
450 },
451 .test_results => {
452 const md = &opt_metadata.*.?;
453
454 const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable;
455 assert(tr_hdr.index == active_test_index);
456
457 switch (tr_hdr.flags.status) {
458 .pass => {},
459 .skip => results.skip_count +|= 1,
460 .fail => results.fail_count +|= 1,
461 }
462 const leak_count = tr_hdr.flags.leak_count;
463 const log_err_count = tr_hdr.flags.log_err_count;
464 results.leak_count +|= leak_count;
465 results.log_err_count +|= log_err_count;
466
467 if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
468
469 if (tr_hdr.flags.status == .fail) {
470 const name = md.testName(tr_hdr.index);
471 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
472 stderr.tossBuffered();
473 if (stderr_bytes.len == 0) {
474 try run.step.addError("'{s}' failed without output", .{name});
475 } else {
476 try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes });
477 }
478 } else if (leak_count > 0) {
479 const name = md.testName(tr_hdr.index);
480 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
481 stderr.tossBuffered();
482 try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
483 } else if (log_err_count > 0) {
484 const name = md.testName(tr_hdr.index);
485 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
486 stderr.tossBuffered();
487 try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
488 }
489
490 active_test_index = null;
491
492 const now: Io.Clock.Timestamp = .now(io, .awake);
493 md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
494 last_update = now;
495
496 requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
497 },
498 else => {}, // ignore other messages
499 }
500 }
501}
502
503const FuzzTestRunner = struct {
504 run: *Run,
505 ctx: FuzzContext,
506 coverage_id: ?u64,
507
508 instances: []Instance,
509 /// The indexes of this are layed out such that it is effectively an array
510 /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr.
511 batch: Io.Batch,
512 /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter.
513 pending_broadcasts: std.ArrayList(u8),
514 broadcast: std.ArrayList(u8),
515 broadcast_undelivered: u32,
516
517 const Instance = struct {
518 child: process.Child,
519 message: std.ArrayListAligned(u8, .@"4"),
520 broadcast_written: usize,
521 stderr: std.ArrayList(u8),
522 stdin_vec: [1][]u8,
523 stdout_vec: [1][]u8,
524 stderr_vec: [1][]u8,
525 progress_node: std.Progress.Node,
526
527 fn messageHeader(instance: *Instance) InHeader {
528 assert(instance.message.items.len >= @sizeOf(InHeader));
529 const header_ptr: *InHeader = @ptrCast(instance.message.items);
530 var header = header_ptr.*;
531 if (std.builtin.Endian.native != .little) {
532 std.mem.byteSwapAllFields(InHeader, &header);
533 }
534 return header;
535 }
536 };
537
538 const PendingBroadcastFooter = struct {
539 from_id: u32,
540 body_len: u32,
541 };
542
543 const InHeader = std.zig.Server.Message.Header;
544 const OutHeader = std.zig.Client.Message.Header;
545
546 const stdin_i = 0;
547 const stdout_i = 1;
548 const stderr_i = 2;
549
550 fn init(
551 run: *Run,
552 ctx: FuzzContext,
553 progress_node: std.Progress.Node,
554 spawn_options: process.SpawnOptions,
555 ) !FuzzTestRunner {
556 const step_owner = run.step.owner;
557 const gpa = step_owner.allocator;
558 const io = step_owner.graph.io;
559
560 const n_instances = switch (ctx.fuzz.mode) {
561 .forever => step_owner.graph.max_jobs orelse @min(
562 std.Thread.getCpuCount() catch 1,
563 (std.math.maxInt(u32) - 2) / 3,
564 ),
565 .limit => 1,
566 };
567 const instances = try gpa.alloc(Instance, n_instances);
568 errdefer gpa.free(instances);
569 const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3);
570 errdefer gpa.free(batch_storage);
571
572 @memset(instances, .{
573 .child = undefined,
574 .message = .empty,
575 .broadcast_written = undefined,
576 .stderr = .empty,
577 .stdin_vec = undefined,
578 .stdout_vec = undefined,
579 .stderr_vec = undefined,
580 .progress_node = undefined,
581 });
582 for (0.., instances) |id, *instance| {
583 errdefer for (instances[0..id]) |*spawned| {
584 spawned.child.kill(io);
585 spawned.progress_node.end();
586 };
587 instance.child = try process.spawn(io, spawn_options);
588 instance.progress_node = progress_node.start("starting fuzzer", 0);
589 }
590
591 return .{
592 .run = run,
593 .ctx = ctx,
594 .coverage_id = null,
595
596 .instances = instances,
597 .batch = .init(batch_storage),
598 .pending_broadcasts = .empty,
599 .broadcast = .empty,
600 .broadcast_undelivered = 0,
601 };
602 }
603
604 fn deinit(f: *FuzzTestRunner) void {
605 const step_owner = f.run.step.owner;
606 const gpa = step_owner.allocator;
607 const io = step_owner.graph.io;
608
609 f.batch.cancel(io);
610 gpa.free(f.batch.storage);
611 var total_rss: usize = 0;
612 for (f.instances) |*instance| {
613 instance.child.kill(io);
614 instance.message.deinit(gpa);
615 instance.stderr.deinit(gpa);
616 instance.progress_node.end();
617 total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0;
618 }
619 f.run.step.result_peak_rss = @max(f.run.step.result_peak_rss, total_rss);
620 gpa.free(f.instances);
621 }
622
623 fn startInstances(f: *FuzzTestRunner) !void {
624 const step_owner = f.run.step.owner;
625 const io = step_owner.graph.io;
626
627 for (0.., f.instances) |id, *instance| {
628 const id32: u32 = @intCast(id);
629 (switch (f.ctx.fuzz.mode) {
630 .forever => sendRunFuzzTestMessage(
631 io,
632 instance.child.stdin.?,
633 f.run.fuzz_tests.items,
634 .forever,
635 id32,
636 ),
637 .limit => |limit| sendRunFuzzTestMessage(
638 io,
639 instance.child.stdin.?,
640 f.run.fuzz_tests.items,
641 .iterations,
642 limit.amount,
643 ),
644 }) catch |write_err| {
645 // The runner unexpectedly closed stdin, which means it crashed during initialization.
646 // Clean up everything and wait for the child to exit.
647 instance.child.stdin.?.close(io);
648 instance.child.stdin = null;
649 const term = try instance.child.wait(io);
650 return f.run.step.fail(
651 "unable to write stdin ({t}); test process unexpectedly {f}",
652 .{ write_err, fmtTerm(term) },
653 );
654 };
655
656 try f.addStdoutRead(id32, @sizeOf(InHeader));
657 try f.addStderrRead(id32);
658 }
659 }
660
661 fn listen(f: *FuzzTestRunner) !void {
662 const step_owner = f.run.step.owner;
663 const io = step_owner.graph.io;
664
665 while (true) {
666 try f.batch.awaitConcurrent(io, .none);
667 while (f.batch.next()) |completion| {
668 const id = completion.index / 3;
669 const result = completion.result;
670 switch (completion.index % 3) {
671 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) {
672 // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
673 // that all stderr is collected.
674 error.BrokenPipe => continue,
675 else => |write_e| return write_e,
676 }),
677 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) {
678 // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
679 // that all stderr is collected.
680 error.EndOfStream => continue,
681 else => |read_e| return read_e,
682 }),
683 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) {
684 error.EndOfStream => return f.instanceEos(id),
685 else => |read_e| return read_e,
686 }),
687 else => unreachable,
688 }
689 }
690 }
691 }
692
693 fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
694 const step_owner = f.run.step.owner;
695 const gpa = step_owner.allocator;
696 const io = step_owner.graph.io;
697 const instance = &f.instances[id];
698
699 instance.message.items.len += n;
700 const total_read = instance.message.items.len;
701 if (total_read < @sizeOf(InHeader)) {
702 try f.addStdoutRead(id, @sizeOf(InHeader));
703 return;
704 }
705
706 const header = instance.messageHeader();
707 const body = instance.message.items[@sizeOf(InHeader)..];
708 if (body.len != header.bytes_len) {
709 try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len);
710 return;
711 }
712
713 switch (header.tag) {
714 .zig_version => {
715 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return f.run.step.fail(
716 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
717 .{ builtin.zig_version_string, body },
718 );
719 },
720 .coverage_id => {
721 var body_r: Io.Reader = .fixed(body);
722 f.coverage_id = body_r.takeInt(u64, .little) catch unreachable;
723 const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable;
724 const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable;
725 const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable;
726
727 const fuzz = f.ctx.fuzz;
728 fuzz.queue_mutex.lockUncancelable(io);
729 defer fuzz.queue_mutex.unlock(io);
730 try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{
731 .id = f.coverage_id.?,
732 .cumulative = .{
733 .runs = cumulative_runs,
734 .unique = cumulative_unique,
735 .coverage = cumulative_coverage,
736 },
737 .run = f.run,
738 } });
739 fuzz.queue_cond.signal(io);
740 },
741 .fuzz_start_addr => {
742 var body_r: Io.Reader = .fixed(body);
743 const fuzz = f.ctx.fuzz;
744 const addr = body_r.takeInt(u64, .little) catch unreachable;
745
746 fuzz.queue_mutex.lockUncancelable(io);
747 defer fuzz.queue_mutex.unlock(io);
748 try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{
749 .addr = addr,
750 .coverage_id = f.coverage_id.?,
751 } });
752 fuzz.queue_cond.signal(io);
753 },
754 .fuzz_test_change => {
755 const test_i = std.mem.readInt(u32, body[0..4], .little);
756 instance.progress_node.setName(f.run.fuzz_tests.items[test_i]);
757 },
758 .broadcast_fuzz_input => {
759 if (f.instances.len == 1) {
760 // No other processes to broadcast to.
761 } else if (f.broadcast_undelivered == 0) {
762 try f.instanceBroadcast(id, body);
763 } else {
764 const footer: PendingBroadcastFooter = .{
765 .from_id = id,
766 .body_len = @intCast(body.len),
767 };
768 // There is another broadcast in progress so add this one to the queue.
769 const size = @sizeOf(PendingBroadcastFooter) + body.len;
770 try f.pending_broadcasts.ensureUnusedCapacity(gpa, size);
771 f.pending_broadcasts.appendSliceAssumeCapacity(body);
772 f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer));
773 }
774 },
775 else => {}, // ignore other messages
776 }
777
778 instance.message.clearRetainingCapacity();
779 try f.addStdoutRead(id, @sizeOf(InHeader));
780 }
781
782 fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
783 const instance = &f.instances[id];
784 instance.stderr.items.len += n;
785 try f.addStderrRead(id);
786 }
787
788 fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void {
789 const instance = &f.instances[id];
790
791 instance.broadcast_written += n;
792 if (instance.broadcast_written == f.broadcast.items.len) {
793 f.broadcast_undelivered -= 1;
794 if (f.broadcast_undelivered == 0) {
795 try f.broadcastComplete();
796 }
797 } else {
798 f.addStdinWrite(id);
799 }
800 }
801
802 fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void {
803 const step_owner = f.run.step.owner;
804 const gpa = step_owner.allocator;
805 const instance = &f.instances[id];
806
807 try instance.message.ensureTotalCapacity(gpa, end);
808 const start = instance.message.items.len;
809 instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]};
810 f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{
811 .file = instance.child.stdout.?,
812 .data = &instance.stdout_vec,
813 } });
814 }
815
816 fn addStderrRead(f: *FuzzTestRunner, id: u32) !void {
817 const step_owner = f.run.step.owner;
818 const gpa = step_owner.allocator;
819 const instance = &f.instances[id];
820
821 try instance.stderr.ensureUnusedCapacity(gpa, 1);
822 instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()};
823 f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{
824 .file = instance.child.stderr.?,
825 .data = &instance.stderr_vec,
826 } });
827 }
828
829 fn addStdinWrite(f: *FuzzTestRunner, id: u32) void {
830 const instance = &f.instances[id];
831
832 assert(f.broadcast.items.len != instance.broadcast_written);
833 instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]};
834 f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{
835 .file = instance.child.stdin.?,
836 .data = &instance.stdin_vec,
837 } });
838 }
839
840 fn instanceEos(f: *FuzzTestRunner, id: u32) !void {
841 const step_owner = f.run.step.owner;
842 const io = step_owner.graph.io;
843 const instance = &f.instances[id];
844
845 instance.child.stdin.?.close(io);
846 instance.child.stdin = null;
847 const term = try instance.child.wait(io);
848 if (!termMatches(.{ .exited = 0 }, term)) {
849 f.run.step.result_stderr = try f.mergedStderr();
850 try f.saveCrash(id, term);
851 return f.run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)});
852 }
853 }
854
855 fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void {
856 const step = &f.run.step;
857 const b = step.owner;
858 const io = b.graph.io;
859
860 if (f.coverage_id == null) return;
861
862 // Search for the input file corresponding to the instance
863 const InputHeader = Build.abi.fuzz.MmapInputHeader;
864 var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined;
865 var in_r: Io.File.Reader = undefined;
866 var in_f: Io.File = undefined;
867 var in_name_buf: [12]u8 = undefined;
868 var in_name: []const u8 = undefined;
869 var i: u32 = 0;
870 const header: InputHeader = while (true) : ({
871 if (i == std.math.maxInt(u32)) return;
872 i += 1;
873 }) {
874 const name_prefix = "f" ++ Io.Dir.path.sep_str ++ "in";
875 in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable;
876 in_f = b.cache_root.handle.openFile(io, in_name, .{
877 .lock = .exclusive,
878 .lock_nonblocking = true,
879 }) catch |e| switch (e) {
880 error.FileNotFound => return,
881 error.WouldBlock => continue, // Can not be from
882 // the crashed instance since it is still locked.
883 else => return step.fail("failed to open file '{f}{s}': {t}", .{
884 b.cache_root, in_name, e,
885 }),
886 };
887
888 in_r = in_f.readerStreaming(io, &in_r_buf);
889 const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| {
890 in_f.close(io);
891 switch (e) {
892 error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{
893 b.cache_root, in_name, in_r.err.?,
894 }),
895 error.EndOfStream => continue,
896 }
897 };
898
899 if (header.pc_digest == f.coverage_id.? and
900 header.instance_id == id and
901 header.test_i < f.run.fuzz_tests.items.len)
902 {
903 break header;
904 }
905
906 in_f.close(io);
907 };
908 defer in_f.close(io);
909
910 // Save it to a seperate file
911 const crash_name = "f" ++ Io.Dir.path.sep_str ++ "crash";
912 const out = b.cache_root.handle.createFile(io, crash_name, .{
913 .lock = .exclusive, // Multiple run steps could have found a crash at the same time
914 }) catch |e| return step.fail("failed to create file '{f}{s}': {t}", .{
915 b.cache_root, crash_name, e,
916 });
917 defer out.close(io);
918
919 var out_w_buf: [512]u8 = undefined;
920 var out_w = out.writerStreaming(io, &out_w_buf);
921 _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) {
922 error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{
923 b.cache_root, in_name, in_r.err.?,
924 }),
925 error.WriteFailed => return step.fail("failed to write file '{f}{s}': {t}", .{
926 b.cache_root, crash_name, out_w.err.?,
927 }),
928 };
929
930 return f.run.step.fail("test '{s}' {f}; input saved to '{f}{s}'", .{
931 f.run.fuzz_tests.items[header.test_i],
932 fmtTerm(term),
933 b.cache_root,
934 crash_name,
935 });
936 }
937
938 fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void {
939 assert(f.instances.len > 1);
940 assert(f.broadcast_undelivered == 0); // no other broadcast is progress
941 assert(f.broadcast.items.len == 0);
942 assert(from_id < f.instances.len);
943
944 const step_owner = f.run.step.owner;
945 const gpa = step_owner.allocator;
946
947 var out_header: OutHeader = .{
948 .tag = .new_fuzz_input,
949 .bytes_len = @intCast(bytes.len),
950 };
951 if (std.builtin.Endian.native != .little) {
952 std.mem.byteSwapAllFields(OutHeader, &out_header);
953 }
954 try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len);
955 f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header));
956 f.broadcast.appendSliceAssumeCapacity(bytes);
957
958 f.broadcast_undelivered = @intCast(f.instances.len - 1);
959 for (0.., f.instances) |to_id, *instance| {
960 if (to_id == from_id) continue;
961 instance.broadcast_written = 0;
962 f.addStdinWrite(@intCast(to_id));
963 }
964 }
965
966 fn broadcastComplete(f: *FuzzTestRunner) !void {
967 assert(f.instances.len > 1);
968 assert(f.broadcast_undelivered == 0);
969 f.broadcast.clearRetainingCapacity();
970
971 const pending = &f.pending_broadcasts;
972 if (pending.items.len != 0) {
973 // Another broadcast is pending; copy it over to `broadcast`
974
975 const footer_len = @sizeOf(PendingBroadcastFooter);
976 const footer_bytes = pending.items[pending.items.len - footer_len ..];
977 const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes);
978 pending.items.len -= footer_len;
979
980 const body = pending.items[pending.items.len - footer.body_len ..];
981 try f.instanceBroadcast(footer.from_id, body);
982 pending.items.len -= body.len;
983 }
984 }
985
986 fn mergedStderr(f: *FuzzTestRunner) std.mem.Allocator.Error![]const u8 {
987 const step_owner = f.run.step.owner;
988 const arena = step_owner.allocator;
989
990 // Collect any available stderr
991 while (f.batch.next()) |completion| {
992 if (completion.index % 3 != 2) continue;
993 const len = completion.result.file_read_streaming catch continue;
994 f.instances[completion.index / 3].stderr.items.len += len;
995 }
996
997 var stderr_len: usize = 0;
998 for (f.instances) |*instance| stderr_len += instance.stderr.items.len;
999 const stderr = try arena.alloc(u8, stderr_len);
1000
1001 stderr_len = 0;
1002 for (f.instances) |*instance| {
1003 @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items);
1004 stderr_len += instance.stderr.items.len;
1005 }
1006 return stderr;
1007 }
1008};
1009
1010fn evalFuzzTest(
1011 run: *Run,
1012 spawn_options: process.SpawnOptions,
1013 options: Step.MakeOptions,
1014 fuzz_context: FuzzContext,
1015) !void {
1016 var f: FuzzTestRunner = try .init(run, fuzz_context, options.progress_node, spawn_options);
1017 defer f.deinit();
1018 try f.startInstances();
1019 try f.listen();
1020}
1021
1022const StdioPollEnum = enum { stdout, stderr };
1023
1024fn evalZigTest(
1025 run: *Run,
1026 spawn_options: process.SpawnOptions,
1027 options: Step.MakeOptions,
1028 fuzz_context: ?FuzzContext,
1029) !void {
1030 if (fuzz_context != null) {
1031 try evalFuzzTest(run, spawn_options, options, fuzz_context.?);
1032 return;
1033 }
1034
1035 const step_owner = run.step.owner;
1036 const gpa = step_owner.allocator;
1037 const arena = step_owner.allocator;
1038 const io = step_owner.graph.io;
1039
1040 // We will update this every time a child runs.
1041 run.step.result_peak_rss = 0;
1042
1043 var test_results: Step.TestResults = .{
1044 .test_count = 0,
1045 .skip_count = 0,
1046 .fail_count = 0,
1047 .crash_count = 0,
1048 .timeout_count = 0,
1049 .leak_count = 0,
1050 .log_err_count = 0,
1051 };
1052 var test_metadata: ?TestMetadata = null;
1053
1054 while (true) {
1055 var child = try process.spawn(io, spawn_options);
1056 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
1057 var multi_reader: Io.File.MultiReader = undefined;
1058 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
1059 var child_killed = false;
1060 defer if (!child_killed) {
1061 child.kill(io);
1062 multi_reader.deinit();
1063 run.step.result_peak_rss = @max(
1064 run.step.result_peak_rss,
1065 child.resource_usage_statistics.getMaxRss() orelse 0,
1066 );
1067 };
1068
1069 switch (try waitZigTest(
1070 run,
1071 &child,
1072 options,
1073 &multi_reader,
1074 &test_metadata,
1075 &test_results,
1076 )) {
1077 .write_failed => |err| {
1078 // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured
1079 // all available stderr to make our error output as useful as possible.
1080 const stderr_fr = multi_reader.fileReader(1);
1081 while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) {
1082 error.ReadFailed => return stderr_fr.err.?,
1083 error.EndOfStream => {},
1084 }
1085 run.step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered());
1086
1087 // Clean up everything and wait for the child to exit.
1088 child.stdin.?.close(io);
1089 child.stdin = null;
1090 multi_reader.deinit();
1091 child_killed = true;
1092 const term = try child.wait(io);
1093 run.step.result_peak_rss = @max(
1094 run.step.result_peak_rss,
1095 child.resource_usage_statistics.getMaxRss() orelse 0,
1096 );
1097
1098 // The individual unit test results are irrelevant: the test runner itself broke!
1099 // Fail immediately without populating `s.test_results`.
1100 return run.step.fail("unable to write stdin ({t}); test process unexpectedly {f}", .{ err, fmtTerm(term) });
1101 },
1102 .no_poll => |no_poll| {
1103 // This might be a success (we requested exit and the child dutifully closed stdout) or
1104 // a crash of some kind. Either way, the child will terminate by itself -- wait for it.
1105 const stderr_reader = multi_reader.reader(1);
1106 const stderr_owned = try arena.dupe(u8, stderr_reader.buffered());
1107
1108 // Clean up everything and wait for the child to exit.
1109 child.stdin.?.close(io);
1110 child.stdin = null;
1111 multi_reader.deinit();
1112 child_killed = true;
1113 const term = try child.wait(io);
1114 run.step.result_peak_rss = @max(
1115 run.step.result_peak_rss,
1116 child.resource_usage_statistics.getMaxRss() orelse 0,
1117 );
1118
1119 if (no_poll.active_test_index) |test_index| {
1120 // A test was running, so this is definitely a crash. Report it against that
1121 // test, and continue to the next test.
1122 test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed;
1123 test_results.crash_count += 1;
1124 try run.step.addError("'{s}' {f}{s}{s}", .{
1125 test_metadata.?.testName(test_index),
1126 fmtTerm(term),
1127 if (stderr_owned.len != 0) " with stderr:\n" else "",
1128 std.mem.trim(u8, stderr_owned, "\n"),
1129 });
1130 continue;
1131 }
1132
1133 // Report an error if the child terminated uncleanly or if we were still trying to run more tests.
1134 run.step.result_stderr = stderr_owned;
1135 const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32);
1136 if (!tests_done or !termMatches(.{ .exited = 0 }, term)) {
1137 // The individual unit test results are irrelevant: the test runner itself broke!
1138 // Fail immediately without populating `s.test_results`.
1139 return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)});
1140 }
1141
1142 // We're done with all of the tests! Commit the test results and return.
1143 run.step.test_results = test_results;
1144 if (test_metadata) |tm| {
1145 run.cached_test_metadata = tm.toCachedTestMetadata();
1146 if (options.web_server) |ws| {
1147 if (run.step.owner.graph.time_report) {
1148 ws.updateTimeReportRunTest(
1149 run,
1150 &run.cached_test_metadata.?,
1151 tm.ns_per_test,
1152 );
1153 }
1154 }
1155 }
1156 return;
1157 },
1158 .timeout => |timeout| {
1159 const stderr_reader = multi_reader.reader(1);
1160 const stderr = stderr_reader.buffered();
1161 stderr_reader.tossBuffered();
1162 if (timeout.active_test_index) |test_index| {
1163 // A test was running. Report the timeout against that test, and continue on to
1164 // the next test.
1165 test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed;
1166 test_results.timeout_count += 1;
1167 try run.step.addError("'{s}' timed out after {f}{s}{s}", .{
1168 test_metadata.?.testName(test_index),
1169 Io.Duration{ .nanoseconds = timeout.ns_elapsed },
1170 if (stderr.len != 0) " with stderr:\n" else "",
1171 std.mem.trim(u8, stderr, "\n"),
1172 });
1173 continue;
1174 }
1175 // Just log an error and let the child be killed.
1176 run.step.result_stderr = try arena.dupe(u8, stderr);
1177 // The individual unit test results in `results` are irrelevant: the test runner
1178 // is broken! Fail immediately without populating `s.test_results`.
1179 return run.step.fail("test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }});
1180 },
1181 }
1182 comptime unreachable;
1183 }
1184}
1185
1186const TestMetadata = struct {
1187 names: []const u32,
1188 ns_per_test: []u64,
1189 expected_panic_msgs: []const u32,
1190 string_bytes: []const u8,
1191 next_index: u32,
1192 prog_node: std.Progress.Node,
1193
1194 fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata {
1195 return .{
1196 .names = tm.names,
1197 .string_bytes = tm.string_bytes,
1198 };
1199 }
1200
1201 fn testName(tm: TestMetadata, index: u32) []const u8 {
1202 return tm.toCachedTestMetadata().testName(index);
1203 }
1204};
1205
1206pub const CachedTestMetadata = struct {
1207 names: []const u32,
1208 string_bytes: []const u8,
1209
1210 pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 {
1211 return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0);
1212 }
1213};
1214
1215fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
1216 while (metadata.next_index < metadata.names.len) {
1217 const i = metadata.next_index;
1218 metadata.next_index += 1;
1219
1220 if (metadata.expected_panic_msgs[i] != 0) continue;
1221
1222 const name = metadata.testName(i);
1223 if (sub_prog_node.*) |n| n.end();
1224 sub_prog_node.* = metadata.prog_node.start(name, 0);
1225
1226 try sendRunTestMessage(io, in, .run_test, i);
1227 return;
1228 } else {
1229 metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done
1230 try sendMessage(io, in, .exit);
1231 }
1232}
1233
1234fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
1235 const header: std.zig.Client.Message.Header = .{
1236 .tag = tag,
1237 .bytes_len = 0,
1238 };
1239 var w = file.writerStreaming(io, &.{});
1240 w.interface.writeStruct(header, .little) catch |err| switch (err) {
1241 error.WriteFailed => return w.err.?,
1242 };
1243}
1244
1245fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void {
1246 const header: std.zig.Client.Message.Header = .{
1247 .tag = tag,
1248 .bytes_len = 4,
1249 };
1250 var w = file.writerStreaming(io, &.{});
1251 w.interface.writeStruct(header, .little) catch |err| switch (err) {
1252 error.WriteFailed => return w.err.?,
1253 };
1254 w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
1255 error.WriteFailed => return w.err.?,
1256 };
1257}
1258
1259fn sendRunFuzzTestMessage(
1260 io: Io,
1261 file: Io.File,
1262 test_names: []const []const u8,
1263 kind: std.Build.abi.fuzz.LimitKind,
1264 amount_or_instance: u64,
1265) !void {
1266 const header: std.zig.Client.Message.Header = .{
1267 .tag = .start_fuzzing,
1268 .bytes_len = 1 + 8 + 4 + count: {
1269 var c: u32 = @intCast(test_names.len * 4);
1270 for (test_names) |name| {
1271 c += @intCast(name.len);
1272 }
1273 break :count c;
1274 },
1275 };
1276 var w = file.writerStreaming(io, &.{});
1277 w.interface.writeStruct(header, .little) catch |err| switch (err) {
1278 error.WriteFailed => return w.err.?,
1279 };
1280 w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) {
1281 error.WriteFailed => return w.err.?,
1282 };
1283 w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) {
1284 error.WriteFailed => return w.err.?,
1285 };
1286 w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) {
1287 error.WriteFailed => return w.err.?,
1288 };
1289 for (test_names) |test_name| {
1290 w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) {
1291 error.WriteFailed => return w.err.?,
1292 };
1293 w.interface.writeAll(test_name) catch |err| switch (err) {
1294 error.WriteFailed => return w.err.?,
1295 };
1296 }
1297}
1298
1299fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult {
1300 const b = run.step.owner;
1301 const io = b.graph.io;
1302 const arena = b.allocator;
1303 const gpa = b.allocator;
1304
1305 var child = try process.spawn(io, spawn_options);
1306 defer child.kill(io);
1307
1308 switch (run.stdin) {
1309 .bytes => |bytes| {
1310 child.stdin.?.writeStreamingAll(io, bytes) catch |err| {
1311 return run.step.fail("unable to write stdin: {t}", .{err});
1312 };
1313 child.stdin.?.close(io);
1314 child.stdin = null;
1315 },
1316 .lazy_path => |lazy_path| {
1317 const path = lazy_path.getPath3(b, &run.step);
1318 const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| {
1319 return run.step.fail("unable to open stdin file: {t}", .{err});
1320 };
1321 defer file.close(io);
1322 // TODO https://github.com/ziglang/zig/issues/23955
1323 var read_buffer: [1024]u8 = undefined;
1324 var file_reader = file.reader(io, &read_buffer);
1325 var write_buffer: [1024]u8 = undefined;
1326 var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer);
1327 _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
1328 error.ReadFailed => return run.step.fail("failed to read from {f}: {t}", .{
1329 path, file_reader.err.?,
1330 }),
1331 error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
1332 stdin_writer.err.?,
1333 }),
1334 };
1335 stdin_writer.interface.flush() catch |err| switch (err) {
1336 error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
1337 stdin_writer.err.?,
1338 }),
1339 };
1340 child.stdin.?.close(io);
1341 child.stdin = null;
1342 },
1343 .none => {},
1344 }
1345
1346 var stdout_bytes: ?[]const u8 = null;
1347 var stderr_bytes: ?[]const u8 = null;
1348
1349 if (child.stdout) |stdout| {
1350 if (child.stderr) |stderr| {
1351 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
1352 var multi_reader: Io.File.MultiReader = undefined;
1353 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr });
1354 defer multi_reader.deinit();
1355
1356 const stdout_reader = multi_reader.reader(0);
1357 const stderr_reader = multi_reader.reader(1);
1358
1359 while (multi_reader.fill(64, .none)) |_| {
1360 if (run.stdio_limit.toInt()) |limit| {
1361 if (stdout_reader.buffered().len > limit)
1362 return error.StdoutStreamTooLong;
1363 if (stderr_reader.buffered().len > limit)
1364 return error.StderrStreamTooLong;
1365 }
1366 } else |err| switch (err) {
1367 error.Timeout => unreachable,
1368 error.EndOfStream => {},
1369 else => |e| return e,
1370 }
1371
1372 try multi_reader.checkAnyError();
1373
1374 // TODO: this string can leak since alloc below can return error.
1375 stdout_bytes = try multi_reader.toOwnedSlice(0);
1376 // TODO: this string can leak since its allocated using gpa and `try child.wait(io)` below can fail.
1377 stderr_bytes = try multi_reader.toOwnedSlice(1);
1378 } else {
1379 var stdout_reader = stdout.readerStreaming(io, &.{});
1380 stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
1381 error.OutOfMemory => |e| return e,
1382 error.ReadFailed => return stdout_reader.err.?,
1383 error.StreamTooLong => return error.StdoutStreamTooLong,
1384 };
1385 }
1386 } else if (child.stderr) |stderr| {
1387 var stderr_reader = stderr.readerStreaming(io, &.{});
1388 stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
1389 error.OutOfMemory => |e| return e,
1390 error.ReadFailed => return stderr_reader.err.?,
1391 error.StreamTooLong => return error.StderrStreamTooLong,
1392 };
1393 }
1394
1395 if (stderr_bytes) |bytes| if (bytes.len > 0) {
1396 // Treat stderr as an error message.
1397 const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) {
1398 .check => |checks| !checksContainStderr(checks.items),
1399 else => true,
1400 };
1401 if (stderr_is_diagnostic) {
1402 run.step.result_stderr = bytes;
1403 }
1404 };
1405
1406 run.step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0;
1407
1408 return .{
1409 .term = try child.wait(io),
1410 .stdout = stdout_bytes,
1411 .stderr = stderr_bytes,
1412 };
1413}
1414
1415const IndexedOutput = struct {
1416 index: usize,
1417 tag: @typeInfo(Arg).@"union".tag_type.?,
1418 output: *Output,
1419};
1420
1421pub fn rerunInFuzzMode(
1422 run: *Run,
1423 fuzz: *std.Build.Fuzz,
1424 prog_node: std.Progress.Node,
1425) !void {
1426 const step = &run.step;
1427 const b = step.owner;
1428 const io = b.graph.io;
1429 const arena = b.allocator;
1430 var argv_list: std.ArrayList([]const u8) = .empty;
1431 for (run.argv.items) |arg| {
1432 switch (arg) {
1433 .bytes => |bytes| {
1434 try argv_list.append(arena, bytes);
1435 },
1436 .lazy_path => |file| {
1437 const file_path = file.lazy_path.getPath3(b, step);
1438 try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
1439 },
1440 .decorated_directory => |dd| {
1441 const file_path = dd.lazy_path.getPath3(b, step);
1442 try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }));
1443 },
1444 .file_content => |file_plp| {
1445 const file_path = file_plp.lazy_path.getPath3(b, step);
1446
1447 var result: std.Io.Writer.Allocating = .init(arena);
1448 errdefer result.deinit();
1449 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
1450
1451 const file = try file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{});
1452 defer file.close(io);
1453
1454 var buf: [1024]u8 = undefined;
1455 var file_reader = file.reader(io, &buf);
1456 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
1457 error.ReadFailed => return file_reader.err.?,
1458 error.WriteFailed => return error.OutOfMemory,
1459 };
1460
1461 try argv_list.append(arena, result.written());
1462 },
1463 .artifact => |pa| {
1464 const artifact = pa.artifact;
1465 const file_path: []const u8 = p: {
1466 if (artifact == run.producer.?) break :p b.fmt("{f}", .{run.rebuilt_executable.?});
1467 break :p artifact.installed_path orelse artifact.generated_bin.?.path.?;
1468 };
1469 try argv_list.append(arena, b.fmt("{s}{s}", .{
1470 pa.prefix,
1471 run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
1472 }));
1473 },
1474 .output_file, .output_directory => unreachable,
1475 }
1476 }
1477
1478 if (run.step.result_failed_command) |cmd| {
1479 fuzz.gpa.free(cmd);
1480 run.step.result_failed_command = null;
1481 }
1482
1483 const has_side_effects = false;
1484 var rand_int: u64 = undefined;
1485 io.random(@ptrCast(&rand_int));
1486 const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
1487 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{
1488 .progress_node = prog_node,
1489 .watch = undefined, // not used by `runCommand`
1490 .web_server = null, // only needed for time reports
1491 .unit_test_timeout_ns = null, // don't time out fuzz tests for now
1492 .gpa = fuzz.gpa,
1493 }, .{
1494 .fuzz = fuzz,
1495 });
1496}
1497
1498fn populateGeneratedPaths(
1499 arena: std.mem.Allocator,
1500 output_placeholders: []const IndexedOutput,
1501 captured_stdout: ?*CapturedStdIo,
1502 captured_stderr: ?*CapturedStdIo,
1503 cache_root: Cache.Directory,
1504 digest: *const Cache.HexDigest,
1505) !void {
1506 for (output_placeholders) |placeholder| {
1507 placeholder.output.generated_file.path = try cache_root.join(arena, &.{
1508 "o", digest, placeholder.output.basename,
1509 });
1510 }
1511
1512 if (captured_stdout) |captured| {
1513 captured.output.generated_file.path = try cache_root.join(arena, &.{
1514 "o", digest, captured.output.basename,
1515 });
1516 }
1517
1518 if (captured_stderr) |captured| {
1519 captured.output.generated_file.path = try cache_root.join(arena, &.{
1520 "o", digest, captured.output.basename,
1521 });
1522 }
1523}
1524
1525fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
1526 if (term) |t| switch (t) {
1527 .exited => |code| try w.print("exited with code {d}", .{code}),
1528 .signal => |sig| try w.print("terminated with signal {t}", .{sig}),
1529 .stopped => |sig| try w.print("stopped with signal {t}", .{sig}),
1530 .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
1531 } else {
1532 try w.writeAll("exited with any code");
1533 }
1534}
1535fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) {
1536 return .{ .data = term };
1537}
1538
1539const FuzzContext = struct {
1540 fuzz: *std.Build.Fuzz,
1541};
1542
1543fn runCommand(
1544 run: *Run,
1545 argv: []const []const u8,
1546 has_side_effects: bool,
1547 output_dir_path: []const u8,
1548 options: Step.MakeOptions,
1549 fuzz_context: ?FuzzContext,
1550) !void {
1551 const step = &run.step;
1552 const b = step.owner;
1553 const arena = b.allocator;
1554 const gpa = options.gpa;
1555 const io = b.graph.io;
1556
1557 const cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, step) } else .inherit;
1558
1559 try step.handleChildProcUnsupported();
1560 try Step.handleVerbose2(step.owner, cwd, run.environ_map, argv);
1561
1562 const allow_skip = switch (run.stdio) {
1563 .check, .zig_test => run.skip_foreign_checks,
1564 else => false,
1565 };
1566
1567 var interp_argv = std.array_list.Managed([]const u8).init(b.allocator);
1568 defer interp_argv.deinit();
1569
1570 var environ_map: EnvMap = env: {
1571 const orig = run.environ_map orelse &b.graph.environ_map;
1572 break :env try orig.clone(gpa);
1573 };
1574 defer environ_map.deinit();
1575
1576 const opt_generic_result = spawnChildAndCollect(run, argv, &environ_map, has_side_effects, options, fuzz_context) catch |err| term: {
1577 // InvalidExe: cpu arch mismatch
1578 // FileNotFound: can happen with a wrong dynamic linker path
1579 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
1580 // TODO: learn the target from the binary directly rather than from
1581 // relying on it being a Compile step. This will make this logic
1582 // work even for the edge case that the binary was produced by a
1583 // third party.
1584 const exe = switch (run.argv.items[0]) {
1585 .artifact => |exe| exe.artifact,
1586 else => break :interpret,
1587 };
1588 switch (exe.kind) {
1589 .exe, .@"test" => {},
1590 else => break :interpret,
1591 }
1592
1593 const root_target = exe.rootModuleTarget();
1594 const need_cross_libc = exe.is_linking_libc and
1595 (root_target.isGnuLibC() or (root_target.isMuslLibC() and exe.linkage == .dynamic));
1596 const other_target = exe.root_module.resolved_target.?.result;
1597 switch (std.zig.system.getExternalExecutor(io, &b.graph.host.result, &other_target, .{
1598 .qemu_fixes_dl = need_cross_libc and b.libc_runtimes_dir != null,
1599 .link_libc = exe.is_linking_libc,
1600 })) {
1601 .native, .rosetta => {
1602 if (allow_skip) return error.MakeSkipped;
1603 break :interpret;
1604 },
1605 .wine => |bin_name| {
1606 if (b.enable_wine) {
1607 try interp_argv.append(bin_name);
1608 try interp_argv.appendSlice(argv);
1609
1610 // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but
1611 // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.
1612 if (environ_map.get("WINEDEBUG") == null) {
1613 try environ_map.put("WINEDEBUG", "-all");
1614 }
1615 } else {
1616 return failForeign(run, "-fwine", argv[0], exe);
1617 }
1618 },
1619 .qemu => |bin_name| {
1620 if (b.enable_qemu) {
1621 try interp_argv.append(bin_name);
1622
1623 if (need_cross_libc) {
1624 if (b.libc_runtimes_dir) |dir| {
1625 try interp_argv.append("-L");
1626 try interp_argv.append(b.pathJoin(&.{
1627 dir,
1628 try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple(
1629 b.allocator,
1630 root_target.cpu.arch,
1631 root_target.os.tag,
1632 root_target.abi,
1633 ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple(
1634 b.allocator,
1635 root_target.cpu.arch,
1636 root_target.abi,
1637 ) else unreachable,
1638 }));
1639 } else return failForeign(run, "--libc-runtimes", argv[0], exe);
1640 }
1641
1642 try interp_argv.appendSlice(argv);
1643 } else return failForeign(run, "-fqemu", argv[0], exe);
1644 },
1645 .darling => |bin_name| {
1646 if (b.enable_darling) {
1647 try interp_argv.append(bin_name);
1648 try interp_argv.appendSlice(argv);
1649 } else {
1650 return failForeign(run, "-fdarling", argv[0], exe);
1651 }
1652 },
1653 .wasmtime => |bin_name| {
1654 if (b.enable_wasmtime) {
1655 try interp_argv.append(bin_name);
1656 try interp_argv.append("--dir=.");
1657 // Wasmtime doeesn't inherit environment variables from the parent process
1658 // by default. '-S inherit-env' was added in Wasmtime version 20.
1659 try interp_argv.append("-Sinherit-env");
1660 try interp_argv.append(argv[0]);
1661 try interp_argv.appendSlice(argv[1..]);
1662 } else {
1663 return failForeign(run, "-fwasmtime", argv[0], exe);
1664 }
1665 },
1666 .bad_dl => |foreign_dl| {
1667 if (allow_skip) return error.MakeSkipped;
1668
1669 const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)";
1670
1671 return step.fail(
1672 \\the host system is unable to execute binaries from the target
1673 \\ because the host dynamic linker is '{s}',
1674 \\ while the target dynamic linker is '{s}'.
1675 \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step
1676 , .{ host_dl, foreign_dl });
1677 },
1678 .bad_os_or_cpu => {
1679 if (allow_skip) return error.MakeSkipped;
1680
1681 const host_name = try b.graph.host.result.zigTriple(b.allocator);
1682 const foreign_name = try root_target.zigTriple(b.allocator);
1683
1684 return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{
1685 host_name, foreign_name,
1686 });
1687 },
1688 }
1689
1690 if (root_target.os.tag == .windows) {
1691 // On Windows we don't have rpaths so we have to add .dll search paths to PATH
1692 run.addPathForDynLibs(exe);
1693 }
1694
1695 gpa.free(step.result_failed_command.?);
1696 step.result_failed_command = null;
1697 try Step.handleVerbose2(step.owner, cwd, run.environ_map, interp_argv.items);
1698
1699 break :term spawnChildAndCollect(run, interp_argv.items, &environ_map, has_side_effects, options, fuzz_context) catch |e| {
1700 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
1701 if (e == error.MakeFailed) return error.MakeFailed; // error already reported
1702 return step.fail("unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e });
1703 };
1704 }
1705 if (err == error.MakeFailed) return error.MakeFailed; // error already reported
1706
1707 return step.fail("failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err });
1708 };
1709
1710 const generic_result = opt_generic_result orelse {
1711 assert(run.stdio == .zig_test);
1712 // Specific errors have already been reported, and test results are populated. All we need
1713 // to do is report step failure if any test failed.
1714 if (!step.test_results.isSuccess()) return error.MakeFailed;
1715 return;
1716 };
1717
1718 assert(fuzz_context == null);
1719 assert(run.stdio != .zig_test);
1720
1721 // Capture stdout and stderr to GeneratedFile objects.
1722 const Stream = struct {
1723 captured: ?*CapturedStdIo,
1724 bytes: ?[]const u8,
1725 };
1726 for ([_]Stream{
1727 .{
1728 .captured = run.captured_stdout,
1729 .bytes = generic_result.stdout,
1730 },
1731 .{
1732 .captured = run.captured_stderr,
1733 .bytes = generic_result.stderr,
1734 },
1735 }) |stream| {
1736 if (stream.captured) |captured| {
1737 const output_components = .{ output_dir_path, captured.output.basename };
1738 const output_path = try b.cache_root.join(arena, &output_components);
1739 captured.output.generated_file.path = output_path;
1740
1741 const sub_path = b.pathJoin(&output_components);
1742 const sub_path_dirname = Dir.path.dirname(sub_path).?;
1743 b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| {
1744 return step.fail("unable to make path '{f}{s}': {s}", .{
1745 b.cache_root, sub_path_dirname, @errorName(err),
1746 });
1747 };
1748 const data = switch (captured.trim_whitespace) {
1749 .none => stream.bytes.?,
1750 .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace),
1751 .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace),
1752 .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace),
1753 };
1754 b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = data }) catch |err| {
1755 return step.fail("unable to write file '{f}{s}': {s}", .{
1756 b.cache_root, sub_path, @errorName(err),
1757 });
1758 };
1759 }
1760 }
1761
1762 switch (run.stdio) {
1763 .zig_test => unreachable,
1764 .check => |checks| for (checks.items) |check| switch (check) {
1765 .expect_stderr_exact => |expected_bytes| {
1766 if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) {
1767 return step.fail(
1768 \\========= expected this stderr: =========
1769 \\{s}
1770 \\========= but found: ====================
1771 \\{s}
1772 , .{
1773 expected_bytes,
1774 generic_result.stderr.?,
1775 });
1776 }
1777 },
1778 .expect_stderr_match => |match| {
1779 if (mem.find(u8, generic_result.stderr.?, match) == null) {
1780 return step.fail(
1781 \\========= expected to find in stderr: =========
1782 \\{s}
1783 \\========= but stderr does not contain it: =====
1784 \\{s}
1785 , .{
1786 match,
1787 generic_result.stderr.?,
1788 });
1789 }
1790 },
1791 .expect_stdout_exact => |expected_bytes| {
1792 if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) {
1793 return step.fail(
1794 \\========= expected this stdout: =========
1795 \\{s}
1796 \\========= but found: ====================
1797 \\{s}
1798 , .{
1799 expected_bytes,
1800 generic_result.stdout.?,
1801 });
1802 }
1803 },
1804 .expect_stdout_match => |match| {
1805 if (mem.find(u8, generic_result.stdout.?, match) == null) {
1806 return step.fail(
1807 \\========= expected to find in stdout: =========
1808 \\{s}
1809 \\========= but stdout does not contain it: =====
1810 \\{s}
1811 , .{
1812 match,
1813 generic_result.stdout.?,
1814 });
1815 }
1816 },
1817 .expect_term => |expected_term| {
1818 if (!termMatches(expected_term, generic_result.term)) {
1819 return step.fail("process {f} (expected {f})", .{
1820 fmtTerm(generic_result.term),
1821 fmtTerm(expected_term),
1822 });
1823 }
1824 },
1825 },
1826 else => {
1827 // On failure, report captured stderr like normal standard error output.
1828 const bad_exit = switch (generic_result.term) {
1829 .exited => |code| code != 0,
1830 .signal, .stopped, .unknown => true,
1831 };
1832 if (bad_exit) {
1833 if (generic_result.stderr) |bytes| {
1834 run.step.result_stderr = bytes;
1835 }
1836 }
1837
1838 try step.handleChildProcessTerm(generic_result.term);
1839 },
1840 }
1841}
1842
1843const EvalGenericResult = struct {
1844 term: process.Child.Term,
1845 stdout: ?[]const u8,
1846 stderr: ?[]const u8,
1847};
1848
1849fn spawnChildAndCollect(
1850 run: *Run,
1851 argv: []const []const u8,
1852 environ_map: *EnvMap,
1853 has_side_effects: bool,
1854 options: Step.MakeOptions,
1855 fuzz_context: ?FuzzContext,
1856) !?EvalGenericResult {
1857 const b = run.step.owner;
1858 const graph = b.graph;
1859 const io = graph.io;
1860
1861 if (fuzz_context != null) {
1862 assert(!has_side_effects);
1863 assert(run.stdio == .zig_test);
1864 }
1865
1866 const child_cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, &run.step) } else .inherit;
1867
1868 // If an error occurs, it's caused by this command:
1869 assert(run.step.result_failed_command == null);
1870 run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{
1871 .child = environ_map,
1872 .parent = &graph.environ_map,
1873 }, argv);
1874
1875 var spawn_options: process.SpawnOptions = .{
1876 .argv = argv,
1877 .cwd = child_cwd,
1878 .environ_map = environ_map,
1879 .request_resource_usage_statistics = true,
1880 .stdin = if (run.stdin != .none) s: {
1881 assert(run.stdio != .inherit);
1882 break :s .pipe;
1883 } else switch (run.stdio) {
1884 .infer_from_args => if (has_side_effects) .inherit else .ignore,
1885 .inherit => .inherit,
1886 .check => .ignore,
1887 .zig_test => .pipe,
1888 },
1889 .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) {
1890 .infer_from_args => if (has_side_effects) .inherit else .ignore,
1891 .inherit => .inherit,
1892 .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore,
1893 .zig_test => .pipe,
1894 },
1895 .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) {
1896 .infer_from_args => if (has_side_effects) .inherit else .pipe,
1897 .inherit => .inherit,
1898 .check => .pipe,
1899 .zig_test => .pipe,
1900 },
1901 };
1902
1903 if (run.stdio == .zig_test) {
1904 const started: Io.Clock.Timestamp = .now(io, .awake);
1905 const result = evalZigTest(run, spawn_options, options, fuzz_context) catch |err| switch (err) {
1906 error.Canceled => |e| return e,
1907 else => |e| e,
1908 };
1909 run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
1910 try result;
1911 return null;
1912 } else {
1913 const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit;
1914 if (!run.disable_zig_progress and !inherit) {
1915 spawn_options.progress_node = options.progress_node;
1916 }
1917 const terminal_mode: Io.Terminal.Mode = if (inherit) m: {
1918 const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
1919 break :m stderr.terminal_mode;
1920 } else .no_color;
1921 defer if (inherit) io.unlockStderr();
1922 try setColorEnvironmentVariables(run, environ_map, terminal_mode);
1923
1924 const started: Io.Clock.Timestamp = .now(io, .awake);
1925 const result = evalGeneric(run, spawn_options) catch |err| switch (err) {
1926 error.Canceled => |e| return e,
1927 else => |e| e,
1928 };
1929 run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
1930 return try result;
1931 }
1932}
1933
1934fn hashStdIo(hh: *Cache.HashHelper, stdio: StdIo) void {
1935 switch (stdio) {
1936 .infer_from_args, .inherit, .zig_test => {},
1937 .check => |checks| for (checks.items) |check| {
1938 hh.add(@as(std.meta.Tag(StdIo.Check), check));
1939 switch (check) {
1940 .expect_stderr_exact,
1941 .expect_stderr_match,
1942 .expect_stdout_exact,
1943 .expect_stdout_match,
1944 => |s| hh.addBytes(s),
1945
1946 .expect_term => |term| {
1947 hh.add(@as(std.meta.Tag(process.Child.Term), term));
1948 switch (term) {
1949 inline .exited, .signal, .stopped => |x| hh.add(x),
1950 .unknown => |x| hh.add(x),
1951 }
1952 },
1953 }
1954 },
1955 }
1956}
1957fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
1958 return if (expected) |e| switch (e) {
1959 .exited => |expected_code| switch (actual) {
1960 .exited => |actual_code| expected_code == actual_code,
1961 else => false,
1962 },
1963 .signal => |expected_sig| switch (actual) {
1964 .signal => |actual_sig| expected_sig == actual_sig,
1965 else => false,
1966 },
1967 .stopped => |expected_sig| switch (actual) {
1968 .stopped => |actual_sig| expected_sig == actual_sig,
1969 else => false,
1970 },
1971 .unknown => |expected_code| switch (actual) {
1972 .unknown => |actual_code| expected_code == actual_code,
1973 else => false,
1974 },
1975 } else switch (actual) {
1976 .exited => true,
1977 else => false,
1978 };
1979}
1980
1981fn setColorEnvironmentVariables(run: *Run, environ_map: *EnvMap, terminal_mode: Io.Terminal.Mode) !void {
1982 color: switch (run.color) {
1983 .manual => {},
1984 .enable => {
1985 try environ_map.put("CLICOLOR_FORCE", "1");
1986 _ = environ_map.swapRemove("NO_COLOR");
1987 },
1988 .disable => {
1989 try environ_map.put("NO_COLOR", "1");
1990 _ = environ_map.swapRemove("CLICOLOR_FORCE");
1991 },
1992 .inherit => switch (terminal_mode) {
1993 .no_color, .windows_api => continue :color .disable,
1994 .escape_codes => continue :color .enable,
1995 },
1996 .auto => {
1997 const capture_stderr = run.captured_stderr != null or switch (run.stdio) {
1998 .check => |checks| checksContainStderr(checks.items),
1999 .infer_from_args, .inherit, .zig_test => false,
2000 };
2001 if (capture_stderr) {
2002 continue :color .disable;
2003 } else {
2004 continue :color .inherit;
2005 }
2006 },
2007 }
2008}
2009
2010fn checksContainStdout(checks: []const StdIo.Check) bool {
2011 for (checks) |check| switch (check) {
2012 .expect_stderr_exact,
2013 .expect_stderr_match,
2014 .expect_term,
2015 => continue,
2016
2017 .expect_stdout_exact,
2018 .expect_stdout_match,
2019 => return true,
2020 };
2021 return false;
2022}
2023
2024fn checksContainStderr(checks: []const StdIo.Check) bool {
2025 for (checks) |check| switch (check) {
2026 .expect_stdout_exact,
2027 .expect_stdout_match,
2028 .expect_term,
2029 => continue,
2030
2031 .expect_stderr_exact,
2032 .expect_stderr_match,
2033 => return true,
2034 };
2035 return false;
2036}
2037
2038/// Returns whether the Run step has side effects *other than* updating the output arguments.
2039fn hasSideEffects(run: Run) bool {
2040 if (run.has_side_effects) return true;
2041 return switch (run.stdio) {
2042 .infer_from_args => !run.hasAnyOutputArgs(),
2043 .inherit => true,
2044 .check => false,
2045 .zig_test => false,
2046 };
2047}
2048
2049fn hasAnyOutputArgs(run: Run) bool {
2050 if (run.captured_stdout != null) return true;
2051 if (run.captured_stderr != null) return true;
2052 for (run.argv.items) |arg| switch (arg) {
2053 .output_file, .output_directory => return true,
2054 else => continue,
2055 };
2056 return false;
2057}
2058
2059/// If `path` is cwd-relative, make it relative to the cwd of the child instead.
2060///
2061/// Whenever a path is included in the argv of a child, it should be put through this function first
2062/// to make sure the child doesn't see paths relative to a cwd other than its own.
2063fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 {
2064 const b = run.step.owner;
2065 const graph = b.graph;
2066 const arena = graph.arena;
2067
2068 const path_str = path.toString(arena) catch @panic("OOM");
2069 if (Dir.path.isAbsolute(path_str)) {
2070 // Absolute paths don't need changing.
2071 return path_str;
2072 }
2073 const child_cwd_rel: []const u8 = rel: {
2074 const child_lazy_cwd = run.cwd orelse break :rel path_str;
2075 const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(arena) catch @panic("OOM");
2076 // Convert it from relative to *our* cwd, to relative to the *child's* cwd.
2077 break :rel Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str) catch @panic("OOM");
2078 };
2079 // Not every path can be made relative, e.g. if the path and the child cwd are on different
2080 // disk designators on Windows. In that case, `relative` will return an absolute path which we can
2081 // just return.
2082 if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel;
2083
2084 // We're not done yet. In some cases this path must be prefixed with './':
2085 // * On POSIX, the executable name cannot be a single component like 'foo'
2086 // * Some executables might treat a leading '-' like a flag, which we must avoid
2087 // There's no harm in it, so just *always* apply this prefix.
2088 return Dir.path.join(arena, &.{ ".", child_cwd_rel }) catch @panic("OOM");
2089}
2090
2091fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void {
2092 const b = run.step.owner;
2093 const compiles = artifact.getCompileDependencies(true);
2094 for (compiles) |compile| {
2095 if (compile.root_module.resolved_target.?.result.os.tag == .windows and
2096 compile.isDynamicLibrary())
2097 {
2098 addPathDir(run, Dir.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?);
2099 }
2100 }
2101}
2102
2103fn failForeign(
2104 run: *Run,
2105 suggested_flag: []const u8,
2106 argv0: []const u8,
2107 exe: *Step.Compile,
2108) error{ MakeFailed, MakeSkipped, OutOfMemory } {
2109 switch (run.stdio) {
2110 .check, .zig_test => {
2111 if (run.skip_foreign_checks)
2112 return error.MakeSkipped;
2113
2114 const b = run.step.owner;
2115 const host_name = try b.graph.host.result.zigTriple(b.allocator);
2116 const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator);
2117
2118 return run.step.fail(
2119 \\unable to spawn foreign binary '{s}' ({s}) on host system ({s})
2120 \\ consider using {s} or enabling skip_foreign_checks in the Run step
2121 , .{ argv0, foreign_name, host_name, suggested_flag });
2122 },
2123 else => {
2124 return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0});
2125 },
2126 }
2127}
lib/compiler/maker/Step/WriteFile.zig created+206
......@@ -0,0 +1,206 @@
1
2fn make(step: *Step, options: Step.MakeOptions) !void {
3 _ = options;
4 const b = step.owner;
5 const graph = b.graph;
6 const io = graph.io;
7 const arena = b.allocator;
8 const gpa = graph.cache.gpa;
9 const write_file: *WriteFile = @fieldParentPtr("step", step);
10
11 const open_dir_cache = try arena.alloc(Io.Dir, write_file.directories.items.len);
12 var open_dirs_count: usize = 0;
13 defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]);
14
15 switch (write_file.mode) {
16 .whole_cached => {
17 step.clearWatchInputs();
18
19 // The cache is used here not really as a way to speed things up - because writing
20 // the data to a file would probably be very fast - but as a way to find a canonical
21 // location to put build artifacts.
22
23 // If, for example, a hard-coded path was used as the location to put WriteFile
24 // files, then two WriteFiles executing in parallel might clobber each other.
25
26 var man = b.graph.cache.obtain();
27 defer man.deinit();
28
29 for (write_file.files.items) |file| {
30 man.hash.addBytes(file.sub_path);
31
32 switch (file.contents) {
33 .bytes => |bytes| {
34 man.hash.addBytes(bytes);
35 },
36 .copy => |lazy_path| {
37 const path = lazy_path.getPath3(b, step);
38 _ = try man.addFilePath(path, null);
39 try step.addWatchInput(lazy_path);
40 },
41 }
42 }
43
44 for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| {
45 man.hash.addBytes(dir.sub_path);
46 for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext);
47 if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc);
48
49 const need_derived_inputs = try step.addDirectoryWatchInput(dir.source);
50 const src_dir_path = dir.source.getPath3(b, step);
51
52 var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
53 return step.fail("unable to open source directory '{f}': {s}", .{
54 src_dir_path, @errorName(err),
55 });
56 };
57 open_dir_cache_elem.* = src_dir;
58 open_dirs_count += 1;
59
60 var it = try src_dir.walk(gpa);
61 defer it.deinit();
62 while (try it.next(io)) |entry| {
63 if (!dir.options.pathIncluded(entry.path)) continue;
64
65 switch (entry.kind) {
66 .directory => {
67 if (need_derived_inputs) {
68 const entry_path = try src_dir_path.join(arena, entry.path);
69 try step.addDirectoryWatchInputFromPath(entry_path);
70 }
71 },
72 .file => {
73 const entry_path = try src_dir_path.join(arena, entry.path);
74 _ = try man.addFilePath(entry_path, null);
75 },
76 else => continue,
77 }
78 }
79 }
80
81 if (try step.cacheHit(&man)) {
82 const digest = man.final();
83 write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest });
84 assert(step.result_cached);
85 return;
86 }
87
88 const digest = man.final();
89 const cache_path = "o" ++ Dir.path.sep_str ++ digest;
90
91 write_file.generated_directory.path = try b.cache_root.join(arena, &.{cache_path});
92
93 try operate(write_file, open_dir_cache, .{
94 .root_dir = b.cache_root,
95 .sub_path = cache_path,
96 });
97
98 try step.writeManifest(&man);
99 },
100 .tmp => {
101 step.result_cached = false;
102
103 var rand_int: u64 = undefined;
104 io.random(@ptrCast(&rand_int));
105 const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
106
107 write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path});
108
109 try operate(write_file, open_dir_cache, .{
110 .root_dir = b.cache_root,
111 .sub_path = tmp_dir_sub_path,
112 });
113 },
114 .mutate => |lp| {
115 step.result_cached = false;
116 const root_path = try lp.getPath4(b, step);
117 write_file.generated_directory.path = try root_path.toString(arena);
118 try operate(write_file, open_dir_cache, root_path);
119 },
120 }
121}
122
123fn operate(write_file: *WriteFile, open_dir_cache: []const Io.Dir, root_path: std.Build.Cache.Path) !void {
124 const step = &write_file.step;
125 const b = step.owner;
126 const io = b.graph.io;
127 const gpa = b.graph.cache.gpa;
128 const arena = b.allocator;
129
130 var cache_dir = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err|
131 return step.fail("unable to make path {f}: {t}", .{ root_path, err });
132 defer cache_dir.close(io);
133
134 for (write_file.files.items) |file| {
135 if (Dir.path.dirname(file.sub_path)) |dirname| {
136 cache_dir.createDirPath(io, dirname) catch |err| {
137 return step.fail("unable to make path '{f}{c}{s}': {t}", .{
138 root_path, Dir.path.sep, dirname, err,
139 });
140 };
141 }
142 switch (file.contents) {
143 .bytes => |bytes| {
144 cache_dir.writeFile(io, .{ .sub_path = file.sub_path, .data = bytes }) catch |err| {
145 return step.fail("unable to write file '{f}{c}{s}': {t}", .{
146 root_path, Dir.path.sep, file.sub_path, err,
147 });
148 };
149 },
150 .copy => |file_source| {
151 const source_path = file_source.getPath2(b, step);
152 const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir, file.sub_path, .{}) catch |err| {
153 return step.fail("unable to update file from '{s}' to '{f}{c}{s}': {t}", .{
154 source_path, root_path, Dir.path.sep, file.sub_path, err,
155 });
156 };
157 // At this point we already will mark the step as a cache miss.
158 // But this is kind of a partial cache hit since individual
159 // file copies may be avoided. Oh well, this information is
160 // discarded.
161 _ = prev_status;
162 },
163 }
164 }
165
166 for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| {
167 const src_dir_path = dir.source.getPath3(b, step);
168 const dest_dirname = dir.sub_path;
169
170 if (dest_dirname.len != 0) {
171 cache_dir.createDirPath(io, dest_dirname) catch |err| {
172 return step.fail("unable to make path '{f}{c}{s}': {t}", .{
173 root_path, Dir.path.sep, dest_dirname, err,
174 });
175 };
176 }
177
178 var it = try already_open_dir.walk(gpa);
179 defer it.deinit();
180 while (try it.next(io)) |entry| {
181 if (!dir.options.pathIncluded(entry.path)) continue;
182
183 const src_entry_path = try src_dir_path.join(arena, entry.path);
184 const dest_path = b.pathJoin(&.{ dest_dirname, entry.path });
185 switch (entry.kind) {
186 .directory => try cache_dir.createDirPath(io, dest_path),
187 .file => {
188 const prev_status = Io.Dir.updateFile(
189 src_entry_path.root_dir.handle,
190 io,
191 src_entry_path.sub_path,
192 cache_dir,
193 dest_path,
194 .{},
195 ) catch |err| {
196 return step.fail("unable to update file from '{f}' to '{f}{c}{s}': {t}", .{
197 src_entry_path, root_path, Dir.path.sep, dest_path, err,
198 });
199 };
200 _ = prev_status;
201 },
202 else => continue,
203 }
204 }
205 }
206}
lib/compiler/maker/Watch.zig created+968
......@@ -0,0 +1,968 @@
1const builtin = @import("builtin");
2
3const std = @import("../std.zig");
4const Io = std.Io;
5const Step = std.Build.Step;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8const fatal = std.process.fatal;
9const Watch = @This();
10const FsEvents = @import("Watch/FsEvents.zig");
11
12os: Os,
13/// The number to show as the number of directories being watched.
14dir_count: usize,
15// These fields are common to most implementations so are kept here for simplicity.
16// They are `undefined` on implementations which do not utilize then.
17dir_table: DirTable,
18generation: Generation,
19
20pub const have_impl = Os != void;
21
22/// Key is the directory to watch which contains one or more files we are
23/// interested in noticing changes to.
24///
25/// Value is generation.
26const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false);
27
28/// Special key of "." means any changes in this directory trigger the steps.
29const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet);
30const StepSet = std.AutoArrayHashMapUnmanaged(*Step, Generation);
31
32const Generation = u8;
33
34const Hash = std.hash.Wyhash;
35const Cache = std.Build.Cache;
36
37const Os = switch (builtin.os.tag) {
38 .linux => struct {
39 const posix = std.posix;
40
41 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
42 handle_table: HandleTable,
43 /// fanotify file descriptors are keyed by mount id since marks
44 /// are limited to a single filesystem.
45 poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd),
46
47 const MountId = i32;
48 const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false);
49
50 const fan_mask: std.os.linux.fanotify.MarkMask = .{
51 .CLOSE_WRITE = true,
52 .CREATE = true,
53 .DELETE = true,
54 .DELETE_SELF = true,
55 .EVENT_ON_CHILD = true,
56 .MOVED_FROM = true,
57 .MOVED_TO = true,
58 .MOVE_SELF = true,
59 .ONDIR = true,
60 };
61
62 const FileHandle = struct {
63 handle: *align(1) std.os.linux.file_handle,
64
65 fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
66 const bytes = lfh.slice();
67 const new_ptr = try gpa.alignedAlloc(
68 u8,
69 .of(std.os.linux.file_handle),
70 @sizeOf(std.os.linux.file_handle) + bytes.len,
71 );
72 const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
73 new_header.* = lfh.handle.*;
74 const new: FileHandle = .{ .handle = new_header };
75 @memcpy(new.slice(), lfh.slice());
76 return new;
77 }
78
79 fn destroy(lfh: FileHandle, gpa: Allocator) void {
80 const ptr: [*]u8 = @ptrCast(lfh.handle);
81 const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
82 return gpa.free(allocated_slice);
83 }
84
85 fn slice(lfh: FileHandle) []u8 {
86 const ptr: [*]u8 = &lfh.handle.f_handle;
87 return ptr[0..lfh.handle.handle_bytes];
88 }
89
90 const Adapter = struct {
91 pub fn hash(self: Adapter, a: FileHandle) u32 {
92 _ = self;
93 const unsigned_type: u32 = @bitCast(a.handle.handle_type);
94 return @truncate(Hash.hash(unsigned_type, a.slice()));
95 }
96 pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
97 _ = self;
98 _ = b_index;
99 return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
100 }
101 };
102 };
103
104 fn init(cwd_path: []const u8) !Watch {
105 _ = cwd_path;
106 return .{
107 .dir_table = .{},
108 .dir_count = 0,
109 .os = switch (builtin.os.tag) {
110 .linux => .{
111 .handle_table = .{},
112 .poll_fds = .{},
113 },
114 else => {},
115 },
116 .generation = 0,
117 };
118 }
119
120 fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
121 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
122 var buf: [std.fs.max_path_bytes]u8 = undefined;
123 const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
124 path.sub_path,
125 }) catch return error.NameTooLong;
126 const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
127 stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
128 try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID);
129 const stack_lfh: FileHandle = .{ .handle = stack_ptr };
130 return stack_lfh.clone(gpa);
131 }
132
133 fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool {
134 const fanotify = std.os.linux.fanotify;
135 const M = fanotify.event_metadata;
136 var events_buf: [256 + 4096]u8 = undefined;
137 var any_dirty = false;
138 while (true) {
139 var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) {
140 error.WouldBlock => return any_dirty,
141 else => |e| return e,
142 };
143 var meta: [*]align(1) M = @ptrCast(&events_buf);
144 while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
145 len -= meta[0].event_len;
146 meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
147 }) {
148 assert(meta[0].vers == M.VERSION);
149 if (meta[0].mask.Q_OVERFLOW) {
150 any_dirty = true;
151 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
152 markAllFilesDirty(w, gpa);
153 return true;
154 }
155 const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
156 switch (fid.hdr.info_type) {
157 .DFID_NAME => {
158 const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
159 const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
160 const file_name = std.mem.span(file_name_z);
161 const lfh: FileHandle = .{ .handle = file_handle };
162 if (w.os.handle_table.getPtr(lfh)) |value| {
163 if (value.reaction_set.getPtr(".")) |glob_set|
164 any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
165 if (value.reaction_set.getPtr(file_name)) |step_set|
166 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
167 }
168 },
169 else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
170 }
171 }
172 }
173 }
174
175 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
176 // Add missing marks and note persisted ones.
177 for (steps) |step| {
178 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
179 const reaction_set = rs: {
180 const gop = try w.dir_table.getOrPut(gpa, path);
181 if (!gop.found_existing) {
182 var mount_id: MountId = undefined;
183 const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
184 error.FileNotFound => {
185 std.debug.assert(w.dir_table.swapRemove(path));
186 continue;
187 },
188 else => return err,
189 };
190 const fan_fd = blk: {
191 const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id);
192 if (!fd_gop.found_existing) {
193 const fan_fd = std.posix.fanotify_init(.{
194 .CLASS = .NOTIF,
195 .CLOEXEC = true,
196 .NONBLOCK = true,
197 .REPORT_NAME = true,
198 .REPORT_DIR_FID = true,
199 .REPORT_FID = true,
200 .REPORT_TARGET_FID = true,
201 }, 0) catch |err| switch (err) {
202 error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}),
203 else => |e| return e,
204 };
205 fd_gop.value_ptr.* = .{
206 .fd = fan_fd,
207 .events = std.posix.POLL.IN,
208 .revents = undefined,
209 };
210 }
211 break :blk fd_gop.value_ptr.*.fd;
212 };
213 // `dir_handle` may already be present in the table in
214 // the case that we have multiple Cache.Path instances
215 // that compare inequal but ultimately point to the same
216 // directory on the file system.
217 // In such case, we must revert adding this directory, but keep
218 // the additions to the step set.
219 const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle);
220 if (dh_gop.found_existing) {
221 _ = w.dir_table.pop();
222 } else {
223 assert(dh_gop.index == gop.index);
224 dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
225 posix.fanotify_mark(fan_fd, .{
226 .ADD = true,
227 .ONLYDIR = true,
228 }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| {
229 fatal("unable to watch {f}: {s}", .{ path, @errorName(err) });
230 };
231 }
232 break :rs &dh_gop.value_ptr.reaction_set;
233 }
234 break :rs &w.os.handle_table.values()[gop.index].reaction_set;
235 };
236 for (files.items) |basename| {
237 const gop = try reaction_set.getOrPut(gpa, basename);
238 if (!gop.found_existing) gop.value_ptr.* = .{};
239 try gop.value_ptr.put(gpa, step, w.generation);
240 }
241 }
242 }
243
244 {
245 // Remove marks for files that are no longer inputs.
246 var i: usize = 0;
247 while (i < w.os.handle_table.entries.len) {
248 {
249 const reaction_set = &w.os.handle_table.values()[i].reaction_set;
250 var step_set_i: usize = 0;
251 while (step_set_i < reaction_set.entries.len) {
252 const step_set = &reaction_set.values()[step_set_i];
253 var dirent_i: usize = 0;
254 while (dirent_i < step_set.entries.len) {
255 const generations = step_set.values();
256 if (generations[dirent_i] == w.generation) {
257 dirent_i += 1;
258 continue;
259 }
260 step_set.swapRemoveAt(dirent_i);
261 }
262 if (step_set.entries.len > 0) {
263 step_set_i += 1;
264 continue;
265 }
266 reaction_set.swapRemoveAt(step_set_i);
267 }
268 if (reaction_set.entries.len > 0) {
269 i += 1;
270 continue;
271 }
272 }
273
274 const path = w.dir_table.keys()[i];
275
276 const mount_id = w.os.handle_table.values()[i].mount_id;
277 const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd;
278 posix.fanotify_mark(fan_fd, .{
279 .REMOVE = true,
280 .ONLYDIR = true,
281 }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) {
282 error.FileNotFound => {}, // Expected, harmless.
283 else => |e| std.log.warn("unable to unwatch '{f}': {s}", .{ path, @errorName(e) }),
284 };
285
286 w.dir_table.swapRemoveAt(i);
287 w.os.handle_table.swapRemoveAt(i);
288 }
289 w.generation +%= 1;
290 }
291 w.dir_count = w.dir_table.count();
292 }
293
294 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
295 _ = io;
296 const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms());
297 if (events_len == 0)
298 return .timeout;
299 for (w.os.poll_fds.values()) |poll_fd| {
300 if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try markDirtySteps(w, gpa, poll_fd.fd))
301 return .dirty;
302 }
303 return .clean;
304 }
305 },
306 .windows => struct {
307 const windows = std.os.windows;
308
309 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
310 handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false),
311 ready_dirs: std.DoublyLinkedList,
312
313 const FileId = struct {
314 volumeSerialNumber: windows.ULONG,
315 indexNumber: windows.LARGE_INTEGER,
316 };
317
318 const Directory = struct {
319 reaction_set: ReactionSet,
320 id: FileId,
321 file: Io.File,
322 state: enum { idle, listening, ready },
323 iosb: windows.IO_STATUS_BLOCK,
324 // 64 KB is the packet size limit when monitoring over a network.
325 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks
326 buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)),
327 ready_node: std.DoublyLinkedList.Node,
328
329 /// Start listening for events, buffer field will be overwritten eventually.
330 fn startListening(dir: *Directory, w: *Watch) !void {
331 assert(dir.file.flags.nonblocking);
332 assert(dir.state == .idle);
333 switch (windows.ntdll.NtNotifyChangeDirectoryFileEx(
334 dir.file.handle,
335 null,
336 &notifyApc,
337 w,
338 &dir.iosb,
339 &dir.buffer,
340 dir.buffer.len,
341 .{
342 .FILE_NAME = true,
343 .DIR_NAME = true,
344 .SIZE = true,
345 .LAST_WRITE = true,
346 .CREATION = true,
347 },
348 .FALSE,
349 .Notify,
350 )) {
351 .SUCCESS, .PENDING => dir.state = .listening,
352 .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported,
353 else => |status| return windows.unexpectedStatus(status),
354 }
355 }
356
357 fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void {
358 const w: *Watch = @ptrCast(@alignCast(apc_context));
359 const dir: *Directory = @fieldParentPtr("iosb", iosb);
360 assert(iosb.u.Status != .PENDING);
361 assert(dir.state == .listening);
362 w.os.ready_dirs.append(&dir.ready_node);
363 dir.state = .ready;
364 }
365
366 fn init(gpa: Allocator, path: Cache.Path) !*Directory {
367 // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW)
368 // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW.
369 var dir_handle: windows.HANDLE = undefined;
370 const root_fd = path.root_dir.handle.handle;
371 const sub_path = path.subPathOrDot();
372 const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call
373 var iosb: windows.IO_STATUS_BLOCK = undefined;
374 switch (windows.ntdll.NtCreateFile(
375 &dir_handle,
376 .{
377 .SPECIFIC = .{ .FILE_DIRECTORY = .{
378 .LIST = true,
379 } },
380 .STANDARD = .{ .SYNCHRONIZE = true },
381 .GENERIC = .{ .READ = true },
382 },
383 &.{
384 .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd,
385 .ObjectName = @constCast(&sub_path_w.string()),
386 },
387 &iosb,
388 null,
389 .{},
390 .VALID_FLAGS,
391 .OPEN,
392 .{
393 .DIRECTORY_FILE = true,
394 .IO = .ASYNCHRONOUS,
395 .OPEN_FOR_BACKUP_INTENT = true,
396 },
397 null,
398 0,
399 )) {
400 .SUCCESS => {},
401 .OBJECT_NAME_INVALID => return error.BadPathName,
402 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
403 .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
404 .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
405 .NOT_A_DIRECTORY => return error.NotDir,
406 // This can happen if the directory has 'List folder contents' permission set to 'Deny'
407 .ACCESS_DENIED => return error.AccessDenied,
408 .INVALID_PARAMETER => unreachable,
409 else => |rc| return windows.unexpectedStatus(rc),
410 }
411 assert(dir_handle != windows.INVALID_HANDLE_VALUE);
412 errdefer windows.CloseHandle(dir_handle);
413
414 const dir_id = try getFileId(dir_handle);
415
416 const dir = try gpa.create(Directory);
417 dir.* = .{
418 .reaction_set = .empty,
419 .id = dir_id,
420 .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } },
421 .state = .idle,
422 .iosb = undefined,
423 .buffer = undefined,
424 .ready_node = undefined,
425 };
426 return dir;
427 }
428
429 fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void {
430 state: switch (dir.state) {
431 .idle => {},
432 .listening => {
433 var cancel_iosb: windows.IO_STATUS_BLOCK = undefined;
434 _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb);
435 while (switch (dir.state) {
436 .idle => unreachable,
437 .listening => true,
438 .ready => false,
439 }) Io.Threaded.waitForApcOrAlert();
440 continue :state .ready;
441 },
442 .ready => w.os.ready_dirs.remove(&dir.ready_node),
443 }
444 windows.CloseHandle(dir.file.handle);
445 gpa.destroy(dir);
446 }
447
448 /// Useful to make `*Directory` a key in `std.ArrayHashMap`.
449 const TableAdapter = struct {
450 pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 {
451 return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber)));
452 }
453 pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool {
454 _ = rhs_index;
455 return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and
456 lhs_dir.id.indexNumber == rhs_dir.id.indexNumber;
457 }
458 };
459 };
460
461 fn init(cwd_path: []const u8) !Watch {
462 _ = cwd_path;
463 return .{
464 .dir_table = .{},
465 .dir_count = 0,
466 .os = switch (builtin.os.tag) {
467 .windows => .{
468 .handle_table = .empty,
469 .ready_dirs = .{},
470 },
471 else => {},
472 },
473 .generation = 0,
474 };
475 }
476
477 fn getFileId(handle: windows.HANDLE) !FileId {
478 var file_id: FileId = undefined;
479 var io_status: windows.IO_STATUS_BLOCK = undefined;
480 var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined;
481 switch (windows.ntdll.NtQueryVolumeInformationFile(
482 handle,
483 &io_status,
484 &volume_info,
485 @sizeOf(windows.FILE.FS_VOLUME_INFORMATION),
486 .Volume,
487 )) {
488 .SUCCESS => {},
489 // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
490 // size provided. This is treated as success because the type of variable-length information that this would be relevant for
491 // (name, volume name, etc) we don't care about.
492 .BUFFER_OVERFLOW => {},
493 else => |rc| return windows.unexpectedStatus(rc),
494 }
495 file_id.volumeSerialNumber = volume_info.VolumeSerialNumber;
496 var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined;
497 switch (windows.ntdll.NtQueryInformationFile(
498 handle,
499 &io_status,
500 &internal_info,
501 @sizeOf(windows.FILE.INTERNAL_INFORMATION),
502 .Internal,
503 )) {
504 .SUCCESS => {},
505 else => |rc| return windows.unexpectedStatus(rc),
506 }
507 file_id.indexNumber = internal_info.IndexNumber;
508 return file_id;
509 }
510
511 fn markDirtySteps(w: *Watch, gpa: Allocator, dir: *Directory) !bool {
512 var any_dirty = false;
513 const bytes_returned = dir.iosb.Information;
514 if (bytes_returned == 0) {
515 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
516 markAllFilesDirty(w, gpa);
517 try dir.startListening(w);
518 return true;
519 }
520 var file_name_buf: [std.fs.max_path_bytes]u8 = undefined;
521 var offset: usize = 0;
522 while (true) {
523 const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset]));
524 const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())];
525 if (dir.reaction_set.getPtr(".")) |glob_set|
526 any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
527 if (dir.reaction_set.getPtr(file_name)) |step_set|
528 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
529 if (notify.NextEntryOffset == 0)
530 break;
531
532 offset += notify.NextEntryOffset;
533 }
534
535 // We call this now since at this point we have finished reading dir.buffer.
536 try dir.startListening(w);
537 return any_dirty;
538 }
539
540 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
541 // Add missing marks and note persisted ones.
542 for (steps) |step| {
543 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
544 const dir = dir: {
545 const gop = try w.dir_table.getOrPut(gpa, path);
546 if (!gop.found_existing) {
547 const dir: *Directory = try .init(gpa, path);
548 errdefer dir.deinit(gpa, w);
549 // `dir.id` may already be present in the table in
550 // the case that we have multiple Cache.Path instances
551 // that compare inequal but ultimately point to the same
552 // directory on the file system.
553 // In such case, we must revert adding this directory, but keep
554 // the additions to the step set.
555 const dh_gop = try w.os.handle_table.getOrPut(gpa, dir);
556 if (dh_gop.found_existing) {
557 dir.deinit(gpa, w);
558 _ = w.dir_table.pop();
559 break :dir w.os.handle_table.keys()[dh_gop.index];
560 } else {
561 assert(dh_gop.index == gop.index);
562 try dir.startListening(w);
563 break :dir dir;
564 }
565 }
566 break :dir w.os.handle_table.keys()[gop.index];
567 };
568 for (files.items) |basename| {
569 const gop = try dir.reaction_set.getOrPut(gpa, basename);
570 if (!gop.found_existing) gop.value_ptr.* = .{};
571 try gop.value_ptr.put(gpa, step, w.generation);
572 }
573 }
574 }
575
576 {
577 // Remove marks for files that are no longer inputs.
578 var i: usize = 0;
579 while (i < w.os.handle_table.entries.len) {
580 const dir = w.os.handle_table.keys()[i];
581 {
582 var step_set_i: usize = 0;
583 while (step_set_i < dir.reaction_set.entries.len) {
584 const step_set = &dir.reaction_set.values()[step_set_i];
585 var dirent_i: usize = 0;
586 while (dirent_i < step_set.entries.len) {
587 const generations = step_set.values();
588 if (generations[dirent_i] == w.generation) {
589 dirent_i += 1;
590 continue;
591 }
592 step_set.swapRemoveAt(dirent_i);
593 }
594 if (step_set.entries.len > 0) {
595 step_set_i += 1;
596 continue;
597 }
598 dir.reaction_set.swapRemoveAt(step_set_i);
599 }
600 if (dir.reaction_set.entries.len > 0) {
601 i += 1;
602 continue;
603 }
604 }
605
606 w.dir_table.swapRemoveAt(i);
607 w.os.handle_table.swapRemoveAt(i);
608 dir.deinit(gpa, w);
609 }
610 w.generation +%= 1;
611 }
612 w.dir_count = w.dir_table.count();
613 }
614
615 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
616 for (0..2) |attempt| {
617 while (w.os.ready_dirs.popFirst()) |ready_node| {
618 const dir: *Directory = @fieldParentPtr("ready_node", ready_node);
619 assert(dir.state == .ready);
620 dir.state = .idle;
621 switch (dir.iosb.u.Status) {
622 .SUCCESS => return if (try markDirtySteps(w, gpa, dir)) .dirty else .clean,
623 .PENDING => unreachable,
624 .CANCELLED => {},
625 else => |status| return windows.unexpectedStatus(status),
626 }
627 try dir.startListening(w);
628 }
629 try io.checkCancel();
630 if (attempt == 1) return .timeout;
631 const delay_interval: windows.LARGE_INTEGER = switch (timeout) {
632 .none => std.math.minInt(windows.LARGE_INTEGER),
633 .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100),
634 };
635 _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval);
636 } else unreachable;
637 }
638 },
639 .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct {
640 const posix = std.posix;
641
642 kq_fd: i32,
643 /// Indexes correspond 1:1 with `dir_table`.
644 handles: std.MultiArrayList(struct {
645 rs: ReactionSet,
646 /// If the corresponding dir_table Path has sub_path == "", then it
647 /// suffices as the open directory handle, and this value will be
648 /// -1. Otherwise, it needs to be opened in update(), and will be
649 /// stored here.
650 dir_fd: i32,
651 }),
652
653 const dir_open_flags: posix.O = f: {
654 var f: posix.O = .{
655 .ACCMODE = .RDONLY,
656 .NOFOLLOW = false,
657 .DIRECTORY = true,
658 .CLOEXEC = true,
659 };
660 if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true;
661 if (@hasField(posix.O, "PATH")) f.PATH = true;
662 break :f f;
663 };
664
665 const EV = std.c.EV;
666 const NOTE = std.c.NOTE;
667
668 fn init(cwd_path: []const u8) !Watch {
669 _ = cwd_path;
670 return .{
671 .dir_table = .{},
672 .dir_count = 0,
673 .os = .{
674 .kq_fd = try Io.Kqueue.createFileDescriptor(),
675 .handles = .empty,
676 },
677 .generation = 0,
678 };
679 }
680
681 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
682 const handles = &w.os.handles;
683 for (steps) |step| {
684 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
685 const reaction_set = rs: {
686 const gop = try w.dir_table.getOrPut(gpa, path);
687 if (!gop.found_existing) {
688 const skip_open_dir = path.sub_path.len == 0;
689 const dir_fd = if (skip_open_dir)
690 path.root_dir.handle.handle
691 else
692 posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| {
693 fatal("failed to open directory {f}: {t}", .{ path, err });
694 };
695 // Empirically the dir has to stay open or else no events are triggered.
696 errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd);
697 const changes = [1]posix.Kevent{.{
698 .ident = @bitCast(@as(isize, dir_fd)),
699 .filter = std.c.EVFILT.VNODE,
700 .flags = EV.ADD | EV.ENABLE | EV.CLEAR,
701 .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
702 .data = 0,
703 .udata = gop.index,
704 }};
705 _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null);
706 assert(handles.len == gop.index);
707 try handles.append(gpa, .{
708 .rs = .{},
709 .dir_fd = if (skip_open_dir) -1 else dir_fd,
710 });
711 }
712
713 break :rs &handles.items(.rs)[gop.index];
714 };
715 for (files.items) |basename| {
716 const gop = try reaction_set.getOrPut(gpa, basename);
717 if (!gop.found_existing) gop.value_ptr.* = .{};
718 try gop.value_ptr.put(gpa, step, w.generation);
719 }
720 }
721 }
722
723 {
724 // Remove marks for files that are no longer inputs.
725 var i: usize = 0;
726 while (i < handles.len) {
727 {
728 const reaction_set = &handles.items(.rs)[i];
729 var step_set_i: usize = 0;
730 while (step_set_i < reaction_set.entries.len) {
731 const step_set = &reaction_set.values()[step_set_i];
732 var dirent_i: usize = 0;
733 while (dirent_i < step_set.entries.len) {
734 const generations = step_set.values();
735 if (generations[dirent_i] == w.generation) {
736 dirent_i += 1;
737 continue;
738 }
739 step_set.swapRemoveAt(dirent_i);
740 }
741 if (step_set.entries.len > 0) {
742 step_set_i += 1;
743 continue;
744 }
745 reaction_set.swapRemoveAt(step_set_i);
746 }
747 if (reaction_set.entries.len > 0) {
748 i += 1;
749 continue;
750 }
751 }
752
753 // If the sub_path == "" then this patch has already the
754 // dir fd that we need to use as the ident to remove the
755 // event. If it was opened above with openat() then we need
756 // to access that data via the dir_fd field.
757 const path = w.dir_table.keys()[i];
758 const dir_fd = if (path.sub_path.len == 0)
759 path.root_dir.handle.handle
760 else
761 handles.items(.dir_fd)[i];
762 assert(dir_fd != -1);
763
764 // The changelist also needs to update the udata field of the last
765 // event, since we are doing a swap remove, and we store the dir_table
766 // index in the udata field.
767 const last_dir_fd = fd: {
768 const last_path = w.dir_table.keys()[handles.len - 1];
769 const last_dir_fd = if (last_path.sub_path.len == 0)
770 last_path.root_dir.handle.handle
771 else
772 handles.items(.dir_fd)[handles.len - 1];
773 assert(last_dir_fd != -1);
774 break :fd last_dir_fd;
775 };
776 const changes = [_]posix.Kevent{
777 .{
778 .ident = @bitCast(@as(isize, dir_fd)),
779 .filter = std.c.EVFILT.VNODE,
780 .flags = EV.DELETE,
781 .fflags = 0,
782 .data = 0,
783 .udata = i,
784 },
785 .{
786 .ident = @bitCast(@as(isize, last_dir_fd)),
787 .filter = std.c.EVFILT.VNODE,
788 .flags = EV.ADD,
789 .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
790 .data = 0,
791 .udata = i,
792 },
793 };
794 const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes;
795 _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null);
796 if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd);
797
798 w.dir_table.swapRemoveAt(i);
799 handles.swapRemove(i);
800 }
801 w.generation +%= 1;
802 }
803 w.dir_count = w.dir_table.count();
804 }
805
806 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
807 _ = io;
808 var timespec_buffer: posix.timespec = undefined;
809 var event_buffer: [100]posix.Kevent = undefined;
810 var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer));
811 if (n == 0) return .timeout;
812 const reaction_sets = w.os.handles.items(.rs);
813 var any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], false);
814 timespec_buffer = .{ .sec = 0, .nsec = 0 };
815 while (n == event_buffer.len) {
816 n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer);
817 if (n == 0) break;
818 any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], any_dirty);
819 }
820 return if (any_dirty) .dirty else .clean;
821 }
822
823 fn markDirtySteps(
824 gpa: Allocator,
825 reaction_sets: []ReactionSet,
826 events: []const std.c.Kevent,
827 start_any_dirty: bool,
828 ) bool {
829 var any_dirty = start_any_dirty;
830 for (events) |event| {
831 const index: usize = @intCast(event.udata);
832 const reaction_set = &reaction_sets[index];
833 // If we knew the basename of the changed file, here we would
834 // mark only the step set dirty, and possibly the glob set:
835 //if (reaction_set.getPtr(".")) |glob_set|
836 // any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
837 //if (reaction_set.getPtr(file_name)) |step_set|
838 // any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
839 // However we don't know the file name so just mark all the
840 // sets dirty for this directory.
841 for (reaction_set.values()) |*step_set| {
842 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
843 }
844 }
845 return any_dirty;
846 }
847 },
848 .macos => struct {
849 fse: FsEvents,
850
851 fn init(cwd_path: []const u8) !Watch {
852 return .{
853 .os = .{ .fse = try .init(cwd_path) },
854 .dir_count = 0,
855 .dir_table = undefined,
856 .generation = undefined,
857 };
858 }
859 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
860 try w.os.fse.setPaths(gpa, steps);
861 w.dir_count = w.os.fse.watch_roots.len;
862 }
863 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
864 _ = io;
865 return w.os.fse.wait(gpa, switch (timeout) {
866 .none => null,
867 .ms => |ms| @as(u64, ms) * std.time.ns_per_ms,
868 });
869 }
870 },
871 else => void,
872};
873
874pub fn init(cwd_path: []const u8) !Watch {
875 return Os.init(cwd_path);
876}
877
878pub const Match = struct {
879 /// Relative to the watched directory, the file path that triggers this
880 /// match.
881 basename: []const u8,
882 /// The step to re-run when file corresponding to `basename` is changed.
883 step: *Step,
884
885 pub const Context = struct {
886 pub fn hash(self: Context, a: Match) u32 {
887 _ = self;
888 var hasher = Hash.init(0);
889 std.hash.autoHash(&hasher, a.step);
890 hasher.update(a.basename);
891 return @truncate(hasher.final());
892 }
893 pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool {
894 _ = self;
895 _ = b_index;
896 return a.step == b.step and std.mem.eql(u8, a.basename, b.basename);
897 }
898 };
899};
900
901fn markAllFilesDirty(w: *Watch, gpa: Allocator) void {
902 for (switch (builtin.os.tag) {
903 .windows => w.os.handle_table.keys(),
904 else => w.os.handle_table.values(),
905 }) |item| {
906 const reaction_set = switch (builtin.os.tag) {
907 .linux, .windows => item.reaction_set,
908 else => item,
909 };
910 for (reaction_set.values()) |step_set| {
911 for (step_set.keys()) |step| {
912 _ = step.invalidateResult(gpa);
913 }
914 }
915 }
916}
917
918fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
919 var this_any_dirty = false;
920 for (step_set.keys()) |step| {
921 if (step.invalidateResult(gpa)) this_any_dirty = true;
922 }
923 return any_dirty or this_any_dirty;
924}
925
926pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
927 return Os.update(w, gpa, steps);
928}
929
930pub const Timeout = union(enum) {
931 none,
932 ms: u16,
933
934 pub fn to_i32_ms(t: Timeout) i32 {
935 return switch (t) {
936 .none => -1,
937 .ms => |ms| ms,
938 };
939 }
940
941 pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec {
942 return switch (t) {
943 .none => null,
944 .ms => |ms_u16| {
945 const ms: isize = ms_u16;
946 buf.* = .{
947 .sec = @divTrunc(ms, std.time.ms_per_s),
948 .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms,
949 };
950 return buf;
951 },
952 };
953 }
954};
955
956pub const WaitResult = enum {
957 timeout,
958 /// File system watching triggered on files that were marked as inputs to at least one Step.
959 /// Relevant steps have been marked dirty.
960 dirty,
961 /// File system watching triggered but none of the events were relevant to
962 /// what we are listening to. There is nothing to do.
963 clean,
964};
965
966pub fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
967 return Os.wait(w, gpa, io, timeout);
968}
lib/compiler/maker/Watch/FsEvents.zig created+479
......@@ -0,0 +1,479 @@
1//! An implementation of file-system watching based on the `FSEventStream` API in macOS.
2//! While macOS supports kqueue, it does not allow detecting changes to files without
3//! placing watches on each individual file, meaning FD limits are reached incredibly
4//! quickly. The File System Events API works differently: it implements *recursive*
5//! directory watches, managed by a system service. Rather than being in libc, the API is
6//! exposed by the CoreServices framework. To avoid a compile dependency on the framework
7//! bundle, we dynamically load CoreServices with `std.DynLib`.
8//!
9//! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been
10//! made to keep that specialization to a minimum. Other use cases could be served with
11//! relatively minimal modifications to the `watch_paths` field and its usages (in
12//! particular the `setPaths` function). We avoid using the global GCD dispatch queue in
13//! favour of creating our own and synchronizing with an explicit semaphore, meaning this
14//! logic is thread-safe and does not affect process-global state.
15//!
16//! In theory, this API is quite good at avoiding filesystem race conditions. In practice,
17//! the logic that would avoid them is currently disabled, because the build system kind
18//! of relies on them at the time of writing to avoid redundant work -- see the comment at
19//! the top of `wait` for details.
20
21const enable_debug_logs = false;
22
23core_services: std.DynLib,
24resolved_symbols: ResolvedSymbols,
25
26paths_arena: std.heap.ArenaAllocator.State,
27/// The roots of the recursive watches. FSEvents has relatively small limits on the number
28/// of watched paths, so this slice must not be too long. The paths themselves are allocated
29/// into `paths_arena`, but this slice is allocated into the GPA.
30watch_roots: [][:0]const u8,
31/// All of the paths being watched. Value is the set of steps which depend on the file/directory.
32/// Keys and values are in `paths_arena`, but this map is allocated into the GPA.
33watch_paths: std.StringArrayHashMapUnmanaged([]const *std.Build.Step),
34
35/// The semaphore we use to block the thread calling `wait` until the callback determines a relevant
36/// event has occurred. This is retained across `wait` calls for simplicity and efficiency.
37waiting_semaphore: dispatch.semaphore_t,
38/// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the
39/// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained
40/// across `wait` calls for simplicity and efficiency.
41dispatch_queue: dispatch.queue_t,
42/// In theory, this field avoids race conditions. In practice, it is essentially unused at the time
43/// of writing. See the comment at the start of `wait` for details.
44since_event: FSEventStreamEventId,
45
46cwd_path: []const u8,
47
48/// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols
49/// is not present, `init` will close the framework and return an error.
50const ResolvedSymbols = struct {
51 FSEventStreamCreate: *const fn (
52 allocator: CFAllocatorRef,
53 callback: FSEventStreamCallback,
54 ctx: ?*const FSEventStreamContext,
55 paths_to_watch: CFArrayRef,
56 since_when: FSEventStreamEventId,
57 latency: CFTimeInterval,
58 flags: FSEventStreamCreateFlags,
59 ) callconv(.c) FSEventStreamRef,
60 FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void,
61 FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool,
62 FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void,
63 FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void,
64 FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void,
65 FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId,
66 FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId,
67 CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void,
68 CFArrayCreate: *const fn (
69 allocator: CFAllocatorRef,
70 values: [*]const usize,
71 num_values: CFIndex,
72 call_backs: ?*const CFArrayCallBacks,
73 ) callconv(.c) CFArrayRef,
74 CFStringCreateWithCString: *const fn (
75 alloc: CFAllocatorRef,
76 c_str: [*:0]const u8,
77 encoding: CFStringEncoding,
78 ) callconv(.c) CFStringRef,
79 CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef,
80 kCFAllocatorUseContext: *const CFAllocatorRef,
81};
82
83pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents {
84 var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch
85 return error.OpenFrameworkFailed;
86 errdefer core_services.close();
87
88 var resolved_symbols: ResolvedSymbols = undefined;
89 inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| {
90 @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol;
91 }
92
93 return .{
94 .core_services = core_services,
95 .resolved_symbols = resolved_symbols,
96 .paths_arena = .{},
97 .watch_roots = &.{},
98 .watch_paths = .empty,
99 .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources,
100 .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources,
101 // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order
102 // to notice any changes which happened during said work.
103 .since_event = resolved_symbols.FSEventsGetCurrentEventId(),
104 .cwd_path = cwd_path,
105 };
106}
107
108pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void {
109 fse.waiting_semaphore.as_object().release();
110 fse.dispatch_queue.as_object().release();
111 fse.core_services.close(io);
112
113 gpa.free(fse.watch_roots);
114 fse.watch_paths.deinit(gpa);
115 {
116 var paths_arena = fse.paths_arena.promote(gpa);
117 paths_arena.deinit();
118 }
119}
120
121pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) !void {
122 var paths_arena_instance = fse.paths_arena.promote(gpa);
123 defer fse.paths_arena = paths_arena_instance.state;
124 const paths_arena = paths_arena_instance.allocator();
125
126 var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty;
127 defer need_dirs.deinit(gpa);
128
129 fse.watch_paths.clearRetainingCapacity();
130
131 // We take `step` by pointer for a slight memory optimization in a moment.
132 for (steps) |*step| {
133 for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| {
134 const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{
135 fse.cwd_path, path.root_dir.path orelse ".", path.sub_path,
136 });
137 try need_dirs.put(gpa, resolved_dir, {});
138 for (files.items) |file_name| {
139 const watch_path = if (std.mem.eql(u8, file_name, "."))
140 resolved_dir
141 else
142 try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name });
143 const gop = try fse.watch_paths.getOrPut(gpa, watch_path);
144 if (gop.found_existing) {
145 const old_steps = gop.value_ptr.*;
146 const new_steps = try paths_arena.alloc(*std.Build.Step, old_steps.len + 1);
147 @memcpy(new_steps[0..old_steps.len], old_steps);
148 new_steps[old_steps.len] = step.*;
149 gop.value_ptr.* = new_steps;
150 } else {
151 // This is why we captured `step` by pointer! We can avoid allocating a slice of one
152 // step in the arena in the common case where a file is referenced by only one step.
153 gop.value_ptr.* = step[0..1];
154 }
155 }
156 }
157 }
158
159 {
160 // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar").
161 // To eliminate these, we'll re-add directories in order of path length with a redundancy check.
162 const old_dirs = try gpa.dupe([]const u8, need_dirs.keys());
163 defer gpa.free(old_dirs);
164 std.mem.sort([]const u8, old_dirs, {}, struct {
165 fn lessThan(ctx: void, a: []const u8, b: []const u8) bool {
166 ctx;
167 return std.mem.lessThan(u8, a, b);
168 }
169 }.lessThan);
170 need_dirs.clearRetainingCapacity();
171 for (old_dirs) |dir_path| {
172 var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
173 while (it.next()) |component| {
174 if (need_dirs.contains(component.path)) {
175 // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added
176 break;
177 }
178 } else {
179 need_dirs.putAssumeCapacityNoClobber(dir_path, {});
180 }
181 }
182 }
183
184 // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very
185 // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/`
186 // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit
187 // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be
188 // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below*
189 // that known limit.
190 if (need_dirs.count() > 2048) {
191 // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P
192 if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{});
193 fse.watch_roots = try gpa.realloc(fse.watch_roots, 1);
194 fse.watch_roots[0] = "/";
195 } else {
196 fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count());
197 for (fse.watch_roots, need_dirs.keys()) |*out, in| {
198 out.* = try paths_arena.dupeSentinel(u8, in, 0);
199 }
200 }
201 if (enable_debug_logs) {
202 watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len });
203 for (fse.watch_roots) |dir_path| {
204 watch_log.debug("- '{s}'", .{dir_path});
205 }
206 }
207}
208
209pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!std.Build.Watch.WaitResult {
210 if (fse.watch_roots.len == 0) @panic("nothing to watch");
211
212 const rs = fse.resolved_symbols;
213
214 // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds
215 // to occur, because one step modifies a file which is an input to another step. The solution
216 // to this problem will probably be either:
217 //
218 // a) Don't include the output of one step as a watch input of another; only mark external
219 // files as watch inputs. Or...
220 //
221 // b) Note the current event ID when a step begins, and disregard events preceding that ID
222 // when considering whether to dirty that step in `eventCallback`.
223 //
224 // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does
225 // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those
226 // too at the time of writing, so this is kind of expected.
227 fse.since_event = .since_now;
228
229 const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{
230 .version = 0,
231 .info = @constCast(&gpa),
232 .retain = null,
233 .release = null,
234 .copy_description = null,
235 .allocate = &cf_alloc_callbacks.allocate,
236 .reallocate = &cf_alloc_callbacks.reallocate,
237 .deallocate = &cf_alloc_callbacks.deallocate,
238 .preferred_size = null,
239 }) orelse return error.OutOfMemory;
240 defer rs.CFRelease(cf_allocator);
241
242 const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len);
243 @memset(cf_paths, null);
244 defer {
245 for (cf_paths) |o| if (o) |p| rs.CFRelease(p);
246 gpa.free(cf_paths);
247 }
248 for (fse.watch_roots, cf_paths) |raw_path, *cf_path| {
249 cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8);
250 }
251 const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null);
252 defer rs.CFRelease(cf_paths_array);
253
254 const callback_ctx: EventCallbackCtx = .{
255 .fse = fse,
256 .gpa = gpa,
257 };
258 const event_stream = rs.FSEventStreamCreate(
259 null,
260 &eventCallback,
261 &.{
262 .version = 0,
263 .info = @constCast(&callback_ctx),
264 .retain = null,
265 .release = null,
266 .copy_description = null,
267 },
268 cf_paths_array,
269 fse.since_event,
270 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events
271 .{ .watch_root = true, .file_events = true },
272 );
273 defer rs.FSEventStreamRelease(event_stream);
274 rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue);
275 defer rs.FSEventStreamInvalidate(event_stream);
276 if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed;
277 defer rs.FSEventStreamStop(event_stream);
278 const result = fse.waiting_semaphore.wait(timeout: {
279 const ns = timeout_ns orelse break :timeout .FOREVER;
280 break :timeout .time(.NOW, @intCast(ns));
281 });
282 return switch (result) {
283 0 => .dirty,
284 else => .timeout,
285 };
286}
287
288const cf_alloc_callbacks = struct {
289 const log = std.log.scoped(.cf_alloc);
290 fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
291 if (enable_debug_logs) log.debug("allocate {d}", .{size});
292 _ = hint;
293 const gpa: *const Allocator = @ptrCast(@alignCast(info));
294 const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null;
295 const metadata: *usize = @ptrCast(mem);
296 metadata.* = @intCast(size);
297 return mem[@sizeOf(usize)..].ptr;
298 }
299 fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
300 if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size });
301 _ = hint;
302 if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL
303 const gpa: *const Allocator = @ptrCast(@alignCast(info));
304 const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
305 const old_size = @as(*const usize, @ptrCast(old_base)).*;
306 const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
307 const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null;
308 const metadata: *usize = @ptrCast(new_mem);
309 metadata.* = @intCast(new_size);
310 return new_mem[@sizeOf(usize)..].ptr;
311 }
312 fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void {
313 if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr});
314 const gpa: *const Allocator = @ptrCast(@alignCast(info));
315 const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
316 const old_size = @as(*const usize, @ptrCast(old_base)).*;
317 const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
318 gpa.free(old_mem);
319 }
320};
321
322const EventCallbackCtx = struct {
323 fse: *FsEvents,
324 gpa: Allocator,
325};
326
327fn eventCallback(
328 stream: ConstFSEventStreamRef,
329 client_callback_info: ?*anyopaque,
330 num_events: usize,
331 events_paths_ptr: *anyopaque,
332 events_flags_ptr: [*]const FSEventStreamEventFlags,
333 events_ids_ptr: [*]const FSEventStreamEventId,
334) callconv(.c) void {
335 const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info));
336 const fse = ctx.fse;
337 const gpa = ctx.gpa;
338 const rs = fse.resolved_symbols;
339 const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr));
340 const events_paths = events_paths_ptr_casted[0..num_events];
341 const events_ids = events_ids_ptr[0..num_events];
342 const events_flags = events_flags_ptr[0..num_events];
343 var any_dirty = false;
344 for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| {
345 _ = event_id;
346 if (event_flags.history_done) continue; // sentinel
347 const event_path = std.mem.span(event_path_nts);
348 switch (event_flags.must_scan_sub_dirs) {
349 false => {
350 if (fse.watch_paths.get(event_path)) |steps| {
351 assert(steps.len > 0);
352 for (steps) |s| {
353 if (s.invalidateResult(gpa)) any_dirty = true;
354 }
355 }
356 if (std.fs.path.dirname(event_path)) |event_dirname| {
357 // Modifying '/foo/bar' triggers the watch on '/foo'.
358 if (fse.watch_paths.get(event_dirname)) |steps| {
359 assert(steps.len > 0);
360 for (steps) |s| {
361 if (s.invalidateResult(gpa)) any_dirty = true;
362 }
363 }
364 }
365 },
366 true => {
367 // This is unlikely, but can occasionally happen when bottlenecked: events have been
368 // coalesced into one. We want to see if any of these events are actually relevant
369 // to us. The only way we can reasonably do that in this rare edge case is iterate
370 // the watch paths and see if any is under this directory. That's acceptable because
371 // we would otherwise kick off a rebuild which would be clearing those paths anyway.
372 const changed_path = std.fs.path.dirname(event_path) orelse event_path;
373 for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| {
374 if (dirStartsWith(watching_path, changed_path)) {
375 for (steps) |s| {
376 if (s.invalidateResult(gpa)) any_dirty = true;
377 }
378 }
379 }
380 },
381 }
382 }
383 if (any_dirty) {
384 fse.since_event = rs.FSEventStreamGetLatestEventId(stream);
385 _ = fse.waiting_semaphore.signal();
386 }
387}
388fn dirStartsWith(path: []const u8, prefix: []const u8) bool {
389 if (std.mem.eql(u8, path, prefix)) return true;
390 if (!std.mem.startsWith(u8, path, prefix)) return false;
391 if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar`
392 return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar`
393}
394
395const CFAllocatorRef = ?*const opaque {};
396const CFArrayRef = *const opaque {};
397const CFStringRef = *const opaque {};
398const CFTimeInterval = f64;
399const CFIndex = i32;
400const CFOptionFlags = enum(u32) { _ };
401const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque;
402const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void;
403const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef;
404const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
405const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
406const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void;
407const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex;
408const CFAllocatorContext = extern struct {
409 version: CFIndex,
410 info: ?*anyopaque,
411 retain: ?CFAllocatorRetainCallBack,
412 release: ?CFAllocatorReleaseCallBack,
413 copy_description: ?CFAllocatorCopyDescriptionCallBack,
414 allocate: CFAllocatorAllocateCallBack,
415 reallocate: ?CFAllocatorReallocateCallBack,
416 deallocate: ?CFAllocatorDeallocateCallBack,
417 preferred_size: ?CFAllocatorPreferredSizeCallBack,
418};
419const CFArrayCallBacks = opaque {};
420const CFStringEncoding = enum(u32) {
421 invalid_id = std.math.maxInt(u32),
422 mac_roman = 0,
423 windows_latin_1 = 0x500,
424 iso_latin_1 = 0x201,
425 next_step_latin = 0xB01,
426 ascii = 0x600,
427 unicode = 0x100,
428 utf8 = 0x8000100,
429 non_lossy_ascii = 0xBFF,
430};
431
432const FSEventStreamRef = *opaque {};
433const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child;
434const FSEventStreamCallback = *const fn (
435 stream: ConstFSEventStreamRef,
436 client_callback_info: ?*anyopaque,
437 num_events: usize,
438 event_paths: *anyopaque,
439 event_flags: [*]const FSEventStreamEventFlags,
440 event_ids: [*]const FSEventStreamEventId,
441) callconv(.c) void;
442const FSEventStreamContext = extern struct {
443 version: CFIndex,
444 info: ?*anyopaque,
445 retain: ?CFAllocatorRetainCallBack,
446 release: ?CFAllocatorReleaseCallBack,
447 copy_description: ?CFAllocatorCopyDescriptionCallBack,
448};
449const FSEventStreamEventId = enum(u64) {
450 since_now = std.math.maxInt(u64),
451 _,
452};
453const FSEventStreamCreateFlags = packed struct(u32) {
454 use_cf_types: bool = false,
455 no_defer: bool = false,
456 watch_root: bool = false,
457 ignore_self: bool = false,
458 file_events: bool = false,
459 _: u27 = 0,
460};
461const FSEventStreamEventFlags = packed struct(u32) {
462 must_scan_sub_dirs: bool,
463 user_dropped: bool,
464 kernel_dropped: bool,
465 event_ids_wrapped: bool,
466 history_done: bool,
467 root_changed: bool,
468 mount: bool,
469 unmount: bool,
470 _: u24 = 0,
471};
472
473const dispatch = std.c.dispatch;
474const std = @import("std");
475const Io = std.Io;
476const assert = std.debug.assert;
477const Allocator = std.mem.Allocator;
478const watch_log = std.log.scoped(.watch);
479const FsEvents = @This();
lib/compiler/maker/WebServer.zig created+926
......@@ -0,0 +1,926 @@
1gpa: Allocator,
2graph: *const Build.Graph,
3all_steps: []const *Build.Step,
4listen_address: net.IpAddress,
5root_prog_node: std.Progress.Node,
6watch: bool,
7
8tcp_server: ?net.Server,
9serve_task: ?Io.Future(Io.Cancelable!void),
10
11/// Uses `Io.Clock.awake`.
12base_timestamp: Io.Timestamp,
13/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
14step_names_trailing: []u8,
15
16/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
17/// Accessed atomically.
18step_status_bits: []u8,
19
20fuzz: ?Fuzz,
21time_report_mutex: Io.Mutex,
22time_report_msgs: [][]u8,
23time_report_update_times: []i64,
24
25build_status: std.atomic.Value(abi.BuildStatus),
26/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
27/// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so
28/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it
29/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For
30/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes,
31/// because this value changes quickly so this would result in constantly spamming all clients with
32/// an unreasonable number of packets.
33update_id: std.atomic.Value(u32),
34
35runner_request_mutex: Io.Mutex,
36runner_request_ready_cond: Io.Condition,
37runner_request_empty_cond: Io.Condition,
38runner_request: ?RunnerRequest,
39
40/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
41/// on a fixed interval of this many milliseconds.
42const default_update_interval_ms = 500;
43
44pub const base_clock: Io.Clock = .awake;
45
46/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
47pub fn notifyUpdate(ws: *WebServer) void {
48 _ = ws.update_id.rmw(.Add, 1, .release);
49 ws.graph.io.futexWake(u32, &ws.update_id.raw, 16);
50}
51
52pub const Options = struct {
53 gpa: Allocator,
54 graph: *const std.Build.Graph,
55 all_steps: []const *Build.Step,
56 root_prog_node: std.Progress.Node,
57 watch: bool,
58 listen_address: net.IpAddress,
59 base_timestamp: Io.Clock.Timestamp,
60};
61pub fn init(opts: Options) WebServer {
62 // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent`
63 // instead of threads, so that the web server can function in single-threaded builds.
64 comptime assert(!builtin.single_threaded);
65 assert(opts.base_timestamp.clock == base_clock);
66
67 const all_steps = opts.all_steps;
68
69 const step_names_trailing = opts.gpa.alloc(u8, len: {
70 var name_bytes: usize = 0;
71 for (all_steps) |step| name_bytes += step.name.len;
72 break :len name_bytes + all_steps.len * 4;
73 }) catch @panic("out of memory");
74 {
75 const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
76 var idx: usize = all_steps.len * 4;
77 for (all_steps, step_name_lens) |step, *name_len| {
78 name_len.* = @intCast(step.name.len);
79 @memcpy(step_names_trailing[idx..][0..step.name.len], step.name);
80 idx += step.name.len;
81 }
82 assert(idx == step_names_trailing.len);
83 }
84
85 const step_status_bits = opts.gpa.alloc(
86 u8,
87 std.math.divCeil(usize, all_steps.len, 4) catch unreachable,
88 ) catch @panic("out of memory");
89 @memset(step_status_bits, 0);
90
91 const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0;
92 const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory");
93 const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory");
94 @memset(time_report_msgs, &.{});
95 @memset(time_report_update_times, std.math.minInt(i64));
96
97 return .{
98 .gpa = opts.gpa,
99 .graph = opts.graph,
100 .all_steps = all_steps,
101 .listen_address = opts.listen_address,
102 .root_prog_node = opts.root_prog_node,
103 .watch = opts.watch,
104
105 .tcp_server = null,
106 .serve_task = null,
107
108 .base_timestamp = opts.base_timestamp.raw,
109 .step_names_trailing = step_names_trailing,
110
111 .step_status_bits = step_status_bits,
112
113 .fuzz = null,
114 .time_report_mutex = .init,
115 .time_report_msgs = time_report_msgs,
116 .time_report_update_times = time_report_update_times,
117
118 .build_status = .init(.idle),
119 .update_id = .init(0),
120
121 .runner_request_mutex = .init,
122 .runner_request_ready_cond = .init,
123 .runner_request_empty_cond = .init,
124 .runner_request = null,
125 };
126}
127pub fn deinit(ws: *WebServer) void {
128 const gpa = ws.gpa;
129 const io = ws.graph.io;
130
131 gpa.free(ws.step_names_trailing);
132 gpa.free(ws.step_status_bits);
133
134 if (ws.fuzz) |*f| f.deinit();
135 for (ws.time_report_msgs) |msg| gpa.free(msg);
136 gpa.free(ws.time_report_msgs);
137 gpa.free(ws.time_report_update_times);
138
139 if (ws.serve_task) |t| {
140 if (ws.tcp_server) |*s| s.stream.close(io);
141 t.await();
142 }
143 if (ws.tcp_server) |*s| s.deinit();
144
145 gpa.free(ws.step_names_trailing);
146}
147pub fn start(ws: *WebServer) error{AlreadyReported}!void {
148 assert(ws.tcp_server == null);
149 assert(ws.serve_task == null);
150 const io = ws.graph.io;
151
152 ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
153 log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err });
154 return error.AlreadyReported;
155 };
156 ws.serve_task = io.concurrent(serve, .{ws}) catch |err| {
157 log.err("unable to spawn web server thread: {t}", .{err});
158 ws.tcp_server.?.deinit(io);
159 ws.tcp_server = null;
160 return error.AlreadyReported;
161 };
162
163 log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address});
164 if (ws.listen_address.getPort() == 0) {
165 log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address});
166 }
167}
168fn serve(ws: *WebServer) Io.Cancelable!void {
169 const io = ws.graph.io;
170 var group: Io.Group = .init;
171 defer group.cancel(io);
172 while (true) {
173 var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) {
174 error.Canceled => |e| return e,
175 else => |e| {
176 log.err("failed to accept connection: {t}", .{e});
177 return;
178 },
179 };
180 group.concurrent(io, accept, .{ ws, stream }) catch |err| {
181 log.err("unable to spawn connection thread: {t}", .{err});
182 stream.close(io);
183 continue;
184 };
185 }
186}
187
188pub fn startBuild(ws: *WebServer) void {
189 if (ws.fuzz) |*fuzz| {
190 fuzz.deinit();
191 ws.fuzz = null;
192 }
193 for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
194 ws.build_status.store(.running, .monotonic);
195 ws.notifyUpdate();
196}
197
198pub fn updateStepStatus(ws: *WebServer, step: *Build.Step, new_status: abi.StepUpdate.Status) void {
199 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
200 if (s == step) break @intCast(i);
201 } else unreachable;
202 const ptr = &ws.step_status_bits[step_idx / 4];
203 const bit_offset: u3 = @intCast((step_idx % 4) * 2);
204 const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
205 const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset;
206 _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
207 ws.notifyUpdate();
208}
209
210pub fn finishBuild(ws: *WebServer, opts: struct {
211 fuzz: bool,
212}) void {
213 if (opts.fuzz) {
214 switch (builtin.os.tag) {
215 // Current implementation depends on two things that need to be ported to Windows:
216 // * Memory-mapping to share data between the fuzzer and build runner.
217 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
218 // many addresses to source locations).
219 .windows => std.process.fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
220 else => {},
221 }
222 if (@bitSizeOf(usize) != 64) {
223 // Current implementation depends on posix.mmap()'s second
224 // parameter, `length: usize`, being compatible with file system's
225 // u64 return value. This is not the case on 32-bit platforms.
226 // Affects or affected by issues #5185, #22523, and #22464.
227 std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
228 }
229
230 assert(ws.fuzz == null);
231
232 ws.build_status.store(.fuzz_init, .monotonic);
233 ws.notifyUpdate();
234
235 ws.fuzz = Fuzz.init(
236 ws.gpa,
237 ws.graph.io,
238 ws.all_steps,
239 ws.root_prog_node,
240 .{ .forever = .{ .ws = ws } },
241 ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
242 ws.fuzz.?.start();
243 }
244
245 ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic);
246 ws.notifyUpdate();
247}
248
249pub fn now(s: *const WebServer) i64 {
250 const io = s.graph.io;
251 const ts = base_clock.now(io);
252 return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds());
253}
254
255fn accept(ws: *WebServer, stream: net.Stream) void {
256 const io = ws.graph.io;
257 defer {
258 // `net.Stream.close` wants to helpfully overwrite `stream` with
259 // `undefined`, but it cannot do so since it is an immutable parameter.
260 var copy = stream;
261 copy.close(io);
262 }
263 var send_buffer: [4096]u8 = undefined;
264 var recv_buffer: [4096]u8 = undefined;
265 var connection_reader = stream.reader(io, &recv_buffer);
266 var connection_writer = stream.writer(io, &send_buffer);
267 var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface);
268
269 while (true) {
270 var request = server.receiveHead() catch |err| switch (err) {
271 error.HttpConnectionClosing => return,
272 else => return log.err("failed to receive http request: {t}", .{err}),
273 };
274 switch (request.upgradeRequested()) {
275 .websocket => |opt_key| {
276 const key = opt_key orelse return log.err("missing websocket key", .{});
277 var web_socket = request.respondWebSocket(.{ .key = key }) catch {
278 return log.err("failed to respond web socket: {t}", .{connection_writer.err.?});
279 };
280 ws.serveWebSocket(&web_socket) catch |err| {
281 log.err("failed to serve websocket: {t}", .{err});
282 return;
283 };
284 comptime unreachable;
285 },
286 .other => |name| return log.err("unknown upgrade request: {s}", .{name}),
287 .none => {
288 ws.serveRequest(&request) catch |err| switch (err) {
289 error.AlreadyReported => return,
290 else => {
291 log.err("failed to serve '{s}': {t}", .{ request.head.target, err });
292 return;
293 },
294 };
295 },
296 }
297 }
298}
299
300fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
301 const io = ws.graph.io;
302
303 var prev_build_status = ws.build_status.load(.monotonic);
304
305 const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len);
306 defer ws.gpa.free(prev_step_status_bits);
307 for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| {
308 copy.* = @atomicLoad(u8, shared, .monotonic);
309 }
310
311 var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock });
312 defer recv_thread.cancel(io);
313
314 {
315 const hello_header: abi.Hello = .{
316 .status = prev_build_status,
317 .flags = .{
318 .time_report = ws.graph.time_report,
319 },
320 .timestamp = ws.now(),
321 .steps_len = @intCast(ws.all_steps.len),
322 };
323 var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits };
324 try sock.writeMessageVec(&bufs, .binary);
325 }
326
327 var prev_fuzz: Fuzz.Previous = .init;
328 var prev_time: i64 = std.math.minInt(i64);
329 while (true) {
330 const start_time = ws.now();
331 const start_update_id = ws.update_id.load(.acquire);
332
333 if (ws.fuzz) |*fuzz| {
334 try fuzz.sendUpdate(sock, &prev_fuzz);
335 }
336
337 {
338 try ws.time_report_mutex.lock(io);
339 defer ws.time_report_mutex.unlock(io);
340 for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| {
341 if (update_time <= prev_time) continue;
342 // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so
343 // that we don't hold up the build system on the client accepting this packet.
344 const owned_msg = try ws.gpa.dupe(u8, msg);
345 defer ws.gpa.free(owned_msg);
346 // Temporarily unlock, then re-lock after the message is sent.
347 ws.time_report_mutex.unlock(io);
348 defer ws.time_report_mutex.lockUncancelable(io);
349 try sock.writeMessage(owned_msg, .binary);
350 }
351 }
352
353 {
354 const build_status = ws.build_status.load(.monotonic);
355 if (build_status != prev_build_status) {
356 prev_build_status = build_status;
357 const msg: abi.StatusUpdate = .{ .new = build_status };
358 try sock.writeMessage(@ptrCast(&msg), .binary);
359 }
360 }
361
362 for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
363 const cur_byte = @atomicLoad(u8, shared, .monotonic);
364 if (prev_byte.* == cur_byte) continue;
365 const cur: [4]abi.StepUpdate.Status = .{
366 @enumFromInt(@as(u2, @truncate(cur_byte >> 0))),
367 @enumFromInt(@as(u2, @truncate(cur_byte >> 2))),
368 @enumFromInt(@as(u2, @truncate(cur_byte >> 4))),
369 @enumFromInt(@as(u2, @truncate(cur_byte >> 6))),
370 };
371 const prev: [4]abi.StepUpdate.Status = .{
372 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))),
373 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))),
374 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))),
375 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))),
376 };
377 for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| {
378 const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } };
379 if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary);
380 }
381 prev_byte.* = cur_byte;
382 }
383
384 prev_time = start_time;
385
386 const old_cp = io.swapCancelProtection(.blocked);
387 defer _ = io.swapCancelProtection(old_cp);
388 io.futexWaitTimeout(
389 u32,
390 &ws.update_id.raw,
391 start_update_id,
392 .{ .duration = .{
393 .clock = .awake,
394 .raw = .fromMilliseconds(default_update_interval_ms),
395 } },
396 ) catch |err| switch (err) {
397 error.Canceled => unreachable,
398 };
399 }
400}
401fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
402 const io = ws.graph.io;
403
404 while (true) {
405 const msg = sock.readSmallMessage() catch return;
406 if (msg.opcode != .binary) continue;
407 if (msg.data.len == 0) continue;
408 const tag: abi.ToServerTag = @enumFromInt(msg.data[0]);
409 switch (tag) {
410 _ => continue,
411 .rebuild => while (true) {
412 ws.runner_request_mutex.lock(io) catch |err| switch (err) {
413 error.Canceled => return,
414 };
415 defer ws.runner_request_mutex.unlock(io);
416 if (ws.runner_request == null) {
417 ws.runner_request = .rebuild;
418 ws.runner_request_ready_cond.signal(io);
419 break;
420 }
421 ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return;
422 },
423 }
424 }
425}
426
427fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void {
428 // Strip an optional leading '/debug' component from the request.
429 const target: []const u8, const debug: bool = target: {
430 if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true };
431 if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true };
432 if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true };
433 break :target .{ req.head.target, false };
434 };
435
436 if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html");
437 if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript");
438 if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css");
439 if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css");
440 if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast);
441
442 if (ws.fuzz) |*fuzz| {
443 if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req);
444 }
445
446 try req.respond("not found", .{
447 .status = .not_found,
448 .extra_headers = &.{
449 .{ .name = "Content-Type", .value = "text/plain" },
450 },
451 });
452}
453
454fn serveLibFile(
455 ws: *WebServer,
456 request: *http.Server.Request,
457 sub_path: []const u8,
458 content_type: []const u8,
459) !void {
460 return serveFile(ws, request, .{
461 .root_dir = ws.graph.zig_lib_directory,
462 .sub_path = sub_path,
463 }, content_type);
464}
465fn serveClientWasm(
466 ws: *WebServer,
467 req: *http.Server.Request,
468 optimize_mode: std.builtin.OptimizeMode,
469) !void {
470 var arena_state: std.heap.ArenaAllocator = .init(ws.gpa);
471 defer arena_state.deinit();
472 const arena = arena_state.allocator();
473
474 // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page.
475 const bin_path = try buildClientWasm(ws, arena, optimize_mode);
476 return serveFile(ws, req, bin_path, "application/wasm");
477}
478
479pub fn serveFile(
480 ws: *WebServer,
481 request: *http.Server.Request,
482 path: Cache.Path,
483 content_type: []const u8,
484) !void {
485 const gpa = ws.gpa;
486 const io = ws.graph.io;
487 // The desired API is actually sendfile, which will require enhancing http.Server.
488 // We load the file with every request so that the user can make changes to the file
489 // and refresh the HTML page without restarting this server.
490 const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| {
491 log.err("failed to read '{f}': {t}", .{ path, err });
492 return error.AlreadyReported;
493 };
494 defer gpa.free(file_contents);
495 try request.respond(file_contents, .{
496 .extra_headers = &.{
497 .{ .name = "Content-Type", .value = content_type },
498 cache_control_header,
499 },
500 });
501}
502pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
503 const graph = ws.graph;
504 const io = graph.io;
505
506 var send_buffer: [0x4000]u8 = undefined;
507 var response = try request.respondStreaming(&send_buffer, .{
508 .respond_options = .{
509 .extra_headers = &.{
510 .{ .name = "Content-Type", .value = "application/x-tar" },
511 cache_control_header,
512 },
513 },
514 });
515
516 var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
517
518 for (paths) |path| {
519 var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| {
520 log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
521 continue;
522 };
523 defer file.close(io);
524 const stat = try file.stat(io);
525 var read_buffer: [1024]u8 = undefined;
526 var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size);
527
528 // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can
529 // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI:
530 // it turns out the WASM treats the first path component as the module name, typically
531 // resulting in modules named "" and "src". The compiler needs to tell the build system
532 // about the module graph so that the build system can correctly encode this information in
533 // the tar file.
534 //
535 // Additionally, this needs to ensure that all path separators for both prefix and
536 // sub_path are using the POSIX-style `/` on platforms that don't use it as their native
537 // path separator.
538 archiver.prefix = path.root_dir.path orelse graph.cache.cwd;
539 try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds()));
540 }
541
542 // intentionally not calling `archiver.finishPedantically`
543 try response.end();
544}
545
546fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path {
547 const root_name = "build-web";
548 const arch_os_abi = "wasm32-freestanding";
549 const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
550
551 const gpa = ws.gpa;
552 const graph = ws.graph;
553 const io = graph.io;
554
555 const main_src_path: Cache.Path = .{
556 .root_dir = graph.zig_lib_directory,
557 .sub_path = "build-web/main.zig",
558 };
559 const walk_src_path: Cache.Path = .{
560 .root_dir = graph.zig_lib_directory,
561 .sub_path = "docs/wasm/Walk.zig",
562 };
563 const html_render_src_path: Cache.Path = .{
564 .root_dir = graph.zig_lib_directory,
565 .sub_path = "docs/wasm/html_render.zig",
566 };
567
568 var argv: std.ArrayList([]const u8) = .empty;
569
570 try argv.appendSlice(arena, &.{
571 graph.zig_exe, "build-exe", //
572 "-fno-entry", //
573 "-O", @tagName(optimize), //
574 "-target", arch_os_abi, //
575 "-mcpu", cpu_features, //
576 "--cache-dir", graph.global_cache_root.path orelse ".", //
577 "--global-cache-dir", graph.global_cache_root.path orelse ".", //
578 "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
579 "--name", root_name, //
580 "-rdynamic", //
581 "-fsingle-threaded", //
582 "--dep", "Walk", //
583 "--dep", "html_render", //
584 try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), //
585 try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), //
586 "--dep", "Walk", //
587 try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), //
588 "--listen=-",
589 });
590
591 var child = try std.process.spawn(io, .{
592 .argv = argv.items,
593 .environ_map = &graph.environ_map,
594 .stdin = .pipe,
595 .stdout = .pipe,
596 .stderr = .pipe,
597 });
598 defer child.kill(io);
599
600 var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited });
601 defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
602
603 var stdout_buffer: [512]u8 = undefined;
604 var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
605 const stdout = &stdout_reader.interface;
606
607 {
608 var w = child.stdin.?.writer(io, &.{});
609 w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
610 error.WriteFailed => return w.err.?,
611 };
612 w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
613 error.WriteFailed => return w.err.?,
614 };
615 }
616
617 const Header = std.zig.Server.Message.Header;
618
619 var result: ?Cache.Path = null;
620 var result_error_bundle = std.zig.ErrorBundle.empty;
621 var body_buffer: std.ArrayList(u8) = .empty;
622 defer body_buffer.deinit(gpa);
623
624 while (true) {
625 const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
626 error.ReadFailed => |e| return e,
627 error.EndOfStream => break,
628 };
629 body_buffer.clearRetainingCapacity();
630 try stdout.appendExact(gpa, &body_buffer, header.bytes_len);
631 const body = body_buffer.items;
632
633 switch (header.tag) {
634 .zig_version => {
635 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
636 return error.ZigProtocolVersionMismatch;
637 }
638 },
639 .error_bundle => {
640 result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
641 },
642 .emit_digest => {
643 const EmitDigest = std.zig.Server.Message.EmitDigest;
644 const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
645 if (!ebp_hdr.flags.cache_hit) {
646 log.info("source changes detected; rebuilt wasm component", .{});
647 }
648 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
649 result = .{
650 .root_dir = graph.global_cache_root,
651 .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
652 };
653 },
654 else => {}, // ignore other messages
655 }
656 }
657
658 const stderr_contents = try stderr_task.await(io);
659 if (stderr_contents.len > 0) {
660 std.debug.print("{s}", .{stderr_contents});
661 }
662
663 // Send EOF to stdin.
664 child.stdin.?.close(io);
665 child.stdin = null;
666
667 switch (try child.wait(io)) {
668 .exited => |code| {
669 if (code != 0) {
670 log.err(
671 "the following command exited with error code {d}:\n{s}",
672 .{ code, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
673 );
674 return error.WasmCompilationFailed;
675 }
676 },
677 .signal => |sig| {
678 log.err(
679 "the following command terminated with signal {t}:\n{s}",
680 .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
681 );
682 return error.WasmCompilationFailed;
683 },
684 .stopped => |sig| {
685 log.err(
686 "the following command stopped unexpectedly with signal {t}:\n{s}",
687 .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
688 );
689 return error.WasmCompilationFailed;
690 },
691 .unknown => {
692 log.err(
693 "the following command terminated unexpectedly:\n{s}",
694 .{try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items)},
695 );
696 return error.WasmCompilationFailed;
697 },
698 }
699
700 if (result_error_bundle.errorMessageCount() > 0) {
701 try result_error_bundle.renderToStderr(io, .{}, .auto);
702 log.err("the following command failed with {d} compilation errors:\n{s}", .{
703 result_error_bundle.errorMessageCount(),
704 try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
705 });
706 return error.WasmCompilationFailed;
707 }
708
709 const base_path = result orelse {
710 log.err("child process failed to report result\n{s}", .{
711 try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
712 });
713 return error.WasmCompilationFailed;
714 };
715 const bin_name = try std.zig.binNameAlloc(arena, .{
716 .root_name = root_name,
717 .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
718 .arch_os_abi = arch_os_abi,
719 .cpu_features = cpu_features,
720 }) catch unreachable) catch unreachable),
721 .output_mode = .Exe,
722 });
723 return base_path.join(arena, bin_name);
724}
725
726fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
727 var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
728 return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
729 error.ReadFailed => return file_reader.err.?,
730 else => |e| return e,
731 };
732}
733
734pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
735 compile: *Build.Step.Compile,
736
737 use_llvm: bool,
738 stats: abi.time_report.CompileResult.Stats,
739 ns_total: u64,
740
741 llvm_pass_timings_len: u32,
742 files_len: u32,
743 decls_len: u32,
744
745 /// The trailing data of `abi.time_report.CompileResult`, except the step name.
746 trailing: []const u8,
747}) void {
748 const gpa = ws.gpa;
749 const io = ws.graph.io;
750
751 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
752 if (s == &opts.compile.step) break @intCast(i);
753 } else unreachable;
754
755 const old_buf = old: {
756 ws.time_report_mutex.lock(io) catch return;
757 defer ws.time_report_mutex.unlock(io);
758 const old = ws.time_report_msgs[step_idx];
759 ws.time_report_msgs[step_idx] = &.{};
760 break :old old;
761 };
762 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
763
764 const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]);
765 out_header.* = .{
766 .step_idx = step_idx,
767 .flags = .{
768 .use_llvm = opts.use_llvm,
769 },
770 .stats = opts.stats,
771 .ns_total = opts.ns_total,
772 .llvm_pass_timings_len = opts.llvm_pass_timings_len,
773 .files_len = opts.files_len,
774 .decls_len = opts.decls_len,
775 };
776 @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
777
778 {
779 ws.time_report_mutex.lock(io) catch return;
780 defer ws.time_report_mutex.unlock(io);
781 assert(ws.time_report_msgs[step_idx].len == 0);
782 ws.time_report_msgs[step_idx] = buf;
783 ws.time_report_update_times[step_idx] = ws.now();
784 }
785 ws.notifyUpdate();
786}
787
788pub fn updateTimeReportGeneric(ws: *WebServer, step: *Build.Step, duration: Io.Duration) void {
789 const gpa = ws.gpa;
790 const io = ws.graph.io;
791
792 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
793 if (s == step) break @intCast(i);
794 } else unreachable;
795
796 const old_buf = old: {
797 ws.time_report_mutex.lock(io) catch return;
798 defer ws.time_report_mutex.unlock(io);
799 const old = ws.time_report_msgs[step_idx];
800 ws.time_report_msgs[step_idx] = &.{};
801 break :old old;
802 };
803 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
804 const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf);
805 out.* = .{
806 .step_idx = step_idx,
807 .ns_total = @intCast(duration.toNanoseconds()),
808 };
809 {
810 ws.time_report_mutex.lock(io) catch return;
811 defer ws.time_report_mutex.unlock(io);
812 assert(ws.time_report_msgs[step_idx].len == 0);
813 ws.time_report_msgs[step_idx] = buf;
814 ws.time_report_update_times[step_idx] = ws.now();
815 }
816 ws.notifyUpdate();
817}
818
819pub fn updateTimeReportRunTest(
820 ws: *WebServer,
821 run: *Build.Step.Run,
822 tests: *const Build.Step.Run.CachedTestMetadata,
823 ns_per_test: []const u64,
824) void {
825 const gpa = ws.gpa;
826 const io = ws.graph.io;
827
828 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
829 if (s == &run.step) break @intCast(i);
830 } else unreachable;
831
832 assert(tests.names.len == ns_per_test.len);
833 const tests_len: u32 = @intCast(tests.names.len);
834
835 const new_len: u64 = len: {
836 var names_len: u64 = 0;
837 for (0..tests_len) |i| {
838 names_len += tests.testName(@intCast(i)).len + 1;
839 }
840 break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
841 };
842 const old_buf = old: {
843 ws.time_report_mutex.lock(io) catch return;
844 defer ws.time_report_mutex.unlock(io);
845 const old = ws.time_report_msgs[step_idx];
846 ws.time_report_msgs[step_idx] = &.{};
847 break :old old;
848 };
849 const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
850
851 const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]);
852 out_header.* = .{
853 .step_idx = step_idx,
854 .tests_len = tests_len,
855 };
856 var offset: usize = @sizeOf(abi.time_report.RunTestResult);
857 const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]);
858 @memcpy(ns_per_test_out, ns_per_test);
859 offset += tests_len * 8;
860 for (0..tests_len) |i| {
861 const name = tests.testName(@intCast(i));
862 @memcpy(buf[offset..][0..name.len], name);
863 buf[offset..][name.len] = 0;
864 offset += name.len + 1;
865 }
866 assert(offset == buf.len);
867
868 {
869 ws.time_report_mutex.lock(io) catch return;
870 defer ws.time_report_mutex.unlock(io);
871 assert(ws.time_report_msgs[step_idx].len == 0);
872 ws.time_report_msgs[step_idx] = buf;
873 ws.time_report_update_times[step_idx] = ws.now();
874 }
875 ws.notifyUpdate();
876}
877
878const RunnerRequest = union(enum) {
879 rebuild,
880};
881pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
882 const io = ws.graph.io;
883 ws.runner_request_mutex.lock(io) catch return;
884 defer ws.runner_request_mutex.unlock(io);
885 if (ws.runner_request) |req| {
886 ws.runner_request = null;
887 ws.runner_request_empty_cond.signal();
888 return req;
889 }
890 return null;
891}
892pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
893 const io = ws.graph.io;
894 try ws.runner_request_mutex.lock(io);
895 defer ws.runner_request_mutex.unlock(io);
896 while (true) {
897 if (ws.runner_request) |req| {
898 ws.runner_request = null;
899 ws.runner_request_empty_cond.signal(io);
900 return req;
901 }
902 try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex);
903 }
904}
905
906const cache_control_header: http.Header = .{
907 .name = "Cache-Control",
908 .value = "max-age=0, must-revalidate",
909};
910
911const builtin = @import("builtin");
912
913const std = @import("std");
914const Io = std.Io;
915const net = std.Io.net;
916const assert = std.debug.assert;
917const mem = std.mem;
918const log = std.log.scoped(.web_server);
919const Allocator = std.mem.Allocator;
920const Build = std.Build;
921const Cache = Build.Cache;
922const Fuzz = Build.Fuzz;
923const abi = Build.abi;
924const http = std.http;
925
926const WebServer = @This();
lib/std/Build.zig+23-175
......@@ -19,15 +19,14 @@ const ArrayList = std.ArrayList;
1919pub const Cache = @import("Build/Cache.zig");
2020pub const Step = @import("Build/Step.zig");
2121pub const Module = @import("Build/Module.zig");
22pub const Watch = @import("Build/Watch.zig");
23pub const Fuzz = @import("Build/Fuzz.zig");
24pub const WebServer = @import("Build/WebServer.zig");
2522pub const abi = @import("Build/abi.zig");
23/// The serialized output of configure phase ingested by make phase.
24pub const Configuration = @import("zig/Configuration.zig");
2625
2726/// Shared state among all Build instances.
2827graph: *Graph,
29install_tls: TopLevelStep,
30uninstall_tls: TopLevelStep,
28install_tls: Step.TopLevel,
29uninstall_tls: Step.TopLevel,
3130allocator: Allocator,
3231user_input_options: UserInputOptionsMap,
3332available_options_map: AvailableOptionsMap,
......@@ -39,28 +38,17 @@ verbose_air: bool,
3938verbose_llvm_ir: ?[]const u8,
4039verbose_llvm_bc: ?[]const u8,
4140verbose_llvm_cpu_features: bool,
42reference_trace: ?u32 = null,
4341invalid_user_input: bool,
4442default_step: *Step,
45top_level_steps: std.StringArrayHashMapUnmanaged(*TopLevelStep),
43top_level_steps: std.StringArrayHashMapUnmanaged(*Step.TopLevel),
4644install_prefix: []const u8,
47dest_dir: ?[]const u8,
48lib_dir: []const u8,
49exe_dir: []const u8,
50h_dir: []const u8,
51install_path: []const u8,
52sysroot: ?[]const u8 = null,
53search_prefixes: ArrayList([]const u8),
54libc_file: ?[]const u8 = null,
5545/// Path to the directory containing build.zig.
5646build_root: Cache.Directory,
5747cache_root: Cache.Directory,
5848pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null,
59args: ?[]const []const u8 = null,
6049debug_log_scopes: []const []const u8 = &.{},
6150debug_compile_errors: bool = false,
6251debug_incremental: bool = false,
63debug_pkg_config: bool = false,
6452/// Number of stack frames captured when a `StackTrace` is recorded for debug purposes,
6553/// in particular at `Step` creation.
6654/// Set to 0 to disable stack collection.
......@@ -76,12 +64,6 @@ enable_rosetta: bool = false,
7664enable_wasmtime: bool = false,
7765/// Use system Wine installation to run cross compiled Windows build artifacts.
7866enable_wine: bool = false,
79/// After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md,
80/// this will be the directory $glibc-build-dir/install/glibcs
81/// Given the example of the aarch64 target, this is the directory
82/// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`.
83/// Also works for dynamic musl.
84libc_runtimes_dir: ?[]const u8 = null,
8567
8668dep_prefix: []const u8 = "",
8769
......@@ -94,8 +76,6 @@ pkg_hash: []const u8,
9476/// A mapping from dependency names to package hashes.
9577available_deps: AvailableDeps,
9678
97release_mode: ReleaseMode,
98
9979build_id: ?std.zig.BuildId = null,
10080
10181pub const ReleaseMode = enum {
......@@ -116,14 +96,13 @@ pub const Graph = struct {
11696 system_package_mode: bool = false,
11797 debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null,
11898 cache: Cache,
119 zig_exe: [:0]const u8,
99 zig_exe: []const u8,
120100 environ_map: process.Environ.Map,
121101 global_cache_root: Cache.Directory,
122102 zig_lib_directory: Cache.Directory,
123103 needed_lazy_dependencies: std.StringArrayHashMapUnmanaged(void) = .empty,
124104 /// Information about the native target. Computed before build() is invoked.
125105 host: ResolvedTarget,
126 incremental: ?bool = null,
127106 random_seed: u32 = 0,
128107 dependency_cache: InitializedDepMap = .empty,
129108 allow_so_scripts: ?bool = null,
......@@ -134,11 +113,12 @@ pub const Graph = struct {
134113 /// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also
135114 /// respects the '--color' flag.
136115 stderr_mode: ?Io.Terminal.Mode = null,
116 release_mode: ReleaseMode = .off,
137117};
138118
139119const AvailableDeps = []const struct { []const u8, []const u8 };
140120
141const SystemLibraryMode = enum {
121pub const SystemLibraryMode = enum {
142122 /// User asked for the library to be disabled.
143123 /// The build runner has not confirmed whether the setting is recognized yet.
144124 user_disabled,
......@@ -245,19 +225,6 @@ const TypeId = enum {
245225 lazy_path_list,
246226};
247227
248const TopLevelStep = struct {
249 pub const base_id: Step.Id = .top_level;
250
251 step: Step,
252 description: []const u8,
253};
254
255pub const DirList = struct {
256 lib_dir: ?[]const u8 = null,
257 exe_dir: ?[]const u8 = null,
258 include_dir: ?[]const u8 = null,
259};
260
261228pub fn create(
262229 graph: *Graph,
263230 build_root: Cache.Directory,
......@@ -285,15 +252,10 @@ pub fn create(
285252 .available_options_list = std.array_list.Managed(AvailableOption).init(arena),
286253 .top_level_steps = .{},
287254 .default_step = undefined,
288 .search_prefixes = .empty,
289255 .install_prefix = undefined,
290 .lib_dir = undefined,
291 .exe_dir = undefined,
292 .h_dir = undefined,
293 .dest_dir = graph.environ_map.get("DESTDIR"),
294256 .install_tls = .{
295257 .step = .init(.{
296 .id = TopLevelStep.base_id,
258 .tag = .top_level,
297259 .name = "install",
298260 .owner = b,
299261 }),
......@@ -301,21 +263,17 @@ pub fn create(
301263 },
302264 .uninstall_tls = .{
303265 .step = .init(.{
304 .id = TopLevelStep.base_id,
266 .tag = .top_level,
305267 .name = "uninstall",
306268 .owner = b,
307 .makeFn = makeUninstall,
308269 }),
309270 .description = "Remove build artifacts from prefix path",
310271 },
311 .install_path = undefined,
312 .args = null,
313272 .modules = .empty,
314273 .named_writefiles = .empty,
315274 .named_lazy_paths = .empty,
316275 .pkg_hash = "",
317276 .available_deps = available_deps,
318 .release_mode = .off,
319277 };
320278 try b.top_level_steps.put(arena, b.install_tls.step.name, &b.install_tls);
321279 try b.top_level_steps.put(arena, b.uninstall_tls.step.name, &b.uninstall_tls);
......@@ -330,19 +288,6 @@ fn createChild(
330288 pkg_hash: []const u8,
331289 pkg_deps: AvailableDeps,
332290 user_input_options: UserInputOptionsMap,
333) error{OutOfMemory}!*Build {
334 const child = try createChildOnly(parent, dep_name, build_root, pkg_hash, pkg_deps, user_input_options);
335 try determineAndApplyInstallPrefix(child);
336 return child;
337}
338
339fn createChildOnly(
340 parent: *Build,
341 dep_name: []const u8,
342 build_root: Cache.Directory,
343 pkg_hash: []const u8,
344 pkg_deps: AvailableDeps,
345 user_input_options: UserInputOptionsMap,
346291) error{OutOfMemory}!*Build {
347292 const allocator = parent.allocator;
348293 const child = try allocator.create(Build);
......@@ -351,7 +296,7 @@ fn createChildOnly(
351296 .allocator = allocator,
352297 .install_tls = .{
353298 .step = .init(.{
354 .id = TopLevelStep.base_id,
299 .tag = .top_level,
355300 .name = "install",
356301 .owner = child,
357302 }),
......@@ -359,10 +304,9 @@ fn createChildOnly(
359304 },
360305 .uninstall_tls = .{
361306 .step = .init(.{
362 .id = TopLevelStep.base_id,
307 .tag = .top_level,
363308 .name = "uninstall",
364309 .owner = child,
365 .makeFn = makeUninstall,
366310 }),
367311 .description = "Remove build artifacts from prefix path",
368312 },
......@@ -376,38 +320,31 @@ fn createChildOnly(
376320 .verbose_llvm_ir = parent.verbose_llvm_ir,
377321 .verbose_llvm_bc = parent.verbose_llvm_bc,
378322 .verbose_llvm_cpu_features = parent.verbose_llvm_cpu_features,
379 .reference_trace = parent.reference_trace,
380323 .invalid_user_input = false,
381324 .default_step = undefined,
382325 .top_level_steps = .{},
383326 .install_prefix = undefined,
384 .dest_dir = parent.dest_dir,
385327 .lib_dir = parent.lib_dir,
386328 .exe_dir = parent.exe_dir,
387329 .h_dir = parent.h_dir,
388330 .install_path = parent.install_path,
389331 .sysroot = parent.sysroot,
390 .search_prefixes = parent.search_prefixes,
391 .libc_file = parent.libc_file,
392332 .build_root = build_root,
393333 .cache_root = parent.cache_root,
394334 .debug_log_scopes = parent.debug_log_scopes,
395335 .debug_compile_errors = parent.debug_compile_errors,
396336 .debug_incremental = parent.debug_incremental,
397 .debug_pkg_config = parent.debug_pkg_config,
398337 .enable_darling = parent.enable_darling,
399338 .enable_qemu = parent.enable_qemu,
400339 .enable_rosetta = parent.enable_rosetta,
401340 .enable_wasmtime = parent.enable_wasmtime,
402341 .enable_wine = parent.enable_wine,
403 .libc_runtimes_dir = parent.libc_runtimes_dir,
404342 .dep_prefix = parent.fmt("{s}{s}.", .{ parent.dep_prefix, dep_name }),
405343 .modules = .empty,
406344 .named_writefiles = .empty,
407345 .named_lazy_paths = .empty,
408346 .pkg_hash = pkg_hash,
409347 .available_deps = pkg_deps,
410 .release_mode = parent.release_mode,
411348 };
412349 try child.top_level_steps.put(allocator, child.install_tls.step.name, &child.install_tls);
413350 try child.top_level_steps.put(allocator, child.uninstall_tls.step.name, &child.uninstall_tls);
......@@ -702,59 +639,6 @@ fn hashUserInputOptionsMap(allocator: Allocator, user_input_options: UserInputOp
702639 user_option.hash(hasher);
703640}
704641
705fn determineAndApplyInstallPrefix(b: *Build) error{OutOfMemory}!void {
706 // Create an installation directory local to this package. This will be used when
707 // dependant packages require a standard prefix, such as include directories for C headers.
708 var hash = b.graph.cache.hash;
709 // Random bytes to make unique. Refresh this with new random bytes when
710 // implementation is modified in a non-backwards-compatible way.
711 hash.add(@as(u32, 0xd8cb0055));
712 hash.addBytes(b.dep_prefix);
713
714 var wyhash = std.hash.Wyhash.init(0);
715 hashUserInputOptionsMap(b.allocator, b.user_input_options, &wyhash);
716 hash.add(wyhash.final());
717
718 const digest = hash.final();
719 const install_prefix = try b.cache_root.join(b.allocator, &.{ "i", &digest });
720 b.resolveInstallPrefix(install_prefix, .{});
721}
722
723/// This function is intended to be called by lib/build_runner.zig, not a build.zig file.
724pub fn resolveInstallPrefix(b: *Build, install_prefix: ?[]const u8, dir_list: DirList) void {
725 if (b.dest_dir) |dest_dir| {
726 b.install_prefix = install_prefix orelse "/usr";
727 b.install_path = b.pathJoin(&.{ dest_dir, b.install_prefix });
728 } else {
729 b.install_prefix = install_prefix orelse
730 (b.build_root.join(b.allocator, &.{"zig-out"}) catch @panic("unhandled error"));
731 b.install_path = b.install_prefix;
732 }
733
734 var lib_list = [_][]const u8{ b.install_path, "lib" };
735 var exe_list = [_][]const u8{ b.install_path, "bin" };
736 var h_list = [_][]const u8{ b.install_path, "include" };
737
738 if (dir_list.lib_dir) |dir| {
739 if (fs.path.isAbsolute(dir)) lib_list[0] = b.dest_dir orelse "";
740 lib_list[1] = dir;
741 }
742
743 if (dir_list.exe_dir) |dir| {
744 if (fs.path.isAbsolute(dir)) exe_list[0] = b.dest_dir orelse "";
745 exe_list[1] = dir;
746 }
747
748 if (dir_list.include_dir) |dir| {
749 if (fs.path.isAbsolute(dir)) h_list[0] = b.dest_dir orelse "";
750 h_list[1] = dir;
751 }
752
753 b.lib_dir = b.pathJoin(&lib_list);
754 b.exe_dir = b.pathJoin(&exe_list);
755 b.h_dir = b.pathJoin(&h_list);
756}
757
758642/// Create a set of key-value pairs that can be converted into a Zig source
759643/// file and then inserted into a Zig compilation's module table for importing.
760644/// In other words, this provides a way to expose build.zig values to Zig
......@@ -1121,15 +1005,6 @@ pub fn getUninstallStep(b: *Build) *Step {
11211005 return &b.uninstall_tls.step;
11221006}
11231007
1124fn makeUninstall(uninstall_step: *Step, options: Step.MakeOptions) anyerror!void {
1125 _ = options;
1126 const uninstall_tls: *TopLevelStep = @fieldParentPtr("step", uninstall_step);
1127 const b: *Build = @fieldParentPtr("uninstall_tls", uninstall_tls);
1128
1129 _ = b;
1130 @panic("TODO implement https://github.com/ziglang/zig/issues/14943");
1131}
1132
11331008/// Creates a configuration option to be passed to the build.zig script.
11341009/// When a user directly runs `zig build`, they can set these options with `-D` arguments.
11351010/// When a project depends on a Zig package as a dependency, it programmatically sets
......@@ -1350,10 +1225,10 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
13501225}
13511226
13521227pub fn step(b: *Build, name: []const u8, description: []const u8) *Step {
1353 const step_info = b.allocator.create(TopLevelStep) catch @panic("OOM");
1228 const step_info = b.allocator.create(Step.TopLevel) catch @panic("OOM");
13541229 step_info.* = .{
13551230 .step = .init(.{
1356 .id = TopLevelStep.base_id,
1231 .tag = .top_level,
13571232 .name = name,
13581233 .owner = b,
13591234 }),
......@@ -1373,8 +1248,10 @@ pub const StandardOptimizeOptionOptions = struct {
13731248};
13741249
13751250pub fn standardOptimizeOption(b: *Build, options: StandardOptimizeOptionOptions) std.builtin.OptimizeMode {
1251 const graph = b.graph;
1252
13761253 if (options.preferred_optimize_mode) |mode| {
1377 if (b.option(bool, "release", "optimize for end users") orelse (b.release_mode != .off)) {
1254 if (b.option(bool, "release", "optimize for end users") orelse (graph.release_mode != .off)) {
13781255 return mode;
13791256 } else {
13801257 return .Debug;
......@@ -1389,7 +1266,7 @@ pub fn standardOptimizeOption(b: *Build, options: StandardOptimizeOptionOptions)
13891266 return mode;
13901267 }
13911268
1392 return switch (b.release_mode) {
1269 return switch (graph.release_mode) {
13931270 .off => .Debug,
13941271 .any => {
13951272 std.debug.print("the project does not declare a preferred optimization mode. choose: --release=fast, --release=safe, or --release=small\n", .{});
......@@ -1824,36 +1701,11 @@ fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 {
18241701 return null;
18251702}
18261703
1827pub fn findProgram(b: *Build, names: []const []const u8, paths: []const []const u8) error{FileNotFound}![]const u8 {
1828 // TODO report error for ambiguous situations
1829 for (b.search_prefixes.items) |search_prefix| {
1830 for (names) |name| {
1831 if (fs.path.isAbsolute(name)) {
1832 return name;
1833 }
1834 return tryFindProgram(b, b.pathJoin(&.{ search_prefix, "bin", name })) orelse continue;
1835 }
1836 }
1837 if (b.graph.environ_map.get("PATH")) |PATH| {
1838 for (names) |name| {
1839 if (fs.path.isAbsolute(name)) {
1840 return name;
1841 }
1842 var it = mem.tokenizeScalar(u8, PATH, fs.path.delimiter);
1843 while (it.next()) |p| {
1844 return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue;
1845 }
1846 }
1847 }
1848 for (names) |name| {
1849 if (fs.path.isAbsolute(name)) {
1850 return name;
1851 }
1852 for (paths) |p| {
1853 return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue;
1854 }
1855 }
1856 return error.FileNotFound;
1704pub fn findProgram(b: *Build, names: []const []const u8, paths: []const []const u8) LazyPath {
1705 _ = b;
1706 _ = names;
1707 _ = paths;
1708 @panic("TODO rework findProgram to be based on LazyPath");
18571709}
18581710
18591711pub fn runAllowFail(
......@@ -1918,10 +1770,6 @@ pub fn run(b: *Build, argv: []const []const u8) []u8 {
19181770 );
19191771}
19201772
1921pub fn addSearchPrefix(b: *Build, search_prefix: []const u8) void {
1922 b.search_prefixes.append(b.allocator, b.dupePath(search_prefix)) catch @panic("OOM");
1923}
1924
19251773pub fn getInstallPath(b: *Build, dir: InstallDir, dest_rel_path: []const u8) []const u8 {
19261774 assert(!fs.path.isAbsolute(dest_rel_path)); // Install paths must be relative to the prefix
19271775 const base_dir = switch (dir) {
lib/std/Build/Fuzz.zig deleted-597
......@@ -1,597 +0,0 @@
1const std = @import("../std.zig");
2const Io = std.Io;
3const Build = std.Build;
4const Cache = Build.Cache;
5const Step = std.Build.Step;
6const assert = std.debug.assert;
7const fatal = std.process.fatal;
8const Allocator = std.mem.Allocator;
9const log = std.log;
10const Coverage = std.debug.Coverage;
11const abi = Build.abi.fuzz;
12
13const Fuzz = @This();
14const build_runner = @import("root");
15
16gpa: Allocator,
17io: Io,
18mode: Mode,
19
20/// Allocated into `gpa`.
21run_steps: []const *Step.Run,
22
23group: Io.Group,
24root_prog_node: std.Progress.Node,
25prog_node: std.Progress.Node,
26
27/// Protects `coverage_files`.
28coverage_mutex: Io.Mutex,
29coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap),
30
31queue_mutex: Io.Mutex,
32queue_cond: Io.Condition,
33msg_queue: std.ArrayList(Msg),
34
35pub const Mode = union(enum) {
36 forever: struct { ws: *Build.WebServer },
37 limit: Limited,
38
39 pub const Limited = struct {
40 amount: u64,
41 };
42};
43
44const Msg = union(enum) {
45 coverage: struct {
46 id: u64,
47 cumulative: struct {
48 runs: u64,
49 unique: u64,
50 coverage: u64,
51 },
52 run: *Step.Run,
53 },
54 entry_point: struct {
55 coverage_id: u64,
56 addr: u64,
57 },
58};
59
60const CoverageMap = struct {
61 mapped_memory: []align(std.heap.page_size_min) const u8,
62 coverage: Coverage,
63 source_locations: []Coverage.SourceLocation,
64 /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
65 entry_points: std.ArrayList(u32),
66 start_timestamp: i64,
67 start_n_runs: u64,
68
69 fn deinit(cm: *CoverageMap, gpa: Allocator) void {
70 std.posix.munmap(cm.mapped_memory);
71 cm.coverage.deinit(gpa);
72 cm.* = undefined;
73 }
74};
75
76pub fn init(
77 gpa: Allocator,
78 io: Io,
79 all_steps: []const *Build.Step,
80 root_prog_node: std.Progress.Node,
81 mode: Mode,
82) error{ OutOfMemory, Canceled }!Fuzz {
83 const run_steps: []const *Step.Run = steps: {
84 var steps: std.ArrayList(*Step.Run) = .empty;
85 defer steps.deinit(gpa);
86 const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0);
87 defer rebuild_node.end();
88 var rebuild_group: Io.Group = .init;
89 defer rebuild_group.cancel(io);
90
91 for (all_steps) |step| {
92 const run = step.cast(Step.Run) orelse continue;
93 if (run.producer == null) continue;
94 if (run.fuzz_tests.items.len == 0) continue;
95 try steps.append(gpa, run);
96 rebuild_group.async(io, rebuildTestsWorkerRun, .{ run, gpa, rebuild_node });
97 }
98
99 if (steps.items.len == 0) fatal("no fuzz tests found", .{});
100 rebuild_node.setEstimatedTotalItems(steps.items.len);
101 const run_steps = try gpa.dupe(*Step.Run, steps.items);
102 try rebuild_group.await(io);
103 break :steps run_steps;
104 };
105 errdefer gpa.free(run_steps);
106
107 for (run_steps) |run| {
108 assert(run.fuzz_tests.items.len > 0);
109 if (run.rebuilt_executable == null)
110 fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
111 }
112
113 return .{
114 .gpa = gpa,
115 .io = io,
116 .mode = mode,
117 .run_steps = run_steps,
118 .group = .init,
119 .root_prog_node = root_prog_node,
120 .prog_node = .none,
121 .coverage_files = .empty,
122 .coverage_mutex = .init,
123 .queue_mutex = .init,
124 .queue_cond = .init,
125 .msg_queue = .empty,
126 };
127}
128
129pub fn start(fuzz: *Fuzz) void {
130 const io = fuzz.io;
131 fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0);
132
133 if (fuzz.mode == .forever) {
134 // For polling messages and sending updates to subscribers.
135 fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err|
136 fatal("unable to spawn coverage task: {t}", .{err});
137 }
138
139 for (fuzz.run_steps) |run| {
140 assert(run.rebuilt_executable != null);
141 fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run });
142 }
143}
144
145pub fn deinit(fuzz: *Fuzz) void {
146 const io = fuzz.io;
147 fuzz.group.cancel(io);
148 fuzz.prog_node.end();
149 fuzz.gpa.free(fuzz.run_steps);
150}
151
152fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) void {
153 rebuildTestsWorkerRunFallible(run, gpa, parent_prog_node) catch |err| {
154 const compile = run.producer.?;
155 log.err("step '{s}': failed to rebuild in fuzz mode: {t}", .{ compile.step.name, err });
156 };
157}
158
159fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) !void {
160 const graph = run.step.owner.graph;
161 const io = graph.io;
162 const compile = run.producer.?;
163 const prog_node = parent_prog_node.start(compile.step.name, 0);
164 defer prog_node.end();
165
166 const result = compile.rebuildInFuzzMode(gpa, prog_node);
167
168 const show_compile_errors = compile.step.result_error_bundle.errorMessageCount() > 0;
169 const show_error_msgs = compile.step.result_error_msgs.items.len > 0;
170 const show_stderr = compile.step.result_stderr.len > 0;
171
172 if (show_error_msgs or show_compile_errors or show_stderr) {
173 var buf: [256]u8 = undefined;
174 const stderr = try io.lockStderr(&buf, graph.stderr_mode);
175 defer io.unlockStderr();
176 build_runner.printErrorMessages(gpa, &compile.step, .{}, stderr.terminal(), .verbose, .indent) catch {};
177 }
178
179 const rebuilt_bin_path = result catch |err| switch (err) {
180 error.MakeFailed => return,
181 else => |other| return other,
182 };
183 run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename);
184}
185
186fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run) void {
187 const owner = run.step.owner;
188 const gpa = owner.allocator;
189 const graph = owner.graph;
190 const io = graph.io;
191
192 run.rerunInFuzzMode(fuzz, fuzz.prog_node) catch |err| switch (err) {
193 error.MakeFailed => {
194 var buf: [256]u8 = undefined;
195 const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) {
196 error.Canceled => return,
197 };
198 defer io.unlockStderr();
199 build_runner.printErrorMessages(gpa, &run.step, .{}, stderr.terminal(), .verbose, .indent) catch {};
200 return;
201 },
202 else => {
203 log.err("step '{s}': failed to rerun in fuzz mode: {t}", .{ run.step.name, err });
204 return;
205 },
206 };
207}
208
209pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void {
210 assert(fuzz.mode == .forever);
211
212 var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa);
213 defer arena_state.deinit();
214 const arena = arena_state.allocator();
215
216 const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
217 var dedup_table: DedupTable = .empty;
218 defer dedup_table.deinit(fuzz.gpa);
219
220 for (fuzz.run_steps) |run_step| {
221 const compile_inputs = run_step.producer.?.step.inputs.table;
222 for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| {
223 try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len);
224 for (file_list.items) |sub_path| {
225 if (!std.mem.endsWith(u8, sub_path, ".zig")) continue;
226 const joined_path = try dir_path.join(arena, sub_path);
227 dedup_table.putAssumeCapacity(joined_path, {});
228 }
229 }
230 }
231
232 const deduped_paths = dedup_table.keys();
233 const SortContext = struct {
234 pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
235 _ = this;
236 return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
237 .lt => true,
238 .gt => false,
239 .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
240 };
241 }
242 };
243 std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
244 return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths);
245}
246
247pub const Previous = struct {
248 unique_runs: usize,
249 entry_points: usize,
250 sent_source_index: bool,
251 pub const init: Previous = .{
252 .unique_runs = 0,
253 .entry_points = 0,
254 .sent_source_index = false,
255 };
256};
257pub fn sendUpdate(
258 fuzz: *Fuzz,
259 socket: *std.http.Server.WebSocket,
260 prev: *Previous,
261) !void {
262 const io = fuzz.io;
263
264 try fuzz.coverage_mutex.lock(io);
265 defer fuzz.coverage_mutex.unlock(io);
266
267 const coverage_maps = fuzz.coverage_files.values();
268 if (coverage_maps.len == 0) return;
269 // TODO: handle multiple fuzz steps in the WebSocket packets
270 const coverage_map = &coverage_maps[0];
271 const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
272 // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the
273 // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the
274 // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass
275 // this data straight to the socket with sendfile...
276 const seen_pcs = cov_header.seenBits();
277 const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
278 const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
279 {
280 if (!prev.sent_source_index) {
281 prev.sent_source_index = true;
282 // We need to send initial context.
283 const header: abi.SourceIndexHeader = .{
284 .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
285 .files_len = @intCast(coverage_map.coverage.files.entries.len),
286 .source_locations_len = @intCast(coverage_map.source_locations.len),
287 .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
288 .start_timestamp = coverage_map.start_timestamp,
289 .start_n_runs = coverage_map.start_n_runs,
290 };
291 var iovecs: [5][]const u8 = .{
292 @ptrCast(&header),
293 @ptrCast(coverage_map.coverage.directories.keys()),
294 @ptrCast(coverage_map.coverage.files.keys()),
295 @ptrCast(coverage_map.source_locations),
296 coverage_map.coverage.string_bytes.items,
297 };
298 try socket.writeMessageVec(&iovecs, .binary);
299 }
300
301 const header: abi.CoverageUpdateHeader = .{
302 .n_runs = n_runs,
303 .unique_runs = unique_runs,
304 };
305 var iovecs: [2][]const u8 = .{
306 @ptrCast(&header),
307 @ptrCast(seen_pcs),
308 };
309 try socket.writeMessageVec(&iovecs, .binary);
310
311 prev.unique_runs = unique_runs;
312 }
313
314 if (prev.entry_points != coverage_map.entry_points.items.len) {
315 const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len));
316 var iovecs: [2][]const u8 = .{
317 @ptrCast(&header),
318 @ptrCast(coverage_map.entry_points.items),
319 };
320 try socket.writeMessageVec(&iovecs, .binary);
321
322 prev.entry_points = coverage_map.entry_points.items.len;
323 }
324}
325
326fn coverageRun(fuzz: *Fuzz) void {
327 coverageRunCancelable(fuzz) catch |err| switch (err) {
328 error.Canceled => return,
329 };
330}
331
332fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void {
333 const io = fuzz.io;
334
335 try fuzz.queue_mutex.lock(io);
336 defer fuzz.queue_mutex.unlock(io);
337
338 while (true) {
339 try fuzz.queue_cond.wait(io, &fuzz.queue_mutex);
340 for (fuzz.msg_queue.items) |msg| switch (msg) {
341 .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) {
342 error.AlreadyReported => continue,
343 error.Canceled => return,
344 else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
345 },
346 .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
347 error.AlreadyReported => continue,
348 error.Canceled => return,
349 else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
350 },
351 };
352 fuzz.msg_queue.clearRetainingCapacity();
353 }
354}
355fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void {
356 assert(fuzz.mode == .forever);
357 const ws = fuzz.mode.forever.ws;
358 const gpa = fuzz.gpa;
359 const io = fuzz.io;
360
361 try fuzz.coverage_mutex.lock(io);
362 defer fuzz.coverage_mutex.unlock(io);
363
364 const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id);
365 if (gop.found_existing) {
366 // We are fuzzing the same executable with multiple threads.
367 // Perhaps the same unit test; perhaps a different one. In any
368 // case, since the coverage file is the same, we only have to
369 // notice changes to that one file in order to learn coverage for
370 // this particular executable.
371 return;
372 }
373 errdefer _ = fuzz.coverage_files.pop();
374
375 gop.value_ptr.* = .{
376 .coverage = std.debug.Coverage.init,
377 .mapped_memory = undefined, // populated below
378 .source_locations = undefined, // populated below
379 .entry_points = .empty,
380 .start_timestamp = ws.now(),
381 .start_n_runs = undefined, // populated below
382 };
383 errdefer gop.value_ptr.coverage.deinit(gpa);
384
385 const rebuilt_exe_path = run_step.rebuilt_executable.?;
386 const target = run_step.producer.?.rootModuleTarget();
387 var debug_info = std.debug.Info.load(
388 gpa,
389 io,
390 rebuilt_exe_path,
391 &gop.value_ptr.coverage,
392 target.ofmt,
393 target.cpu.arch,
394 ) catch |err| {
395 log.err("step '{s}': failed to load debug information for '{f}': {t}", .{
396 run_step.step.name, rebuilt_exe_path, err,
397 });
398 return error.AlreadyReported;
399 };
400 defer debug_info.deinit(gpa);
401
402 const coverage_file_path: Build.Cache.Path = .{
403 .root_dir = run_step.step.owner.cache_root,
404 .sub_path = "v/" ++ std.fmt.hex(coverage_id),
405 };
406 var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
407 log.err("step '{s}': failed to load coverage file '{f}': {t}", .{
408 run_step.step.name, coverage_file_path, err,
409 });
410 return error.AlreadyReported;
411 };
412 defer coverage_file.close(io);
413
414 const file_size = coverage_file.length(io) catch |err| {
415 log.err("unable to check len of coverage file '{f}': {t}", .{ coverage_file_path, err });
416 return error.AlreadyReported;
417 };
418
419 const mapped_memory = std.posix.mmap(
420 null,
421 file_size,
422 .{ .READ = true },
423 .{ .TYPE = .SHARED },
424 coverage_file.handle,
425 0,
426 ) catch |err| {
427 log.err("failed to map coverage file '{f}': {t}", .{ coverage_file_path, err });
428 return error.AlreadyReported;
429 };
430 gop.value_ptr.mapped_memory = mapped_memory;
431
432 const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
433 const pcs = header.pcAddrs();
434 const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
435 errdefer gpa.free(source_locations);
436
437 // Unfortunately the PCs array that LLVM gives us from the 8-bit PC
438 // counters feature is not sorted.
439 var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty;
440 defer sorted_pcs.deinit(gpa);
441 try sorted_pcs.resize(gpa, pcs.len);
442 @memcpy(sorted_pcs.items(.pc), pcs);
443 for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i);
444 sorted_pcs.sortUnstable(struct {
445 addrs: []const u64,
446
447 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
448 return ctx.addrs[a_index] < ctx.addrs[b_index];
449 }
450 }{ .addrs = sorted_pcs.items(.pc) });
451
452 debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
453 log.err("failed to resolve addresses to source locations: {t}", .{err});
454 return error.AlreadyReported;
455 };
456
457 for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl;
458 gop.value_ptr.source_locations = source_locations;
459 gop.value_ptr.start_n_runs = header.n_runs;
460
461 ws.notifyUpdate();
462}
463
464fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void {
465 const io = fuzz.io;
466
467 try fuzz.coverage_mutex.lock(io);
468 defer fuzz.coverage_mutex.unlock(io);
469
470 const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?;
471 const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
472 const pcs = header.pcAddrs();
473
474 // Since this pcs list is unsorted, we must linear scan for the best index.
475 const index = i: {
476 var best: usize = 0;
477 for (pcs[1..], 1..) |elem_addr, i| {
478 if (elem_addr == addr) break :i i;
479 if (elem_addr > addr) continue;
480 if (elem_addr > pcs[best]) best = i;
481 }
482 break :i best;
483 };
484 if (index >= pcs.len) {
485 log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
486 addr, pcs[0], pcs[pcs.len - 1],
487 });
488 return error.AlreadyReported;
489 }
490 if (false) {
491 const sl = coverage_map.source_locations[index];
492 const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
493 if (pcs.len == 1) {
494 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{
495 addr, file_name, sl.line, sl.column,
496 });
497 } else if (index == 0) {
498 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{
499 addr, file_name, sl.line, sl.column, pcs[index + 1],
500 });
501 } else if (index == pcs.len - 1) {
502 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{
503 addr, file_name, sl.line, sl.column, index, pcs[index - 1],
504 });
505 } else {
506 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
507 addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
508 });
509 }
510 }
511 try coverage_map.entry_points.append(fuzz.gpa, @intCast(index));
512}
513
514pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
515 assert(fuzz.mode == .limit);
516 const io = fuzz.io;
517
518 try fuzz.group.await(io);
519 fuzz.group = .init;
520
521 std.debug.print("======= FUZZING REPORT =======\n", .{});
522 for (fuzz.msg_queue.items) |msg| {
523 if (msg != .coverage) continue;
524
525 const cov = msg.coverage;
526 const coverage_file_path: std.Build.Cache.Path = .{
527 .root_dir = cov.run.step.owner.cache_root,
528 .sub_path = "v/" ++ std.fmt.hex(cov.id),
529 };
530 var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
531 fatal("step '{s}': failed to load coverage file '{f}': {t}", .{
532 cov.run.step.name, coverage_file_path, err,
533 });
534 };
535 defer coverage_file.close(io);
536
537 const fuzz_abi = std.Build.abi.fuzz;
538 var rbuf: [0x1000]u8 = undefined;
539 var r = coverage_file.reader(io, &rbuf);
540
541 var header: fuzz_abi.SeenPcsHeader = undefined;
542 r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
543 fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{
544 cov.run.step.name, coverage_file_path, err,
545 });
546 };
547
548 if (header.pcs_len == 0) {
549 fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{
550 cov.run.step.name, coverage_file_path,
551 });
552 }
553
554 var seen_count: usize = 0;
555 const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
556 for (0..chunk_count) |_| {
557 const seen = r.interface.takeInt(usize, .little) catch |err| {
558 fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{
559 cov.run.step.name, coverage_file_path, err,
560 });
561 };
562 seen_count += @popCount(seen);
563 }
564
565 const seen_f: f64 = @floatFromInt(seen_count);
566 const total_f: f64 = @floatFromInt(header.pcs_len);
567 const ratio = seen_f / total_f;
568 std.debug.print(
569 \\Step: {s}
570 \\Fuzz test: "{s}" ({x})
571 \\Runs: {} -> {}
572 \\Unique runs: {} -> {}
573 \\Coverage: {}/{} -> {}/{} ({:.02}%)
574 \\
575 , .{
576 cov.run.step.name,
577 cov.run.fuzz_tests.items[0],
578 cov.id,
579 cov.cumulative.runs,
580 header.n_runs,
581 cov.cumulative.unique,
582 header.unique_runs,
583 cov.cumulative.coverage,
584 header.pcs_len,
585 seen_count,
586 header.pcs_len,
587 ratio * 100,
588 });
589
590 std.debug.print("------------------------------\n", .{});
591 }
592 std.debug.print(
593 \\Values are accumulated across multiple runs when preserving the cache.
594 \\==============================
595 \\
596 , .{});
597}
lib/std/Build/Step.zig+33-882
......@@ -10,24 +10,11 @@ const Cache = Build.Cache;
1010const Path = Cache.Path;
1111const ArrayList = std.ArrayList;
1212
13id: Id,
13tag: std.Build.Configuration.Step.Tag,
1414name: []const u8,
1515owner: *Build,
16makeFn: MakeFn,
1716
18dependencies: std.array_list.Managed(*Step),
19/// This field is empty during execution of the user's build script, and
20/// then populated during dependency loop checking in the build runner.
21dependants: ArrayList(*Step),
22/// Collects the set of files that retrigger this step to run.
23///
24/// This is used by the build system's implementation of `--watch` but it can
25/// also be potentially useful for IDEs to know what effects editing a
26/// particular file has.
27///
28/// Populated within `make`. Implementation may choose to clear and repopulate,
29/// retain previous value, or update.
30inputs: Inputs,
17dependencies: ArrayList(*Step),
3118
3219/// Set this field to declare an upper bound on the amount of bytes of memory it will
3320/// take to run the step. Zero means no limit.
......@@ -51,77 +38,11 @@ inputs: Inputs,
5138max_rss: usize,
5239
5340state: State,
54pending_deps: u32,
55
56result_error_msgs: ArrayList([]const u8),
57result_error_bundle: std.zig.ErrorBundle,
58result_stderr: []const u8,
59result_cached: bool,
60result_duration_ns: ?u64,
61/// 0 means unavailable or not reported.
62result_peak_rss: usize,
63/// If the step is failed and this field is populated, this is the command which failed.
64/// This field may be populated even if the step succeeded.
65result_failed_command: ?[]const u8,
66test_results: TestResults,
6741
6842/// The return address associated with creation of this step that can be useful
6943/// to print along with debugging messages.
7044debug_stack_trace: std.debug.StackTrace,
7145
72pub const TestResults = struct {
73 /// The total number of tests in the step. Every test has a "status" from the following:
74 /// * passed
75 /// * skipped
76 /// * failed cleanly
77 /// * crashed
78 /// * timed out
79 test_count: u32 = 0,
80
81 /// The number of tests which were skipped (`error.SkipZigTest`).
82 skip_count: u32 = 0,
83 /// The number of tests which failed cleanly.
84 fail_count: u32 = 0,
85 /// The number of tests which terminated unexpectedly, i.e. crashed.
86 crash_count: u32 = 0,
87 /// The number of tests which timed out.
88 timeout_count: u32 = 0,
89
90 /// The number of detected memory leaks. The associated test may still have passed; indeed, *all*
91 /// individual tests may have passed. However, the step as a whole fails if any test has leaks.
92 leak_count: u32 = 0,
93 /// The number of detected error logs. The associated test may still have passed; indeed, *all*
94 /// individual tests may have passed. However, the step as a whole fails if any test logs errors.
95 log_err_count: u32 = 0,
96
97 pub fn isSuccess(tr: TestResults) bool {
98 // all steps are success or skip
99 return tr.fail_count == 0 and
100 tr.crash_count == 0 and
101 tr.timeout_count == 0 and
102 // no (otherwise successful) step leaked memory or logged errors
103 tr.leak_count == 0 and
104 tr.log_err_count == 0;
105 }
106
107 /// Computes the number of tests which passed from the other values.
108 pub fn passCount(tr: TestResults) u32 {
109 return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count;
110 }
111};
112
113pub const MakeOptions = struct {
114 progress_node: std.Progress.Node,
115 watch: bool,
116 web_server: ?*Build.WebServer,
117 /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds.
118 unit_test_timeout_ns: ?u64,
119 /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`.
120 gpa: Allocator,
121};
122
123pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void;
124
12546pub const State = enum {
12647 precheck_unstarted,
12748 precheck_started,
......@@ -132,57 +53,29 @@ pub const State = enum {
13253 /// be re-evaluated.
13354 precheck_done,
13455 dependency_failure,
135 success,
136 failure,
137 /// This state indicates that the step did not complete, however, it also did not fail,
138 /// and it is safe to continue executing its dependencies.
139 skipped,
140 /// This step was skipped because it specified a max_rss that exceeded the runner's maximum.
141 /// It is not safe to run its dependencies.
142 skipped_oom,
14356};
14457
145pub const Id = enum {
146 top_level,
147 compile,
148 install_artifact,
149 install_file,
150 install_dir,
151 remove_dir,
152 fail,
153 fmt,
154 translate_c,
155 write_file,
156 update_source_files,
157 run,
158 check_file,
159 check_object,
160 config_header,
161 objcopy,
162 options,
163 custom,
164
165 pub fn Type(comptime id: Id) type {
166 return switch (id) {
167 .top_level => Build.TopLevelStep,
168 .compile => Compile,
169 .install_artifact => InstallArtifact,
170 .install_file => InstallFile,
171 .install_dir => InstallDir,
172 .fail => Fail,
173 .fmt => Fmt,
174 .translate_c => TranslateC,
175 .write_file => WriteFile,
176 .update_source_files => UpdateSourceFiles,
177 .run => Run,
178 .check_file => CheckFile,
179 .config_header => ConfigHeader,
180 .objcopy => ObjCopy,
181 .options => Options,
182 .custom => @compileError("no type available for custom step"),
183 };
184 }
185};
58pub const Tag = std.Build.Configuration.Step.Tag;
59
60pub fn Type(comptime tag: Tag) type {
61 return switch (tag) {
62 .top_level => Build.TopLevelStep,
63 .compile => Compile,
64 .install_artifact => InstallArtifact,
65 .install_file => InstallFile,
66 .install_dir => InstallDir,
67 .fail => Fail,
68 .fmt => Fmt,
69 .translate_c => TranslateC,
70 .write_file => WriteFile,
71 .update_source_files => UpdateSourceFiles,
72 .run => Run,
73 .check_file => CheckFile,
74 .config_header => ConfigHeader,
75 .objcopy => ObjCopy,
76 .options => Options,
77 };
78}
18679
18780pub const CheckFile = @import("Step/CheckFile.zig");
18881pub const ConfigHeader = @import("Step/ConfigHeader.zig");
......@@ -199,32 +92,17 @@ pub const TranslateC = @import("Step/TranslateC.zig");
19992pub const WriteFile = @import("Step/WriteFile.zig");
20093pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
20194
202pub const Inputs = struct {
203 table: Table,
204
205 pub const init: Inputs = .{
206 .table = .{},
207 };
95pub const TopLevel = struct {
96 pub const base_tag: Step.Tag = .top_level;
20897
209 pub const Table = std.ArrayHashMapUnmanaged(Build.Cache.Path, Files, Build.Cache.Path.TableAdapter, false);
210 /// The special file name "." means any changes inside the directory.
211 pub const Files = ArrayList([]const u8);
212
213 pub fn populated(inputs: *Inputs) bool {
214 return inputs.table.count() != 0;
215 }
216
217 pub fn clear(inputs: *Inputs, gpa: Allocator) void {
218 for (inputs.table.values()) |*files| files.deinit(gpa);
219 inputs.table.clearRetainingCapacity();
220 }
98 step: Step,
99 description: []const u8,
221100};
222101
223102pub const StepOptions = struct {
224 id: Id,
103 tag: Tag,
225104 name: []const u8,
226105 owner: *Build,
227 makeFn: MakeFn = makeNoOp,
228106 first_ret_addr: ?usize = null,
229107 max_rss: usize = 0,
230108};
......@@ -233,90 +111,27 @@ pub fn init(options: StepOptions) Step {
233111 const arena = options.owner.allocator;
234112
235113 return .{
236 .id = options.id,
114 .tag = options.tag,
237115 .name = arena.dupe(u8, options.name) catch @panic("OOM"),
238116 .owner = options.owner,
239 .makeFn = options.makeFn,
240 .dependencies = std.array_list.Managed(*Step).init(arena),
241 .dependants = .empty,
242 .inputs = Inputs.init,
117 .dependencies = .empty,
243118 .state = .precheck_unstarted,
244 .pending_deps = undefined, // initialized by build runner
245119 .max_rss = options.max_rss,
246120 .debug_stack_trace = blk: {
247121 const addr_buf = arena.alloc(usize, options.owner.debug_stack_frames_count) catch @panic("OOM");
248122 const first_ret_addr = options.first_ret_addr orelse @returnAddress();
249123 break :blk std.debug.captureCurrentStackTrace(.{ .first_address = first_ret_addr }, addr_buf);
250124 },
251 .result_error_msgs = .empty,
252 .result_error_bundle = std.zig.ErrorBundle.empty,
253 .result_stderr = "",
254 .result_cached = false,
255 .result_duration_ns = null,
256 .result_peak_rss = 0,
257 .result_failed_command = null,
258 .test_results = .{},
259 };
260}
261
262/// If the Step's `make` function reports `error.MakeFailed`, it indicates they
263/// have already reported the error. Otherwise, we add a simple error report
264/// here.
265pub fn make(s: *Step, options: MakeOptions) error{ MakeFailed, MakeSkipped }!void {
266 const arena = s.owner.allocator;
267 const graph = s.owner.graph;
268 const io = graph.io;
269
270 var start_ts: ?Io.Timestamp = t: {
271 if (!graph.time_report) break :t null;
272 if (s.id == .compile) break :t null;
273 if (s.id == .run and s.cast(Run).?.stdio == .zig_test) break :t null;
274 break :t Io.Clock.awake.now(io);
275125 };
276 const make_result = s.makeFn(s, options);
277 if (start_ts) |*ts| {
278 const duration = ts.untilNow(io, .awake);
279 options.web_server.?.updateTimeReportGeneric(s, duration);
280 }
281
282 make_result catch |err| switch (err) {
283 error.MakeFailed, error.MakeSkipped => |e| return e,
284 else => {
285 s.result_error_msgs.append(arena, @errorName(err)) catch @panic("OOM");
286 return error.MakeFailed;
287 },
288 };
289
290 if (!s.test_results.isSuccess()) {
291 return error.MakeFailed;
292 }
293
294 if (s.max_rss != 0 and s.result_peak_rss > s.max_rss) {
295 const msg = std.fmt.allocPrint(arena, "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", .{
296 s.result_peak_rss, s.max_rss,
297 }) catch @panic("OOM");
298 s.result_error_msgs.append(arena, msg) catch @panic("OOM");
299 }
300126}
301127
302128pub fn dependOn(step: *Step, other: *Step) void {
303 step.dependencies.append(other) catch @panic("OOM");
304}
305
306fn makeNoOp(step: *Step, options: MakeOptions) anyerror!void {
307 _ = options;
308
309 var all_cached = true;
310
311 for (step.dependencies.items) |dep| {
312 all_cached = all_cached and dep.result_cached;
313 }
314
315 step.result_cached = all_cached;
129 const arena = step.owner.allocator;
130 step.dependencies.append(arena, other) catch @panic("OOM");
316131}
317132
318133pub fn cast(step: *Step, comptime T: type) ?*T {
319 if (step.id == T.base_id) {
134 if (step.tag == T.base_tag) {
320135 return @fieldParentPtr("step", step);
321136 }
322137 return null;
......@@ -337,670 +152,6 @@ pub fn dump(step: *Step, t: Io.Terminal) void {
337152 }
338153}
339154
340/// Populates `s.result_failed_command`.
341pub fn captureChildProcess(
342 s: *Step,
343 gpa: Allocator,
344 progress_node: std.Progress.Node,
345 argv: []const []const u8,
346) !std.process.RunResult {
347 const graph = s.owner.graph;
348 const arena = graph.arena;
349 const io = graph.io;
350
351 // If an error occurs, it's happened in this command:
352 assert(s.result_failed_command == null);
353 s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv);
354
355 try handleChildProcUnsupported(s);
356 try handleVerbose(s.owner, .inherit, argv);
357
358 const result = std.process.run(arena, io, .{
359 .argv = argv,
360 .environ_map = &graph.environ_map,
361 .progress_node = progress_node,
362 }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err });
363
364 if (result.stderr.len > 0) {
365 try s.result_error_msgs.append(arena, result.stderr);
366 }
367
368 return result;
369}
370
371pub fn fail(step: *Step, comptime fmt: []const u8, args: anytype) error{ OutOfMemory, MakeFailed } {
372 try step.addError(fmt, args);
373 return error.MakeFailed;
374}
375
376pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void {
377 const arena = step.owner.allocator;
378 const msg = try std.fmt.allocPrint(arena, fmt, args);
379 try step.result_error_msgs.append(arena, msg);
380}
381
382pub const ZigProcess = struct {
383 child: std.process.Child,
384 multi_reader_buffer: Io.File.MultiReader.Buffer(2),
385 multi_reader: Io.File.MultiReader,
386 progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn,
387
388 pub const StreamEnum = enum { stdout, stderr };
389
390 pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void {
391 zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null;
392 }
393
394 pub fn deinit(zp: *ZigProcess, io: Io) void {
395 zp.child.kill(io);
396 zp.multi_reader.deinit();
397 zp.* = undefined;
398 }
399};
400
401/// Assumes that argv contains `--listen=-` and that the process being spawned
402/// is the zig compiler - the same version that compiled the build runner.
403/// Populates `s.result_failed_command`.
404pub fn evalZigProcess(
405 s: *Step,
406 argv: []const []const u8,
407 prog_node: std.Progress.Node,
408 watch: bool,
409 web_server: ?*Build.WebServer,
410 gpa: Allocator,
411) !?Path {
412 const b = s.owner;
413 const io = b.graph.io;
414
415 // If an error occurs, it's happened in this command:
416 assert(s.result_failed_command == null);
417 s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv);
418
419 if (s.getZigProcess()) |zp| update: {
420 assert(watch);
421 if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index);
422 zp.progress_ipc_index = null;
423 var exited = false;
424 defer if (exited) {
425 s.cast(Compile).?.zig_process = null;
426 zp.deinit(io);
427 gpa.destroy(zp);
428 } else zp.saveState(prog_node);
429 const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) {
430 error.BrokenPipe, error.EndOfStream => |reason| {
431 std.log.info("{s} restart required: {t}", .{ argv[0], reason });
432 // Process restart required.
433 const term = zp.child.wait(io) catch |e| {
434 return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
435 };
436 _ = term;
437 exited = true;
438 break :update;
439 },
440 else => |e| return e,
441 };
442
443 if (s.result_error_bundle.errorMessageCount() > 0) {
444 return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
445 }
446
447 if (s.result_error_msgs.items.len > 0 and result == null) {
448 // Crash detected.
449 const term = zp.child.wait(io) catch |e| {
450 return s.fail("unable to wait for {s}: {t}", .{ argv[0], e });
451 };
452 s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
453 exited = true;
454 try handleChildProcessTerm(s, term);
455 return error.MakeFailed;
456 }
457
458 return result;
459 }
460 assert(argv.len != 0);
461
462 try handleChildProcUnsupported(s);
463 try handleVerbose(s.owner, .inherit, argv);
464
465 const zp = try gpa.create(ZigProcess);
466 defer if (!watch) gpa.destroy(zp);
467
468 zp.child = std.process.spawn(io, .{
469 .argv = argv,
470 .environ_map = &b.graph.environ_map,
471 .stdin = .pipe,
472 .stdout = .pipe,
473 .stderr = .pipe,
474 .request_resource_usage_statistics = true,
475 .progress_node = prog_node,
476 }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err });
477
478 zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{
479 zp.child.stdout.?, zp.child.stderr.?,
480 });
481 if (watch) s.cast(Compile).?.zig_process = zp;
482 defer if (!watch) zp.deinit(io);
483
484 const result = result: {
485 defer if (watch) zp.saveState(prog_node);
486 break :result try zigProcessUpdate(s, zp, watch, web_server, gpa);
487 };
488
489 if (!watch) {
490 // Send EOF to stdin.
491 zp.child.stdin.?.close(io);
492 zp.child.stdin = null;
493
494 const term = zp.child.wait(io) catch |err| {
495 return s.fail("unable to wait for {s}: {t}", .{ argv[0], err });
496 };
497 s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0;
498
499 // Special handling for Compile step that is expecting compile errors.
500 if (s.cast(Compile)) |compile| switch (term) {
501 .exited => {
502 // Note that the exit code may be 0 in this case due to the
503 // compiler server protocol.
504 if (compile.expect_errors != null) {
505 return error.NeedCompileErrorCheck;
506 }
507 },
508 else => {},
509 };
510
511 try handleChildProcessTerm(s, term);
512 }
513
514 if (s.result_error_bundle.errorMessageCount() > 0) {
515 return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()});
516 }
517
518 return result;
519}
520
521/// Wrapper around `Io.Dir.updateFile` that handles verbose and error output.
522pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus {
523 const b = s.owner;
524 const io = b.graph.io;
525 const src_path = src_lazy_path.getPath3(b, s);
526 try handleVerbose(b, .inherit, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path });
527 return Io.Dir.updateFile(src_path.root_dir.handle, io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err|
528 return s.fail("unable to update file from '{f}' to '{s}': {t}", .{ src_path, dest_path, err });
529}
530
531/// Wrapper around `Io.Dir.createDirPathStatus` that handles verbose and error output.
532pub fn installDir(s: *Step, dest_path: []const u8) !Io.Dir.CreatePathStatus {
533 const b = s.owner;
534 const io = b.graph.io;
535 try handleVerbose(b, .inherit, &.{ "install", "-d", dest_path });
536 return Io.Dir.cwd().createDirPathStatus(io, dest_path, .default_dir) catch |err|
537 return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err });
538}
539
540fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.WebServer, gpa: Allocator) !?Path {
541 const b = s.owner;
542 const arena = b.allocator;
543 const io = b.graph.io;
544
545 const start_ts = Io.Clock.awake.now(io);
546
547 try sendMessage(io, zp.child.stdin.?, .update);
548 if (!watch) try sendMessage(io, zp.child.stdin.?, .exit);
549
550 var result: ?Path = null;
551 var eos_err: error{EndOfStream}!void = {};
552
553 const stdout = zp.multi_reader.fileReader(0);
554
555 while (true) {
556 const Header = std.zig.Server.Message.Header;
557 const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) {
558 error.EndOfStream => break,
559 error.ReadFailed => return stdout.err.?,
560 };
561 const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) {
562 error.EndOfStream => |e| {
563 // Better to report the crash with stderr below, but we set
564 // this in case the child exits successfully while violating
565 // this protocol.
566 eos_err = e;
567 break;
568 },
569 error.ReadFailed => return stdout.err.?,
570 };
571 switch (header.tag) {
572 .zig_version => {
573 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
574 return s.fail(
575 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
576 .{ builtin.zig_version_string, body },
577 );
578 }
579 },
580 .error_bundle => {
581 s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body);
582 // This message indicates the end of the update.
583 if (watch) break;
584 },
585 .emit_digest => {
586 const EmitDigest = std.zig.Server.Message.EmitDigest;
587 const emit_digest: *align(1) const EmitDigest = @ptrCast(body);
588 s.result_cached = emit_digest.flags.cache_hit;
589 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
590 result = .{
591 .root_dir = b.cache_root,
592 .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
593 };
594 },
595 .file_system_inputs => {
596 s.clearWatchInputs();
597 var it = std.mem.splitScalar(u8, body, 0);
598 while (it.next()) |prefixed_path| {
599 const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1);
600 const sub_path = try arena.dupe(u8, prefixed_path[1..]);
601 const sub_path_dirname = std.fs.path.dirname(sub_path) orelse "";
602 switch (prefix_index) {
603 .cwd => {
604 const path: Build.Cache.Path = .{
605 .root_dir = Build.Cache.Directory.cwd(),
606 .sub_path = sub_path_dirname,
607 };
608 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
609 },
610 .zig_lib => zl: {
611 if (s.cast(Step.Compile)) |compile| {
612 if (compile.zig_lib_dir) |zig_lib_dir| {
613 const lp = try zig_lib_dir.join(arena, sub_path);
614 try addWatchInput(s, lp);
615 break :zl;
616 }
617 }
618 const path: Build.Cache.Path = .{
619 .root_dir = s.owner.graph.zig_lib_directory,
620 .sub_path = sub_path_dirname,
621 };
622 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
623 },
624 .local_cache => {
625 const path: Build.Cache.Path = .{
626 .root_dir = b.cache_root,
627 .sub_path = sub_path_dirname,
628 };
629 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
630 },
631 .global_cache => {
632 const path: Build.Cache.Path = .{
633 .root_dir = s.owner.graph.global_cache_root,
634 .sub_path = sub_path_dirname,
635 };
636 try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path));
637 },
638 }
639 }
640 },
641 .time_report => if (web_server) |ws| {
642 const TimeReport = std.zig.Server.Message.TimeReport;
643 const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]);
644 ws.updateTimeReportCompile(.{
645 .compile = s.cast(Step.Compile).?,
646 .use_llvm = tr.flags.use_llvm,
647 .stats = tr.stats,
648 .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()),
649 .llvm_pass_timings_len = tr.llvm_pass_timings_len,
650 .files_len = tr.files_len,
651 .decls_len = tr.decls_len,
652 .trailing = body[@sizeOf(TimeReport)..],
653 });
654 },
655 else => {}, // ignore other messages
656 }
657 }
658
659 s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds());
660
661 const stderr_contents = zp.multi_reader.reader(1).buffered();
662 if (stderr_contents.len > 0) {
663 try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents));
664 }
665
666 try eos_err;
667
668 return result;
669}
670
671pub fn getZigProcess(s: *Step) ?*ZigProcess {
672 return switch (s.id) {
673 .compile => s.cast(Compile).?.zig_process,
674 else => null,
675 };
676}
677
678fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
679 const header: std.zig.Client.Message.Header = .{
680 .tag = tag,
681 .bytes_len = 0,
682 };
683 var w = file.writer(io, &.{});
684 w.interface.writeStruct(header, .little) catch |err| switch (err) {
685 error.WriteFailed => return w.err.?,
686 };
687}
688
689pub fn handleVerbose(
690 b: *Build,
691 cwd: std.process.Child.Cwd,
692 argv: []const []const u8,
693) error{OutOfMemory}!void {
694 return handleVerbose2(b, cwd, null, argv);
695}
696
697pub fn handleVerbose2(
698 b: *Build,
699 cwd: std.process.Child.Cwd,
700 opt_env: ?*const std.process.Environ.Map,
701 argv: []const []const u8,
702) error{OutOfMemory}!void {
703 if (b.verbose) {
704 const graph = b.graph;
705 // Intention of verbose is to print all sub-process command lines to
706 // stderr before spawning them.
707 const text = try allocPrintCmd(b.allocator, cwd, if (opt_env) |env| .{
708 .child = env,
709 .parent = &graph.environ_map,
710 } else null, argv);
711 std.debug.print("{s}\n", .{text});
712 }
713}
714
715/// Asserts that the caller has already populated `s.result_failed_command`.
716pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void {
717 if (!std.process.can_spawn) {
718 return s.fail("unable to spawn process: host cannot spawn child processes", .{});
719 }
720}
721
722/// Asserts that the caller has already populated `s.result_failed_command`.
723pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void {
724 assert(s.result_failed_command != null);
725 return switch (term) {
726 .exited => |code| if (code != 0) s.fail("process exited with error code {d}", .{code}),
727 .signal => |sig| s.fail("process terminated with signal {t}", .{sig}),
728 .stopped => |sig| s.fail("process stopped with signal {t}", .{sig}),
729 .unknown => s.fail("process terminated unexpectedly", .{}),
730 };
731}
732
733pub fn allocPrintCmd(
734 gpa: Allocator,
735 cwd: std.process.Child.Cwd,
736 opt_env: ?struct {
737 child: *const std.process.Environ.Map,
738 parent: *const std.process.Environ.Map,
739 },
740 argv: []const []const u8,
741) Allocator.Error![]u8 {
742 const shell = struct {
743 fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
744 for (string) |c| {
745 if (switch (c) {
746 else => true,
747 '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
748 '=' => is_argv0,
749 }) break;
750 } else return writer.writeAll(string);
751
752 try writer.writeByte('"');
753 for (string) |c| {
754 if (switch (c) {
755 std.ascii.control_code.nul => break,
756 '!', '"', '$', '\\', '`' => true,
757 else => !std.ascii.isPrint(c),
758 }) try writer.writeByte('\\');
759 switch (c) {
760 std.ascii.control_code.nul => unreachable,
761 std.ascii.control_code.bel => try writer.writeByte('a'),
762 std.ascii.control_code.bs => try writer.writeByte('b'),
763 std.ascii.control_code.ht => try writer.writeByte('t'),
764 std.ascii.control_code.lf => try writer.writeByte('n'),
765 std.ascii.control_code.vt => try writer.writeByte('v'),
766 std.ascii.control_code.ff => try writer.writeByte('f'),
767 std.ascii.control_code.cr => try writer.writeByte('r'),
768 std.ascii.control_code.esc => try writer.writeByte('E'),
769 ' '...'~' => try writer.writeByte(c),
770 else => try writer.print("{o:0>3}", .{c}),
771 }
772 }
773 try writer.writeByte('"');
774 }
775 };
776
777 var aw: Io.Writer.Allocating = .init(gpa);
778 defer aw.deinit();
779 const writer = &aw.writer;
780 switch (cwd) {
781 .inherit => {},
782 .path => |path| writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory,
783 .dir => @panic("TODO"),
784 }
785 if (opt_env) |env| {
786 var it = env.child.iterator();
787 while (it.next()) |entry| {
788 const key = entry.key_ptr.*;
789 const value = entry.value_ptr.*;
790 if (env.parent.get(key)) |process_value| {
791 if (std.mem.eql(u8, value, process_value)) continue;
792 }
793 writer.print("{s}=", .{key}) catch return error.OutOfMemory;
794 shell.escape(writer, value, false) catch return error.OutOfMemory;
795 writer.writeByte(' ') catch return error.OutOfMemory;
796 }
797 }
798 shell.escape(writer, argv[0], true) catch return error.OutOfMemory;
799 for (argv[1..]) |arg| {
800 writer.writeByte(' ') catch return error.OutOfMemory;
801 shell.escape(writer, arg, false) catch return error.OutOfMemory;
802 }
803 return aw.toOwnedSlice();
804}
805
806/// Prefer `cacheHitAndWatch` unless you already added watch inputs
807/// separately from using the cache system.
808pub fn cacheHit(s: *Step, man: *Build.Cache.Manifest) !bool {
809 s.result_cached = man.hit() catch |err| return failWithCacheError(s, man, err);
810 return s.result_cached;
811}
812
813/// Clears previous watch inputs, if any, and then populates watch inputs from
814/// the full set of files picked up by the cache manifest.
815///
816/// Must be accompanied with `writeManifestAndWatch`.
817pub fn cacheHitAndWatch(s: *Step, man: *Build.Cache.Manifest) !bool {
818 const is_hit = man.hit() catch |err| return failWithCacheError(s, man, err);
819 s.result_cached = is_hit;
820 // The above call to hit() populates the manifest with files, so in case of
821 // a hit, we need to populate watch inputs.
822 if (is_hit) try setWatchInputsFromManifest(s, man);
823 return is_hit;
824}
825
826fn failWithCacheError(
827 s: *Step,
828 man: *const Build.Cache.Manifest,
829 err: Build.Cache.Manifest.HitError,
830) error{ OutOfMemory, Canceled, MakeFailed } {
831 switch (err) {
832 error.CacheCheckFailed => switch (man.diagnostic) {
833 .none => unreachable,
834 .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {t} {t}", .{
835 man.diagnostic, e,
836 }),
837 .file_open, .file_stat, .file_read, .file_hash => |op| {
838 const pp = man.files.keys()[op.file_index].prefixed_path;
839 const prefix = man.cache.prefixes()[pp.prefix].path orelse "";
840 return s.fail("failed to check cache: '{s}{c}{s}' {t} {t}", .{
841 prefix, std.fs.path.sep, pp.sub_path, man.diagnostic, op.err,
842 });
843 },
844 },
845 error.OutOfMemory, error.Canceled => |e| return e,
846 error.InvalidFormat => return s.fail("failed to check cache: invalid manifest file format", .{}),
847 }
848}
849
850/// Prefer `writeManifestAndWatch` unless you already added watch inputs
851/// separately from using the cache system.
852pub fn writeManifest(s: *Step, man: *Build.Cache.Manifest) !void {
853 if (s.test_results.isSuccess()) {
854 man.writeManifest() catch |err| {
855 try s.addError("unable to write cache manifest: {t}", .{err});
856 };
857 }
858}
859
860/// Clears previous watch inputs, if any, and then populates watch inputs from
861/// the full set of files picked up by the cache manifest.
862///
863/// Must be accompanied with `cacheHitAndWatch`.
864pub fn writeManifestAndWatch(s: *Step, man: *Build.Cache.Manifest) !void {
865 try writeManifest(s, man);
866 try setWatchInputsFromManifest(s, man);
867}
868
869fn setWatchInputsFromManifest(s: *Step, man: *Build.Cache.Manifest) !void {
870 const arena = s.owner.allocator;
871 const prefixes = man.cache.prefixes();
872 clearWatchInputs(s);
873 for (man.files.keys()) |file| {
874 // The file path data is freed when the cache manifest is cleaned up at the end of `make`.
875 const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path);
876 try addWatchInputFromPath(s, .{
877 .root_dir = prefixes[file.prefixed_path.prefix],
878 .sub_path = std.fs.path.dirname(sub_path) orelse "",
879 }, std.fs.path.basename(sub_path));
880 }
881}
882
883/// For steps that have a single input that never changes when re-running `make`.
884pub fn singleUnchangingWatchInput(step: *Step, lazy_path: Build.LazyPath) Allocator.Error!void {
885 if (!step.inputs.populated()) try step.addWatchInput(lazy_path);
886}
887
888pub fn clearWatchInputs(step: *Step) void {
889 const gpa = step.owner.allocator;
890 step.inputs.clear(gpa);
891}
892
893/// Places a *file* dependency on the path.
894pub fn addWatchInput(step: *Step, lazy_file: Build.LazyPath) Allocator.Error!void {
895 switch (lazy_file) {
896 .src_path => |src_path| try addWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
897 .dependency => |d| try addWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
898 .cwd_relative => |path_string| {
899 try addWatchInputFromPath(step, .{
900 .root_dir = .{
901 .path = null,
902 .handle = Io.Dir.cwd(),
903 },
904 .sub_path = std.fs.path.dirname(path_string) orelse "",
905 }, std.fs.path.basename(path_string));
906 },
907 // Nothing to watch because this dependency edge is modeled instead via `dependants`.
908 .generated => {},
909 }
910}
911
912/// Any changes inside the directory will trigger invalidation.
913///
914/// See also `addDirectoryWatchInputFromPath` which takes a `Build.Cache.Path` instead.
915///
916/// Paths derived from this directory should also be manually added via
917/// `addDirectoryWatchInputFromPath` if and only if this function returns
918/// `true`.
919pub fn addDirectoryWatchInput(step: *Step, lazy_directory: Build.LazyPath) Allocator.Error!bool {
920 switch (lazy_directory) {
921 .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
922 .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
923 .cwd_relative => |path_string| {
924 try addDirectoryWatchInputFromPath(step, .{
925 .root_dir = .{
926 .path = null,
927 .handle = Io.Dir.cwd(),
928 },
929 .sub_path = path_string,
930 });
931 },
932 // Nothing to watch because this dependency edge is modeled instead via `dependants`.
933 .generated => return false,
934 }
935 return true;
936}
937
938/// Any changes inside the directory will trigger invalidation.
939///
940/// See also `addDirectoryWatchInput` which takes a `Build.LazyPath` instead.
941///
942/// This function should only be called when it has been verified that the
943/// dependency on `path` is not already accounted for by a `Step` dependency.
944/// In other words, before calling this function, first check that the
945/// `Build.LazyPath` which this `path` is derived from is not `generated`.
946pub fn addDirectoryWatchInputFromPath(step: *Step, path: Build.Cache.Path) !void {
947 return addWatchInputFromPath(step, path, ".");
948}
949
950fn addWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
951 return addWatchInputFromPath(step, .{
952 .root_dir = builder.build_root,
953 .sub_path = std.fs.path.dirname(sub_path) orelse "",
954 }, std.fs.path.basename(sub_path));
955}
956
957fn addDirectoryWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void {
958 return addDirectoryWatchInputFromPath(step, .{
959 .root_dir = builder.build_root,
960 .sub_path = sub_path,
961 });
962}
963
964fn addWatchInputFromPath(step: *Step, path: Build.Cache.Path, basename: []const u8) !void {
965 const gpa = step.owner.allocator;
966 const gop = try step.inputs.table.getOrPut(gpa, path);
967 if (!gop.found_existing) gop.value_ptr.* = .empty;
968 try gop.value_ptr.append(gpa, basename);
969}
970
971/// Implementation detail of file watching and forced rebuilds. Prepares the step for being re-evaluated.
972pub fn reset(step: *Step, gpa: Allocator) void {
973 assert(step.state == .precheck_done);
974
975 if (step.result_failed_command) |cmd| gpa.free(cmd);
976
977 step.result_error_msgs.clearRetainingCapacity();
978 step.result_stderr = "";
979 step.result_cached = false;
980 step.result_duration_ns = null;
981 step.result_peak_rss = 0;
982 step.result_failed_command = null;
983 step.test_results = .{};
984 step.clearWatchInputs();
985
986 step.result_error_bundle.deinit(gpa);
987 step.result_error_bundle = std.zig.ErrorBundle.empty;
988}
989
990/// Implementation detail of file watching. Prepares the step for being re-evaluated.
991/// Returns `true` if the step was newly invalidated, `false` if it was already invalidated.
992pub fn invalidateResult(step: *Step, gpa: Allocator) bool {
993 if (step.state == .precheck_done) return false;
994 assert(step.pending_deps == 0);
995 step.state = .precheck_done;
996 step.reset(gpa);
997 for (step.dependants.items) |dependant| {
998 _ = dependant.invalidateResult(gpa);
999 dependant.pending_deps += 1;
1000 }
1001 return true;
1002}
1003
1004155test {
1005156 _ = CheckFile;
1006157 _ = Fail;
lib/std/Build/Step/CheckFile.zig+2-2
......@@ -16,7 +16,7 @@ expected_exact: ?[]const u8,
1616source: std.Build.LazyPath,
1717max_bytes: usize = 20 * 1024 * 1024,
1818
19pub const base_id: Step.Id = .check_file;
19pub const base_tag: Step.Tag = .check_file;
2020
2121pub const Options = struct {
2222 expected_matches: []const []const u8 = &.{},
......@@ -31,7 +31,7 @@ pub fn create(
3131 const check_file = owner.allocator.create(CheckFile) catch @panic("OOM");
3232 check_file.* = .{
3333 .step = Step.init(.{
34 .id = base_id,
34 .tag = base_tag,
3535 .name = "CheckFile",
3636 .owner = owner,
3737 .makeFn = make,
lib/std/Build/Step/Compile.zig+2-1076
......@@ -20,7 +20,7 @@ const InstallDir = std.Build.InstallDir;
2020const GeneratedFile = std.Build.GeneratedFile;
2121const Path = std.Build.Cache.Path;
2222
23pub const base_id: Step.Id = .compile;
23pub const base_tag: Step.Tag = .compile;
2424
2525step: Step,
2626root_module: *Module,
......@@ -235,10 +235,6 @@ is_linking_libc: bool = false,
235235/// Computed during make().
236236is_linking_libcpp: bool = false,
237237
238/// Populated during the make phase when there is a long-lived compiler process.
239/// Managed by the build runner, not user build script.
240zig_process: ?*Step.ZigProcess,
241
242238/// Enables coverage instrumentation that is only useful if you are using third
243239/// party fuzzers that depend on it. Otherwise, slows down the instrumented
244240/// binary with unnecessary function calls.
......@@ -418,10 +414,9 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
418414 .kind = options.kind,
419415 .name = name,
420416 .step = .init(.{
421 .id = base_id,
417 .tag = base_tag,
422418 .name = step_name,
423419 .owner = owner,
424 .makeFn = make,
425420 .max_rss = options.max_rss,
426421 }),
427422 .version = options.version,
......@@ -452,8 +447,6 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
452447 .use_llvm = options.use_llvm,
453448 .use_lld = options.use_lld,
454449 .use_new_linker = null,
455
456 .zig_process = null,
457450 };
458451
459452 if (options.zig_lib_dir) |lp| {
......@@ -701,122 +694,6 @@ pub fn producesImplib(compile: *Compile) bool {
701694 return compile.isDll();
702695}
703696
704const PkgConfigResult = struct {
705 cflags: []const []const u8,
706 libs: []const []const u8,
707};
708
709/// Run pkg-config for the given library name and parse the output, returning the arguments
710/// that should be passed to zig to link the given library.
711pub fn runPkgConfig(step: *Step, lib_name: []const u8) !PkgConfigResult {
712 const wl_rpath_prefix = "-Wl,-rpath,";
713
714 const b = step.owner;
715 const pkg_name = match: {
716 // First we have to map the library name to pkg config name. Unfortunately,
717 // there are several examples where this is not straightforward:
718 // -lSDL2 -> pkg-config sdl2
719 // -lgdk-3 -> pkg-config gdk-3.0
720 // -latk-1.0 -> pkg-config atk
721 // -lpulse -> pkg-config libpulse
722 const pkgs = try getPkgConfigList(b);
723
724 // Exact match means instant winner.
725 for (pkgs) |pkg| {
726 if (mem.eql(u8, pkg.name, lib_name)) {
727 break :match pkg.name;
728 }
729 }
730
731 // Next we'll try ignoring case.
732 for (pkgs) |pkg| {
733 if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
734 break :match pkg.name;
735 }
736 }
737
738 // Prefixed "lib" or suffixed ".0".
739 for (pkgs) |pkg| {
740 if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| {
741 const prefix = pkg.name[0..pos];
742 const suffix = pkg.name[pos + lib_name.len ..];
743 if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
744 if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
745 break :match pkg.name;
746 }
747 }
748
749 // Trimming "-1.0".
750 if (mem.endsWith(u8, lib_name, "-1.0")) {
751 const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
752 for (pkgs) |pkg| {
753 if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
754 break :match pkg.name;
755 }
756 }
757 }
758
759 return error.PackageNotFound;
760 };
761
762 var code: u8 = undefined;
763 const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
764 const stdout = if (b.runAllowFail(&[_][]const u8{
765 pkg_config_exe,
766 pkg_name,
767 "--cflags",
768 "--libs",
769 }, &code, .ignore)) |stdout| stdout else |err| switch (err) {
770 error.ProcessTerminated => return error.PkgConfigCrashed,
771 error.ExecNotSupported => return error.PkgConfigFailed,
772 error.ExitCodeFailure => return error.PkgConfigFailed,
773 error.FileNotFound => return error.PkgConfigNotInstalled,
774 else => return err,
775 };
776
777 var zig_cflags: std.ArrayList([]const u8) = .empty;
778 defer zig_cflags.deinit(b.allocator);
779 var zig_libs: std.ArrayList([]const u8) = .empty;
780 defer zig_libs.deinit(b.allocator);
781
782 var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
783 while (arg_it.next()) |arg| {
784 if (mem.eql(u8, arg, "-I")) {
785 const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
786 try zig_cflags.appendSlice(b.allocator, &.{ "-I", dir });
787 } else if (mem.startsWith(u8, arg, "-I")) {
788 try zig_cflags.append(b.allocator, arg);
789 } else if (mem.eql(u8, arg, "-L")) {
790 const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
791 try zig_libs.appendSlice(b.allocator, &.{ "-L", dir });
792 } else if (mem.startsWith(u8, arg, "-L")) {
793 try zig_libs.append(b.allocator, arg);
794 } else if (mem.eql(u8, arg, "-l")) {
795 const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput;
796 try zig_libs.appendSlice(b.allocator, &.{ "-l", lib });
797 } else if (mem.startsWith(u8, arg, "-l")) {
798 try zig_libs.append(b.allocator, arg);
799 } else if (mem.eql(u8, arg, "-D")) {
800 const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput;
801 try zig_cflags.appendSlice(b.allocator, &.{ "-D", macro });
802 } else if (mem.startsWith(u8, arg, "-D")) {
803 try zig_cflags.append(b.allocator, arg);
804 } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
805 try zig_cflags.appendSlice(b.allocator, &.{ "-rpath", arg[wl_rpath_prefix.len..] });
806 } else if (b.debug_pkg_config) {
807 return step.fail("unknown pkg-config flag '{s}'", .{arg});
808 }
809 }
810
811 try zig_cflags.shrinkToLen(b.allocator);
812 try zig_libs.shrinkToLen(b.allocator);
813
814 return .{
815 .cflags = zig_cflags.toOwnedSliceAssert(),
816 .libs = zig_libs.toOwnedSliceAssert(),
817 };
818}
819
820697pub fn setVerboseLink(compile: *Compile, value: bool) void {
821698 compile.verbose_link = value;
822699}
......@@ -974,863 +851,6 @@ fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking
974851 return path;
975852}
976853
977fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
978 const step = &compile.step;
979 const b = step.owner;
980 const arena = b.allocator;
981
982 var zig_args = std.array_list.Managed([]const u8).init(arena);
983 defer zig_args.deinit();
984
985 try zig_args.append(b.graph.zig_exe);
986
987 const cmd = switch (compile.kind) {
988 .lib => "build-lib",
989 .exe => "build-exe",
990 .obj => "build-obj",
991 .@"test" => "test",
992 .test_obj => "test-obj",
993 };
994 try zig_args.append(cmd);
995
996 if (b.reference_trace) |some| {
997 try zig_args.append(try std.fmt.allocPrint(arena, "-freference-trace={d}", .{some}));
998 }
999 try addFlag(&zig_args, "allow-so-scripts", compile.allow_so_scripts orelse b.graph.allow_so_scripts);
1000
1001 try addFlag(&zig_args, "llvm", compile.use_llvm);
1002 try addFlag(&zig_args, "lld", compile.use_lld);
1003 try addFlag(&zig_args, "new-linker", compile.use_new_linker);
1004
1005 if (compile.root_module.resolved_target.?.query.ofmt) |ofmt| {
1006 try zig_args.append(try std.fmt.allocPrint(arena, "-ofmt={s}", .{@tagName(ofmt)}));
1007 }
1008
1009 switch (compile.entry) {
1010 .default => {},
1011 .disabled => try zig_args.append("-fno-entry"),
1012 .enabled => try zig_args.append("-fentry"),
1013 .symbol_name => |entry_name| {
1014 try zig_args.append(try std.fmt.allocPrint(arena, "-fentry={s}", .{entry_name}));
1015 },
1016 }
1017
1018 {
1019 var symbol_it = compile.force_undefined_symbols.keyIterator();
1020 while (symbol_it.next()) |symbol_name| {
1021 try zig_args.append("--force_undefined");
1022 try zig_args.append(symbol_name.*);
1023 }
1024 }
1025
1026 if (compile.stack_size) |stack_size| {
1027 try zig_args.append("--stack");
1028 try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size}));
1029 }
1030
1031 if (fuzz) {
1032 try zig_args.append("-ffuzz");
1033 }
1034
1035 {
1036 // Stores system libraries that have already been seen for at least one
1037 // module, along with any arguments that need to be passed to the
1038 // compiler for each module individually.
1039 var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty;
1040 var frameworks: std.StringArrayHashMapUnmanaged(Module.LinkFrameworkOptions) = .empty;
1041
1042 var prev_has_cflags = false;
1043 var prev_has_rcflags = false;
1044 var prev_search_strategy: Module.SystemLib.SearchStrategy = .paths_first;
1045 var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic;
1046 // Track the number of positional arguments so that a nice error can be
1047 // emitted if there is nothing to link.
1048 var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null);
1049
1050 // Fully recursive iteration including dynamic libraries to detect
1051 // libc and libc++ linkage.
1052 for (compile.getCompileDependencies(true)) |some_compile| {
1053 for (some_compile.root_module.getGraph().modules) |mod| {
1054 if (mod.link_libc == true) compile.is_linking_libc = true;
1055 if (mod.link_libcpp == true) compile.is_linking_libcpp = true;
1056 }
1057 }
1058
1059 var cli_named_modules = try CliNamedModules.init(arena, compile.root_module);
1060
1061 // For this loop, don't chase dynamic libraries because their link
1062 // objects are already linked.
1063 for (compile.getCompileDependencies(false)) |dep_compile| {
1064 for (dep_compile.root_module.getGraph().modules) |mod| {
1065 // While walking transitive dependencies, if a given link object is
1066 // already included in a library, it should not redundantly be
1067 // placed on the linker line of the dependee.
1068 const my_responsibility = dep_compile == compile;
1069 const already_linked = !my_responsibility and dep_compile.isDynamicLibrary();
1070
1071 // Inherit dependencies on darwin frameworks.
1072 if (!already_linked) {
1073 for (mod.frameworks.keys(), mod.frameworks.values()) |name, info| {
1074 try frameworks.put(arena, name, info);
1075 }
1076 }
1077
1078 // Inherit dependencies on system libraries and static libraries.
1079 for (mod.link_objects.items) |link_object| {
1080 switch (link_object) {
1081 .static_path => |static_path| {
1082 if (my_responsibility) {
1083 try zig_args.append(static_path.getPath2(mod.owner, step));
1084 total_linker_objects += 1;
1085 }
1086 },
1087 .system_lib => |system_lib| {
1088 const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name);
1089 if (system_lib_gop.found_existing) {
1090 try zig_args.appendSlice(system_lib_gop.value_ptr.*);
1091 continue;
1092 } else {
1093 system_lib_gop.value_ptr.* = &.{};
1094 }
1095
1096 if (already_linked)
1097 continue;
1098
1099 if ((system_lib.search_strategy != prev_search_strategy or
1100 system_lib.preferred_link_mode != prev_preferred_link_mode) and
1101 compile.linkage != .static)
1102 {
1103 switch (system_lib.search_strategy) {
1104 .no_fallback => switch (system_lib.preferred_link_mode) {
1105 .dynamic => try zig_args.append("-search_dylibs_only"),
1106 .static => try zig_args.append("-search_static_only"),
1107 },
1108 .paths_first => switch (system_lib.preferred_link_mode) {
1109 .dynamic => try zig_args.append("-search_paths_first"),
1110 .static => try zig_args.append("-search_paths_first_static"),
1111 },
1112 .mode_first => switch (system_lib.preferred_link_mode) {
1113 .dynamic => try zig_args.append("-search_dylibs_first"),
1114 .static => try zig_args.append("-search_static_first"),
1115 },
1116 }
1117 prev_search_strategy = system_lib.search_strategy;
1118 prev_preferred_link_mode = system_lib.preferred_link_mode;
1119 }
1120
1121 const prefix: []const u8 = prefix: {
1122 if (system_lib.needed) break :prefix "-needed-l";
1123 if (system_lib.weak) break :prefix "-weak-l";
1124 break :prefix "-l";
1125 };
1126 switch (system_lib.use_pkg_config) {
1127 .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })),
1128 .yes, .force => {
1129 if (runPkgConfig(&compile.step, system_lib.name)) |result| {
1130 try zig_args.appendSlice(result.cflags);
1131 try zig_args.appendSlice(result.libs);
1132 try seen_system_libs.put(arena, system_lib.name, result.cflags);
1133 } else |err| switch (err) {
1134 error.PkgConfigInvalidOutput,
1135 error.PkgConfigCrashed,
1136 error.PkgConfigFailed,
1137 error.PkgConfigNotInstalled,
1138 error.PackageNotFound,
1139 => switch (system_lib.use_pkg_config) {
1140 .yes => {
1141 // pkg-config failed, so fall back to linking the library
1142 // by name directly.
1143 try zig_args.append(b.fmt("{s}{s}", .{
1144 prefix,
1145 system_lib.name,
1146 }));
1147 },
1148 .force => {
1149 panic("pkg-config failed for library {s}", .{system_lib.name});
1150 },
1151 .no => unreachable,
1152 },
1153
1154 else => |e| return e,
1155 }
1156 },
1157 }
1158 },
1159 .other_step => |other| {
1160 switch (other.kind) {
1161 .exe => return step.fail("cannot link with an executable build artifact", .{}),
1162 .@"test" => return step.fail("cannot link with a test", .{}),
1163 .obj, .test_obj => {
1164 const included_in_lib_or_obj = !my_responsibility and
1165 (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj);
1166 if (!already_linked and !included_in_lib_or_obj) {
1167 try zig_args.append(other.getEmittedBin().getPath2(b, step));
1168 total_linker_objects += 1;
1169 }
1170 },
1171 .lib => l: {
1172 const other_produces_implib = other.producesImplib();
1173 const other_is_static = other_produces_implib or other.isStaticLibrary();
1174
1175 if (compile.isStaticLibrary() and other_is_static) {
1176 // Avoid putting a static library inside a static library.
1177 break :l;
1178 }
1179
1180 // For DLLs, we must link against the implib.
1181 // For everything else, we directly link
1182 // against the library file.
1183 const full_path_lib = if (other_produces_implib)
1184 try other.getGeneratedFilePath("generated_implib", &compile.step)
1185 else
1186 try other.getGeneratedFilePath("generated_bin", &compile.step);
1187
1188 try zig_args.append(full_path_lib);
1189 total_linker_objects += 1;
1190
1191 if (other.linkage == .dynamic and
1192 compile.rootModuleTarget().os.tag != .windows)
1193 {
1194 if (fs.path.dirname(full_path_lib)) |dirname| {
1195 try zig_args.append("-rpath");
1196 try zig_args.append(dirname);
1197 }
1198 }
1199 },
1200 }
1201 },
1202 .assembly_file => |asm_file| l: {
1203 if (!my_responsibility) break :l;
1204
1205 if (prev_has_cflags) {
1206 try zig_args.append("-cflags");
1207 try zig_args.append("--");
1208 prev_has_cflags = false;
1209 }
1210 try zig_args.append(asm_file.getPath2(mod.owner, step));
1211 total_linker_objects += 1;
1212 },
1213
1214 .c_source_file => |c_source_file| l: {
1215 if (!my_responsibility) break :l;
1216
1217 if (prev_has_cflags or c_source_file.flags.len != 0) {
1218 try zig_args.append("-cflags");
1219 for (c_source_file.flags) |arg| {
1220 try zig_args.append(arg);
1221 }
1222 try zig_args.append("--");
1223 }
1224 prev_has_cflags = (c_source_file.flags.len != 0);
1225
1226 if (c_source_file.language) |lang| {
1227 try zig_args.append("-x");
1228 try zig_args.append(lang.internalIdentifier());
1229 }
1230
1231 try zig_args.append(c_source_file.file.getPath2(mod.owner, step));
1232
1233 if (c_source_file.language != null) {
1234 try zig_args.append("-x");
1235 try zig_args.append("none");
1236 }
1237 total_linker_objects += 1;
1238 },
1239
1240 .c_source_files => |c_source_files| l: {
1241 if (!my_responsibility) break :l;
1242
1243 if (prev_has_cflags or c_source_files.flags.len != 0) {
1244 try zig_args.append("-cflags");
1245 for (c_source_files.flags) |arg| {
1246 try zig_args.append(arg);
1247 }
1248 try zig_args.append("--");
1249 }
1250 prev_has_cflags = (c_source_files.flags.len != 0);
1251
1252 if (c_source_files.language) |lang| {
1253 try zig_args.append("-x");
1254 try zig_args.append(lang.internalIdentifier());
1255 }
1256
1257 const root_path = c_source_files.root.getPath2(mod.owner, step);
1258 for (c_source_files.files) |file| {
1259 try zig_args.append(b.pathJoin(&.{ root_path, file }));
1260 }
1261
1262 if (c_source_files.language != null) {
1263 try zig_args.append("-x");
1264 try zig_args.append("none");
1265 }
1266
1267 total_linker_objects += c_source_files.files.len;
1268 },
1269
1270 .win32_resource_file => |rc_source_file| l: {
1271 if (!my_responsibility) break :l;
1272
1273 if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) {
1274 if (prev_has_rcflags) {
1275 try zig_args.append("-rcflags");
1276 try zig_args.append("--");
1277 prev_has_rcflags = false;
1278 }
1279 } else {
1280 try zig_args.append("-rcflags");
1281 for (rc_source_file.flags) |arg| {
1282 try zig_args.append(arg);
1283 }
1284 for (rc_source_file.include_paths) |include_path| {
1285 try zig_args.append("/I");
1286 try zig_args.append(include_path.getPath2(mod.owner, step));
1287 }
1288 try zig_args.append("--");
1289 prev_has_rcflags = true;
1290 }
1291 try zig_args.append(rc_source_file.file.getPath2(mod.owner, step));
1292 total_linker_objects += 1;
1293 },
1294 }
1295 }
1296
1297 // We need to emit the --mod argument here so that the above link objects
1298 // have the correct parent module, but only if the module is part of
1299 // this compilation.
1300 if (!my_responsibility) continue;
1301 if (cli_named_modules.modules.getIndex(mod)) |module_cli_index| {
1302 const module_cli_name = cli_named_modules.names.keys()[module_cli_index];
1303 try mod.appendZigProcessFlags(&zig_args, step);
1304
1305 // --dep arguments
1306 try zig_args.ensureUnusedCapacity(mod.import_table.count() * 2);
1307 for (mod.import_table.keys(), mod.import_table.values()) |name, import| {
1308 const import_index = cli_named_modules.modules.getIndex(import).?;
1309 const import_cli_name = cli_named_modules.names.keys()[import_index];
1310 zig_args.appendAssumeCapacity("--dep");
1311 if (std.mem.eql(u8, import_cli_name, name)) {
1312 zig_args.appendAssumeCapacity(import_cli_name);
1313 } else {
1314 zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name }));
1315 }
1316 }
1317
1318 // When the CLI sees a -M argument, it determines whether it
1319 // implies the existence of a Zig compilation unit based on
1320 // whether there is a root source file. If there is no root
1321 // source file, then this is not a zig compilation unit - it is
1322 // perhaps a set of linker objects, or C source files instead.
1323 // Linker objects are added to the CLI globally, while C source
1324 // files must have a module parent.
1325 if (mod.root_source_file) |lp| {
1326 const src = lp.getPath2(mod.owner, step);
1327 try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src }));
1328 } else if (moduleNeedsCliArg(mod)) {
1329 try zig_args.append(b.fmt("-M{s}", .{module_cli_name}));
1330 }
1331 }
1332 }
1333 }
1334
1335 if (total_linker_objects == 0) {
1336 return step.fail("the linker needs one or more objects to link", .{});
1337 }
1338
1339 for (frameworks.keys(), frameworks.values()) |name, info| {
1340 if (info.needed) {
1341 try zig_args.append("-needed_framework");
1342 } else if (info.weak) {
1343 try zig_args.append("-weak_framework");
1344 } else {
1345 try zig_args.append("-framework");
1346 }
1347 try zig_args.append(name);
1348 }
1349
1350 if (compile.is_linking_libcpp) {
1351 try zig_args.append("-lc++");
1352 }
1353
1354 if (compile.is_linking_libc) {
1355 try zig_args.append("-lc");
1356 }
1357 }
1358
1359 if (compile.win32_manifest) |manifest_file| {
1360 try zig_args.append(manifest_file.getPath2(b, step));
1361 }
1362
1363 if (compile.win32_module_definition) |module_file| {
1364 try zig_args.append(module_file.getPath2(b, step));
1365 }
1366
1367 if (compile.image_base) |image_base| {
1368 try zig_args.append("--image-base");
1369 try zig_args.append(b.fmt("0x{x}", .{image_base}));
1370 }
1371
1372 for (compile.filters) |filter| {
1373 try zig_args.append("--test-filter");
1374 try zig_args.append(filter);
1375 }
1376
1377 if (compile.test_runner) |test_runner| {
1378 try zig_args.append("--test-runner");
1379 try zig_args.append(test_runner.path.getPath2(b, step));
1380 }
1381
1382 for (b.debug_log_scopes) |log_scope| {
1383 try zig_args.append("--debug-log");
1384 try zig_args.append(log_scope);
1385 }
1386
1387 if (b.debug_compile_errors) {
1388 try zig_args.append("--debug-compile-errors");
1389 }
1390
1391 if (b.debug_incremental) {
1392 try zig_args.append("--debug-incremental");
1393 }
1394
1395 if (b.verbose_air) try zig_args.append("--verbose-air");
1396 if (b.verbose_llvm_ir) |path| try zig_args.append(b.fmt("--verbose-llvm-ir={s}", .{path}));
1397 if (b.verbose_llvm_bc) |path| try zig_args.append(b.fmt("--verbose-llvm-bc={s}", .{path}));
1398 if (b.verbose_link or compile.verbose_link) try zig_args.append("--verbose-link");
1399 if (b.verbose_cc or compile.verbose_cc) try zig_args.append("--verbose-cc");
1400 if (b.verbose_llvm_cpu_features) try zig_args.append("--verbose-llvm-cpu-features");
1401 if (b.graph.time_report) try zig_args.append("--time-report");
1402
1403 if (compile.generated_asm != null) try zig_args.append("-femit-asm");
1404 if (compile.generated_bin == null) try zig_args.append("-fno-emit-bin");
1405 if (compile.generated_docs != null) try zig_args.append("-femit-docs");
1406 if (compile.generated_implib != null) try zig_args.append("-femit-implib");
1407 if (compile.generated_llvm_bc != null) try zig_args.append("-femit-llvm-bc");
1408 if (compile.generated_llvm_ir != null) try zig_args.append("-femit-llvm-ir");
1409 if (compile.generated_h != null) try zig_args.append("-femit-h");
1410
1411 try addFlag(&zig_args, "formatted-panics", compile.formatted_panics);
1412
1413 switch (compile.compress_debug_sections) {
1414 .none => {},
1415 .zlib => try zig_args.append("--compress-debug-sections=zlib"),
1416 .zstd => try zig_args.append("--compress-debug-sections=zstd"),
1417 }
1418
1419 if (compile.link_eh_frame_hdr) {
1420 try zig_args.append("--eh-frame-hdr");
1421 }
1422 if (compile.link_emit_relocs) {
1423 try zig_args.append("--emit-relocs");
1424 }
1425 if (compile.link_function_sections) {
1426 try zig_args.append("-ffunction-sections");
1427 }
1428 if (compile.link_data_sections) {
1429 try zig_args.append("-fdata-sections");
1430 }
1431 if (compile.link_gc_sections) |x| {
1432 try zig_args.append(if (x) "--gc-sections" else "--no-gc-sections");
1433 }
1434 if (!compile.linker_dynamicbase) {
1435 try zig_args.append("--no-dynamicbase");
1436 }
1437 if (compile.linker_allow_shlib_undefined) |x| {
1438 try zig_args.append(if (x) "-fallow-shlib-undefined" else "-fno-allow-shlib-undefined");
1439 }
1440 if (compile.link_z_notext) {
1441 try zig_args.append("-z");
1442 try zig_args.append("notext");
1443 }
1444 if (!compile.link_z_relro) {
1445 try zig_args.append("-z");
1446 try zig_args.append("norelro");
1447 }
1448 if (compile.link_z_lazy) {
1449 try zig_args.append("-z");
1450 try zig_args.append("lazy");
1451 }
1452 if (compile.link_z_common_page_size) |size| {
1453 try zig_args.append("-z");
1454 try zig_args.append(b.fmt("common-page-size={d}", .{size}));
1455 }
1456 if (compile.link_z_max_page_size) |size| {
1457 try zig_args.append("-z");
1458 try zig_args.append(b.fmt("max-page-size={d}", .{size}));
1459 }
1460 if (compile.link_z_defs) {
1461 try zig_args.append("-z");
1462 try zig_args.append("defs");
1463 }
1464
1465 if (compile.libc_file) |libc_file| {
1466 try zig_args.append("--libc");
1467 try zig_args.append(libc_file.getPath2(b, step));
1468 } else if (b.libc_file) |libc_file| {
1469 try zig_args.append("--libc");
1470 try zig_args.append(libc_file);
1471 }
1472
1473 try zig_args.append("--cache-dir");
1474 try zig_args.append(b.cache_root.path orelse ".");
1475
1476 try zig_args.append("--global-cache-dir");
1477 try zig_args.append(b.graph.global_cache_root.path orelse ".");
1478
1479 if (b.graph.debug_compiler_runtime_libs) |mode|
1480 try zig_args.append(b.fmt("--debug-rt={t}", .{mode}));
1481
1482 try zig_args.append("--name");
1483 try zig_args.append(compile.name);
1484
1485 if (compile.linkage) |some| switch (some) {
1486 .dynamic => try zig_args.append("-dynamic"),
1487 .static => try zig_args.append("-static"),
1488 };
1489 if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) {
1490 if (compile.version) |version| {
1491 try zig_args.append("--version");
1492 try zig_args.append(b.fmt("{f}", .{version}));
1493 }
1494
1495 if (compile.rootModuleTarget().os.tag.isDarwin()) {
1496 const install_name = compile.install_name orelse b.fmt("@rpath/{s}{s}{s}", .{
1497 compile.rootModuleTarget().libPrefix(),
1498 compile.name,
1499 compile.rootModuleTarget().dynamicLibSuffix(),
1500 });
1501 try zig_args.append("-install_name");
1502 try zig_args.append(install_name);
1503 }
1504 }
1505
1506 if (compile.entitlements) |entitlements| {
1507 try zig_args.appendSlice(&[_][]const u8{ "--entitlements", entitlements });
1508 }
1509 if (compile.pagezero_size) |pagezero_size| {
1510 const size = try std.fmt.allocPrint(arena, "{x}", .{pagezero_size});
1511 try zig_args.appendSlice(&[_][]const u8{ "-pagezero_size", size });
1512 }
1513 if (compile.headerpad_size) |headerpad_size| {
1514 const size = try std.fmt.allocPrint(arena, "{x}", .{headerpad_size});
1515 try zig_args.appendSlice(&[_][]const u8{ "-headerpad", size });
1516 }
1517 if (compile.headerpad_max_install_names) {
1518 try zig_args.append("-headerpad_max_install_names");
1519 }
1520 if (compile.dead_strip_dylibs) {
1521 try zig_args.append("-dead_strip_dylibs");
1522 }
1523 if (compile.force_load_objc) {
1524 try zig_args.append("-ObjC");
1525 }
1526 if (compile.discard_local_symbols) {
1527 try zig_args.append("--discard-all");
1528 }
1529
1530 try addFlag(&zig_args, "compiler-rt", compile.bundle_compiler_rt);
1531 try addFlag(&zig_args, "ubsan-rt", compile.bundle_ubsan_rt);
1532 try addFlag(&zig_args, "dll-export-fns", compile.dll_export_fns);
1533 if (compile.rdynamic) {
1534 try zig_args.append("-rdynamic");
1535 }
1536 if (compile.import_memory) {
1537 try zig_args.append("--import-memory");
1538 }
1539 if (compile.export_memory) {
1540 try zig_args.append("--export-memory");
1541 }
1542 if (compile.import_symbols) {
1543 try zig_args.append("--import-symbols");
1544 }
1545 if (compile.import_table) {
1546 try zig_args.append("--import-table");
1547 }
1548 if (compile.export_table) {
1549 try zig_args.append("--export-table");
1550 }
1551 if (compile.initial_memory) |initial_memory| {
1552 try zig_args.append(b.fmt("--initial-memory={d}", .{initial_memory}));
1553 }
1554 if (compile.max_memory) |max_memory| {
1555 try zig_args.append(b.fmt("--max-memory={d}", .{max_memory}));
1556 }
1557 if (compile.shared_memory) {
1558 try zig_args.append("--shared-memory");
1559 }
1560 if (compile.global_base) |global_base| {
1561 try zig_args.append(b.fmt("--global-base={d}", .{global_base}));
1562 }
1563
1564 if (compile.wasi_exec_model) |model| {
1565 try zig_args.append(b.fmt("-mexec-model={s}", .{@tagName(model)}));
1566 }
1567 if (compile.linker_script) |linker_script| {
1568 try zig_args.append("--script");
1569 try zig_args.append(linker_script.getPath2(b, step));
1570 }
1571
1572 if (compile.version_script) |version_script| {
1573 try zig_args.append("--version-script");
1574 try zig_args.append(version_script.getPath2(b, step));
1575 }
1576 if (compile.linker_allow_undefined_version) |x| {
1577 try zig_args.append(if (x) "--undefined-version" else "--no-undefined-version");
1578 }
1579
1580 if (compile.linker_enable_new_dtags) |enabled| {
1581 try zig_args.append(if (enabled) "--enable-new-dtags" else "--disable-new-dtags");
1582 }
1583
1584 if (compile.kind == .@"test") {
1585 if (compile.exec_cmd_args) |exec_cmd_args| {
1586 for (exec_cmd_args) |cmd_arg| {
1587 if (cmd_arg) |arg| {
1588 try zig_args.append("--test-cmd");
1589 try zig_args.append(arg);
1590 } else {
1591 try zig_args.append("--test-cmd-bin");
1592 }
1593 }
1594 }
1595 }
1596
1597 if (b.sysroot) |sysroot| {
1598 try zig_args.appendSlice(&[_][]const u8{ "--sysroot", sysroot });
1599 }
1600
1601 // -I and -L arguments that appear after the last --mod argument apply to all modules.
1602 const cwd: Io.Dir = .cwd();
1603 const io = b.graph.io;
1604
1605 for (b.search_prefixes.items) |search_prefix| {
1606 var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| {
1607 return step.fail("unable to open prefix directory '{s}': {s}", .{
1608 search_prefix, @errorName(err),
1609 });
1610 };
1611 defer prefix_dir.close(io);
1612
1613 // Avoid passing -L and -I flags for nonexistent directories.
1614 // This prevents a warning, that should probably be upgraded to an error in Zig's
1615 // CLI parsing code, when the linker sees an -L directory that does not exist.
1616
1617 if (prefix_dir.access(io, "lib", .{})) |_| {
1618 try zig_args.appendSlice(&.{
1619 "-L", b.pathJoin(&.{ search_prefix, "lib" }),
1620 });
1621 } else |err| switch (err) {
1622 error.FileNotFound => {},
1623 else => |e| return step.fail("unable to access '{s}/lib' directory: {s}", .{
1624 search_prefix, @errorName(e),
1625 }),
1626 }
1627
1628 if (prefix_dir.access(io, "include", .{})) |_| {
1629 try zig_args.appendSlice(&.{
1630 "-I", b.pathJoin(&.{ search_prefix, "include" }),
1631 });
1632 } else |err| switch (err) {
1633 error.FileNotFound => {},
1634 else => |e| return step.fail("unable to access '{s}/include' directory: {s}", .{
1635 search_prefix, @errorName(e),
1636 }),
1637 }
1638 }
1639
1640 if (compile.rc_includes != .any) {
1641 try zig_args.append("-rcincludes");
1642 try zig_args.append(@tagName(compile.rc_includes));
1643 }
1644
1645 try addFlag(&zig_args, "each-lib-rpath", compile.each_lib_rpath);
1646
1647 if (compile.build_id orelse b.build_id) |build_id| {
1648 try zig_args.append(switch (build_id) {
1649 .hexstring => |hs| b.fmt("--build-id=0x{x}", .{hs.toSlice()}),
1650 .none, .fast, .uuid, .sha1, .md5 => b.fmt("--build-id={s}", .{@tagName(build_id)}),
1651 });
1652 }
1653
1654 const opt_zig_lib_dir = if (compile.zig_lib_dir) |dir|
1655 dir.getPath2(b, step)
1656 else if (b.graph.zig_lib_directory.path) |_|
1657 b.fmt("{f}", .{b.graph.zig_lib_directory})
1658 else
1659 null;
1660
1661 if (opt_zig_lib_dir) |zig_lib_dir| {
1662 try zig_args.append("--zig-lib-dir");
1663 try zig_args.append(zig_lib_dir);
1664 }
1665
1666 try addFlag(&zig_args, "PIE", compile.pie);
1667
1668 if (compile.lto) |lto| {
1669 try zig_args.append(switch (lto) {
1670 .full => "-flto=full",
1671 .thin => "-flto=thin",
1672 .none => "-fno-lto",
1673 });
1674 }
1675
1676 try addFlag(&zig_args, "sanitize-coverage-trace-pc-guard", compile.sanitize_coverage_trace_pc_guard);
1677
1678 if (compile.subsystem) |subsystem| {
1679 try zig_args.append("--subsystem");
1680 try zig_args.append(@tagName(subsystem));
1681 }
1682
1683 if (compile.mingw_unicode_entry_point) {
1684 try zig_args.append("-municode");
1685 }
1686
1687 if (compile.error_limit) |err_limit| try zig_args.appendSlice(&.{
1688 "--error-limit", b.fmt("{d}", .{err_limit}),
1689 });
1690
1691 try addFlag(&zig_args, "incremental", b.graph.incremental);
1692
1693 try zig_args.append("--listen=-");
1694
1695 // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux
1696 // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and
1697 // pass that to zig, e.g. via 'zig build-lib @args.rsp'
1698 // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html
1699 var args_length: usize = 0;
1700 for (zig_args.items) |arg| {
1701 args_length += arg.len + 1; // +1 to account for null terminator
1702 }
1703 if (args_length >= 30 * 1024) {
1704 try b.cache_root.handle.createDirPath(io, "args");
1705
1706 const args_to_escape = zig_args.items[2..];
1707 var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len);
1708 arg_blk: for (args_to_escape) |arg| {
1709 for (arg, 0..) |c, arg_idx| {
1710 if (c == '\\' or c == '"') {
1711 // Slow path for arguments that need to be escaped. We'll need to allocate and copy
1712 var escaped: std.ArrayList(u8) = .empty;
1713 try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1);
1714 try escaped.appendSlice(arena, arg[0..arg_idx]);
1715 for (arg[arg_idx..]) |to_escape| {
1716 if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\');
1717 try escaped.append(arena, to_escape);
1718 }
1719 escaped_args.appendAssumeCapacity(escaped.items);
1720 continue :arg_blk;
1721 }
1722 }
1723 escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument
1724 }
1725
1726 // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with
1727 // other zig build commands running in parallel.
1728 const partially_quoted = try std.mem.join(arena, "\" \"", escaped_args.items);
1729 const args = try std.mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" });
1730
1731 var args_hash: [Sha256.digest_length]u8 = undefined;
1732 Sha256.hash(args, &args_hash, .{});
1733 var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined;
1734 _ = try std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash});
1735
1736 const args_file = "args" ++ fs.path.sep_str ++ args_hex_hash;
1737 if (b.cache_root.handle.access(io, args_file, .{})) |_| {
1738 // The args file is already present from a previous run.
1739 } else |err| switch (err) {
1740 error.FileNotFound => {
1741 var af = b.cache_root.handle.createFileAtomic(io, args_file, .{
1742 .replace = false,
1743 .make_path = true,
1744 }) catch |e| return step.fail("failed creating tmp args file {f}{s}: {t}", .{
1745 b.cache_root, args_file, e,
1746 });
1747 defer af.deinit(io);
1748
1749 af.file.writeStreamingAll(io, args) catch |e| {
1750 return step.fail("failed writing args data to tmp file {f}{s}: {t}", .{
1751 b.cache_root, args_file, e,
1752 });
1753 };
1754 // Note we can't clean up this file, not even after build
1755 // success, because that might interfere with another build
1756 // process that needs the same file.
1757 af.link(io) catch |e| switch (e) {
1758 error.PathAlreadyExists => {
1759 // The args file was created by another concurrent build process.
1760 },
1761 else => |other_err| return step.fail("failed linking tmp file {f}{s}: {t}", .{
1762 b.cache_root, args_file, other_err,
1763 }),
1764 };
1765 },
1766 else => |other_err| return other_err,
1767 }
1768
1769 const resolved_args_file = try mem.concat(arena, u8, &.{
1770 "@",
1771 try b.cache_root.join(arena, &.{args_file}),
1772 });
1773
1774 zig_args.shrinkRetainingCapacity(2);
1775 try zig_args.append(resolved_args_file);
1776 }
1777
1778 return try zig_args.toOwnedSlice();
1779}
1780
1781fn make(step: *Step, options: Step.MakeOptions) !void {
1782 const b = step.owner;
1783 const compile: *Compile = @fieldParentPtr("step", step);
1784
1785 const zig_args = try getZigArgs(compile, false);
1786
1787 const maybe_output_dir = step.evalZigProcess(
1788 zig_args,
1789 options.progress_node,
1790 (b.graph.incremental == true) and (options.watch or options.web_server != null),
1791 options.web_server,
1792 options.gpa,
1793 ) catch |err| switch (err) {
1794 error.NeedCompileErrorCheck => {
1795 assert(compile.expect_errors != null);
1796 try checkCompileErrors(compile);
1797 return;
1798 },
1799 else => |e| return e,
1800 };
1801
1802 // Update generated files
1803 if (maybe_output_dir) |output_dir| {
1804 if (compile.emit_directory) |lp| {
1805 lp.path = b.fmt("{f}", .{output_dir});
1806 }
1807
1808 // zig fmt: off
1809 if (compile.generated_bin) |lp| lp.path = compile.outputPath(output_dir, .bin);
1810 if (compile.generated_pdb) |lp| lp.path = compile.outputPath(output_dir, .pdb);
1811 // hack for stage2_x86_64 + coff
1812 if (compile.generated_compiler_rt_dyn_lib) |lp| lp.path = compile.outputPath(output_dir, .compiler_rt_dyn_lib);
1813 if (compile.generated_implib) |lp| lp.path = compile.outputPath(output_dir, .implib);
1814 if (compile.generated_h) |lp| lp.path = compile.outputPath(output_dir, .h);
1815 if (compile.generated_docs) |lp| lp.path = compile.outputPath(output_dir, .docs);
1816 if (compile.generated_asm) |lp| lp.path = compile.outputPath(output_dir, .@"asm");
1817 if (compile.generated_llvm_ir) |lp| lp.path = compile.outputPath(output_dir, .llvm_ir);
1818 if (compile.generated_llvm_bc) |lp| lp.path = compile.outputPath(output_dir, .llvm_bc);
1819 // zig fmt: on
1820 }
1821
1822 if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic and
1823 compile.version != null and compile.generated_bin != null and
1824 std.Build.wantSharedLibSymLinks(compile.rootModuleTarget()))
1825 {
1826 try doAtomicSymLinks(
1827 step,
1828 compile.getEmittedBin().getPath2(b, step),
1829 compile.major_only_filename.?,
1830 compile.name_only_filename.?,
1831 );
1832 }
1833}
1834854fn outputPath(c: *Compile, out_dir: std.Build.Cache.Path, ea: std.zig.EmitArtifact) []const u8 {
1835855 const arena = c.step.owner.graph.arena;
1836856 const name = ea.cacheName(arena, .{
......@@ -1847,100 +867,6 @@ fn outputPath(c: *Compile, out_dir: std.Build.Cache.Path, ea: std.zig.EmitArtifa
1847867 return out_dir.joinString(arena, name) catch @panic("OOM");
1848868}
1849869
1850pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progress.Node) !Path {
1851 c.step.result_error_msgs.clearRetainingCapacity();
1852 c.step.result_stderr = "";
1853
1854 c.step.result_error_bundle.deinit(gpa);
1855 c.step.result_error_bundle = std.zig.ErrorBundle.empty;
1856
1857 if (c.step.result_failed_command) |cmd| {
1858 gpa.free(cmd);
1859 c.step.result_failed_command = null;
1860 }
1861
1862 const zig_args = try getZigArgs(c, true);
1863 const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa);
1864 return maybe_output_bin_path.?;
1865}
1866
1867pub fn doAtomicSymLinks(
1868 step: *Step,
1869 output_path: []const u8,
1870 filename_major_only: []const u8,
1871 filename_name_only: []const u8,
1872) !void {
1873 const b = step.owner;
1874 const io = b.graph.io;
1875 const out_dir = fs.path.dirname(output_path) orelse ".";
1876 const out_basename = fs.path.basename(output_path);
1877 // sym link for libfoo.so.1 to libfoo.so.1.2.3
1878 const major_only_path = b.pathJoin(&.{ out_dir, filename_major_only });
1879 const cwd: Io.Dir = .cwd();
1880 cwd.symLinkAtomic(io, out_basename, major_only_path, .{}) catch |err| {
1881 return step.fail("unable to symlink {s} -> {s}: {s}", .{
1882 major_only_path, out_basename, @errorName(err),
1883 });
1884 };
1885 // sym link for libfoo.so to libfoo.so.1
1886 const name_only_path = b.pathJoin(&.{ out_dir, filename_name_only });
1887 cwd.symLinkAtomic(io, filename_major_only, name_only_path, .{}) catch |err| {
1888 return step.fail("Unable to symlink {s} -> {s}: {s}", .{
1889 name_only_path, filename_major_only, @errorName(err),
1890 });
1891 };
1892}
1893
1894fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
1895 const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
1896 const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore);
1897 var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator);
1898 errdefer list.deinit();
1899 var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
1900 while (line_it.next()) |line| {
1901 if (mem.trim(u8, line, " \t").len == 0) continue;
1902 var tok_it = mem.tokenizeAny(u8, line, " \t");
1903 try list.append(PkgConfigPkg{
1904 .name = tok_it.next() orelse return error.PkgConfigInvalidOutput,
1905 .desc = tok_it.rest(),
1906 });
1907 }
1908 return list.toOwnedSlice();
1909}
1910
1911fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
1912 if (b.pkg_config_pkg_list) |res| {
1913 return res;
1914 }
1915 var code: u8 = undefined;
1916 if (execPkgConfigList(b, &code)) |list| {
1917 b.pkg_config_pkg_list = list;
1918 return list;
1919 } else |err| {
1920 const result = switch (err) {
1921 error.ProcessTerminated => error.PkgConfigCrashed,
1922 error.ExecNotSupported => error.PkgConfigFailed,
1923 error.ExitCodeFailure => error.PkgConfigFailed,
1924 error.FileNotFound => error.PkgConfigNotInstalled,
1925 error.InvalidName => error.PkgConfigNotInstalled,
1926 error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
1927 else => return err,
1928 };
1929 b.pkg_config_pkg_list = result;
1930 return result;
1931 }
1932}
1933
1934fn addFlag(args: *std.array_list.Managed([]const u8), comptime name: []const u8, opt: ?bool) !void {
1935 const cond = opt orelse return;
1936 try args.ensureUnusedCapacity(1);
1937 if (cond) {
1938 args.appendAssumeCapacity("-f" ++ name);
1939 } else {
1940 args.appendAssumeCapacity("-fno-" ++ name);
1941 }
1942}
1943
1944870fn checkCompileErrors(compile: *Compile) !void {
1945871 // Clear this field so that it does not get printed by the build runner.
1946872 const actual_eb = compile.step.result_error_bundle;
lib/std/Build/Step/ConfigHeader.zig+2-2
......@@ -47,7 +47,7 @@ max_bytes: usize,
4747include_path: []const u8,
4848include_guard_override: ?[]const u8,
4949
50pub const base_id: Step.Id = .config_header;
50pub const base_tag: Step.Tag = .config_header;
5151
5252pub const Options = struct {
5353 style: Style = .blank,
......@@ -88,7 +88,7 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
8888
8989 config_header.* = .{
9090 .step = .init(.{
91 .id = base_id,
91 .tag = base_tag,
9292 .name = name,
9393 .owner = owner,
9494 .makeFn = make,
lib/std/Build/Step/Fail.zig+2-2
......@@ -6,14 +6,14 @@ const Fail = @This();
66step: Step,
77error_msg: []const u8,
88
9pub const base_id: Step.Id = .fail;
9pub const base_tag: Step.Tag = .fail;
1010
1111pub fn create(owner: *std.Build, error_msg: []const u8) *Fail {
1212 const fail = owner.allocator.create(Fail) catch @panic("OOM");
1313
1414 fail.* = .{
1515 .step = Step.init(.{
16 .id = base_id,
16 .tag = base_tag,
1717 .name = "fail",
1818 .owner = owner,
1919 .makeFn = make,
lib/std/Build/Step/Fmt.zig+2-2
......@@ -10,7 +10,7 @@ paths: []const []const u8,
1010exclude_paths: []const []const u8,
1111check: bool,
1212
13pub const base_id: Step.Id = .fmt;
13pub const base_tag: Step.Tag = .fmt;
1414
1515pub const Options = struct {
1616 paths: []const []const u8 = &.{},
......@@ -24,7 +24,7 @@ pub fn create(owner: *std.Build, options: Options) *Fmt {
2424 const name = if (options.check) "zig fmt --check" else "zig fmt";
2525 fmt.* = .{
2626 .step = Step.init(.{
27 .id = base_id,
27 .tag = base_tag,
2828 .name = name,
2929 .owner = owner,
3030 .makeFn = make,
lib/std/Build/Step/InstallArtifact.zig+2-99
......@@ -33,7 +33,7 @@ const DylibSymlinkInfo = struct {
3333 name_only_filename: []const u8,
3434};
3535
36pub const base_id: Step.Id = .install_artifact;
36pub const base_tag: Step.Tag = .install_artifact;
3737
3838pub const Options = struct {
3939 /// Which installation directory to put the main output file into.
......@@ -69,10 +69,9 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
6969 };
7070 install_artifact.* = .{
7171 .step = Step.init(.{
72 .id = base_id,
72 .tag = base_tag,
7373 .name = owner.fmt("install {s}", .{artifact.name}),
7474 .owner = owner,
75 .makeFn = make,
7675 }),
7776 .dest_dir = dest_dir,
7877 .pdb_dir = switch (options.pdb_dir) {
......@@ -126,99 +125,3 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
126125
127126 return install_artifact;
128127}
129
130fn make(step: *Step, options: Step.MakeOptions) !void {
131 _ = options;
132 const install_artifact: *InstallArtifact = @fieldParentPtr("step", step);
133 const b = step.owner;
134 const io = b.graph.io;
135
136 var all_cached = true;
137
138 if (install_artifact.dest_dir) |dest_dir| {
139 const full_dest_path = b.getInstallPath(dest_dir, install_artifact.dest_sub_path);
140 const p = try step.installFile(install_artifact.emitted_bin.?, full_dest_path);
141 all_cached = all_cached and p == .fresh;
142
143 if (install_artifact.dylib_symlinks) |dls| {
144 try Step.Compile.doAtomicSymLinks(step, full_dest_path, dls.major_only_filename, dls.name_only_filename);
145 }
146
147 install_artifact.artifact.installed_path = full_dest_path;
148 }
149
150 if (install_artifact.compiler_rt_dyn_lib_dir) |compiler_rt_dir| {
151 const full_compiler_rt_path = b.getInstallPath(compiler_rt_dir, install_artifact.emitted_compiler_rt_dyn_lib.?.basename(b, step));
152 const p = try step.installFile(install_artifact.emitted_compiler_rt_dyn_lib.?, full_compiler_rt_path);
153 all_cached = all_cached and p == .fresh;
154 }
155
156 if (install_artifact.implib_dir) |implib_dir| {
157 const full_implib_path = b.getInstallPath(implib_dir, install_artifact.emitted_implib.?.basename(b, step));
158 const p = try step.installFile(install_artifact.emitted_implib.?, full_implib_path);
159 all_cached = all_cached and p == .fresh;
160 }
161
162 if (install_artifact.pdb_dir) |pdb_dir| {
163 const full_pdb_path = b.getInstallPath(pdb_dir, install_artifact.emitted_pdb.?.basename(b, step));
164 const p = try step.installFile(install_artifact.emitted_pdb.?, full_pdb_path);
165 all_cached = all_cached and p == .fresh;
166 }
167
168 if (install_artifact.h_dir) |h_dir| {
169 if (install_artifact.emitted_h) |emitted_h| {
170 const full_h_path = b.getInstallPath(h_dir, emitted_h.basename(b, step));
171 const p = try step.installFile(emitted_h, full_h_path);
172 all_cached = all_cached and p == .fresh;
173 }
174
175 for (install_artifact.artifact.installed_headers.items) |installation| switch (installation) {
176 .file => |file| {
177 const full_h_path = b.getInstallPath(h_dir, file.dest_rel_path);
178 const p = try step.installFile(file.source, full_h_path);
179 all_cached = all_cached and p == .fresh;
180 },
181 .directory => |dir| {
182 const src_dir_path = dir.source.getPath3(b, step);
183 const full_h_prefix = b.getInstallPath(h_dir, dir.dest_rel_path);
184
185 var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
186 return step.fail("unable to open source directory '{f}': {s}", .{
187 src_dir_path, @errorName(err),
188 });
189 };
190 defer src_dir.close(io);
191
192 var it = try src_dir.walk(b.allocator);
193 next_entry: while (try it.next(io)) |entry| {
194 for (dir.options.exclude_extensions) |ext| {
195 if (std.mem.endsWith(u8, entry.path, ext)) continue :next_entry;
196 }
197 if (dir.options.include_extensions) |incs| {
198 for (incs) |inc| {
199 if (std.mem.endsWith(u8, entry.path, inc)) break;
200 } else {
201 continue :next_entry;
202 }
203 }
204
205 const full_dest_path = b.pathJoin(&.{ full_h_prefix, entry.path });
206 switch (entry.kind) {
207 .directory => {
208 try Step.handleVerbose(b, .inherit, &.{ "install", "-d", full_dest_path });
209 const p = try step.installDir(full_dest_path);
210 all_cached = all_cached and p == .existed;
211 },
212 .file => {
213 const p = try step.installFile(try dir.source.join(b.allocator, entry.path), full_dest_path);
214 all_cached = all_cached and p == .fresh;
215 },
216 else => continue,
217 }
218 }
219 },
220 };
221 }
222
223 step.result_cached = all_cached;
224}
lib/std/Build/Step/InstallDir.zig+2-2
......@@ -8,7 +8,7 @@ const InstallDir = @This();
88step: Step,
99options: Options,
1010
11pub const base_id: Step.Id = .install_dir;
11pub const base_tag: Step.Tag = .install_dir;
1212
1313pub const Options = struct {
1414 source_dir: LazyPath,
......@@ -44,7 +44,7 @@ pub fn create(owner: *std.Build, options: Options) *InstallDir {
4444 const install_dir = owner.allocator.create(InstallDir) catch @panic("OOM");
4545 install_dir.* = .{
4646 .step = Step.init(.{
47 .id = base_id,
47 .tag = base_tag,
4848 .name = owner.fmt("install {s}/", .{options.source_dir.getDisplayName()}),
4949 .owner = owner,
5050 .makeFn = make,
lib/std/Build/Step/InstallFile.zig+2-2
......@@ -5,7 +5,7 @@ const InstallDir = std.Build.InstallDir;
55const InstallFile = @This();
66const assert = std.debug.assert;
77
8pub const base_id: Step.Id = .install_file;
8pub const base_tag: Step.Tag = .install_file;
99
1010step: Step,
1111source: LazyPath,
......@@ -22,7 +22,7 @@ pub fn create(
2222 const install_file = owner.allocator.create(InstallFile) catch @panic("OOM");
2323 install_file.* = .{
2424 .step = Step.init(.{
25 .id = base_id,
25 .tag = base_tag,
2626 .name = owner.fmt("install {s} to {s}", .{ source.getDisplayName(), dest_rel_path }),
2727 .owner = owner,
2828 .makeFn = make,
lib/std/Build/Step/ObjCopy.zig+2-2
......@@ -10,7 +10,7 @@ const elf = std.elf;
1010const fs = std.fs;
1111const sort = std.sort;
1212
13pub const base_id: Step.Id = .objcopy;
13pub const base_tag: Step.Tag = .objcopy;
1414
1515pub const RawFormat = enum {
1616 bin,
......@@ -111,7 +111,7 @@ pub fn create(
111111 const objcopy = owner.allocator.create(ObjCopy) catch @panic("OOM");
112112 objcopy.* = ObjCopy{
113113 .step = Step.init(.{
114 .id = base_id,
114 .tag = base_tag,
115115 .name = owner.fmt("objcopy {s}", .{input_file.getDisplayName()}),
116116 .owner = owner,
117117 .makeFn = make,
lib/std/Build/Step/Options.zig+2-2
......@@ -8,7 +8,7 @@ const Step = std.Build.Step;
88const GeneratedFile = std.Build.GeneratedFile;
99const LazyPath = std.Build.LazyPath;
1010
11pub const base_id: Step.Id = .options;
11pub const base_tag: Step.Tag = .options;
1212
1313step: Step,
1414generated_file: GeneratedFile,
......@@ -21,7 +21,7 @@ pub fn create(owner: *std.Build) *Options {
2121 const options = owner.allocator.create(Options) catch @panic("OOM");
2222 options.* = .{
2323 .step = .init(.{
24 .id = base_id,
24 .tag = base_tag,
2525 .name = "options",
2626 .owner = owner,
2727 .makeFn = make,
lib/std/Build/Step/Run.zig+2-2114
......@@ -12,7 +12,7 @@ const EnvMap = std.process.Environ.Map;
1212const assert = std.debug.assert;
1313const Path = std.Build.Cache.Path;
1414
15pub const base_id: Step.Id = .run;
15pub const base_tag: Step.Tag = .run;
1616
1717step: Step,
1818
......@@ -88,12 +88,6 @@ dep_output_file: ?*Output,
8888
8989has_side_effects: bool,
9090
91/// If this is a Zig unit test binary, this tracks the names of the unit
92/// tests that are also fuzz tests. Indexes cannot be used as they may
93/// change between reruns.
94fuzz_tests: std.ArrayList([]const u8),
95cached_test_metadata: ?CachedTestMetadata = null,
96
9791/// Populated during the fuzz phase if this run step corresponds to a unit test
9892/// executable that contains fuzz tests.
9993rebuilt_executable: ?Path,
......@@ -209,10 +203,9 @@ pub fn create(owner: *std.Build, name: []const u8) *Run {
209203 const run = owner.allocator.create(Run) catch @panic("OOM");
210204 run.* = .{
211205 .step = .init(.{
212 .id = base_id,
206 .tag = base_tag,
213207 .name = name,
214208 .owner = owner,
215 .makeFn = make,
216209 }),
217210 .argv = .empty,
218211 .cwd = null,
......@@ -229,7 +222,6 @@ pub fn create(owner: *std.Build, name: []const u8) *Run {
229222 .captured_stderr = null,
230223 .dep_output_file = null,
231224 .has_side_effects = false,
232 .fuzz_tests = .empty,
233225 .rebuilt_executable = null,
234226 .producer = null,
235227 };
......@@ -702,2107 +694,3 @@ pub fn addFileInput(self: *Run, file_input: std.Build.LazyPath) void {
702694 file_input.addStepDependencies(&self.step);
703695 self.file_inputs.append(self.step.owner.allocator, file_input.dupe(self.step.owner)) catch @panic("OOM");
704696}
705
706/// Returns whether the Run step has side effects *other than* updating the output arguments.
707fn hasSideEffects(run: Run) bool {
708 if (run.has_side_effects) return true;
709 return switch (run.stdio) {
710 .infer_from_args => !run.hasAnyOutputArgs(),
711 .inherit => true,
712 .check => false,
713 .zig_test => false,
714 };
715}
716
717fn hasAnyOutputArgs(run: Run) bool {
718 if (run.captured_stdout != null) return true;
719 if (run.captured_stderr != null) return true;
720 for (run.argv.items) |arg| switch (arg) {
721 .output_file, .output_directory => return true,
722 else => continue,
723 };
724 return false;
725}
726
727fn checksContainStdout(checks: []const StdIo.Check) bool {
728 for (checks) |check| switch (check) {
729 .expect_stderr_exact,
730 .expect_stderr_match,
731 .expect_term,
732 => continue,
733
734 .expect_stdout_exact,
735 .expect_stdout_match,
736 => return true,
737 };
738 return false;
739}
740
741fn checksContainStderr(checks: []const StdIo.Check) bool {
742 for (checks) |check| switch (check) {
743 .expect_stdout_exact,
744 .expect_stdout_match,
745 .expect_term,
746 => continue,
747
748 .expect_stderr_exact,
749 .expect_stderr_match,
750 => return true,
751 };
752 return false;
753}
754
755/// If `path` is cwd-relative, make it relative to the cwd of the child instead.
756///
757/// Whenever a path is included in the argv of a child, it should be put through this function first
758/// to make sure the child doesn't see paths relative to a cwd other than its own.
759fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 {
760 const b = run.step.owner;
761 const graph = b.graph;
762 const arena = graph.arena;
763
764 const path_str = path.toString(arena) catch @panic("OOM");
765 if (Dir.path.isAbsolute(path_str)) {
766 // Absolute paths don't need changing.
767 return path_str;
768 }
769 const child_cwd_rel: []const u8 = rel: {
770 const child_lazy_cwd = run.cwd orelse break :rel path_str;
771 const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(arena) catch @panic("OOM");
772 // Convert it from relative to *our* cwd, to relative to the *child's* cwd.
773 break :rel Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str) catch @panic("OOM");
774 };
775 // Not every path can be made relative, e.g. if the path and the child cwd are on different
776 // disk designators on Windows. In that case, `relative` will return an absolute path which we can
777 // just return.
778 if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel;
779
780 // We're not done yet. In some cases this path must be prefixed with './':
781 // * On POSIX, the executable name cannot be a single component like 'foo'
782 // * Some executables might treat a leading '-' like a flag, which we must avoid
783 // There's no harm in it, so just *always* apply this prefix.
784 return Dir.path.join(arena, &.{ ".", child_cwd_rel }) catch @panic("OOM");
785}
786
787const IndexedOutput = struct {
788 index: usize,
789 tag: @typeInfo(Arg).@"union".tag_type.?,
790 output: *Output,
791};
792fn make(step: *Step, options: Step.MakeOptions) !void {
793 const b = step.owner;
794 const io = b.graph.io;
795 const arena = b.allocator;
796 const run: *Run = @fieldParentPtr("step", step);
797 const has_side_effects = run.hasSideEffects();
798
799 var argv_list = std.array_list.Managed([]const u8).init(arena);
800 var output_placeholders = std.array_list.Managed(IndexedOutput).init(arena);
801
802 var man = b.graph.cache.obtain();
803 defer man.deinit();
804
805 if (run.environ_map) |environ_map| {
806 for (environ_map.keys(), environ_map.values()) |key, value| {
807 man.hash.addBytes(key);
808 man.hash.addBytes(value);
809 }
810 }
811
812 man.hash.add(run.color);
813 man.hash.add(run.disable_zig_progress);
814
815 for (run.argv.items) |arg| {
816 switch (arg) {
817 .bytes => |bytes| {
818 try argv_list.append(bytes);
819 man.hash.addBytes(bytes);
820 },
821 .lazy_path => |file| {
822 const file_path = file.lazy_path.getPath3(b, step);
823 try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
824 man.hash.addBytes(file.prefix);
825 _ = try man.addFilePath(file_path, null);
826 },
827 .decorated_directory => |dd| {
828 const file_path = dd.lazy_path.getPath3(b, step);
829 const resolved_arg = b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix });
830 try argv_list.append(resolved_arg);
831 man.hash.addBytes(resolved_arg);
832 },
833 .file_content => |file_plp| {
834 const file_path = file_plp.lazy_path.getPath3(b, step);
835
836 var result: std.Io.Writer.Allocating = .init(arena);
837 errdefer result.deinit();
838 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
839
840 const file = file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}) catch |err| {
841 return step.fail(
842 "unable to open input file '{f}': {t}",
843 .{ file_path, err },
844 );
845 };
846 defer file.close(io);
847
848 var buf: [1024]u8 = undefined;
849 var file_reader = file.reader(io, &buf);
850 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
851 error.ReadFailed => return step.fail(
852 "failed to read from '{f}': {t}",
853 .{ file_path, file_reader.err.? },
854 ),
855 error.WriteFailed => return error.OutOfMemory,
856 };
857
858 try argv_list.append(result.written());
859 man.hash.addBytes(file_plp.prefix);
860 _ = try man.addFilePath(file_path, null);
861 },
862 .artifact => |pa| {
863 const artifact = pa.artifact;
864
865 if (artifact.rootModuleTarget().os.tag == .windows) {
866 // On Windows we don't have rpaths so we have to add .dll search paths to PATH
867 run.addPathForDynLibs(artifact);
868 }
869 const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?;
870
871 try argv_list.append(b.fmt("{s}{s}", .{
872 pa.prefix,
873 run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
874 }));
875
876 _ = try man.addFile(file_path, null);
877 },
878 .output_file, .output_directory => |output| {
879 man.hash.addBytes(output.prefix);
880 man.hash.addBytes(output.basename);
881 // Add a placeholder into the argument list because we need the
882 // manifest hash to be updated with all arguments before the
883 // object directory is computed.
884 try output_placeholders.append(.{
885 .index = argv_list.items.len,
886 .tag = arg,
887 .output = output,
888 });
889 _ = try argv_list.addOne();
890 },
891 }
892 }
893
894 switch (run.stdin) {
895 .bytes => |bytes| {
896 man.hash.addBytes(bytes);
897 },
898 .lazy_path => |lazy_path| {
899 const file_path = lazy_path.getPath2(b, step);
900 _ = try man.addFile(file_path, null);
901 },
902 .none => {},
903 }
904
905 if (run.captured_stdout) |captured| {
906 man.hash.addBytes(captured.output.basename);
907 man.hash.add(captured.trim_whitespace);
908 }
909
910 if (run.captured_stderr) |captured| {
911 man.hash.addBytes(captured.output.basename);
912 man.hash.add(captured.trim_whitespace);
913 }
914
915 hashStdIo(&man.hash, run.stdio);
916
917 for (run.file_inputs.items) |lazy_path| {
918 _ = try man.addFile(lazy_path.getPath2(b, step), null);
919 }
920
921 if (run.cwd) |cwd| {
922 const cwd_path = cwd.getPath3(b, step);
923 _ = man.hash.addBytes(try cwd_path.toString(arena));
924 }
925
926 if (!has_side_effects and try step.cacheHitAndWatch(&man)) {
927 // cache hit, skip running command
928 const digest = man.final();
929
930 try populateGeneratedPaths(
931 arena,
932 output_placeholders.items,
933 run.captured_stdout,
934 run.captured_stderr,
935 b.cache_root,
936 &digest,
937 );
938
939 step.result_cached = true;
940 return;
941 }
942
943 const dep_output_file = run.dep_output_file orelse {
944 // We already know the final output paths, use them directly.
945 const digest = if (has_side_effects)
946 man.hash.final()
947 else
948 man.final();
949
950 try populateGeneratedPaths(
951 arena,
952 output_placeholders.items,
953 run.captured_stdout,
954 run.captured_stderr,
955 b.cache_root,
956 &digest,
957 );
958
959 const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest;
960 for (output_placeholders.items) |placeholder| {
961 const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename });
962 const output_sub_dir_path = switch (placeholder.tag) {
963 .output_file => Dir.path.dirname(output_sub_path).?,
964 .output_directory => output_sub_path,
965 else => unreachable,
966 };
967 b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| {
968 return step.fail("unable to make path '{f}{s}': {s}", .{
969 b.cache_root, output_sub_dir_path, @errorName(err),
970 });
971 };
972 const arg_output_path = run.convertPathArg(.{
973 .root_dir = .cwd(),
974 .sub_path = placeholder.output.generated_file.getPath(),
975 });
976 argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0)
977 arg_output_path
978 else
979 b.fmt("{s}{s}", .{ placeholder.output.prefix, arg_output_path });
980 }
981
982 try runCommand(run, argv_list.items, has_side_effects, output_dir_path, options, null);
983 if (!has_side_effects) try step.writeManifestAndWatch(&man);
984 return;
985 };
986
987 // We do not know the final output paths yet, use temp paths to run the command.
988 var rand_int: u64 = undefined;
989 io.random(@ptrCast(&rand_int));
990 const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
991
992 for (output_placeholders.items) |placeholder| {
993 const output_components = .{ tmp_dir_path, placeholder.output.basename };
994 const output_sub_path = b.pathJoin(&output_components);
995 const output_sub_dir_path = switch (placeholder.tag) {
996 .output_file => Dir.path.dirname(output_sub_path).?,
997 .output_directory => output_sub_path,
998 else => unreachable,
999 };
1000 b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| {
1001 return step.fail("unable to make path '{f}{s}': {s}", .{
1002 b.cache_root, output_sub_dir_path, @errorName(err),
1003 });
1004 };
1005 const raw_output_path: Build.Cache.Path = .{
1006 .root_dir = b.cache_root,
1007 .sub_path = b.pathJoin(&output_components),
1008 };
1009 placeholder.output.generated_file.path = raw_output_path.toString(b.graph.arena) catch @panic("OOM");
1010 argv_list.items[placeholder.index] = b.fmt("{s}{s}", .{
1011 placeholder.output.prefix,
1012 run.convertPathArg(raw_output_path),
1013 });
1014 }
1015
1016 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, options, null);
1017
1018 const dep_file_dir = Dir.cwd();
1019 const dep_file_basename = dep_output_file.generated_file.getPath2(b, step);
1020 if (has_side_effects)
1021 try man.addDepFile(dep_file_dir, dep_file_basename)
1022 else
1023 try man.addDepFilePost(dep_file_dir, dep_file_basename);
1024
1025 const digest = if (has_side_effects)
1026 man.hash.final()
1027 else
1028 man.final();
1029
1030 const any_output = output_placeholders.items.len > 0 or
1031 run.captured_stdout != null or run.captured_stderr != null;
1032
1033 // Rename into place
1034 if (any_output) {
1035 const o_sub_path = "o" ++ Dir.path.sep_str ++ &digest;
1036
1037 b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |err| switch (err) {
1038 Dir.RenameError.DirNotEmpty => {
1039 b.cache_root.handle.deleteTree(io, o_sub_path) catch |del_err| {
1040 return step.fail("unable to remove dir '{f}'{s}: {t}", .{
1041 b.cache_root, tmp_dir_path, del_err,
1042 });
1043 };
1044 b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |retry_err| {
1045 return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{
1046 b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, retry_err,
1047 });
1048 };
1049 },
1050 else => return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{
1051 b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, err,
1052 }),
1053 };
1054 }
1055
1056 if (!has_side_effects) try step.writeManifestAndWatch(&man);
1057
1058 try populateGeneratedPaths(
1059 arena,
1060 output_placeholders.items,
1061 run.captured_stdout,
1062 run.captured_stderr,
1063 b.cache_root,
1064 &digest,
1065 );
1066}
1067
1068pub fn rerunInFuzzMode(
1069 run: *Run,
1070 fuzz: *std.Build.Fuzz,
1071 prog_node: std.Progress.Node,
1072) !void {
1073 const step = &run.step;
1074 const b = step.owner;
1075 const io = b.graph.io;
1076 const arena = b.allocator;
1077 var argv_list: std.ArrayList([]const u8) = .empty;
1078 for (run.argv.items) |arg| {
1079 switch (arg) {
1080 .bytes => |bytes| {
1081 try argv_list.append(arena, bytes);
1082 },
1083 .lazy_path => |file| {
1084 const file_path = file.lazy_path.getPath3(b, step);
1085 try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) }));
1086 },
1087 .decorated_directory => |dd| {
1088 const file_path = dd.lazy_path.getPath3(b, step);
1089 try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }));
1090 },
1091 .file_content => |file_plp| {
1092 const file_path = file_plp.lazy_path.getPath3(b, step);
1093
1094 var result: std.Io.Writer.Allocating = .init(arena);
1095 errdefer result.deinit();
1096 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
1097
1098 const file = try file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{});
1099 defer file.close(io);
1100
1101 var buf: [1024]u8 = undefined;
1102 var file_reader = file.reader(io, &buf);
1103 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
1104 error.ReadFailed => return file_reader.err.?,
1105 error.WriteFailed => return error.OutOfMemory,
1106 };
1107
1108 try argv_list.append(arena, result.written());
1109 },
1110 .artifact => |pa| {
1111 const artifact = pa.artifact;
1112 const file_path: []const u8 = p: {
1113 if (artifact == run.producer.?) break :p b.fmt("{f}", .{run.rebuilt_executable.?});
1114 break :p artifact.installed_path orelse artifact.generated_bin.?.path.?;
1115 };
1116 try argv_list.append(arena, b.fmt("{s}{s}", .{
1117 pa.prefix,
1118 run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }),
1119 }));
1120 },
1121 .output_file, .output_directory => unreachable,
1122 }
1123 }
1124
1125 if (run.step.result_failed_command) |cmd| {
1126 fuzz.gpa.free(cmd);
1127 run.step.result_failed_command = null;
1128 }
1129
1130 const has_side_effects = false;
1131 var rand_int: u64 = undefined;
1132 io.random(@ptrCast(&rand_int));
1133 const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
1134 try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{
1135 .progress_node = prog_node,
1136 .watch = undefined, // not used by `runCommand`
1137 .web_server = null, // only needed for time reports
1138 .unit_test_timeout_ns = null, // don't time out fuzz tests for now
1139 .gpa = fuzz.gpa,
1140 }, .{
1141 .fuzz = fuzz,
1142 });
1143}
1144
1145fn populateGeneratedPaths(
1146 arena: std.mem.Allocator,
1147 output_placeholders: []const IndexedOutput,
1148 captured_stdout: ?*CapturedStdIo,
1149 captured_stderr: ?*CapturedStdIo,
1150 cache_root: Build.Cache.Directory,
1151 digest: *const Build.Cache.HexDigest,
1152) !void {
1153 for (output_placeholders) |placeholder| {
1154 placeholder.output.generated_file.path = try cache_root.join(arena, &.{
1155 "o", digest, placeholder.output.basename,
1156 });
1157 }
1158
1159 if (captured_stdout) |captured| {
1160 captured.output.generated_file.path = try cache_root.join(arena, &.{
1161 "o", digest, captured.output.basename,
1162 });
1163 }
1164
1165 if (captured_stderr) |captured| {
1166 captured.output.generated_file.path = try cache_root.join(arena, &.{
1167 "o", digest, captured.output.basename,
1168 });
1169 }
1170}
1171
1172fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void {
1173 if (term) |t| switch (t) {
1174 .exited => |code| try w.print("exited with code {d}", .{code}),
1175 .signal => |sig| try w.print("terminated with signal {t}", .{sig}),
1176 .stopped => |sig| try w.print("stopped with signal {t}", .{sig}),
1177 .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}),
1178 } else {
1179 try w.writeAll("exited with any code");
1180 }
1181}
1182fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) {
1183 return .{ .data = term };
1184}
1185
1186fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
1187 return if (expected) |e| switch (e) {
1188 .exited => |expected_code| switch (actual) {
1189 .exited => |actual_code| expected_code == actual_code,
1190 else => false,
1191 },
1192 .signal => |expected_sig| switch (actual) {
1193 .signal => |actual_sig| expected_sig == actual_sig,
1194 else => false,
1195 },
1196 .stopped => |expected_sig| switch (actual) {
1197 .stopped => |actual_sig| expected_sig == actual_sig,
1198 else => false,
1199 },
1200 .unknown => |expected_code| switch (actual) {
1201 .unknown => |actual_code| expected_code == actual_code,
1202 else => false,
1203 },
1204 } else switch (actual) {
1205 .exited => true,
1206 else => false,
1207 };
1208}
1209
1210const FuzzContext = struct {
1211 fuzz: *std.Build.Fuzz,
1212};
1213
1214fn runCommand(
1215 run: *Run,
1216 argv: []const []const u8,
1217 has_side_effects: bool,
1218 output_dir_path: []const u8,
1219 options: Step.MakeOptions,
1220 fuzz_context: ?FuzzContext,
1221) !void {
1222 const step = &run.step;
1223 const b = step.owner;
1224 const arena = b.allocator;
1225 const gpa = options.gpa;
1226 const io = b.graph.io;
1227
1228 const cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, step) } else .inherit;
1229
1230 try step.handleChildProcUnsupported();
1231 try Step.handleVerbose2(step.owner, cwd, run.environ_map, argv);
1232
1233 const allow_skip = switch (run.stdio) {
1234 .check, .zig_test => run.skip_foreign_checks,
1235 else => false,
1236 };
1237
1238 var interp_argv = std.array_list.Managed([]const u8).init(b.allocator);
1239 defer interp_argv.deinit();
1240
1241 var environ_map: EnvMap = env: {
1242 const orig = run.environ_map orelse &b.graph.environ_map;
1243 break :env try orig.clone(gpa);
1244 };
1245 defer environ_map.deinit();
1246
1247 const opt_generic_result = spawnChildAndCollect(run, argv, &environ_map, has_side_effects, options, fuzz_context) catch |err| term: {
1248 // InvalidExe: cpu arch mismatch
1249 // FileNotFound: can happen with a wrong dynamic linker path
1250 if (err == error.InvalidExe or err == error.FileNotFound) interpret: {
1251 // TODO: learn the target from the binary directly rather than from
1252 // relying on it being a Compile step. This will make this logic
1253 // work even for the edge case that the binary was produced by a
1254 // third party.
1255 const exe = switch (run.argv.items[0]) {
1256 .artifact => |exe| exe.artifact,
1257 else => break :interpret,
1258 };
1259 switch (exe.kind) {
1260 .exe, .@"test" => {},
1261 else => break :interpret,
1262 }
1263
1264 const root_target = exe.rootModuleTarget();
1265 const need_cross_libc = exe.is_linking_libc and
1266 (root_target.isGnuLibC() or (root_target.isMuslLibC() and exe.linkage == .dynamic));
1267 const other_target = exe.root_module.resolved_target.?.result;
1268 switch (std.zig.system.getExternalExecutor(io, &b.graph.host.result, &other_target, .{
1269 .qemu_fixes_dl = need_cross_libc and b.libc_runtimes_dir != null,
1270 .link_libc = exe.is_linking_libc,
1271 })) {
1272 .native, .rosetta => {
1273 if (allow_skip) return error.MakeSkipped;
1274 break :interpret;
1275 },
1276 .wine => |bin_name| {
1277 if (b.enable_wine) {
1278 try interp_argv.append(bin_name);
1279 try interp_argv.appendSlice(argv);
1280
1281 // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but
1282 // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired.
1283 if (environ_map.get("WINEDEBUG") == null) {
1284 try environ_map.put("WINEDEBUG", "-all");
1285 }
1286 } else {
1287 return failForeign(run, "-fwine", argv[0], exe);
1288 }
1289 },
1290 .qemu => |bin_name| {
1291 if (b.enable_qemu) {
1292 try interp_argv.append(bin_name);
1293
1294 if (need_cross_libc) {
1295 if (b.libc_runtimes_dir) |dir| {
1296 try interp_argv.append("-L");
1297 try interp_argv.append(b.pathJoin(&.{
1298 dir,
1299 try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple(
1300 b.allocator,
1301 root_target.cpu.arch,
1302 root_target.os.tag,
1303 root_target.abi,
1304 ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple(
1305 b.allocator,
1306 root_target.cpu.arch,
1307 root_target.abi,
1308 ) else unreachable,
1309 }));
1310 } else return failForeign(run, "--libc-runtimes", argv[0], exe);
1311 }
1312
1313 try interp_argv.appendSlice(argv);
1314 } else return failForeign(run, "-fqemu", argv[0], exe);
1315 },
1316 .darling => |bin_name| {
1317 if (b.enable_darling) {
1318 try interp_argv.append(bin_name);
1319 try interp_argv.appendSlice(argv);
1320 } else {
1321 return failForeign(run, "-fdarling", argv[0], exe);
1322 }
1323 },
1324 .wasmtime => |bin_name| {
1325 if (b.enable_wasmtime) {
1326 try interp_argv.append(bin_name);
1327 try interp_argv.append("--dir=.");
1328 // Wasmtime doeesn't inherit environment variables from the parent process
1329 // by default. '-S inherit-env' was added in Wasmtime version 20.
1330 try interp_argv.append("-Sinherit-env");
1331 try interp_argv.append(argv[0]);
1332 try interp_argv.appendSlice(argv[1..]);
1333 } else {
1334 return failForeign(run, "-fwasmtime", argv[0], exe);
1335 }
1336 },
1337 .bad_dl => |foreign_dl| {
1338 if (allow_skip) return error.MakeSkipped;
1339
1340 const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)";
1341
1342 return step.fail(
1343 \\the host system is unable to execute binaries from the target
1344 \\ because the host dynamic linker is '{s}',
1345 \\ while the target dynamic linker is '{s}'.
1346 \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step
1347 , .{ host_dl, foreign_dl });
1348 },
1349 .bad_os_or_cpu => {
1350 if (allow_skip) return error.MakeSkipped;
1351
1352 const host_name = try b.graph.host.result.zigTriple(b.allocator);
1353 const foreign_name = try root_target.zigTriple(b.allocator);
1354
1355 return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{
1356 host_name, foreign_name,
1357 });
1358 },
1359 }
1360
1361 if (root_target.os.tag == .windows) {
1362 // On Windows we don't have rpaths so we have to add .dll search paths to PATH
1363 run.addPathForDynLibs(exe);
1364 }
1365
1366 gpa.free(step.result_failed_command.?);
1367 step.result_failed_command = null;
1368 try Step.handleVerbose2(step.owner, cwd, run.environ_map, interp_argv.items);
1369
1370 break :term spawnChildAndCollect(run, interp_argv.items, &environ_map, has_side_effects, options, fuzz_context) catch |e| {
1371 if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped;
1372 if (e == error.MakeFailed) return error.MakeFailed; // error already reported
1373 return step.fail("unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e });
1374 };
1375 }
1376 if (err == error.MakeFailed) return error.MakeFailed; // error already reported
1377
1378 return step.fail("failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err });
1379 };
1380
1381 const generic_result = opt_generic_result orelse {
1382 assert(run.stdio == .zig_test);
1383 // Specific errors have already been reported, and test results are populated. All we need
1384 // to do is report step failure if any test failed.
1385 if (!step.test_results.isSuccess()) return error.MakeFailed;
1386 return;
1387 };
1388
1389 assert(fuzz_context == null);
1390 assert(run.stdio != .zig_test);
1391
1392 // Capture stdout and stderr to GeneratedFile objects.
1393 const Stream = struct {
1394 captured: ?*CapturedStdIo,
1395 bytes: ?[]const u8,
1396 };
1397 for ([_]Stream{
1398 .{
1399 .captured = run.captured_stdout,
1400 .bytes = generic_result.stdout,
1401 },
1402 .{
1403 .captured = run.captured_stderr,
1404 .bytes = generic_result.stderr,
1405 },
1406 }) |stream| {
1407 if (stream.captured) |captured| {
1408 const output_components = .{ output_dir_path, captured.output.basename };
1409 const output_path = try b.cache_root.join(arena, &output_components);
1410 captured.output.generated_file.path = output_path;
1411
1412 const sub_path = b.pathJoin(&output_components);
1413 const sub_path_dirname = Dir.path.dirname(sub_path).?;
1414 b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| {
1415 return step.fail("unable to make path '{f}{s}': {s}", .{
1416 b.cache_root, sub_path_dirname, @errorName(err),
1417 });
1418 };
1419 const data = switch (captured.trim_whitespace) {
1420 .none => stream.bytes.?,
1421 .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace),
1422 .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace),
1423 .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace),
1424 };
1425 b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = data }) catch |err| {
1426 return step.fail("unable to write file '{f}{s}': {s}", .{
1427 b.cache_root, sub_path, @errorName(err),
1428 });
1429 };
1430 }
1431 }
1432
1433 switch (run.stdio) {
1434 .zig_test => unreachable,
1435 .check => |checks| for (checks.items) |check| switch (check) {
1436 .expect_stderr_exact => |expected_bytes| {
1437 if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) {
1438 return step.fail(
1439 \\========= expected this stderr: =========
1440 \\{s}
1441 \\========= but found: ====================
1442 \\{s}
1443 , .{
1444 expected_bytes,
1445 generic_result.stderr.?,
1446 });
1447 }
1448 },
1449 .expect_stderr_match => |match| {
1450 if (mem.find(u8, generic_result.stderr.?, match) == null) {
1451 return step.fail(
1452 \\========= expected to find in stderr: =========
1453 \\{s}
1454 \\========= but stderr does not contain it: =====
1455 \\{s}
1456 , .{
1457 match,
1458 generic_result.stderr.?,
1459 });
1460 }
1461 },
1462 .expect_stdout_exact => |expected_bytes| {
1463 if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) {
1464 return step.fail(
1465 \\========= expected this stdout: =========
1466 \\{s}
1467 \\========= but found: ====================
1468 \\{s}
1469 , .{
1470 expected_bytes,
1471 generic_result.stdout.?,
1472 });
1473 }
1474 },
1475 .expect_stdout_match => |match| {
1476 if (mem.find(u8, generic_result.stdout.?, match) == null) {
1477 return step.fail(
1478 \\========= expected to find in stdout: =========
1479 \\{s}
1480 \\========= but stdout does not contain it: =====
1481 \\{s}
1482 , .{
1483 match,
1484 generic_result.stdout.?,
1485 });
1486 }
1487 },
1488 .expect_term => |expected_term| {
1489 if (!termMatches(expected_term, generic_result.term)) {
1490 return step.fail("process {f} (expected {f})", .{
1491 fmtTerm(generic_result.term),
1492 fmtTerm(expected_term),
1493 });
1494 }
1495 },
1496 },
1497 else => {
1498 // On failure, report captured stderr like normal standard error output.
1499 const bad_exit = switch (generic_result.term) {
1500 .exited => |code| code != 0,
1501 .signal, .stopped, .unknown => true,
1502 };
1503 if (bad_exit) {
1504 if (generic_result.stderr) |bytes| {
1505 run.step.result_stderr = bytes;
1506 }
1507 }
1508
1509 try step.handleChildProcessTerm(generic_result.term);
1510 },
1511 }
1512}
1513
1514const EvalGenericResult = struct {
1515 term: process.Child.Term,
1516 stdout: ?[]const u8,
1517 stderr: ?[]const u8,
1518};
1519
1520fn spawnChildAndCollect(
1521 run: *Run,
1522 argv: []const []const u8,
1523 environ_map: *EnvMap,
1524 has_side_effects: bool,
1525 options: Step.MakeOptions,
1526 fuzz_context: ?FuzzContext,
1527) !?EvalGenericResult {
1528 const b = run.step.owner;
1529 const graph = b.graph;
1530 const io = graph.io;
1531
1532 if (fuzz_context != null) {
1533 assert(!has_side_effects);
1534 assert(run.stdio == .zig_test);
1535 }
1536
1537 const child_cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, &run.step) } else .inherit;
1538
1539 // If an error occurs, it's caused by this command:
1540 assert(run.step.result_failed_command == null);
1541 run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{
1542 .child = environ_map,
1543 .parent = &graph.environ_map,
1544 }, argv);
1545
1546 var spawn_options: process.SpawnOptions = .{
1547 .argv = argv,
1548 .cwd = child_cwd,
1549 .environ_map = environ_map,
1550 .request_resource_usage_statistics = true,
1551 .stdin = if (run.stdin != .none) s: {
1552 assert(run.stdio != .inherit);
1553 break :s .pipe;
1554 } else switch (run.stdio) {
1555 .infer_from_args => if (has_side_effects) .inherit else .ignore,
1556 .inherit => .inherit,
1557 .check => .ignore,
1558 .zig_test => .pipe,
1559 },
1560 .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) {
1561 .infer_from_args => if (has_side_effects) .inherit else .ignore,
1562 .inherit => .inherit,
1563 .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore,
1564 .zig_test => .pipe,
1565 },
1566 .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) {
1567 .infer_from_args => if (has_side_effects) .inherit else .pipe,
1568 .inherit => .inherit,
1569 .check => .pipe,
1570 .zig_test => .pipe,
1571 },
1572 };
1573
1574 if (run.stdio == .zig_test) {
1575 const started: Io.Clock.Timestamp = .now(io, .awake);
1576 const result = evalZigTest(run, spawn_options, options, fuzz_context) catch |err| switch (err) {
1577 error.Canceled => |e| return e,
1578 else => |e| e,
1579 };
1580 run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
1581 try result;
1582 return null;
1583 } else {
1584 const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit;
1585 if (!run.disable_zig_progress and !inherit) {
1586 spawn_options.progress_node = options.progress_node;
1587 }
1588 const terminal_mode: Io.Terminal.Mode = if (inherit) m: {
1589 const stderr = try io.lockStderr(&.{}, graph.stderr_mode);
1590 break :m stderr.terminal_mode;
1591 } else .no_color;
1592 defer if (inherit) io.unlockStderr();
1593 try setColorEnvironmentVariables(run, environ_map, terminal_mode);
1594
1595 const started: Io.Clock.Timestamp = .now(io, .awake);
1596 const result = evalGeneric(run, spawn_options) catch |err| switch (err) {
1597 error.Canceled => |e| return e,
1598 else => |e| e,
1599 };
1600 run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds);
1601 return try result;
1602 }
1603}
1604
1605fn setColorEnvironmentVariables(run: *Run, environ_map: *EnvMap, terminal_mode: Io.Terminal.Mode) !void {
1606 color: switch (run.color) {
1607 .manual => {},
1608 .enable => {
1609 try environ_map.put("CLICOLOR_FORCE", "1");
1610 _ = environ_map.swapRemove("NO_COLOR");
1611 },
1612 .disable => {
1613 try environ_map.put("NO_COLOR", "1");
1614 _ = environ_map.swapRemove("CLICOLOR_FORCE");
1615 },
1616 .inherit => switch (terminal_mode) {
1617 .no_color, .windows_api => continue :color .disable,
1618 .escape_codes => continue :color .enable,
1619 },
1620 .auto => {
1621 const capture_stderr = run.captured_stderr != null or switch (run.stdio) {
1622 .check => |checks| checksContainStderr(checks.items),
1623 .infer_from_args, .inherit, .zig_test => false,
1624 };
1625 if (capture_stderr) {
1626 continue :color .disable;
1627 } else {
1628 continue :color .inherit;
1629 }
1630 },
1631 }
1632}
1633
1634const StdioPollEnum = enum { stdout, stderr };
1635
1636fn evalZigTest(
1637 run: *Run,
1638 spawn_options: process.SpawnOptions,
1639 options: Step.MakeOptions,
1640 fuzz_context: ?FuzzContext,
1641) !void {
1642 if (fuzz_context != null) {
1643 try evalFuzzTest(run, spawn_options, options, fuzz_context.?);
1644 return;
1645 }
1646
1647 const step_owner = run.step.owner;
1648 const gpa = step_owner.allocator;
1649 const arena = step_owner.allocator;
1650 const io = step_owner.graph.io;
1651
1652 // We will update this every time a child runs.
1653 run.step.result_peak_rss = 0;
1654
1655 var test_results: Step.TestResults = .{
1656 .test_count = 0,
1657 .skip_count = 0,
1658 .fail_count = 0,
1659 .crash_count = 0,
1660 .timeout_count = 0,
1661 .leak_count = 0,
1662 .log_err_count = 0,
1663 };
1664 var test_metadata: ?TestMetadata = null;
1665
1666 while (true) {
1667 var child = try process.spawn(io, spawn_options);
1668 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
1669 var multi_reader: Io.File.MultiReader = undefined;
1670 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
1671 var child_killed = false;
1672 defer if (!child_killed) {
1673 child.kill(io);
1674 multi_reader.deinit();
1675 run.step.result_peak_rss = @max(
1676 run.step.result_peak_rss,
1677 child.resource_usage_statistics.getMaxRss() orelse 0,
1678 );
1679 };
1680
1681 switch (try waitZigTest(
1682 run,
1683 &child,
1684 options,
1685 &multi_reader,
1686 &test_metadata,
1687 &test_results,
1688 )) {
1689 .write_failed => |err| {
1690 // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured
1691 // all available stderr to make our error output as useful as possible.
1692 const stderr_fr = multi_reader.fileReader(1);
1693 while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) {
1694 error.ReadFailed => return stderr_fr.err.?,
1695 error.EndOfStream => {},
1696 }
1697 run.step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered());
1698
1699 // Clean up everything and wait for the child to exit.
1700 child.stdin.?.close(io);
1701 child.stdin = null;
1702 multi_reader.deinit();
1703 child_killed = true;
1704 const term = try child.wait(io);
1705 run.step.result_peak_rss = @max(
1706 run.step.result_peak_rss,
1707 child.resource_usage_statistics.getMaxRss() orelse 0,
1708 );
1709
1710 // The individual unit test results are irrelevant: the test runner itself broke!
1711 // Fail immediately without populating `s.test_results`.
1712 return run.step.fail("unable to write stdin ({t}); test process unexpectedly {f}", .{ err, fmtTerm(term) });
1713 },
1714 .no_poll => |no_poll| {
1715 // This might be a success (we requested exit and the child dutifully closed stdout) or
1716 // a crash of some kind. Either way, the child will terminate by itself -- wait for it.
1717 const stderr_reader = multi_reader.reader(1);
1718 const stderr_owned = try arena.dupe(u8, stderr_reader.buffered());
1719
1720 // Clean up everything and wait for the child to exit.
1721 child.stdin.?.close(io);
1722 child.stdin = null;
1723 multi_reader.deinit();
1724 child_killed = true;
1725 const term = try child.wait(io);
1726 run.step.result_peak_rss = @max(
1727 run.step.result_peak_rss,
1728 child.resource_usage_statistics.getMaxRss() orelse 0,
1729 );
1730
1731 if (no_poll.active_test_index) |test_index| {
1732 // A test was running, so this is definitely a crash. Report it against that
1733 // test, and continue to the next test.
1734 test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed;
1735 test_results.crash_count += 1;
1736 try run.step.addError("'{s}' {f}{s}{s}", .{
1737 test_metadata.?.testName(test_index),
1738 fmtTerm(term),
1739 if (stderr_owned.len != 0) " with stderr:\n" else "",
1740 std.mem.trim(u8, stderr_owned, "\n"),
1741 });
1742 continue;
1743 }
1744
1745 // Report an error if the child terminated uncleanly or if we were still trying to run more tests.
1746 run.step.result_stderr = stderr_owned;
1747 const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32);
1748 if (!tests_done or !termMatches(.{ .exited = 0 }, term)) {
1749 // The individual unit test results are irrelevant: the test runner itself broke!
1750 // Fail immediately without populating `s.test_results`.
1751 return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)});
1752 }
1753
1754 // We're done with all of the tests! Commit the test results and return.
1755 run.step.test_results = test_results;
1756 if (test_metadata) |tm| {
1757 run.cached_test_metadata = tm.toCachedTestMetadata();
1758 if (options.web_server) |ws| {
1759 if (run.step.owner.graph.time_report) {
1760 ws.updateTimeReportRunTest(
1761 run,
1762 &run.cached_test_metadata.?,
1763 tm.ns_per_test,
1764 );
1765 }
1766 }
1767 }
1768 return;
1769 },
1770 .timeout => |timeout| {
1771 const stderr_reader = multi_reader.reader(1);
1772 const stderr = stderr_reader.buffered();
1773 stderr_reader.tossBuffered();
1774 if (timeout.active_test_index) |test_index| {
1775 // A test was running. Report the timeout against that test, and continue on to
1776 // the next test.
1777 test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed;
1778 test_results.timeout_count += 1;
1779 try run.step.addError("'{s}' timed out after {f}{s}{s}", .{
1780 test_metadata.?.testName(test_index),
1781 Io.Duration{ .nanoseconds = timeout.ns_elapsed },
1782 if (stderr.len != 0) " with stderr:\n" else "",
1783 std.mem.trim(u8, stderr, "\n"),
1784 });
1785 continue;
1786 }
1787 // Just log an error and let the child be killed.
1788 run.step.result_stderr = try arena.dupe(u8, stderr);
1789 // The individual unit test results in `results` are irrelevant: the test runner
1790 // is broken! Fail immediately without populating `s.test_results`.
1791 return run.step.fail("test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }});
1792 },
1793 }
1794 comptime unreachable;
1795 }
1796}
1797
1798/// Reads stdout of a Zig test process until a termination condition is reached:
1799/// * A write fails, indicating the child unexpectedly closed stdin
1800/// * A test (or a response from the test runner) times out
1801/// * The wait fails, indicating the child closed stdout and stderr
1802fn waitZigTest(
1803 run: *Run,
1804 child: *process.Child,
1805 options: Step.MakeOptions,
1806 multi_reader: *Io.File.MultiReader,
1807 opt_metadata: *?TestMetadata,
1808 results: *Step.TestResults,
1809) !union(enum) {
1810 write_failed: anyerror,
1811 no_poll: struct {
1812 active_test_index: ?u32,
1813 ns_elapsed: u64,
1814 },
1815 timeout: struct {
1816 active_test_index: ?u32,
1817 ns_elapsed: u64,
1818 },
1819} {
1820 const gpa = run.step.owner.allocator;
1821 const arena = run.step.owner.allocator;
1822 const io = run.step.owner.graph.io;
1823
1824 var sub_prog_node: ?std.Progress.Node = null;
1825 defer if (sub_prog_node) |n| n.end();
1826
1827 if (opt_metadata.*) |*md| {
1828 // Previous unit test process died or was killed; we're continuing where it left off
1829 requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
1830 } else {
1831 // Running unit tests normally
1832 run.fuzz_tests.clearRetainingCapacity();
1833 sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err };
1834 }
1835
1836 var active_test_index: ?u32 = null;
1837
1838 var last_update: Io.Clock.Timestamp = .now(io, .awake);
1839
1840 // This timeout is used when we're waiting on the test runner itself rather than a user-specified
1841 // test. For instance, if the test runner leaves this much time between us requesting a test to
1842 // start and it acknowledging the test starting, we terminate the child and raise an error. This
1843 // *should* never happen, but could in theory be caused by some very unlucky IB in a test.
1844 const response_timeout: Io.Clock.Duration = t: {
1845 const ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s);
1846 break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) };
1847 };
1848 const test_timeout: ?Io.Clock.Duration = if (options.unit_test_timeout_ns) |ns| .{
1849 .clock = .awake,
1850 .raw = .fromNanoseconds(ns),
1851 } else null;
1852
1853 const stdout = multi_reader.reader(0);
1854 const stderr = multi_reader.reader(1);
1855 const Header = std.zig.Server.Message.Header;
1856
1857 while (true) {
1858 const timeout: Io.Timeout = t: {
1859 const opt_duration = if (active_test_index == null) response_timeout else test_timeout;
1860 const duration = opt_duration orelse break :t .none;
1861 break :t .{ .deadline = last_update.addDuration(duration) };
1862 };
1863
1864 // This block is exited when `stdout` contains enough bytes for a `Header`.
1865 header_ready: {
1866 if (stdout.buffered().len >= @sizeOf(Header)) {
1867 // We already have one, no need to poll!
1868 break :header_ready;
1869 }
1870
1871 multi_reader.fill(64, timeout) catch |err| switch (err) {
1872 error.Timeout => return .{ .timeout = .{
1873 .active_test_index = active_test_index,
1874 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
1875 } },
1876 error.EndOfStream => return .{ .no_poll = .{
1877 .active_test_index = active_test_index,
1878 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
1879 } },
1880 else => |e| return e,
1881 };
1882
1883 continue;
1884 }
1885 // There is definitely a header available now -- read it.
1886 const header = stdout.takeStruct(Header, .little) catch unreachable;
1887
1888 while (stdout.buffered().len < header.bytes_len) {
1889 multi_reader.fill(64, timeout) catch |err| switch (err) {
1890 error.Timeout => return .{ .timeout = .{
1891 .active_test_index = active_test_index,
1892 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
1893 } },
1894 error.EndOfStream => return .{ .no_poll = .{
1895 .active_test_index = active_test_index,
1896 .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds),
1897 } },
1898 else => |e| return e,
1899 };
1900 }
1901
1902 const body = stdout.take(header.bytes_len) catch unreachable;
1903 var body_r: std.Io.Reader = .fixed(body);
1904 switch (header.tag) {
1905 .zig_version => {
1906 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return run.step.fail(
1907 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
1908 .{ builtin.zig_version_string, body },
1909 );
1910 },
1911 .test_metadata => {
1912 // `metadata` would only be populated if we'd already seen a `test_metadata`, but we
1913 // only request it once (and importantly, we don't re-request it if we kill and
1914 // restart the test runner).
1915 assert(opt_metadata.* == null);
1916
1917 const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable;
1918 results.test_count = tm_hdr.tests_len;
1919
1920 const names = try arena.alloc(u32, results.test_count);
1921 for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
1922
1923 const expected_panic_msgs = try arena.alloc(u32, results.test_count);
1924 for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable;
1925
1926 const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable;
1927
1928 options.progress_node.setEstimatedTotalItems(names.len);
1929 opt_metadata.* = .{
1930 .string_bytes = try arena.dupe(u8, string_bytes),
1931 .ns_per_test = try arena.alloc(u64, results.test_count),
1932 .names = names,
1933 .expected_panic_msgs = expected_panic_msgs,
1934 .next_index = 0,
1935 .prog_node = options.progress_node,
1936 };
1937 @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64));
1938
1939 active_test_index = null;
1940 last_update = .now(io, .awake);
1941
1942 requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err };
1943 },
1944 .test_started => {
1945 active_test_index = opt_metadata.*.?.next_index - 1;
1946 last_update = .now(io, .awake);
1947 },
1948 .test_results => {
1949 const md = &opt_metadata.*.?;
1950
1951 const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable;
1952 assert(tr_hdr.index == active_test_index);
1953
1954 switch (tr_hdr.flags.status) {
1955 .pass => {},
1956 .skip => results.skip_count +|= 1,
1957 .fail => results.fail_count +|= 1,
1958 }
1959 const leak_count = tr_hdr.flags.leak_count;
1960 const log_err_count = tr_hdr.flags.log_err_count;
1961 results.leak_count +|= leak_count;
1962 results.log_err_count +|= log_err_count;
1963
1964 if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
1965
1966 if (tr_hdr.flags.status == .fail) {
1967 const name = md.testName(tr_hdr.index);
1968 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
1969 stderr.tossBuffered();
1970 if (stderr_bytes.len == 0) {
1971 try run.step.addError("'{s}' failed without output", .{name});
1972 } else {
1973 try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes });
1974 }
1975 } else if (leak_count > 0) {
1976 const name = md.testName(tr_hdr.index);
1977 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
1978 stderr.tossBuffered();
1979 try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
1980 } else if (log_err_count > 0) {
1981 const name = md.testName(tr_hdr.index);
1982 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
1983 stderr.tossBuffered();
1984 try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
1985 }
1986
1987 active_test_index = null;
1988
1989 const now: Io.Clock.Timestamp = .now(io, .awake);
1990 md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds);
1991 last_update = now;
1992
1993 requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err };
1994 },
1995 else => {}, // ignore other messages
1996 }
1997 }
1998}
1999
2000const FuzzTestRunner = struct {
2001 run: *Run,
2002 ctx: FuzzContext,
2003 coverage_id: ?u64,
2004
2005 instances: []Instance,
2006 /// The indexes of this are layed out such that it is effectively an array
2007 /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr.
2008 batch: Io.Batch,
2009 /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter.
2010 pending_broadcasts: std.ArrayList(u8),
2011 broadcast: std.ArrayList(u8),
2012 broadcast_undelivered: u32,
2013
2014 const Instance = struct {
2015 child: process.Child,
2016 message: std.ArrayListAligned(u8, .@"4"),
2017 broadcast_written: usize,
2018 stderr: std.ArrayList(u8),
2019 stdin_vec: [1][]u8,
2020 stdout_vec: [1][]u8,
2021 stderr_vec: [1][]u8,
2022 progress_node: std.Progress.Node,
2023
2024 fn messageHeader(instance: *Instance) InHeader {
2025 assert(instance.message.items.len >= @sizeOf(InHeader));
2026 const header_ptr: *InHeader = @ptrCast(instance.message.items);
2027 var header = header_ptr.*;
2028 if (std.builtin.Endian.native != .little) {
2029 std.mem.byteSwapAllFields(InHeader, &header);
2030 }
2031 return header;
2032 }
2033 };
2034
2035 const PendingBroadcastFooter = struct {
2036 from_id: u32,
2037 body_len: u32,
2038 };
2039
2040 const InHeader = std.zig.Server.Message.Header;
2041 const OutHeader = std.zig.Client.Message.Header;
2042
2043 const stdin_i = 0;
2044 const stdout_i = 1;
2045 const stderr_i = 2;
2046
2047 fn init(
2048 run: *Run,
2049 ctx: FuzzContext,
2050 progress_node: std.Progress.Node,
2051 spawn_options: process.SpawnOptions,
2052 ) !FuzzTestRunner {
2053 const step_owner = run.step.owner;
2054 const gpa = step_owner.allocator;
2055 const io = step_owner.graph.io;
2056
2057 const n_instances = switch (ctx.fuzz.mode) {
2058 .forever => step_owner.graph.max_jobs orelse @min(
2059 std.Thread.getCpuCount() catch 1,
2060 (std.math.maxInt(u32) - 2) / 3,
2061 ),
2062 .limit => 1,
2063 };
2064 const instances = try gpa.alloc(Instance, n_instances);
2065 errdefer gpa.free(instances);
2066 const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3);
2067 errdefer gpa.free(batch_storage);
2068
2069 @memset(instances, .{
2070 .child = undefined,
2071 .message = .empty,
2072 .broadcast_written = undefined,
2073 .stderr = .empty,
2074 .stdin_vec = undefined,
2075 .stdout_vec = undefined,
2076 .stderr_vec = undefined,
2077 .progress_node = undefined,
2078 });
2079 for (0.., instances) |id, *instance| {
2080 errdefer for (instances[0..id]) |*spawned| {
2081 spawned.child.kill(io);
2082 spawned.progress_node.end();
2083 };
2084 instance.child = try process.spawn(io, spawn_options);
2085 instance.progress_node = progress_node.start("starting fuzzer", 0);
2086 }
2087
2088 return .{
2089 .run = run,
2090 .ctx = ctx,
2091 .coverage_id = null,
2092
2093 .instances = instances,
2094 .batch = .init(batch_storage),
2095 .pending_broadcasts = .empty,
2096 .broadcast = .empty,
2097 .broadcast_undelivered = 0,
2098 };
2099 }
2100
2101 fn deinit(f: *FuzzTestRunner) void {
2102 const step_owner = f.run.step.owner;
2103 const gpa = step_owner.allocator;
2104 const io = step_owner.graph.io;
2105
2106 f.batch.cancel(io);
2107 gpa.free(f.batch.storage);
2108 var total_rss: usize = 0;
2109 for (f.instances) |*instance| {
2110 instance.child.kill(io);
2111 instance.message.deinit(gpa);
2112 instance.stderr.deinit(gpa);
2113 instance.progress_node.end();
2114 total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0;
2115 }
2116 f.run.step.result_peak_rss = @max(f.run.step.result_peak_rss, total_rss);
2117 gpa.free(f.instances);
2118 }
2119
2120 fn startInstances(f: *FuzzTestRunner) !void {
2121 const step_owner = f.run.step.owner;
2122 const io = step_owner.graph.io;
2123
2124 for (0.., f.instances) |id, *instance| {
2125 const id32: u32 = @intCast(id);
2126 (switch (f.ctx.fuzz.mode) {
2127 .forever => sendRunFuzzTestMessage(
2128 io,
2129 instance.child.stdin.?,
2130 f.run.fuzz_tests.items,
2131 .forever,
2132 id32,
2133 ),
2134 .limit => |limit| sendRunFuzzTestMessage(
2135 io,
2136 instance.child.stdin.?,
2137 f.run.fuzz_tests.items,
2138 .iterations,
2139 limit.amount,
2140 ),
2141 }) catch |write_err| {
2142 // The runner unexpectedly closed stdin, which means it crashed during initialization.
2143 // Clean up everything and wait for the child to exit.
2144 instance.child.stdin.?.close(io);
2145 instance.child.stdin = null;
2146 const term = try instance.child.wait(io);
2147 return f.run.step.fail(
2148 "unable to write stdin ({t}); test process unexpectedly {f}",
2149 .{ write_err, fmtTerm(term) },
2150 );
2151 };
2152
2153 try f.addStdoutRead(id32, @sizeOf(InHeader));
2154 try f.addStderrRead(id32);
2155 }
2156 }
2157
2158 fn listen(f: *FuzzTestRunner) !void {
2159 const step_owner = f.run.step.owner;
2160 const io = step_owner.graph.io;
2161
2162 while (true) {
2163 try f.batch.awaitConcurrent(io, .none);
2164 while (f.batch.next()) |completion| {
2165 const id = completion.index / 3;
2166 const result = completion.result;
2167 switch (completion.index % 3) {
2168 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) {
2169 // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
2170 // that all stderr is collected.
2171 error.BrokenPipe => continue,
2172 else => |write_e| return write_e,
2173 }),
2174 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) {
2175 // Avoid calling `instanceEos` until EndOfStream is seen with stderr so
2176 // that all stderr is collected.
2177 error.EndOfStream => continue,
2178 else => |read_e| return read_e,
2179 }),
2180 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) {
2181 error.EndOfStream => return f.instanceEos(id),
2182 else => |read_e| return read_e,
2183 }),
2184 else => unreachable,
2185 }
2186 }
2187 }
2188 }
2189
2190 fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
2191 const step_owner = f.run.step.owner;
2192 const gpa = step_owner.allocator;
2193 const io = step_owner.graph.io;
2194 const instance = &f.instances[id];
2195
2196 instance.message.items.len += n;
2197 const total_read = instance.message.items.len;
2198 if (total_read < @sizeOf(InHeader)) {
2199 try f.addStdoutRead(id, @sizeOf(InHeader));
2200 return;
2201 }
2202
2203 const header = instance.messageHeader();
2204 const body = instance.message.items[@sizeOf(InHeader)..];
2205 if (body.len != header.bytes_len) {
2206 try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len);
2207 return;
2208 }
2209
2210 switch (header.tag) {
2211 .zig_version => {
2212 if (!std.mem.eql(u8, builtin.zig_version_string, body)) return f.run.step.fail(
2213 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
2214 .{ builtin.zig_version_string, body },
2215 );
2216 },
2217 .coverage_id => {
2218 var body_r: Io.Reader = .fixed(body);
2219 f.coverage_id = body_r.takeInt(u64, .little) catch unreachable;
2220 const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable;
2221 const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable;
2222 const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable;
2223
2224 const fuzz = f.ctx.fuzz;
2225 fuzz.queue_mutex.lockUncancelable(io);
2226 defer fuzz.queue_mutex.unlock(io);
2227 try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{
2228 .id = f.coverage_id.?,
2229 .cumulative = .{
2230 .runs = cumulative_runs,
2231 .unique = cumulative_unique,
2232 .coverage = cumulative_coverage,
2233 },
2234 .run = f.run,
2235 } });
2236 fuzz.queue_cond.signal(io);
2237 },
2238 .fuzz_start_addr => {
2239 var body_r: Io.Reader = .fixed(body);
2240 const fuzz = f.ctx.fuzz;
2241 const addr = body_r.takeInt(u64, .little) catch unreachable;
2242
2243 fuzz.queue_mutex.lockUncancelable(io);
2244 defer fuzz.queue_mutex.unlock(io);
2245 try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{
2246 .addr = addr,
2247 .coverage_id = f.coverage_id.?,
2248 } });
2249 fuzz.queue_cond.signal(io);
2250 },
2251 .fuzz_test_change => {
2252 const test_i = std.mem.readInt(u32, body[0..4], .little);
2253 instance.progress_node.setName(f.run.fuzz_tests.items[test_i]);
2254 },
2255 .broadcast_fuzz_input => {
2256 if (f.instances.len == 1) {
2257 // No other processes to broadcast to.
2258 } else if (f.broadcast_undelivered == 0) {
2259 try f.instanceBroadcast(id, body);
2260 } else {
2261 const footer: PendingBroadcastFooter = .{
2262 .from_id = id,
2263 .body_len = @intCast(body.len),
2264 };
2265 // There is another broadcast in progress so add this one to the queue.
2266 const size = @sizeOf(PendingBroadcastFooter) + body.len;
2267 try f.pending_broadcasts.ensureUnusedCapacity(gpa, size);
2268 f.pending_broadcasts.appendSliceAssumeCapacity(body);
2269 f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer));
2270 }
2271 },
2272 else => {}, // ignore other messages
2273 }
2274
2275 instance.message.clearRetainingCapacity();
2276 try f.addStdoutRead(id, @sizeOf(InHeader));
2277 }
2278
2279 fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void {
2280 const instance = &f.instances[id];
2281 instance.stderr.items.len += n;
2282 try f.addStderrRead(id);
2283 }
2284
2285 fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void {
2286 const instance = &f.instances[id];
2287
2288 instance.broadcast_written += n;
2289 if (instance.broadcast_written == f.broadcast.items.len) {
2290 f.broadcast_undelivered -= 1;
2291 if (f.broadcast_undelivered == 0) {
2292 try f.broadcastComplete();
2293 }
2294 } else {
2295 f.addStdinWrite(id);
2296 }
2297 }
2298
2299 fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void {
2300 const step_owner = f.run.step.owner;
2301 const gpa = step_owner.allocator;
2302 const instance = &f.instances[id];
2303
2304 try instance.message.ensureTotalCapacity(gpa, end);
2305 const start = instance.message.items.len;
2306 instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]};
2307 f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{
2308 .file = instance.child.stdout.?,
2309 .data = &instance.stdout_vec,
2310 } });
2311 }
2312
2313 fn addStderrRead(f: *FuzzTestRunner, id: u32) !void {
2314 const step_owner = f.run.step.owner;
2315 const gpa = step_owner.allocator;
2316 const instance = &f.instances[id];
2317
2318 try instance.stderr.ensureUnusedCapacity(gpa, 1);
2319 instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()};
2320 f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{
2321 .file = instance.child.stderr.?,
2322 .data = &instance.stderr_vec,
2323 } });
2324 }
2325
2326 fn addStdinWrite(f: *FuzzTestRunner, id: u32) void {
2327 const instance = &f.instances[id];
2328
2329 assert(f.broadcast.items.len != instance.broadcast_written);
2330 instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]};
2331 f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{
2332 .file = instance.child.stdin.?,
2333 .data = &instance.stdin_vec,
2334 } });
2335 }
2336
2337 fn instanceEos(f: *FuzzTestRunner, id: u32) !void {
2338 const step_owner = f.run.step.owner;
2339 const io = step_owner.graph.io;
2340 const instance = &f.instances[id];
2341
2342 instance.child.stdin.?.close(io);
2343 instance.child.stdin = null;
2344 const term = try instance.child.wait(io);
2345 if (!termMatches(.{ .exited = 0 }, term)) {
2346 f.run.step.result_stderr = try f.mergedStderr();
2347 try f.saveCrash(id, term);
2348 return f.run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)});
2349 }
2350 }
2351
2352 fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void {
2353 const step = &f.run.step;
2354 const b = step.owner;
2355 const io = b.graph.io;
2356
2357 if (f.coverage_id == null) return;
2358
2359 // Search for the input file corresponding to the instance
2360 const InputHeader = Build.abi.fuzz.MmapInputHeader;
2361 var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined;
2362 var in_r: Io.File.Reader = undefined;
2363 var in_f: Io.File = undefined;
2364 var in_name_buf: [12]u8 = undefined;
2365 var in_name: []const u8 = undefined;
2366 var i: u32 = 0;
2367 const header: InputHeader = while (true) : ({
2368 if (i == std.math.maxInt(u32)) return;
2369 i += 1;
2370 }) {
2371 const name_prefix = "f" ++ Io.Dir.path.sep_str ++ "in";
2372 in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable;
2373 in_f = b.cache_root.handle.openFile(io, in_name, .{
2374 .lock = .exclusive,
2375 .lock_nonblocking = true,
2376 }) catch |e| switch (e) {
2377 error.FileNotFound => return,
2378 error.WouldBlock => continue, // Can not be from
2379 // the crashed instance since it is still locked.
2380 else => return step.fail("failed to open file '{f}{s}': {t}", .{
2381 b.cache_root, in_name, e,
2382 }),
2383 };
2384
2385 in_r = in_f.readerStreaming(io, &in_r_buf);
2386 const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| {
2387 in_f.close(io);
2388 switch (e) {
2389 error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{
2390 b.cache_root, in_name, in_r.err.?,
2391 }),
2392 error.EndOfStream => continue,
2393 }
2394 };
2395
2396 if (header.pc_digest == f.coverage_id.? and
2397 header.instance_id == id and
2398 header.test_i < f.run.fuzz_tests.items.len)
2399 {
2400 break header;
2401 }
2402
2403 in_f.close(io);
2404 };
2405 defer in_f.close(io);
2406
2407 // Save it to a seperate file
2408 const crash_name = "f" ++ Io.Dir.path.sep_str ++ "crash";
2409 const out = b.cache_root.handle.createFile(io, crash_name, .{
2410 .lock = .exclusive, // Multiple run steps could have found a crash at the same time
2411 }) catch |e| return step.fail("failed to create file '{f}{s}': {t}", .{
2412 b.cache_root, crash_name, e,
2413 });
2414 defer out.close(io);
2415
2416 var out_w_buf: [512]u8 = undefined;
2417 var out_w = out.writerStreaming(io, &out_w_buf);
2418 _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) {
2419 error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{
2420 b.cache_root, in_name, in_r.err.?,
2421 }),
2422 error.WriteFailed => return step.fail("failed to write file '{f}{s}': {t}", .{
2423 b.cache_root, crash_name, out_w.err.?,
2424 }),
2425 };
2426
2427 return f.run.step.fail("test '{s}' {f}; input saved to '{f}{s}'", .{
2428 f.run.fuzz_tests.items[header.test_i],
2429 fmtTerm(term),
2430 b.cache_root,
2431 crash_name,
2432 });
2433 }
2434
2435 fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void {
2436 assert(f.instances.len > 1);
2437 assert(f.broadcast_undelivered == 0); // no other broadcast is progress
2438 assert(f.broadcast.items.len == 0);
2439 assert(from_id < f.instances.len);
2440
2441 const step_owner = f.run.step.owner;
2442 const gpa = step_owner.allocator;
2443
2444 var out_header: OutHeader = .{
2445 .tag = .new_fuzz_input,
2446 .bytes_len = @intCast(bytes.len),
2447 };
2448 if (std.builtin.Endian.native != .little) {
2449 std.mem.byteSwapAllFields(OutHeader, &out_header);
2450 }
2451 try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len);
2452 f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header));
2453 f.broadcast.appendSliceAssumeCapacity(bytes);
2454
2455 f.broadcast_undelivered = @intCast(f.instances.len - 1);
2456 for (0.., f.instances) |to_id, *instance| {
2457 if (to_id == from_id) continue;
2458 instance.broadcast_written = 0;
2459 f.addStdinWrite(@intCast(to_id));
2460 }
2461 }
2462
2463 fn broadcastComplete(f: *FuzzTestRunner) !void {
2464 assert(f.instances.len > 1);
2465 assert(f.broadcast_undelivered == 0);
2466 f.broadcast.clearRetainingCapacity();
2467
2468 const pending = &f.pending_broadcasts;
2469 if (pending.items.len != 0) {
2470 // Another broadcast is pending; copy it over to `broadcast`
2471
2472 const footer_len = @sizeOf(PendingBroadcastFooter);
2473 const footer_bytes = pending.items[pending.items.len - footer_len ..];
2474 const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes);
2475 pending.items.len -= footer_len;
2476
2477 const body = pending.items[pending.items.len - footer.body_len ..];
2478 try f.instanceBroadcast(footer.from_id, body);
2479 pending.items.len -= body.len;
2480 }
2481 }
2482
2483 fn mergedStderr(f: *FuzzTestRunner) std.mem.Allocator.Error![]const u8 {
2484 const step_owner = f.run.step.owner;
2485 const arena = step_owner.allocator;
2486
2487 // Collect any available stderr
2488 while (f.batch.next()) |completion| {
2489 if (completion.index % 3 != 2) continue;
2490 const len = completion.result.file_read_streaming catch continue;
2491 f.instances[completion.index / 3].stderr.items.len += len;
2492 }
2493
2494 var stderr_len: usize = 0;
2495 for (f.instances) |*instance| stderr_len += instance.stderr.items.len;
2496 const stderr = try arena.alloc(u8, stderr_len);
2497
2498 stderr_len = 0;
2499 for (f.instances) |*instance| {
2500 @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items);
2501 stderr_len += instance.stderr.items.len;
2502 }
2503 return stderr;
2504 }
2505};
2506
2507fn evalFuzzTest(
2508 run: *Run,
2509 spawn_options: process.SpawnOptions,
2510 options: Step.MakeOptions,
2511 fuzz_context: FuzzContext,
2512) !void {
2513 var f: FuzzTestRunner = try .init(run, fuzz_context, options.progress_node, spawn_options);
2514 defer f.deinit();
2515 try f.startInstances();
2516 try f.listen();
2517}
2518
2519const TestMetadata = struct {
2520 names: []const u32,
2521 ns_per_test: []u64,
2522 expected_panic_msgs: []const u32,
2523 string_bytes: []const u8,
2524 next_index: u32,
2525 prog_node: std.Progress.Node,
2526
2527 fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata {
2528 return .{
2529 .names = tm.names,
2530 .string_bytes = tm.string_bytes,
2531 };
2532 }
2533
2534 fn testName(tm: TestMetadata, index: u32) []const u8 {
2535 return tm.toCachedTestMetadata().testName(index);
2536 }
2537};
2538
2539pub const CachedTestMetadata = struct {
2540 names: []const u32,
2541 string_bytes: []const u8,
2542
2543 pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 {
2544 return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0);
2545 }
2546};
2547
2548fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void {
2549 while (metadata.next_index < metadata.names.len) {
2550 const i = metadata.next_index;
2551 metadata.next_index += 1;
2552
2553 if (metadata.expected_panic_msgs[i] != 0) continue;
2554
2555 const name = metadata.testName(i);
2556 if (sub_prog_node.*) |n| n.end();
2557 sub_prog_node.* = metadata.prog_node.start(name, 0);
2558
2559 try sendRunTestMessage(io, in, .run_test, i);
2560 return;
2561 } else {
2562 metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done
2563 try sendMessage(io, in, .exit);
2564 }
2565}
2566
2567fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void {
2568 const header: std.zig.Client.Message.Header = .{
2569 .tag = tag,
2570 .bytes_len = 0,
2571 };
2572 var w = file.writerStreaming(io, &.{});
2573 w.interface.writeStruct(header, .little) catch |err| switch (err) {
2574 error.WriteFailed => return w.err.?,
2575 };
2576}
2577
2578fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void {
2579 const header: std.zig.Client.Message.Header = .{
2580 .tag = tag,
2581 .bytes_len = 4,
2582 };
2583 var w = file.writerStreaming(io, &.{});
2584 w.interface.writeStruct(header, .little) catch |err| switch (err) {
2585 error.WriteFailed => return w.err.?,
2586 };
2587 w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
2588 error.WriteFailed => return w.err.?,
2589 };
2590}
2591
2592fn sendRunFuzzTestMessage(
2593 io: Io,
2594 file: Io.File,
2595 test_names: []const []const u8,
2596 kind: std.Build.abi.fuzz.LimitKind,
2597 amount_or_instance: u64,
2598) !void {
2599 const header: std.zig.Client.Message.Header = .{
2600 .tag = .start_fuzzing,
2601 .bytes_len = 1 + 8 + 4 + count: {
2602 var c: u32 = @intCast(test_names.len * 4);
2603 for (test_names) |name| {
2604 c += @intCast(name.len);
2605 }
2606 break :count c;
2607 },
2608 };
2609 var w = file.writerStreaming(io, &.{});
2610 w.interface.writeStruct(header, .little) catch |err| switch (err) {
2611 error.WriteFailed => return w.err.?,
2612 };
2613 w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) {
2614 error.WriteFailed => return w.err.?,
2615 };
2616 w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) {
2617 error.WriteFailed => return w.err.?,
2618 };
2619 w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) {
2620 error.WriteFailed => return w.err.?,
2621 };
2622 for (test_names) |test_name| {
2623 w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) {
2624 error.WriteFailed => return w.err.?,
2625 };
2626 w.interface.writeAll(test_name) catch |err| switch (err) {
2627 error.WriteFailed => return w.err.?,
2628 };
2629 }
2630}
2631
2632fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult {
2633 const b = run.step.owner;
2634 const io = b.graph.io;
2635 const arena = b.allocator;
2636 const gpa = b.allocator;
2637
2638 var child = try process.spawn(io, spawn_options);
2639 defer child.kill(io);
2640
2641 switch (run.stdin) {
2642 .bytes => |bytes| {
2643 child.stdin.?.writeStreamingAll(io, bytes) catch |err| {
2644 return run.step.fail("unable to write stdin: {t}", .{err});
2645 };
2646 child.stdin.?.close(io);
2647 child.stdin = null;
2648 },
2649 .lazy_path => |lazy_path| {
2650 const path = lazy_path.getPath3(b, &run.step);
2651 const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| {
2652 return run.step.fail("unable to open stdin file: {t}", .{err});
2653 };
2654 defer file.close(io);
2655 // TODO https://github.com/ziglang/zig/issues/23955
2656 var read_buffer: [1024]u8 = undefined;
2657 var file_reader = file.reader(io, &read_buffer);
2658 var write_buffer: [1024]u8 = undefined;
2659 var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer);
2660 _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
2661 error.ReadFailed => return run.step.fail("failed to read from {f}: {t}", .{
2662 path, file_reader.err.?,
2663 }),
2664 error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
2665 stdin_writer.err.?,
2666 }),
2667 };
2668 stdin_writer.interface.flush() catch |err| switch (err) {
2669 error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{
2670 stdin_writer.err.?,
2671 }),
2672 };
2673 child.stdin.?.close(io);
2674 child.stdin = null;
2675 },
2676 .none => {},
2677 }
2678
2679 var stdout_bytes: ?[]const u8 = null;
2680 var stderr_bytes: ?[]const u8 = null;
2681
2682 if (child.stdout) |stdout| {
2683 if (child.stderr) |stderr| {
2684 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
2685 var multi_reader: Io.File.MultiReader = undefined;
2686 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr });
2687 defer multi_reader.deinit();
2688
2689 const stdout_reader = multi_reader.reader(0);
2690 const stderr_reader = multi_reader.reader(1);
2691
2692 while (multi_reader.fill(64, .none)) |_| {
2693 if (run.stdio_limit.toInt()) |limit| {
2694 if (stdout_reader.buffered().len > limit)
2695 return error.StdoutStreamTooLong;
2696 if (stderr_reader.buffered().len > limit)
2697 return error.StderrStreamTooLong;
2698 }
2699 } else |err| switch (err) {
2700 error.Timeout => unreachable,
2701 error.EndOfStream => {},
2702 else => |e| return e,
2703 }
2704
2705 try multi_reader.checkAnyError();
2706
2707 // TODO: this string can leak since alloc below can return error.
2708 stdout_bytes = try multi_reader.toOwnedSlice(0);
2709 // TODO: this string can leak since its allocated using gpa and `try child.wait(io)` below can fail.
2710 stderr_bytes = try multi_reader.toOwnedSlice(1);
2711 } else {
2712 var stdout_reader = stdout.readerStreaming(io, &.{});
2713 stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
2714 error.OutOfMemory => |e| return e,
2715 error.ReadFailed => return stdout_reader.err.?,
2716 error.StreamTooLong => return error.StdoutStreamTooLong,
2717 };
2718 }
2719 } else if (child.stderr) |stderr| {
2720 var stderr_reader = stderr.readerStreaming(io, &.{});
2721 stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) {
2722 error.OutOfMemory => |e| return e,
2723 error.ReadFailed => return stderr_reader.err.?,
2724 error.StreamTooLong => return error.StderrStreamTooLong,
2725 };
2726 }
2727
2728 if (stderr_bytes) |bytes| if (bytes.len > 0) {
2729 // Treat stderr as an error message.
2730 const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) {
2731 .check => |checks| !checksContainStderr(checks.items),
2732 else => true,
2733 };
2734 if (stderr_is_diagnostic) {
2735 run.step.result_stderr = bytes;
2736 }
2737 };
2738
2739 run.step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0;
2740
2741 return .{
2742 .term = try child.wait(io),
2743 .stdout = stdout_bytes,
2744 .stderr = stderr_bytes,
2745 };
2746}
2747
2748fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void {
2749 const b = run.step.owner;
2750 const compiles = artifact.getCompileDependencies(true);
2751 for (compiles) |compile| {
2752 if (compile.root_module.resolved_target.?.result.os.tag == .windows and
2753 compile.isDynamicLibrary())
2754 {
2755 addPathDir(run, Dir.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?);
2756 }
2757 }
2758}
2759
2760fn failForeign(
2761 run: *Run,
2762 suggested_flag: []const u8,
2763 argv0: []const u8,
2764 exe: *Step.Compile,
2765) error{ MakeFailed, MakeSkipped, OutOfMemory } {
2766 switch (run.stdio) {
2767 .check, .zig_test => {
2768 if (run.skip_foreign_checks)
2769 return error.MakeSkipped;
2770
2771 const b = run.step.owner;
2772 const host_name = try b.graph.host.result.zigTriple(b.allocator);
2773 const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator);
2774
2775 return run.step.fail(
2776 \\unable to spawn foreign binary '{s}' ({s}) on host system ({s})
2777 \\ consider using {s} or enabling skip_foreign_checks in the Run step
2778 , .{ argv0, foreign_name, host_name, suggested_flag });
2779 },
2780 else => {
2781 return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0});
2782 },
2783 }
2784}
2785
2786fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void {
2787 switch (stdio) {
2788 .infer_from_args, .inherit, .zig_test => {},
2789 .check => |checks| for (checks.items) |check| {
2790 hh.add(@as(std.meta.Tag(StdIo.Check), check));
2791 switch (check) {
2792 .expect_stderr_exact,
2793 .expect_stderr_match,
2794 .expect_stdout_exact,
2795 .expect_stdout_match,
2796 => |s| hh.addBytes(s),
2797
2798 .expect_term => |term| {
2799 hh.add(@as(std.meta.Tag(process.Child.Term), term));
2800 switch (term) {
2801 inline .exited, .signal, .stopped => |x| hh.add(x),
2802 .unknown => |x| hh.add(x),
2803 }
2804 },
2805 }
2806 },
2807 }
2808}
lib/std/Build/Step/TranslateC.zig+2-2
......@@ -6,7 +6,7 @@ const mem = std.mem;
66
77const TranslateC = @This();
88
9pub const base_id: Step.Id = .translate_c;
9pub const base_tag: Step.Tag = .translate_c;
1010
1111step: Step,
1212source: std.Build.LazyPath,
......@@ -31,7 +31,7 @@ pub fn create(owner: *std.Build, options: Options) *TranslateC {
3131 const source = options.root_source_file.dupe(owner);
3232 translate_c.* = .{
3333 .step = Step.init(.{
34 .id = base_id,
34 .tag = base_tag,
3535 .name = "translate-c",
3636 .owner = owner,
3737 .makeFn = make,
lib/std/Build/Step/UpdateSourceFiles.zig+2-2
......@@ -14,7 +14,7 @@ const ArrayList = std.ArrayList;
1414step: Step,
1515output_source_files: std.ArrayList(OutputSourceFile),
1616
17pub const base_id: Step.Id = .update_source_files;
17pub const base_tag: Step.Tag = .update_source_files;
1818
1919pub const OutputSourceFile = struct {
2020 contents: Contents,
......@@ -30,7 +30,7 @@ pub fn create(owner: *std.Build) *UpdateSourceFiles {
3030 const usf = owner.allocator.create(UpdateSourceFiles) catch @panic("OOM");
3131 usf.* = .{
3232 .step = Step.init(.{
33 .id = base_id,
33 .tag = base_tag,
3434 .name = "UpdateSourceFiles",
3535 .owner = owner,
3636 .makeFn = make,
lib/std/Build/Step/WriteFile.zig+2-209
......@@ -18,7 +18,7 @@ directories: std.ArrayList(Directory),
1818generated_directory: std.Build.GeneratedFile,
1919mode: Mode = .whole_cached,
2020
21pub const base_id: Step.Id = .write_file;
21pub const base_tag: Step.Tag = .write_file;
2222
2323pub const Mode = union(enum) {
2424 /// Default mode. Integrates with the cache system. The directory should be
......@@ -89,10 +89,9 @@ pub fn create(owner: *std.Build) *WriteFile {
8989 const write_file = owner.allocator.create(WriteFile) catch @panic("OOM");
9090 write_file.* = .{
9191 .step = Step.init(.{
92 .id = base_id,
92 .tag = base_tag,
9393 .name = "WriteFile",
9494 .owner = owner,
95 .makeFn = make,
9695 }),
9796 .files = .empty,
9897 .directories = .empty,
......@@ -191,209 +190,3 @@ fn maybeUpdateName(write_file: *WriteFile) void {
191190 }
192191 }
193192}
194
195fn make(step: *Step, options: Step.MakeOptions) !void {
196 _ = options;
197 const b = step.owner;
198 const graph = b.graph;
199 const io = graph.io;
200 const arena = b.allocator;
201 const gpa = graph.cache.gpa;
202 const write_file: *WriteFile = @fieldParentPtr("step", step);
203
204 const open_dir_cache = try arena.alloc(Io.Dir, write_file.directories.items.len);
205 var open_dirs_count: usize = 0;
206 defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]);
207
208 switch (write_file.mode) {
209 .whole_cached => {
210 step.clearWatchInputs();
211
212 // The cache is used here not really as a way to speed things up - because writing
213 // the data to a file would probably be very fast - but as a way to find a canonical
214 // location to put build artifacts.
215
216 // If, for example, a hard-coded path was used as the location to put WriteFile
217 // files, then two WriteFiles executing in parallel might clobber each other.
218
219 var man = b.graph.cache.obtain();
220 defer man.deinit();
221
222 for (write_file.files.items) |file| {
223 man.hash.addBytes(file.sub_path);
224
225 switch (file.contents) {
226 .bytes => |bytes| {
227 man.hash.addBytes(bytes);
228 },
229 .copy => |lazy_path| {
230 const path = lazy_path.getPath3(b, step);
231 _ = try man.addFilePath(path, null);
232 try step.addWatchInput(lazy_path);
233 },
234 }
235 }
236
237 for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| {
238 man.hash.addBytes(dir.sub_path);
239 for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext);
240 if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc);
241
242 const need_derived_inputs = try step.addDirectoryWatchInput(dir.source);
243 const src_dir_path = dir.source.getPath3(b, step);
244
245 var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
246 return step.fail("unable to open source directory '{f}': {s}", .{
247 src_dir_path, @errorName(err),
248 });
249 };
250 open_dir_cache_elem.* = src_dir;
251 open_dirs_count += 1;
252
253 var it = try src_dir.walk(gpa);
254 defer it.deinit();
255 while (try it.next(io)) |entry| {
256 if (!dir.options.pathIncluded(entry.path)) continue;
257
258 switch (entry.kind) {
259 .directory => {
260 if (need_derived_inputs) {
261 const entry_path = try src_dir_path.join(arena, entry.path);
262 try step.addDirectoryWatchInputFromPath(entry_path);
263 }
264 },
265 .file => {
266 const entry_path = try src_dir_path.join(arena, entry.path);
267 _ = try man.addFilePath(entry_path, null);
268 },
269 else => continue,
270 }
271 }
272 }
273
274 if (try step.cacheHit(&man)) {
275 const digest = man.final();
276 write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest });
277 assert(step.result_cached);
278 return;
279 }
280
281 const digest = man.final();
282 const cache_path = "o" ++ Dir.path.sep_str ++ digest;
283
284 write_file.generated_directory.path = try b.cache_root.join(arena, &.{cache_path});
285
286 try operate(write_file, open_dir_cache, .{
287 .root_dir = b.cache_root,
288 .sub_path = cache_path,
289 });
290
291 try step.writeManifest(&man);
292 },
293 .tmp => {
294 step.result_cached = false;
295
296 var rand_int: u64 = undefined;
297 io.random(@ptrCast(&rand_int));
298 const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
299
300 write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path});
301
302 try operate(write_file, open_dir_cache, .{
303 .root_dir = b.cache_root,
304 .sub_path = tmp_dir_sub_path,
305 });
306 },
307 .mutate => |lp| {
308 step.result_cached = false;
309 const root_path = try lp.getPath4(b, step);
310 write_file.generated_directory.path = try root_path.toString(arena);
311 try operate(write_file, open_dir_cache, root_path);
312 },
313 }
314}
315
316fn operate(write_file: *WriteFile, open_dir_cache: []const Io.Dir, root_path: std.Build.Cache.Path) !void {
317 const step = &write_file.step;
318 const b = step.owner;
319 const io = b.graph.io;
320 const gpa = b.graph.cache.gpa;
321 const arena = b.allocator;
322
323 var cache_dir = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err|
324 return step.fail("unable to make path {f}: {t}", .{ root_path, err });
325 defer cache_dir.close(io);
326
327 for (write_file.files.items) |file| {
328 if (Dir.path.dirname(file.sub_path)) |dirname| {
329 cache_dir.createDirPath(io, dirname) catch |err| {
330 return step.fail("unable to make path '{f}{c}{s}': {t}", .{
331 root_path, Dir.path.sep, dirname, err,
332 });
333 };
334 }
335 switch (file.contents) {
336 .bytes => |bytes| {
337 cache_dir.writeFile(io, .{ .sub_path = file.sub_path, .data = bytes }) catch |err| {
338 return step.fail("unable to write file '{f}{c}{s}': {t}", .{
339 root_path, Dir.path.sep, file.sub_path, err,
340 });
341 };
342 },
343 .copy => |file_source| {
344 const source_path = file_source.getPath2(b, step);
345 const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir, file.sub_path, .{}) catch |err| {
346 return step.fail("unable to update file from '{s}' to '{f}{c}{s}': {t}", .{
347 source_path, root_path, Dir.path.sep, file.sub_path, err,
348 });
349 };
350 // At this point we already will mark the step as a cache miss.
351 // But this is kind of a partial cache hit since individual
352 // file copies may be avoided. Oh well, this information is
353 // discarded.
354 _ = prev_status;
355 },
356 }
357 }
358
359 for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| {
360 const src_dir_path = dir.source.getPath3(b, step);
361 const dest_dirname = dir.sub_path;
362
363 if (dest_dirname.len != 0) {
364 cache_dir.createDirPath(io, dest_dirname) catch |err| {
365 return step.fail("unable to make path '{f}{c}{s}': {t}", .{
366 root_path, Dir.path.sep, dest_dirname, err,
367 });
368 };
369 }
370
371 var it = try already_open_dir.walk(gpa);
372 defer it.deinit();
373 while (try it.next(io)) |entry| {
374 if (!dir.options.pathIncluded(entry.path)) continue;
375
376 const src_entry_path = try src_dir_path.join(arena, entry.path);
377 const dest_path = b.pathJoin(&.{ dest_dirname, entry.path });
378 switch (entry.kind) {
379 .directory => try cache_dir.createDirPath(io, dest_path),
380 .file => {
381 const prev_status = Io.Dir.updateFile(
382 src_entry_path.root_dir.handle,
383 io,
384 src_entry_path.sub_path,
385 cache_dir,
386 dest_path,
387 .{},
388 ) catch |err| {
389 return step.fail("unable to update file from '{f}' to '{f}{c}{s}': {t}", .{
390 src_entry_path, root_path, Dir.path.sep, dest_path, err,
391 });
392 };
393 _ = prev_status;
394 },
395 else => continue,
396 }
397 }
398 }
399}
lib/std/Build/Watch.zig deleted-968
......@@ -1,968 +0,0 @@
1const builtin = @import("builtin");
2
3const std = @import("../std.zig");
4const Io = std.Io;
5const Step = std.Build.Step;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8const fatal = std.process.fatal;
9const Watch = @This();
10const FsEvents = @import("Watch/FsEvents.zig");
11
12os: Os,
13/// The number to show as the number of directories being watched.
14dir_count: usize,
15// These fields are common to most implementations so are kept here for simplicity.
16// They are `undefined` on implementations which do not utilize then.
17dir_table: DirTable,
18generation: Generation,
19
20pub const have_impl = Os != void;
21
22/// Key is the directory to watch which contains one or more files we are
23/// interested in noticing changes to.
24///
25/// Value is generation.
26const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false);
27
28/// Special key of "." means any changes in this directory trigger the steps.
29const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet);
30const StepSet = std.AutoArrayHashMapUnmanaged(*Step, Generation);
31
32const Generation = u8;
33
34const Hash = std.hash.Wyhash;
35const Cache = std.Build.Cache;
36
37const Os = switch (builtin.os.tag) {
38 .linux => struct {
39 const posix = std.posix;
40
41 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
42 handle_table: HandleTable,
43 /// fanotify file descriptors are keyed by mount id since marks
44 /// are limited to a single filesystem.
45 poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd),
46
47 const MountId = i32;
48 const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false);
49
50 const fan_mask: std.os.linux.fanotify.MarkMask = .{
51 .CLOSE_WRITE = true,
52 .CREATE = true,
53 .DELETE = true,
54 .DELETE_SELF = true,
55 .EVENT_ON_CHILD = true,
56 .MOVED_FROM = true,
57 .MOVED_TO = true,
58 .MOVE_SELF = true,
59 .ONDIR = true,
60 };
61
62 const FileHandle = struct {
63 handle: *align(1) std.os.linux.file_handle,
64
65 fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
66 const bytes = lfh.slice();
67 const new_ptr = try gpa.alignedAlloc(
68 u8,
69 .of(std.os.linux.file_handle),
70 @sizeOf(std.os.linux.file_handle) + bytes.len,
71 );
72 const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
73 new_header.* = lfh.handle.*;
74 const new: FileHandle = .{ .handle = new_header };
75 @memcpy(new.slice(), lfh.slice());
76 return new;
77 }
78
79 fn destroy(lfh: FileHandle, gpa: Allocator) void {
80 const ptr: [*]u8 = @ptrCast(lfh.handle);
81 const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
82 return gpa.free(allocated_slice);
83 }
84
85 fn slice(lfh: FileHandle) []u8 {
86 const ptr: [*]u8 = &lfh.handle.f_handle;
87 return ptr[0..lfh.handle.handle_bytes];
88 }
89
90 const Adapter = struct {
91 pub fn hash(self: Adapter, a: FileHandle) u32 {
92 _ = self;
93 const unsigned_type: u32 = @bitCast(a.handle.handle_type);
94 return @truncate(Hash.hash(unsigned_type, a.slice()));
95 }
96 pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
97 _ = self;
98 _ = b_index;
99 return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
100 }
101 };
102 };
103
104 fn init(cwd_path: []const u8) !Watch {
105 _ = cwd_path;
106 return .{
107 .dir_table = .{},
108 .dir_count = 0,
109 .os = switch (builtin.os.tag) {
110 .linux => .{
111 .handle_table = .{},
112 .poll_fds = .{},
113 },
114 else => {},
115 },
116 .generation = 0,
117 };
118 }
119
120 fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
121 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
122 var buf: [std.fs.max_path_bytes]u8 = undefined;
123 const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
124 path.sub_path,
125 }) catch return error.NameTooLong;
126 const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
127 stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
128 try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID);
129 const stack_lfh: FileHandle = .{ .handle = stack_ptr };
130 return stack_lfh.clone(gpa);
131 }
132
133 fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool {
134 const fanotify = std.os.linux.fanotify;
135 const M = fanotify.event_metadata;
136 var events_buf: [256 + 4096]u8 = undefined;
137 var any_dirty = false;
138 while (true) {
139 var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) {
140 error.WouldBlock => return any_dirty,
141 else => |e| return e,
142 };
143 var meta: [*]align(1) M = @ptrCast(&events_buf);
144 while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
145 len -= meta[0].event_len;
146 meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
147 }) {
148 assert(meta[0].vers == M.VERSION);
149 if (meta[0].mask.Q_OVERFLOW) {
150 any_dirty = true;
151 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
152 markAllFilesDirty(w, gpa);
153 return true;
154 }
155 const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
156 switch (fid.hdr.info_type) {
157 .DFID_NAME => {
158 const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
159 const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
160 const file_name = std.mem.span(file_name_z);
161 const lfh: FileHandle = .{ .handle = file_handle };
162 if (w.os.handle_table.getPtr(lfh)) |value| {
163 if (value.reaction_set.getPtr(".")) |glob_set|
164 any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
165 if (value.reaction_set.getPtr(file_name)) |step_set|
166 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
167 }
168 },
169 else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
170 }
171 }
172 }
173 }
174
175 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
176 // Add missing marks and note persisted ones.
177 for (steps) |step| {
178 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
179 const reaction_set = rs: {
180 const gop = try w.dir_table.getOrPut(gpa, path);
181 if (!gop.found_existing) {
182 var mount_id: MountId = undefined;
183 const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
184 error.FileNotFound => {
185 std.debug.assert(w.dir_table.swapRemove(path));
186 continue;
187 },
188 else => return err,
189 };
190 const fan_fd = blk: {
191 const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id);
192 if (!fd_gop.found_existing) {
193 const fan_fd = std.posix.fanotify_init(.{
194 .CLASS = .NOTIF,
195 .CLOEXEC = true,
196 .NONBLOCK = true,
197 .REPORT_NAME = true,
198 .REPORT_DIR_FID = true,
199 .REPORT_FID = true,
200 .REPORT_TARGET_FID = true,
201 }, 0) catch |err| switch (err) {
202 error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}),
203 else => |e| return e,
204 };
205 fd_gop.value_ptr.* = .{
206 .fd = fan_fd,
207 .events = std.posix.POLL.IN,
208 .revents = undefined,
209 };
210 }
211 break :blk fd_gop.value_ptr.*.fd;
212 };
213 // `dir_handle` may already be present in the table in
214 // the case that we have multiple Cache.Path instances
215 // that compare inequal but ultimately point to the same
216 // directory on the file system.
217 // In such case, we must revert adding this directory, but keep
218 // the additions to the step set.
219 const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle);
220 if (dh_gop.found_existing) {
221 _ = w.dir_table.pop();
222 } else {
223 assert(dh_gop.index == gop.index);
224 dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} };
225 posix.fanotify_mark(fan_fd, .{
226 .ADD = true,
227 .ONLYDIR = true,
228 }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| {
229 fatal("unable to watch {f}: {s}", .{ path, @errorName(err) });
230 };
231 }
232 break :rs &dh_gop.value_ptr.reaction_set;
233 }
234 break :rs &w.os.handle_table.values()[gop.index].reaction_set;
235 };
236 for (files.items) |basename| {
237 const gop = try reaction_set.getOrPut(gpa, basename);
238 if (!gop.found_existing) gop.value_ptr.* = .{};
239 try gop.value_ptr.put(gpa, step, w.generation);
240 }
241 }
242 }
243
244 {
245 // Remove marks for files that are no longer inputs.
246 var i: usize = 0;
247 while (i < w.os.handle_table.entries.len) {
248 {
249 const reaction_set = &w.os.handle_table.values()[i].reaction_set;
250 var step_set_i: usize = 0;
251 while (step_set_i < reaction_set.entries.len) {
252 const step_set = &reaction_set.values()[step_set_i];
253 var dirent_i: usize = 0;
254 while (dirent_i < step_set.entries.len) {
255 const generations = step_set.values();
256 if (generations[dirent_i] == w.generation) {
257 dirent_i += 1;
258 continue;
259 }
260 step_set.swapRemoveAt(dirent_i);
261 }
262 if (step_set.entries.len > 0) {
263 step_set_i += 1;
264 continue;
265 }
266 reaction_set.swapRemoveAt(step_set_i);
267 }
268 if (reaction_set.entries.len > 0) {
269 i += 1;
270 continue;
271 }
272 }
273
274 const path = w.dir_table.keys()[i];
275
276 const mount_id = w.os.handle_table.values()[i].mount_id;
277 const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd;
278 posix.fanotify_mark(fan_fd, .{
279 .REMOVE = true,
280 .ONLYDIR = true,
281 }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) {
282 error.FileNotFound => {}, // Expected, harmless.
283 else => |e| std.log.warn("unable to unwatch '{f}': {s}", .{ path, @errorName(e) }),
284 };
285
286 w.dir_table.swapRemoveAt(i);
287 w.os.handle_table.swapRemoveAt(i);
288 }
289 w.generation +%= 1;
290 }
291 w.dir_count = w.dir_table.count();
292 }
293
294 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
295 _ = io;
296 const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms());
297 if (events_len == 0)
298 return .timeout;
299 for (w.os.poll_fds.values()) |poll_fd| {
300 if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try markDirtySteps(w, gpa, poll_fd.fd))
301 return .dirty;
302 }
303 return .clean;
304 }
305 },
306 .windows => struct {
307 const windows = std.os.windows;
308
309 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
310 handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false),
311 ready_dirs: std.DoublyLinkedList,
312
313 const FileId = struct {
314 volumeSerialNumber: windows.ULONG,
315 indexNumber: windows.LARGE_INTEGER,
316 };
317
318 const Directory = struct {
319 reaction_set: ReactionSet,
320 id: FileId,
321 file: Io.File,
322 state: enum { idle, listening, ready },
323 iosb: windows.IO_STATUS_BLOCK,
324 // 64 KB is the packet size limit when monitoring over a network.
325 // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks
326 buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)),
327 ready_node: std.DoublyLinkedList.Node,
328
329 /// Start listening for events, buffer field will be overwritten eventually.
330 fn startListening(dir: *Directory, w: *Watch) !void {
331 assert(dir.file.flags.nonblocking);
332 assert(dir.state == .idle);
333 switch (windows.ntdll.NtNotifyChangeDirectoryFileEx(
334 dir.file.handle,
335 null,
336 &notifyApc,
337 w,
338 &dir.iosb,
339 &dir.buffer,
340 dir.buffer.len,
341 .{
342 .FILE_NAME = true,
343 .DIR_NAME = true,
344 .SIZE = true,
345 .LAST_WRITE = true,
346 .CREATION = true,
347 },
348 .FALSE,
349 .Notify,
350 )) {
351 .SUCCESS, .PENDING => dir.state = .listening,
352 .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported,
353 else => |status| return windows.unexpectedStatus(status),
354 }
355 }
356
357 fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void {
358 const w: *Watch = @ptrCast(@alignCast(apc_context));
359 const dir: *Directory = @fieldParentPtr("iosb", iosb);
360 assert(iosb.u.Status != .PENDING);
361 assert(dir.state == .listening);
362 w.os.ready_dirs.append(&dir.ready_node);
363 dir.state = .ready;
364 }
365
366 fn init(gpa: Allocator, path: Cache.Path) !*Directory {
367 // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW)
368 // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW.
369 var dir_handle: windows.HANDLE = undefined;
370 const root_fd = path.root_dir.handle.handle;
371 const sub_path = path.subPathOrDot();
372 const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call
373 var iosb: windows.IO_STATUS_BLOCK = undefined;
374 switch (windows.ntdll.NtCreateFile(
375 &dir_handle,
376 .{
377 .SPECIFIC = .{ .FILE_DIRECTORY = .{
378 .LIST = true,
379 } },
380 .STANDARD = .{ .SYNCHRONIZE = true },
381 .GENERIC = .{ .READ = true },
382 },
383 &.{
384 .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd,
385 .ObjectName = @constCast(&sub_path_w.string()),
386 },
387 &iosb,
388 null,
389 .{},
390 .VALID_FLAGS,
391 .OPEN,
392 .{
393 .DIRECTORY_FILE = true,
394 .IO = .ASYNCHRONOUS,
395 .OPEN_FOR_BACKUP_INTENT = true,
396 },
397 null,
398 0,
399 )) {
400 .SUCCESS => {},
401 .OBJECT_NAME_INVALID => return error.BadPathName,
402 .OBJECT_NAME_NOT_FOUND => return error.FileNotFound,
403 .OBJECT_NAME_COLLISION => return error.PathAlreadyExists,
404 .OBJECT_PATH_NOT_FOUND => return error.FileNotFound,
405 .NOT_A_DIRECTORY => return error.NotDir,
406 // This can happen if the directory has 'List folder contents' permission set to 'Deny'
407 .ACCESS_DENIED => return error.AccessDenied,
408 .INVALID_PARAMETER => unreachable,
409 else => |rc| return windows.unexpectedStatus(rc),
410 }
411 assert(dir_handle != windows.INVALID_HANDLE_VALUE);
412 errdefer windows.CloseHandle(dir_handle);
413
414 const dir_id = try getFileId(dir_handle);
415
416 const dir = try gpa.create(Directory);
417 dir.* = .{
418 .reaction_set = .empty,
419 .id = dir_id,
420 .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } },
421 .state = .idle,
422 .iosb = undefined,
423 .buffer = undefined,
424 .ready_node = undefined,
425 };
426 return dir;
427 }
428
429 fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void {
430 state: switch (dir.state) {
431 .idle => {},
432 .listening => {
433 var cancel_iosb: windows.IO_STATUS_BLOCK = undefined;
434 _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb);
435 while (switch (dir.state) {
436 .idle => unreachable,
437 .listening => true,
438 .ready => false,
439 }) Io.Threaded.waitForApcOrAlert();
440 continue :state .ready;
441 },
442 .ready => w.os.ready_dirs.remove(&dir.ready_node),
443 }
444 windows.CloseHandle(dir.file.handle);
445 gpa.destroy(dir);
446 }
447
448 /// Useful to make `*Directory` a key in `std.ArrayHashMap`.
449 const TableAdapter = struct {
450 pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 {
451 return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber)));
452 }
453 pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool {
454 _ = rhs_index;
455 return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and
456 lhs_dir.id.indexNumber == rhs_dir.id.indexNumber;
457 }
458 };
459 };
460
461 fn init(cwd_path: []const u8) !Watch {
462 _ = cwd_path;
463 return .{
464 .dir_table = .{},
465 .dir_count = 0,
466 .os = switch (builtin.os.tag) {
467 .windows => .{
468 .handle_table = .empty,
469 .ready_dirs = .{},
470 },
471 else => {},
472 },
473 .generation = 0,
474 };
475 }
476
477 fn getFileId(handle: windows.HANDLE) !FileId {
478 var file_id: FileId = undefined;
479 var io_status: windows.IO_STATUS_BLOCK = undefined;
480 var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined;
481 switch (windows.ntdll.NtQueryVolumeInformationFile(
482 handle,
483 &io_status,
484 &volume_info,
485 @sizeOf(windows.FILE.FS_VOLUME_INFORMATION),
486 .Volume,
487 )) {
488 .SUCCESS => {},
489 // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
490 // size provided. This is treated as success because the type of variable-length information that this would be relevant for
491 // (name, volume name, etc) we don't care about.
492 .BUFFER_OVERFLOW => {},
493 else => |rc| return windows.unexpectedStatus(rc),
494 }
495 file_id.volumeSerialNumber = volume_info.VolumeSerialNumber;
496 var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined;
497 switch (windows.ntdll.NtQueryInformationFile(
498 handle,
499 &io_status,
500 &internal_info,
501 @sizeOf(windows.FILE.INTERNAL_INFORMATION),
502 .Internal,
503 )) {
504 .SUCCESS => {},
505 else => |rc| return windows.unexpectedStatus(rc),
506 }
507 file_id.indexNumber = internal_info.IndexNumber;
508 return file_id;
509 }
510
511 fn markDirtySteps(w: *Watch, gpa: Allocator, dir: *Directory) !bool {
512 var any_dirty = false;
513 const bytes_returned = dir.iosb.Information;
514 if (bytes_returned == 0) {
515 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
516 markAllFilesDirty(w, gpa);
517 try dir.startListening(w);
518 return true;
519 }
520 var file_name_buf: [std.fs.max_path_bytes]u8 = undefined;
521 var offset: usize = 0;
522 while (true) {
523 const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset]));
524 const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())];
525 if (dir.reaction_set.getPtr(".")) |glob_set|
526 any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
527 if (dir.reaction_set.getPtr(file_name)) |step_set|
528 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
529 if (notify.NextEntryOffset == 0)
530 break;
531
532 offset += notify.NextEntryOffset;
533 }
534
535 // We call this now since at this point we have finished reading dir.buffer.
536 try dir.startListening(w);
537 return any_dirty;
538 }
539
540 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
541 // Add missing marks and note persisted ones.
542 for (steps) |step| {
543 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
544 const dir = dir: {
545 const gop = try w.dir_table.getOrPut(gpa, path);
546 if (!gop.found_existing) {
547 const dir: *Directory = try .init(gpa, path);
548 errdefer dir.deinit(gpa, w);
549 // `dir.id` may already be present in the table in
550 // the case that we have multiple Cache.Path instances
551 // that compare inequal but ultimately point to the same
552 // directory on the file system.
553 // In such case, we must revert adding this directory, but keep
554 // the additions to the step set.
555 const dh_gop = try w.os.handle_table.getOrPut(gpa, dir);
556 if (dh_gop.found_existing) {
557 dir.deinit(gpa, w);
558 _ = w.dir_table.pop();
559 break :dir w.os.handle_table.keys()[dh_gop.index];
560 } else {
561 assert(dh_gop.index == gop.index);
562 try dir.startListening(w);
563 break :dir dir;
564 }
565 }
566 break :dir w.os.handle_table.keys()[gop.index];
567 };
568 for (files.items) |basename| {
569 const gop = try dir.reaction_set.getOrPut(gpa, basename);
570 if (!gop.found_existing) gop.value_ptr.* = .{};
571 try gop.value_ptr.put(gpa, step, w.generation);
572 }
573 }
574 }
575
576 {
577 // Remove marks for files that are no longer inputs.
578 var i: usize = 0;
579 while (i < w.os.handle_table.entries.len) {
580 const dir = w.os.handle_table.keys()[i];
581 {
582 var step_set_i: usize = 0;
583 while (step_set_i < dir.reaction_set.entries.len) {
584 const step_set = &dir.reaction_set.values()[step_set_i];
585 var dirent_i: usize = 0;
586 while (dirent_i < step_set.entries.len) {
587 const generations = step_set.values();
588 if (generations[dirent_i] == w.generation) {
589 dirent_i += 1;
590 continue;
591 }
592 step_set.swapRemoveAt(dirent_i);
593 }
594 if (step_set.entries.len > 0) {
595 step_set_i += 1;
596 continue;
597 }
598 dir.reaction_set.swapRemoveAt(step_set_i);
599 }
600 if (dir.reaction_set.entries.len > 0) {
601 i += 1;
602 continue;
603 }
604 }
605
606 w.dir_table.swapRemoveAt(i);
607 w.os.handle_table.swapRemoveAt(i);
608 dir.deinit(gpa, w);
609 }
610 w.generation +%= 1;
611 }
612 w.dir_count = w.dir_table.count();
613 }
614
615 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
616 for (0..2) |attempt| {
617 while (w.os.ready_dirs.popFirst()) |ready_node| {
618 const dir: *Directory = @fieldParentPtr("ready_node", ready_node);
619 assert(dir.state == .ready);
620 dir.state = .idle;
621 switch (dir.iosb.u.Status) {
622 .SUCCESS => return if (try markDirtySteps(w, gpa, dir)) .dirty else .clean,
623 .PENDING => unreachable,
624 .CANCELLED => {},
625 else => |status| return windows.unexpectedStatus(status),
626 }
627 try dir.startListening(w);
628 }
629 try io.checkCancel();
630 if (attempt == 1) return .timeout;
631 const delay_interval: windows.LARGE_INTEGER = switch (timeout) {
632 .none => std.math.minInt(windows.LARGE_INTEGER),
633 .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100),
634 };
635 _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval);
636 } else unreachable;
637 }
638 },
639 .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct {
640 const posix = std.posix;
641
642 kq_fd: i32,
643 /// Indexes correspond 1:1 with `dir_table`.
644 handles: std.MultiArrayList(struct {
645 rs: ReactionSet,
646 /// If the corresponding dir_table Path has sub_path == "", then it
647 /// suffices as the open directory handle, and this value will be
648 /// -1. Otherwise, it needs to be opened in update(), and will be
649 /// stored here.
650 dir_fd: i32,
651 }),
652
653 const dir_open_flags: posix.O = f: {
654 var f: posix.O = .{
655 .ACCMODE = .RDONLY,
656 .NOFOLLOW = false,
657 .DIRECTORY = true,
658 .CLOEXEC = true,
659 };
660 if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true;
661 if (@hasField(posix.O, "PATH")) f.PATH = true;
662 break :f f;
663 };
664
665 const EV = std.c.EV;
666 const NOTE = std.c.NOTE;
667
668 fn init(cwd_path: []const u8) !Watch {
669 _ = cwd_path;
670 return .{
671 .dir_table = .{},
672 .dir_count = 0,
673 .os = .{
674 .kq_fd = try Io.Kqueue.createFileDescriptor(),
675 .handles = .empty,
676 },
677 .generation = 0,
678 };
679 }
680
681 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
682 const handles = &w.os.handles;
683 for (steps) |step| {
684 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
685 const reaction_set = rs: {
686 const gop = try w.dir_table.getOrPut(gpa, path);
687 if (!gop.found_existing) {
688 const skip_open_dir = path.sub_path.len == 0;
689 const dir_fd = if (skip_open_dir)
690 path.root_dir.handle.handle
691 else
692 posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| {
693 fatal("failed to open directory {f}: {t}", .{ path, err });
694 };
695 // Empirically the dir has to stay open or else no events are triggered.
696 errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd);
697 const changes = [1]posix.Kevent{.{
698 .ident = @bitCast(@as(isize, dir_fd)),
699 .filter = std.c.EVFILT.VNODE,
700 .flags = EV.ADD | EV.ENABLE | EV.CLEAR,
701 .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
702 .data = 0,
703 .udata = gop.index,
704 }};
705 _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null);
706 assert(handles.len == gop.index);
707 try handles.append(gpa, .{
708 .rs = .{},
709 .dir_fd = if (skip_open_dir) -1 else dir_fd,
710 });
711 }
712
713 break :rs &handles.items(.rs)[gop.index];
714 };
715 for (files.items) |basename| {
716 const gop = try reaction_set.getOrPut(gpa, basename);
717 if (!gop.found_existing) gop.value_ptr.* = .{};
718 try gop.value_ptr.put(gpa, step, w.generation);
719 }
720 }
721 }
722
723 {
724 // Remove marks for files that are no longer inputs.
725 var i: usize = 0;
726 while (i < handles.len) {
727 {
728 const reaction_set = &handles.items(.rs)[i];
729 var step_set_i: usize = 0;
730 while (step_set_i < reaction_set.entries.len) {
731 const step_set = &reaction_set.values()[step_set_i];
732 var dirent_i: usize = 0;
733 while (dirent_i < step_set.entries.len) {
734 const generations = step_set.values();
735 if (generations[dirent_i] == w.generation) {
736 dirent_i += 1;
737 continue;
738 }
739 step_set.swapRemoveAt(dirent_i);
740 }
741 if (step_set.entries.len > 0) {
742 step_set_i += 1;
743 continue;
744 }
745 reaction_set.swapRemoveAt(step_set_i);
746 }
747 if (reaction_set.entries.len > 0) {
748 i += 1;
749 continue;
750 }
751 }
752
753 // If the sub_path == "" then this patch has already the
754 // dir fd that we need to use as the ident to remove the
755 // event. If it was opened above with openat() then we need
756 // to access that data via the dir_fd field.
757 const path = w.dir_table.keys()[i];
758 const dir_fd = if (path.sub_path.len == 0)
759 path.root_dir.handle.handle
760 else
761 handles.items(.dir_fd)[i];
762 assert(dir_fd != -1);
763
764 // The changelist also needs to update the udata field of the last
765 // event, since we are doing a swap remove, and we store the dir_table
766 // index in the udata field.
767 const last_dir_fd = fd: {
768 const last_path = w.dir_table.keys()[handles.len - 1];
769 const last_dir_fd = if (last_path.sub_path.len == 0)
770 last_path.root_dir.handle.handle
771 else
772 handles.items(.dir_fd)[handles.len - 1];
773 assert(last_dir_fd != -1);
774 break :fd last_dir_fd;
775 };
776 const changes = [_]posix.Kevent{
777 .{
778 .ident = @bitCast(@as(isize, dir_fd)),
779 .filter = std.c.EVFILT.VNODE,
780 .flags = EV.DELETE,
781 .fflags = 0,
782 .data = 0,
783 .udata = i,
784 },
785 .{
786 .ident = @bitCast(@as(isize, last_dir_fd)),
787 .filter = std.c.EVFILT.VNODE,
788 .flags = EV.ADD,
789 .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE,
790 .data = 0,
791 .udata = i,
792 },
793 };
794 const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes;
795 _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null);
796 if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd);
797
798 w.dir_table.swapRemoveAt(i);
799 handles.swapRemove(i);
800 }
801 w.generation +%= 1;
802 }
803 w.dir_count = w.dir_table.count();
804 }
805
806 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
807 _ = io;
808 var timespec_buffer: posix.timespec = undefined;
809 var event_buffer: [100]posix.Kevent = undefined;
810 var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer));
811 if (n == 0) return .timeout;
812 const reaction_sets = w.os.handles.items(.rs);
813 var any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], false);
814 timespec_buffer = .{ .sec = 0, .nsec = 0 };
815 while (n == event_buffer.len) {
816 n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer);
817 if (n == 0) break;
818 any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], any_dirty);
819 }
820 return if (any_dirty) .dirty else .clean;
821 }
822
823 fn markDirtySteps(
824 gpa: Allocator,
825 reaction_sets: []ReactionSet,
826 events: []const std.c.Kevent,
827 start_any_dirty: bool,
828 ) bool {
829 var any_dirty = start_any_dirty;
830 for (events) |event| {
831 const index: usize = @intCast(event.udata);
832 const reaction_set = &reaction_sets[index];
833 // If we knew the basename of the changed file, here we would
834 // mark only the step set dirty, and possibly the glob set:
835 //if (reaction_set.getPtr(".")) |glob_set|
836 // any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
837 //if (reaction_set.getPtr(file_name)) |step_set|
838 // any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
839 // However we don't know the file name so just mark all the
840 // sets dirty for this directory.
841 for (reaction_set.values()) |*step_set| {
842 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
843 }
844 }
845 return any_dirty;
846 }
847 },
848 .macos => struct {
849 fse: FsEvents,
850
851 fn init(cwd_path: []const u8) !Watch {
852 return .{
853 .os = .{ .fse = try .init(cwd_path) },
854 .dir_count = 0,
855 .dir_table = undefined,
856 .generation = undefined,
857 };
858 }
859 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
860 try w.os.fse.setPaths(gpa, steps);
861 w.dir_count = w.os.fse.watch_roots.len;
862 }
863 fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
864 _ = io;
865 return w.os.fse.wait(gpa, switch (timeout) {
866 .none => null,
867 .ms => |ms| @as(u64, ms) * std.time.ns_per_ms,
868 });
869 }
870 },
871 else => void,
872};
873
874pub fn init(cwd_path: []const u8) !Watch {
875 return Os.init(cwd_path);
876}
877
878pub const Match = struct {
879 /// Relative to the watched directory, the file path that triggers this
880 /// match.
881 basename: []const u8,
882 /// The step to re-run when file corresponding to `basename` is changed.
883 step: *Step,
884
885 pub const Context = struct {
886 pub fn hash(self: Context, a: Match) u32 {
887 _ = self;
888 var hasher = Hash.init(0);
889 std.hash.autoHash(&hasher, a.step);
890 hasher.update(a.basename);
891 return @truncate(hasher.final());
892 }
893 pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool {
894 _ = self;
895 _ = b_index;
896 return a.step == b.step and std.mem.eql(u8, a.basename, b.basename);
897 }
898 };
899};
900
901fn markAllFilesDirty(w: *Watch, gpa: Allocator) void {
902 for (switch (builtin.os.tag) {
903 .windows => w.os.handle_table.keys(),
904 else => w.os.handle_table.values(),
905 }) |item| {
906 const reaction_set = switch (builtin.os.tag) {
907 .linux, .windows => item.reaction_set,
908 else => item,
909 };
910 for (reaction_set.values()) |step_set| {
911 for (step_set.keys()) |step| {
912 _ = step.invalidateResult(gpa);
913 }
914 }
915 }
916}
917
918fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
919 var this_any_dirty = false;
920 for (step_set.keys()) |step| {
921 if (step.invalidateResult(gpa)) this_any_dirty = true;
922 }
923 return any_dirty or this_any_dirty;
924}
925
926pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
927 return Os.update(w, gpa, steps);
928}
929
930pub const Timeout = union(enum) {
931 none,
932 ms: u16,
933
934 pub fn to_i32_ms(t: Timeout) i32 {
935 return switch (t) {
936 .none => -1,
937 .ms => |ms| ms,
938 };
939 }
940
941 pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec {
942 return switch (t) {
943 .none => null,
944 .ms => |ms_u16| {
945 const ms: isize = ms_u16;
946 buf.* = .{
947 .sec = @divTrunc(ms, std.time.ms_per_s),
948 .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms,
949 };
950 return buf;
951 },
952 };
953 }
954};
955
956pub const WaitResult = enum {
957 timeout,
958 /// File system watching triggered on files that were marked as inputs to at least one Step.
959 /// Relevant steps have been marked dirty.
960 dirty,
961 /// File system watching triggered but none of the events were relevant to
962 /// what we are listening to. There is nothing to do.
963 clean,
964};
965
966pub fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
967 return Os.wait(w, gpa, io, timeout);
968}
lib/std/Build/Watch/FsEvents.zig deleted-479
......@@ -1,479 +0,0 @@
1//! An implementation of file-system watching based on the `FSEventStream` API in macOS.
2//! While macOS supports kqueue, it does not allow detecting changes to files without
3//! placing watches on each individual file, meaning FD limits are reached incredibly
4//! quickly. The File System Events API works differently: it implements *recursive*
5//! directory watches, managed by a system service. Rather than being in libc, the API is
6//! exposed by the CoreServices framework. To avoid a compile dependency on the framework
7//! bundle, we dynamically load CoreServices with `std.DynLib`.
8//!
9//! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been
10//! made to keep that specialization to a minimum. Other use cases could be served with
11//! relatively minimal modifications to the `watch_paths` field and its usages (in
12//! particular the `setPaths` function). We avoid using the global GCD dispatch queue in
13//! favour of creating our own and synchronizing with an explicit semaphore, meaning this
14//! logic is thread-safe and does not affect process-global state.
15//!
16//! In theory, this API is quite good at avoiding filesystem race conditions. In practice,
17//! the logic that would avoid them is currently disabled, because the build system kind
18//! of relies on them at the time of writing to avoid redundant work -- see the comment at
19//! the top of `wait` for details.
20
21const enable_debug_logs = false;
22
23core_services: std.DynLib,
24resolved_symbols: ResolvedSymbols,
25
26paths_arena: std.heap.ArenaAllocator.State,
27/// The roots of the recursive watches. FSEvents has relatively small limits on the number
28/// of watched paths, so this slice must not be too long. The paths themselves are allocated
29/// into `paths_arena`, but this slice is allocated into the GPA.
30watch_roots: [][:0]const u8,
31/// All of the paths being watched. Value is the set of steps which depend on the file/directory.
32/// Keys and values are in `paths_arena`, but this map is allocated into the GPA.
33watch_paths: std.StringArrayHashMapUnmanaged([]const *std.Build.Step),
34
35/// The semaphore we use to block the thread calling `wait` until the callback determines a relevant
36/// event has occurred. This is retained across `wait` calls for simplicity and efficiency.
37waiting_semaphore: dispatch.semaphore_t,
38/// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the
39/// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained
40/// across `wait` calls for simplicity and efficiency.
41dispatch_queue: dispatch.queue_t,
42/// In theory, this field avoids race conditions. In practice, it is essentially unused at the time
43/// of writing. See the comment at the start of `wait` for details.
44since_event: FSEventStreamEventId,
45
46cwd_path: []const u8,
47
48/// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols
49/// is not present, `init` will close the framework and return an error.
50const ResolvedSymbols = struct {
51 FSEventStreamCreate: *const fn (
52 allocator: CFAllocatorRef,
53 callback: FSEventStreamCallback,
54 ctx: ?*const FSEventStreamContext,
55 paths_to_watch: CFArrayRef,
56 since_when: FSEventStreamEventId,
57 latency: CFTimeInterval,
58 flags: FSEventStreamCreateFlags,
59 ) callconv(.c) FSEventStreamRef,
60 FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void,
61 FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool,
62 FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void,
63 FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void,
64 FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void,
65 FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId,
66 FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId,
67 CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void,
68 CFArrayCreate: *const fn (
69 allocator: CFAllocatorRef,
70 values: [*]const usize,
71 num_values: CFIndex,
72 call_backs: ?*const CFArrayCallBacks,
73 ) callconv(.c) CFArrayRef,
74 CFStringCreateWithCString: *const fn (
75 alloc: CFAllocatorRef,
76 c_str: [*:0]const u8,
77 encoding: CFStringEncoding,
78 ) callconv(.c) CFStringRef,
79 CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef,
80 kCFAllocatorUseContext: *const CFAllocatorRef,
81};
82
83pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents {
84 var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch
85 return error.OpenFrameworkFailed;
86 errdefer core_services.close();
87
88 var resolved_symbols: ResolvedSymbols = undefined;
89 inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| {
90 @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol;
91 }
92
93 return .{
94 .core_services = core_services,
95 .resolved_symbols = resolved_symbols,
96 .paths_arena = .{},
97 .watch_roots = &.{},
98 .watch_paths = .empty,
99 .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources,
100 .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources,
101 // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order
102 // to notice any changes which happened during said work.
103 .since_event = resolved_symbols.FSEventsGetCurrentEventId(),
104 .cwd_path = cwd_path,
105 };
106}
107
108pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void {
109 fse.waiting_semaphore.as_object().release();
110 fse.dispatch_queue.as_object().release();
111 fse.core_services.close(io);
112
113 gpa.free(fse.watch_roots);
114 fse.watch_paths.deinit(gpa);
115 {
116 var paths_arena = fse.paths_arena.promote(gpa);
117 paths_arena.deinit();
118 }
119}
120
121pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) !void {
122 var paths_arena_instance = fse.paths_arena.promote(gpa);
123 defer fse.paths_arena = paths_arena_instance.state;
124 const paths_arena = paths_arena_instance.allocator();
125
126 var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty;
127 defer need_dirs.deinit(gpa);
128
129 fse.watch_paths.clearRetainingCapacity();
130
131 // We take `step` by pointer for a slight memory optimization in a moment.
132 for (steps) |*step| {
133 for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| {
134 const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{
135 fse.cwd_path, path.root_dir.path orelse ".", path.sub_path,
136 });
137 try need_dirs.put(gpa, resolved_dir, {});
138 for (files.items) |file_name| {
139 const watch_path = if (std.mem.eql(u8, file_name, "."))
140 resolved_dir
141 else
142 try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name });
143 const gop = try fse.watch_paths.getOrPut(gpa, watch_path);
144 if (gop.found_existing) {
145 const old_steps = gop.value_ptr.*;
146 const new_steps = try paths_arena.alloc(*std.Build.Step, old_steps.len + 1);
147 @memcpy(new_steps[0..old_steps.len], old_steps);
148 new_steps[old_steps.len] = step.*;
149 gop.value_ptr.* = new_steps;
150 } else {
151 // This is why we captured `step` by pointer! We can avoid allocating a slice of one
152 // step in the arena in the common case where a file is referenced by only one step.
153 gop.value_ptr.* = step[0..1];
154 }
155 }
156 }
157 }
158
159 {
160 // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar").
161 // To eliminate these, we'll re-add directories in order of path length with a redundancy check.
162 const old_dirs = try gpa.dupe([]const u8, need_dirs.keys());
163 defer gpa.free(old_dirs);
164 std.mem.sort([]const u8, old_dirs, {}, struct {
165 fn lessThan(ctx: void, a: []const u8, b: []const u8) bool {
166 ctx;
167 return std.mem.lessThan(u8, a, b);
168 }
169 }.lessThan);
170 need_dirs.clearRetainingCapacity();
171 for (old_dirs) |dir_path| {
172 var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path);
173 while (it.next()) |component| {
174 if (need_dirs.contains(component.path)) {
175 // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added
176 break;
177 }
178 } else {
179 need_dirs.putAssumeCapacityNoClobber(dir_path, {});
180 }
181 }
182 }
183
184 // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very
185 // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/`
186 // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit
187 // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be
188 // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below*
189 // that known limit.
190 if (need_dirs.count() > 2048) {
191 // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P
192 if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{});
193 fse.watch_roots = try gpa.realloc(fse.watch_roots, 1);
194 fse.watch_roots[0] = "/";
195 } else {
196 fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count());
197 for (fse.watch_roots, need_dirs.keys()) |*out, in| {
198 out.* = try paths_arena.dupeSentinel(u8, in, 0);
199 }
200 }
201 if (enable_debug_logs) {
202 watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len });
203 for (fse.watch_roots) |dir_path| {
204 watch_log.debug("- '{s}'", .{dir_path});
205 }
206 }
207}
208
209pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!std.Build.Watch.WaitResult {
210 if (fse.watch_roots.len == 0) @panic("nothing to watch");
211
212 const rs = fse.resolved_symbols;
213
214 // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds
215 // to occur, because one step modifies a file which is an input to another step. The solution
216 // to this problem will probably be either:
217 //
218 // a) Don't include the output of one step as a watch input of another; only mark external
219 // files as watch inputs. Or...
220 //
221 // b) Note the current event ID when a step begins, and disregard events preceding that ID
222 // when considering whether to dirty that step in `eventCallback`.
223 //
224 // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does
225 // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those
226 // too at the time of writing, so this is kind of expected.
227 fse.since_event = .since_now;
228
229 const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{
230 .version = 0,
231 .info = @constCast(&gpa),
232 .retain = null,
233 .release = null,
234 .copy_description = null,
235 .allocate = &cf_alloc_callbacks.allocate,
236 .reallocate = &cf_alloc_callbacks.reallocate,
237 .deallocate = &cf_alloc_callbacks.deallocate,
238 .preferred_size = null,
239 }) orelse return error.OutOfMemory;
240 defer rs.CFRelease(cf_allocator);
241
242 const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len);
243 @memset(cf_paths, null);
244 defer {
245 for (cf_paths) |o| if (o) |p| rs.CFRelease(p);
246 gpa.free(cf_paths);
247 }
248 for (fse.watch_roots, cf_paths) |raw_path, *cf_path| {
249 cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8);
250 }
251 const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null);
252 defer rs.CFRelease(cf_paths_array);
253
254 const callback_ctx: EventCallbackCtx = .{
255 .fse = fse,
256 .gpa = gpa,
257 };
258 const event_stream = rs.FSEventStreamCreate(
259 null,
260 &eventCallback,
261 &.{
262 .version = 0,
263 .info = @constCast(&callback_ctx),
264 .retain = null,
265 .release = null,
266 .copy_description = null,
267 },
268 cf_paths_array,
269 fse.since_event,
270 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events
271 .{ .watch_root = true, .file_events = true },
272 );
273 defer rs.FSEventStreamRelease(event_stream);
274 rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue);
275 defer rs.FSEventStreamInvalidate(event_stream);
276 if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed;
277 defer rs.FSEventStreamStop(event_stream);
278 const result = fse.waiting_semaphore.wait(timeout: {
279 const ns = timeout_ns orelse break :timeout .FOREVER;
280 break :timeout .time(.NOW, @intCast(ns));
281 });
282 return switch (result) {
283 0 => .dirty,
284 else => .timeout,
285 };
286}
287
288const cf_alloc_callbacks = struct {
289 const log = std.log.scoped(.cf_alloc);
290 fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
291 if (enable_debug_logs) log.debug("allocate {d}", .{size});
292 _ = hint;
293 const gpa: *const Allocator = @ptrCast(@alignCast(info));
294 const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null;
295 const metadata: *usize = @ptrCast(mem);
296 metadata.* = @intCast(size);
297 return mem[@sizeOf(usize)..].ptr;
298 }
299 fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque {
300 if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size });
301 _ = hint;
302 if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL
303 const gpa: *const Allocator = @ptrCast(@alignCast(info));
304 const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
305 const old_size = @as(*const usize, @ptrCast(old_base)).*;
306 const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
307 const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null;
308 const metadata: *usize = @ptrCast(new_mem);
309 metadata.* = @intCast(new_size);
310 return new_mem[@sizeOf(usize)..].ptr;
311 }
312 fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void {
313 if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr});
314 const gpa: *const Allocator = @ptrCast(@alignCast(info));
315 const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize));
316 const old_size = @as(*const usize, @ptrCast(old_base)).*;
317 const old_mem = old_base[0 .. old_size + @sizeOf(usize)];
318 gpa.free(old_mem);
319 }
320};
321
322const EventCallbackCtx = struct {
323 fse: *FsEvents,
324 gpa: Allocator,
325};
326
327fn eventCallback(
328 stream: ConstFSEventStreamRef,
329 client_callback_info: ?*anyopaque,
330 num_events: usize,
331 events_paths_ptr: *anyopaque,
332 events_flags_ptr: [*]const FSEventStreamEventFlags,
333 events_ids_ptr: [*]const FSEventStreamEventId,
334) callconv(.c) void {
335 const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info));
336 const fse = ctx.fse;
337 const gpa = ctx.gpa;
338 const rs = fse.resolved_symbols;
339 const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr));
340 const events_paths = events_paths_ptr_casted[0..num_events];
341 const events_ids = events_ids_ptr[0..num_events];
342 const events_flags = events_flags_ptr[0..num_events];
343 var any_dirty = false;
344 for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| {
345 _ = event_id;
346 if (event_flags.history_done) continue; // sentinel
347 const event_path = std.mem.span(event_path_nts);
348 switch (event_flags.must_scan_sub_dirs) {
349 false => {
350 if (fse.watch_paths.get(event_path)) |steps| {
351 assert(steps.len > 0);
352 for (steps) |s| {
353 if (s.invalidateResult(gpa)) any_dirty = true;
354 }
355 }
356 if (std.fs.path.dirname(event_path)) |event_dirname| {
357 // Modifying '/foo/bar' triggers the watch on '/foo'.
358 if (fse.watch_paths.get(event_dirname)) |steps| {
359 assert(steps.len > 0);
360 for (steps) |s| {
361 if (s.invalidateResult(gpa)) any_dirty = true;
362 }
363 }
364 }
365 },
366 true => {
367 // This is unlikely, but can occasionally happen when bottlenecked: events have been
368 // coalesced into one. We want to see if any of these events are actually relevant
369 // to us. The only way we can reasonably do that in this rare edge case is iterate
370 // the watch paths and see if any is under this directory. That's acceptable because
371 // we would otherwise kick off a rebuild which would be clearing those paths anyway.
372 const changed_path = std.fs.path.dirname(event_path) orelse event_path;
373 for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| {
374 if (dirStartsWith(watching_path, changed_path)) {
375 for (steps) |s| {
376 if (s.invalidateResult(gpa)) any_dirty = true;
377 }
378 }
379 }
380 },
381 }
382 }
383 if (any_dirty) {
384 fse.since_event = rs.FSEventStreamGetLatestEventId(stream);
385 _ = fse.waiting_semaphore.signal();
386 }
387}
388fn dirStartsWith(path: []const u8, prefix: []const u8) bool {
389 if (std.mem.eql(u8, path, prefix)) return true;
390 if (!std.mem.startsWith(u8, path, prefix)) return false;
391 if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar`
392 return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar`
393}
394
395const CFAllocatorRef = ?*const opaque {};
396const CFArrayRef = *const opaque {};
397const CFStringRef = *const opaque {};
398const CFTimeInterval = f64;
399const CFIndex = i32;
400const CFOptionFlags = enum(u32) { _ };
401const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque;
402const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void;
403const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef;
404const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
405const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque;
406const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void;
407const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex;
408const CFAllocatorContext = extern struct {
409 version: CFIndex,
410 info: ?*anyopaque,
411 retain: ?CFAllocatorRetainCallBack,
412 release: ?CFAllocatorReleaseCallBack,
413 copy_description: ?CFAllocatorCopyDescriptionCallBack,
414 allocate: CFAllocatorAllocateCallBack,
415 reallocate: ?CFAllocatorReallocateCallBack,
416 deallocate: ?CFAllocatorDeallocateCallBack,
417 preferred_size: ?CFAllocatorPreferredSizeCallBack,
418};
419const CFArrayCallBacks = opaque {};
420const CFStringEncoding = enum(u32) {
421 invalid_id = std.math.maxInt(u32),
422 mac_roman = 0,
423 windows_latin_1 = 0x500,
424 iso_latin_1 = 0x201,
425 next_step_latin = 0xB01,
426 ascii = 0x600,
427 unicode = 0x100,
428 utf8 = 0x8000100,
429 non_lossy_ascii = 0xBFF,
430};
431
432const FSEventStreamRef = *opaque {};
433const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child;
434const FSEventStreamCallback = *const fn (
435 stream: ConstFSEventStreamRef,
436 client_callback_info: ?*anyopaque,
437 num_events: usize,
438 event_paths: *anyopaque,
439 event_flags: [*]const FSEventStreamEventFlags,
440 event_ids: [*]const FSEventStreamEventId,
441) callconv(.c) void;
442const FSEventStreamContext = extern struct {
443 version: CFIndex,
444 info: ?*anyopaque,
445 retain: ?CFAllocatorRetainCallBack,
446 release: ?CFAllocatorReleaseCallBack,
447 copy_description: ?CFAllocatorCopyDescriptionCallBack,
448};
449const FSEventStreamEventId = enum(u64) {
450 since_now = std.math.maxInt(u64),
451 _,
452};
453const FSEventStreamCreateFlags = packed struct(u32) {
454 use_cf_types: bool = false,
455 no_defer: bool = false,
456 watch_root: bool = false,
457 ignore_self: bool = false,
458 file_events: bool = false,
459 _: u27 = 0,
460};
461const FSEventStreamEventFlags = packed struct(u32) {
462 must_scan_sub_dirs: bool,
463 user_dropped: bool,
464 kernel_dropped: bool,
465 event_ids_wrapped: bool,
466 history_done: bool,
467 root_changed: bool,
468 mount: bool,
469 unmount: bool,
470 _: u24 = 0,
471};
472
473const dispatch = std.c.dispatch;
474const std = @import("std");
475const Io = std.Io;
476const assert = std.debug.assert;
477const Allocator = std.mem.Allocator;
478const watch_log = std.log.scoped(.watch);
479const FsEvents = @This();
lib/std/Build/WebServer.zig deleted-926
......@@ -1,926 +0,0 @@
1gpa: Allocator,
2graph: *const Build.Graph,
3all_steps: []const *Build.Step,
4listen_address: net.IpAddress,
5root_prog_node: std.Progress.Node,
6watch: bool,
7
8tcp_server: ?net.Server,
9serve_task: ?Io.Future(Io.Cancelable!void),
10
11/// Uses `Io.Clock.awake`.
12base_timestamp: Io.Timestamp,
13/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
14step_names_trailing: []u8,
15
16/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
17/// Accessed atomically.
18step_status_bits: []u8,
19
20fuzz: ?Fuzz,
21time_report_mutex: Io.Mutex,
22time_report_msgs: [][]u8,
23time_report_update_times: []i64,
24
25build_status: std.atomic.Value(abi.BuildStatus),
26/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
27/// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so
28/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it
29/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For
30/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes,
31/// because this value changes quickly so this would result in constantly spamming all clients with
32/// an unreasonable number of packets.
33update_id: std.atomic.Value(u32),
34
35runner_request_mutex: Io.Mutex,
36runner_request_ready_cond: Io.Condition,
37runner_request_empty_cond: Io.Condition,
38runner_request: ?RunnerRequest,
39
40/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
41/// on a fixed interval of this many milliseconds.
42const default_update_interval_ms = 500;
43
44pub const base_clock: Io.Clock = .awake;
45
46/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
47pub fn notifyUpdate(ws: *WebServer) void {
48 _ = ws.update_id.rmw(.Add, 1, .release);
49 ws.graph.io.futexWake(u32, &ws.update_id.raw, 16);
50}
51
52pub const Options = struct {
53 gpa: Allocator,
54 graph: *const std.Build.Graph,
55 all_steps: []const *Build.Step,
56 root_prog_node: std.Progress.Node,
57 watch: bool,
58 listen_address: net.IpAddress,
59 base_timestamp: Io.Clock.Timestamp,
60};
61pub fn init(opts: Options) WebServer {
62 // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent`
63 // instead of threads, so that the web server can function in single-threaded builds.
64 comptime assert(!builtin.single_threaded);
65 assert(opts.base_timestamp.clock == base_clock);
66
67 const all_steps = opts.all_steps;
68
69 const step_names_trailing = opts.gpa.alloc(u8, len: {
70 var name_bytes: usize = 0;
71 for (all_steps) |step| name_bytes += step.name.len;
72 break :len name_bytes + all_steps.len * 4;
73 }) catch @panic("out of memory");
74 {
75 const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
76 var idx: usize = all_steps.len * 4;
77 for (all_steps, step_name_lens) |step, *name_len| {
78 name_len.* = @intCast(step.name.len);
79 @memcpy(step_names_trailing[idx..][0..step.name.len], step.name);
80 idx += step.name.len;
81 }
82 assert(idx == step_names_trailing.len);
83 }
84
85 const step_status_bits = opts.gpa.alloc(
86 u8,
87 std.math.divCeil(usize, all_steps.len, 4) catch unreachable,
88 ) catch @panic("out of memory");
89 @memset(step_status_bits, 0);
90
91 const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0;
92 const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory");
93 const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory");
94 @memset(time_report_msgs, &.{});
95 @memset(time_report_update_times, std.math.minInt(i64));
96
97 return .{
98 .gpa = opts.gpa,
99 .graph = opts.graph,
100 .all_steps = all_steps,
101 .listen_address = opts.listen_address,
102 .root_prog_node = opts.root_prog_node,
103 .watch = opts.watch,
104
105 .tcp_server = null,
106 .serve_task = null,
107
108 .base_timestamp = opts.base_timestamp.raw,
109 .step_names_trailing = step_names_trailing,
110
111 .step_status_bits = step_status_bits,
112
113 .fuzz = null,
114 .time_report_mutex = .init,
115 .time_report_msgs = time_report_msgs,
116 .time_report_update_times = time_report_update_times,
117
118 .build_status = .init(.idle),
119 .update_id = .init(0),
120
121 .runner_request_mutex = .init,
122 .runner_request_ready_cond = .init,
123 .runner_request_empty_cond = .init,
124 .runner_request = null,
125 };
126}
127pub fn deinit(ws: *WebServer) void {
128 const gpa = ws.gpa;
129 const io = ws.graph.io;
130
131 gpa.free(ws.step_names_trailing);
132 gpa.free(ws.step_status_bits);
133
134 if (ws.fuzz) |*f| f.deinit();
135 for (ws.time_report_msgs) |msg| gpa.free(msg);
136 gpa.free(ws.time_report_msgs);
137 gpa.free(ws.time_report_update_times);
138
139 if (ws.serve_task) |t| {
140 if (ws.tcp_server) |*s| s.stream.close(io);
141 t.await();
142 }
143 if (ws.tcp_server) |*s| s.deinit();
144
145 gpa.free(ws.step_names_trailing);
146}
147pub fn start(ws: *WebServer) error{AlreadyReported}!void {
148 assert(ws.tcp_server == null);
149 assert(ws.serve_task == null);
150 const io = ws.graph.io;
151
152 ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
153 log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err });
154 return error.AlreadyReported;
155 };
156 ws.serve_task = io.concurrent(serve, .{ws}) catch |err| {
157 log.err("unable to spawn web server thread: {t}", .{err});
158 ws.tcp_server.?.deinit(io);
159 ws.tcp_server = null;
160 return error.AlreadyReported;
161 };
162
163 log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address});
164 if (ws.listen_address.getPort() == 0) {
165 log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address});
166 }
167}
168fn serve(ws: *WebServer) Io.Cancelable!void {
169 const io = ws.graph.io;
170 var group: Io.Group = .init;
171 defer group.cancel(io);
172 while (true) {
173 var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) {
174 error.Canceled => |e| return e,
175 else => |e| {
176 log.err("failed to accept connection: {t}", .{e});
177 return;
178 },
179 };
180 group.concurrent(io, accept, .{ ws, stream }) catch |err| {
181 log.err("unable to spawn connection thread: {t}", .{err});
182 stream.close(io);
183 continue;
184 };
185 }
186}
187
188pub fn startBuild(ws: *WebServer) void {
189 if (ws.fuzz) |*fuzz| {
190 fuzz.deinit();
191 ws.fuzz = null;
192 }
193 for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
194 ws.build_status.store(.running, .monotonic);
195 ws.notifyUpdate();
196}
197
198pub fn updateStepStatus(ws: *WebServer, step: *Build.Step, new_status: abi.StepUpdate.Status) void {
199 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
200 if (s == step) break @intCast(i);
201 } else unreachable;
202 const ptr = &ws.step_status_bits[step_idx / 4];
203 const bit_offset: u3 = @intCast((step_idx % 4) * 2);
204 const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
205 const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset;
206 _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic);
207 ws.notifyUpdate();
208}
209
210pub fn finishBuild(ws: *WebServer, opts: struct {
211 fuzz: bool,
212}) void {
213 if (opts.fuzz) {
214 switch (builtin.os.tag) {
215 // Current implementation depends on two things that need to be ported to Windows:
216 // * Memory-mapping to share data between the fuzzer and build runner.
217 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
218 // many addresses to source locations).
219 .windows => std.process.fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
220 else => {},
221 }
222 if (@bitSizeOf(usize) != 64) {
223 // Current implementation depends on posix.mmap()'s second
224 // parameter, `length: usize`, being compatible with file system's
225 // u64 return value. This is not the case on 32-bit platforms.
226 // Affects or affected by issues #5185, #22523, and #22464.
227 std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
228 }
229
230 assert(ws.fuzz == null);
231
232 ws.build_status.store(.fuzz_init, .monotonic);
233 ws.notifyUpdate();
234
235 ws.fuzz = Fuzz.init(
236 ws.gpa,
237 ws.graph.io,
238 ws.all_steps,
239 ws.root_prog_node,
240 .{ .forever = .{ .ws = ws } },
241 ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
242 ws.fuzz.?.start();
243 }
244
245 ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic);
246 ws.notifyUpdate();
247}
248
249pub fn now(s: *const WebServer) i64 {
250 const io = s.graph.io;
251 const ts = base_clock.now(io);
252 return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds());
253}
254
255fn accept(ws: *WebServer, stream: net.Stream) void {
256 const io = ws.graph.io;
257 defer {
258 // `net.Stream.close` wants to helpfully overwrite `stream` with
259 // `undefined`, but it cannot do so since it is an immutable parameter.
260 var copy = stream;
261 copy.close(io);
262 }
263 var send_buffer: [4096]u8 = undefined;
264 var recv_buffer: [4096]u8 = undefined;
265 var connection_reader = stream.reader(io, &recv_buffer);
266 var connection_writer = stream.writer(io, &send_buffer);
267 var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface);
268
269 while (true) {
270 var request = server.receiveHead() catch |err| switch (err) {
271 error.HttpConnectionClosing => return,
272 else => return log.err("failed to receive http request: {t}", .{err}),
273 };
274 switch (request.upgradeRequested()) {
275 .websocket => |opt_key| {
276 const key = opt_key orelse return log.err("missing websocket key", .{});
277 var web_socket = request.respondWebSocket(.{ .key = key }) catch {
278 return log.err("failed to respond web socket: {t}", .{connection_writer.err.?});
279 };
280 ws.serveWebSocket(&web_socket) catch |err| {
281 log.err("failed to serve websocket: {t}", .{err});
282 return;
283 };
284 comptime unreachable;
285 },
286 .other => |name| return log.err("unknown upgrade request: {s}", .{name}),
287 .none => {
288 ws.serveRequest(&request) catch |err| switch (err) {
289 error.AlreadyReported => return,
290 else => {
291 log.err("failed to serve '{s}': {t}", .{ request.head.target, err });
292 return;
293 },
294 };
295 },
296 }
297 }
298}
299
300fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
301 const io = ws.graph.io;
302
303 var prev_build_status = ws.build_status.load(.monotonic);
304
305 const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len);
306 defer ws.gpa.free(prev_step_status_bits);
307 for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| {
308 copy.* = @atomicLoad(u8, shared, .monotonic);
309 }
310
311 var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock });
312 defer recv_thread.cancel(io);
313
314 {
315 const hello_header: abi.Hello = .{
316 .status = prev_build_status,
317 .flags = .{
318 .time_report = ws.graph.time_report,
319 },
320 .timestamp = ws.now(),
321 .steps_len = @intCast(ws.all_steps.len),
322 };
323 var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits };
324 try sock.writeMessageVec(&bufs, .binary);
325 }
326
327 var prev_fuzz: Fuzz.Previous = .init;
328 var prev_time: i64 = std.math.minInt(i64);
329 while (true) {
330 const start_time = ws.now();
331 const start_update_id = ws.update_id.load(.acquire);
332
333 if (ws.fuzz) |*fuzz| {
334 try fuzz.sendUpdate(sock, &prev_fuzz);
335 }
336
337 {
338 try ws.time_report_mutex.lock(io);
339 defer ws.time_report_mutex.unlock(io);
340 for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| {
341 if (update_time <= prev_time) continue;
342 // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so
343 // that we don't hold up the build system on the client accepting this packet.
344 const owned_msg = try ws.gpa.dupe(u8, msg);
345 defer ws.gpa.free(owned_msg);
346 // Temporarily unlock, then re-lock after the message is sent.
347 ws.time_report_mutex.unlock(io);
348 defer ws.time_report_mutex.lockUncancelable(io);
349 try sock.writeMessage(owned_msg, .binary);
350 }
351 }
352
353 {
354 const build_status = ws.build_status.load(.monotonic);
355 if (build_status != prev_build_status) {
356 prev_build_status = build_status;
357 const msg: abi.StatusUpdate = .{ .new = build_status };
358 try sock.writeMessage(@ptrCast(&msg), .binary);
359 }
360 }
361
362 for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
363 const cur_byte = @atomicLoad(u8, shared, .monotonic);
364 if (prev_byte.* == cur_byte) continue;
365 const cur: [4]abi.StepUpdate.Status = .{
366 @enumFromInt(@as(u2, @truncate(cur_byte >> 0))),
367 @enumFromInt(@as(u2, @truncate(cur_byte >> 2))),
368 @enumFromInt(@as(u2, @truncate(cur_byte >> 4))),
369 @enumFromInt(@as(u2, @truncate(cur_byte >> 6))),
370 };
371 const prev: [4]abi.StepUpdate.Status = .{
372 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))),
373 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))),
374 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))),
375 @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))),
376 };
377 for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| {
378 const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } };
379 if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary);
380 }
381 prev_byte.* = cur_byte;
382 }
383
384 prev_time = start_time;
385
386 const old_cp = io.swapCancelProtection(.blocked);
387 defer _ = io.swapCancelProtection(old_cp);
388 io.futexWaitTimeout(
389 u32,
390 &ws.update_id.raw,
391 start_update_id,
392 .{ .duration = .{
393 .clock = .awake,
394 .raw = .fromMilliseconds(default_update_interval_ms),
395 } },
396 ) catch |err| switch (err) {
397 error.Canceled => unreachable,
398 };
399 }
400}
401fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
402 const io = ws.graph.io;
403
404 while (true) {
405 const msg = sock.readSmallMessage() catch return;
406 if (msg.opcode != .binary) continue;
407 if (msg.data.len == 0) continue;
408 const tag: abi.ToServerTag = @enumFromInt(msg.data[0]);
409 switch (tag) {
410 _ => continue,
411 .rebuild => while (true) {
412 ws.runner_request_mutex.lock(io) catch |err| switch (err) {
413 error.Canceled => return,
414 };
415 defer ws.runner_request_mutex.unlock(io);
416 if (ws.runner_request == null) {
417 ws.runner_request = .rebuild;
418 ws.runner_request_ready_cond.signal(io);
419 break;
420 }
421 ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return;
422 },
423 }
424 }
425}
426
427fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void {
428 // Strip an optional leading '/debug' component from the request.
429 const target: []const u8, const debug: bool = target: {
430 if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true };
431 if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true };
432 if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true };
433 break :target .{ req.head.target, false };
434 };
435
436 if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html");
437 if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript");
438 if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css");
439 if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css");
440 if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast);
441
442 if (ws.fuzz) |*fuzz| {
443 if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req);
444 }
445
446 try req.respond("not found", .{
447 .status = .not_found,
448 .extra_headers = &.{
449 .{ .name = "Content-Type", .value = "text/plain" },
450 },
451 });
452}
453
454fn serveLibFile(
455 ws: *WebServer,
456 request: *http.Server.Request,
457 sub_path: []const u8,
458 content_type: []const u8,
459) !void {
460 return serveFile(ws, request, .{
461 .root_dir = ws.graph.zig_lib_directory,
462 .sub_path = sub_path,
463 }, content_type);
464}
465fn serveClientWasm(
466 ws: *WebServer,
467 req: *http.Server.Request,
468 optimize_mode: std.builtin.OptimizeMode,
469) !void {
470 var arena_state: std.heap.ArenaAllocator = .init(ws.gpa);
471 defer arena_state.deinit();
472 const arena = arena_state.allocator();
473
474 // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page.
475 const bin_path = try buildClientWasm(ws, arena, optimize_mode);
476 return serveFile(ws, req, bin_path, "application/wasm");
477}
478
479pub fn serveFile(
480 ws: *WebServer,
481 request: *http.Server.Request,
482 path: Cache.Path,
483 content_type: []const u8,
484) !void {
485 const gpa = ws.gpa;
486 const io = ws.graph.io;
487 // The desired API is actually sendfile, which will require enhancing http.Server.
488 // We load the file with every request so that the user can make changes to the file
489 // and refresh the HTML page without restarting this server.
490 const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| {
491 log.err("failed to read '{f}': {t}", .{ path, err });
492 return error.AlreadyReported;
493 };
494 defer gpa.free(file_contents);
495 try request.respond(file_contents, .{
496 .extra_headers = &.{
497 .{ .name = "Content-Type", .value = content_type },
498 cache_control_header,
499 },
500 });
501}
502pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
503 const graph = ws.graph;
504 const io = graph.io;
505
506 var send_buffer: [0x4000]u8 = undefined;
507 var response = try request.respondStreaming(&send_buffer, .{
508 .respond_options = .{
509 .extra_headers = &.{
510 .{ .name = "Content-Type", .value = "application/x-tar" },
511 cache_control_header,
512 },
513 },
514 });
515
516 var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
517
518 for (paths) |path| {
519 var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| {
520 log.err("failed to open '{f}': {s}", .{ path, @errorName(err) });
521 continue;
522 };
523 defer file.close(io);
524 const stat = try file.stat(io);
525 var read_buffer: [1024]u8 = undefined;
526 var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size);
527
528 // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can
529 // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI:
530 // it turns out the WASM treats the first path component as the module name, typically
531 // resulting in modules named "" and "src". The compiler needs to tell the build system
532 // about the module graph so that the build system can correctly encode this information in
533 // the tar file.
534 //
535 // Additionally, this needs to ensure that all path separators for both prefix and
536 // sub_path are using the POSIX-style `/` on platforms that don't use it as their native
537 // path separator.
538 archiver.prefix = path.root_dir.path orelse graph.cache.cwd;
539 try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds()));
540 }
541
542 // intentionally not calling `archiver.finishPedantically`
543 try response.end();
544}
545
546fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path {
547 const root_name = "build-web";
548 const arch_os_abi = "wasm32-freestanding";
549 const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
550
551 const gpa = ws.gpa;
552 const graph = ws.graph;
553 const io = graph.io;
554
555 const main_src_path: Cache.Path = .{
556 .root_dir = graph.zig_lib_directory,
557 .sub_path = "build-web/main.zig",
558 };
559 const walk_src_path: Cache.Path = .{
560 .root_dir = graph.zig_lib_directory,
561 .sub_path = "docs/wasm/Walk.zig",
562 };
563 const html_render_src_path: Cache.Path = .{
564 .root_dir = graph.zig_lib_directory,
565 .sub_path = "docs/wasm/html_render.zig",
566 };
567
568 var argv: std.ArrayList([]const u8) = .empty;
569
570 try argv.appendSlice(arena, &.{
571 graph.zig_exe, "build-exe", //
572 "-fno-entry", //
573 "-O", @tagName(optimize), //
574 "-target", arch_os_abi, //
575 "-mcpu", cpu_features, //
576 "--cache-dir", graph.global_cache_root.path orelse ".", //
577 "--global-cache-dir", graph.global_cache_root.path orelse ".", //
578 "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
579 "--name", root_name, //
580 "-rdynamic", //
581 "-fsingle-threaded", //
582 "--dep", "Walk", //
583 "--dep", "html_render", //
584 try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), //
585 try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), //
586 "--dep", "Walk", //
587 try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), //
588 "--listen=-",
589 });
590
591 var child = try std.process.spawn(io, .{
592 .argv = argv.items,
593 .environ_map = &graph.environ_map,
594 .stdin = .pipe,
595 .stdout = .pipe,
596 .stderr = .pipe,
597 });
598 defer child.kill(io);
599
600 var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited });
601 defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
602
603 var stdout_buffer: [512]u8 = undefined;
604 var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
605 const stdout = &stdout_reader.interface;
606
607 {
608 var w = child.stdin.?.writer(io, &.{});
609 w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
610 error.WriteFailed => return w.err.?,
611 };
612 w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
613 error.WriteFailed => return w.err.?,
614 };
615 }
616
617 const Header = std.zig.Server.Message.Header;
618
619 var result: ?Cache.Path = null;
620 var result_error_bundle = std.zig.ErrorBundle.empty;
621 var body_buffer: std.ArrayList(u8) = .empty;
622 defer body_buffer.deinit(gpa);
623
624 while (true) {
625 const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
626 error.ReadFailed => |e| return e,
627 error.EndOfStream => break,
628 };
629 body_buffer.clearRetainingCapacity();
630 try stdout.appendExact(gpa, &body_buffer, header.bytes_len);
631 const body = body_buffer.items;
632
633 switch (header.tag) {
634 .zig_version => {
635 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
636 return error.ZigProtocolVersionMismatch;
637 }
638 },
639 .error_bundle => {
640 result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
641 },
642 .emit_digest => {
643 const EmitDigest = std.zig.Server.Message.EmitDigest;
644 const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
645 if (!ebp_hdr.flags.cache_hit) {
646 log.info("source changes detected; rebuilt wasm component", .{});
647 }
648 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
649 result = .{
650 .root_dir = graph.global_cache_root,
651 .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
652 };
653 },
654 else => {}, // ignore other messages
655 }
656 }
657
658 const stderr_contents = try stderr_task.await(io);
659 if (stderr_contents.len > 0) {
660 std.debug.print("{s}", .{stderr_contents});
661 }
662
663 // Send EOF to stdin.
664 child.stdin.?.close(io);
665 child.stdin = null;
666
667 switch (try child.wait(io)) {
668 .exited => |code| {
669 if (code != 0) {
670 log.err(
671 "the following command exited with error code {d}:\n{s}",
672 .{ code, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
673 );
674 return error.WasmCompilationFailed;
675 }
676 },
677 .signal => |sig| {
678 log.err(
679 "the following command terminated with signal {t}:\n{s}",
680 .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
681 );
682 return error.WasmCompilationFailed;
683 },
684 .stopped => |sig| {
685 log.err(
686 "the following command stopped unexpectedly with signal {t}:\n{s}",
687 .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) },
688 );
689 return error.WasmCompilationFailed;
690 },
691 .unknown => {
692 log.err(
693 "the following command terminated unexpectedly:\n{s}",
694 .{try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items)},
695 );
696 return error.WasmCompilationFailed;
697 },
698 }
699
700 if (result_error_bundle.errorMessageCount() > 0) {
701 try result_error_bundle.renderToStderr(io, .{}, .auto);
702 log.err("the following command failed with {d} compilation errors:\n{s}", .{
703 result_error_bundle.errorMessageCount(),
704 try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
705 });
706 return error.WasmCompilationFailed;
707 }
708
709 const base_path = result orelse {
710 log.err("child process failed to report result\n{s}", .{
711 try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items),
712 });
713 return error.WasmCompilationFailed;
714 };
715 const bin_name = try std.zig.binNameAlloc(arena, .{
716 .root_name = root_name,
717 .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
718 .arch_os_abi = arch_os_abi,
719 .cpu_features = cpu_features,
720 }) catch unreachable) catch unreachable),
721 .output_mode = .Exe,
722 });
723 return base_path.join(arena, bin_name);
724}
725
726fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
727 var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
728 return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
729 error.ReadFailed => return file_reader.err.?,
730 else => |e| return e,
731 };
732}
733
734pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
735 compile: *Build.Step.Compile,
736
737 use_llvm: bool,
738 stats: abi.time_report.CompileResult.Stats,
739 ns_total: u64,
740
741 llvm_pass_timings_len: u32,
742 files_len: u32,
743 decls_len: u32,
744
745 /// The trailing data of `abi.time_report.CompileResult`, except the step name.
746 trailing: []const u8,
747}) void {
748 const gpa = ws.gpa;
749 const io = ws.graph.io;
750
751 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
752 if (s == &opts.compile.step) break @intCast(i);
753 } else unreachable;
754
755 const old_buf = old: {
756 ws.time_report_mutex.lock(io) catch return;
757 defer ws.time_report_mutex.unlock(io);
758 const old = ws.time_report_msgs[step_idx];
759 ws.time_report_msgs[step_idx] = &.{};
760 break :old old;
761 };
762 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
763
764 const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]);
765 out_header.* = .{
766 .step_idx = step_idx,
767 .flags = .{
768 .use_llvm = opts.use_llvm,
769 },
770 .stats = opts.stats,
771 .ns_total = opts.ns_total,
772 .llvm_pass_timings_len = opts.llvm_pass_timings_len,
773 .files_len = opts.files_len,
774 .decls_len = opts.decls_len,
775 };
776 @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
777
778 {
779 ws.time_report_mutex.lock(io) catch return;
780 defer ws.time_report_mutex.unlock(io);
781 assert(ws.time_report_msgs[step_idx].len == 0);
782 ws.time_report_msgs[step_idx] = buf;
783 ws.time_report_update_times[step_idx] = ws.now();
784 }
785 ws.notifyUpdate();
786}
787
788pub fn updateTimeReportGeneric(ws: *WebServer, step: *Build.Step, duration: Io.Duration) void {
789 const gpa = ws.gpa;
790 const io = ws.graph.io;
791
792 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
793 if (s == step) break @intCast(i);
794 } else unreachable;
795
796 const old_buf = old: {
797 ws.time_report_mutex.lock(io) catch return;
798 defer ws.time_report_mutex.unlock(io);
799 const old = ws.time_report_msgs[step_idx];
800 ws.time_report_msgs[step_idx] = &.{};
801 break :old old;
802 };
803 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
804 const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf);
805 out.* = .{
806 .step_idx = step_idx,
807 .ns_total = @intCast(duration.toNanoseconds()),
808 };
809 {
810 ws.time_report_mutex.lock(io) catch return;
811 defer ws.time_report_mutex.unlock(io);
812 assert(ws.time_report_msgs[step_idx].len == 0);
813 ws.time_report_msgs[step_idx] = buf;
814 ws.time_report_update_times[step_idx] = ws.now();
815 }
816 ws.notifyUpdate();
817}
818
819pub fn updateTimeReportRunTest(
820 ws: *WebServer,
821 run: *Build.Step.Run,
822 tests: *const Build.Step.Run.CachedTestMetadata,
823 ns_per_test: []const u64,
824) void {
825 const gpa = ws.gpa;
826 const io = ws.graph.io;
827
828 const step_idx: u32 = for (ws.all_steps, 0..) |s, i| {
829 if (s == &run.step) break @intCast(i);
830 } else unreachable;
831
832 assert(tests.names.len == ns_per_test.len);
833 const tests_len: u32 = @intCast(tests.names.len);
834
835 const new_len: u64 = len: {
836 var names_len: u64 = 0;
837 for (0..tests_len) |i| {
838 names_len += tests.testName(@intCast(i)).len + 1;
839 }
840 break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
841 };
842 const old_buf = old: {
843 ws.time_report_mutex.lock(io) catch return;
844 defer ws.time_report_mutex.unlock(io);
845 const old = ws.time_report_msgs[step_idx];
846 ws.time_report_msgs[step_idx] = &.{};
847 break :old old;
848 };
849 const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
850
851 const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]);
852 out_header.* = .{
853 .step_idx = step_idx,
854 .tests_len = tests_len,
855 };
856 var offset: usize = @sizeOf(abi.time_report.RunTestResult);
857 const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]);
858 @memcpy(ns_per_test_out, ns_per_test);
859 offset += tests_len * 8;
860 for (0..tests_len) |i| {
861 const name = tests.testName(@intCast(i));
862 @memcpy(buf[offset..][0..name.len], name);
863 buf[offset..][name.len] = 0;
864 offset += name.len + 1;
865 }
866 assert(offset == buf.len);
867
868 {
869 ws.time_report_mutex.lock(io) catch return;
870 defer ws.time_report_mutex.unlock(io);
871 assert(ws.time_report_msgs[step_idx].len == 0);
872 ws.time_report_msgs[step_idx] = buf;
873 ws.time_report_update_times[step_idx] = ws.now();
874 }
875 ws.notifyUpdate();
876}
877
878const RunnerRequest = union(enum) {
879 rebuild,
880};
881pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
882 const io = ws.graph.io;
883 ws.runner_request_mutex.lock(io) catch return;
884 defer ws.runner_request_mutex.unlock(io);
885 if (ws.runner_request) |req| {
886 ws.runner_request = null;
887 ws.runner_request_empty_cond.signal();
888 return req;
889 }
890 return null;
891}
892pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
893 const io = ws.graph.io;
894 try ws.runner_request_mutex.lock(io);
895 defer ws.runner_request_mutex.unlock(io);
896 while (true) {
897 if (ws.runner_request) |req| {
898 ws.runner_request = null;
899 ws.runner_request_empty_cond.signal(io);
900 return req;
901 }
902 try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex);
903 }
904}
905
906const cache_control_header: http.Header = .{
907 .name = "Cache-Control",
908 .value = "max-age=0, must-revalidate",
909};
910
911const builtin = @import("builtin");
912
913const std = @import("std");
914const Io = std.Io;
915const net = std.Io.net;
916const assert = std.debug.assert;
917const mem = std.mem;
918const log = std.log.scoped(.web_server);
919const Allocator = std.mem.Allocator;
920const Build = std.Build;
921const Cache = Build.Cache;
922const Fuzz = Build.Fuzz;
923const abi = Build.abi;
924const http = std.http;
925
926const WebServer = @This();
lib/std/zig.zig-2
......@@ -11,8 +11,6 @@ const Writer = std.Io.Writer;
1111
1212const tokenizer = @import("zig/tokenizer.zig");
1313
14/// The serialized output of configure phase ingested by make phase.
15pub const Configuration = @import("zig/Configuration.zig");
1614pub const ErrorBundle = @import("zig/ErrorBundle.zig");
1715pub const Server = @import("zig/Server.zig");
1816pub const Client = @import("zig/Client.zig");
lib/std/zig/Configuration.zig+230-10
......@@ -3,22 +3,240 @@ const Configuration = @This();
33const std = @import("../std.zig");
44const Io = std.Io;
55const Allocator = std.mem.Allocator;
6const assert = std.debug.assert;
67
78string_bytes: []u8,
89steps: []Step,
910path_deps_base: []Path.Base,
1011path_deps_sub: []String,
1112unlazy_deps: []String,
13extra: []u32,
1214
15/// The field order here matches `Configuration` which documents the order in
16/// the serialized format.
1317pub const Header = extern struct {
1418 string_bytes_len: u32,
1519 steps_len: u32,
1620 path_deps_len: u32,
1721 unlazy_deps_len: u32,
22 extra_len: u32,
23
24 /// Index into `steps`.
25 default_step: u32,
26};
27
28pub const Wip = struct {
29 gpa: Allocator,
30 string_table: StringTable = .empty,
31 deps_table: DepsTable = .empty,
32
33 string_bytes: std.ArrayList(u8) = .empty,
34 unlazy_deps: std.ArrayList(String) = .empty,
35 steps: std.ArrayList(Step) = .empty,
36 path_deps: std.MultiArrayList(Path) = .empty,
37 extra: std.ArrayList(u32) = .empty,
38
39 const DepsTable = std.HashMapUnmanaged(Deps, void, DepsTableContext, std.hash_map.default_max_load_percentage);
40
41 const DepsTableContext = struct {
42 extra: []const u32,
43
44 pub fn eql(ctx: @This(), a: Deps, b: Deps) bool {
45 const len_a = ctx.extra[@intFromEnum(a)];
46 const len_b = ctx.extra[@intFromEnum(b)];
47 const slice_a = ctx.extra[@intFromEnum(a) + 1 ..][0..len_a];
48 const slice_b = ctx.extra[@intFromEnum(b) + 1 ..][0..len_b];
49 return std.mem.eql(u32, slice_a, slice_b);
50 }
51
52 pub fn hash(ctx: @This(), key: Deps) u64 {
53 const len = ctx.extra[@intFromEnum(key)];
54 const slice = ctx.extra[@intFromEnum(key) + 1 ..][0..len];
55 return std.hash_map.hashString(@ptrCast(slice));
56 }
57 };
58
59 const StringTable = std.HashMapUnmanaged(String, void, StringTableContext, std.hash_map.default_max_load_percentage);
60 const StringTableContext = struct {
61 bytes: []const u8,
62
63 pub fn eql(_: @This(), a: String, b: String) bool {
64 return a == b;
65 }
66
67 pub fn hash(ctx: @This(), key: String) u64 {
68 return std.hash_map.hashString(std.mem.sliceTo(ctx.bytes[@intFromEnum(key)..], 0));
69 }
70 };
71
72 const StringTableIndexAdapter = struct {
73 bytes: []const u8,
74
75 pub fn eql(ctx: @This(), a: []const u8, b: String) bool {
76 return std.mem.eql(u8, a, std.mem.sliceTo(ctx.bytes[@intFromEnum(b)..], 0));
77 }
78
79 pub fn hash(_: @This(), adapted_key: []const u8) u64 {
80 assert(std.mem.indexOfScalar(u8, adapted_key, 0) == null);
81 return std.hash_map.hashString(adapted_key);
82 }
83 };
84
85 pub fn init(gpa: Allocator) Wip {
86 return .{ .gpa = gpa };
87 }
88
89 pub fn deinit(wip: *Wip) void {
90 const gpa = wip.gpa;
91 wip.string_bytes.deinit(gpa);
92 wip.unlazy_deps.deinit(gpa);
93 wip.steps.deinit(gpa);
94 wip.path_deps.deinit(gpa);
95 wip.extra.deinit(gpa);
96 wip.* = undefined;
97 }
98
99 pub const Static = struct {
100 default_step: u32,
101 };
102
103 pub fn write(wip: *Wip, w: *Io.Writer, static: Static) Io.Writer.Error!void {
104 const header: Header = .{
105 .string_bytes_len = @intCast(wip.string_bytes.items.len),
106 .steps_len = @intCast(wip.steps.items.len),
107 .path_deps_len = @intCast(wip.path_deps.len),
108 .unlazy_deps_len = @intCast(wip.unlazy_deps.items.len),
109 .extra_len = @intCast(wip.extra.items.len),
110
111 .default_step = static.default_step,
112 };
113 var buffers = [_][]const u8{
114 @ptrCast(&header),
115 wip.string_bytes.items,
116 @ptrCast(wip.steps.items),
117 @ptrCast(wip.path_deps.items(.base)),
118 @ptrCast(wip.path_deps.items(.sub)),
119 @ptrCast(wip.unlazy_deps.items),
120 @ptrCast(wip.extra.items),
121 };
122 try w.writeVecAll(&buffers);
123 }
124
125 pub fn addString(wip: *Wip, bytes: []const u8) Allocator.Error!String {
126 const gpa = wip.gpa;
127 assert(std.mem.indexOfScalar(u8, bytes, 0) == null);
128 const gop = try wip.string_table.getOrPutContextAdapted(
129 gpa,
130 @as([]const u8, bytes),
131 @as(StringTableIndexAdapter, .{ .bytes = wip.string_bytes.items }),
132 @as(StringTableContext, .{ .bytes = wip.string_bytes.items }),
133 );
134 if (gop.found_existing) return gop.key_ptr.*;
135
136 try wip.string_bytes.ensureUnusedCapacity(gpa, bytes.len + 1);
137 const new_off: String = @enumFromInt(wip.string_bytes.items.len);
138
139 wip.string_bytes.appendSliceAssumeCapacity(bytes);
140 wip.string_bytes.appendAssumeCapacity(0);
141
142 gop.key_ptr.* = new_off;
143
144 return new_off;
145 }
146
147 pub fn prepareDeps(wip: *Wip, n: usize) Allocator.Error![]u32 {
148 const slice = try wip.extra.addManyAsSlice(wip.gpa, n + 1);
149 slice[0] = @intCast(n);
150 return slice[1..];
151 }
152
153 pub fn dedupeDeps(wip: *Wip, deps: Deps) Allocator.Error!Deps {
154 const gpa = wip.gpa;
155 const gop = try wip.deps_table.getOrPutContext(gpa, deps, @as(DepsTableContext, .{
156 .extra = wip.extra.items,
157 }));
158 if (gop.found_existing) {
159 wip.extra.items.len = @intFromEnum(deps);
160 return gop.key_ptr.*;
161 } else {
162 return deps;
163 }
164 }
165
166 pub fn addExtra(wip: *Wip, extra: anytype) Allocator.Error!u32 {
167 const gpa = wip.gpa;
168 const fields = @typeInfo(@TypeOf(extra)).@"struct".fields;
169 try wip.extra.ensureUnusedCapacity(gpa, fields.len);
170 return addExtraAssumeCapacity(wip, extra);
171 }
172
173 pub fn addExtraAssumeCapacity(wip: *Wip, extra: anytype) u32 {
174 const fields = @typeInfo(@TypeOf(extra)).@"struct".fields;
175 const result: u32 = @intCast(wip.extra.items.len);
176 wip.extra.items.len += fields.len;
177 setExtra(wip, result, extra);
178 return result;
179 }
180
181 fn setExtra(wip: *Wip, index: usize, extra: anytype) void {
182 const fields = @typeInfo(@TypeOf(extra)).@"struct".fields;
183 var i = index;
184 inline for (fields) |field| {
185 wip.extra.items[i] = switch (field.type) {
186 u32 => @field(extra, field.name),
187 String, Deps => @intFromEnum(@field(extra, field.name)),
188 else => @compileError("bad field type"),
189 };
190 i += 1;
191 }
192 }
18193};
19194
20195pub const Step = extern struct {
21196 name: String,
197 flags: Flags,
198 deps: Deps,
199 /// Points into `extra` for step-specific data.
200 extra_index: u32,
201
202 pub const Flags = packed struct(u32) {
203 tag: Tag,
204 _: u24 = 0,
205 };
206
207 pub const Index = enum(u32) {
208 _,
209 };
210
211 pub const Tag = enum(u8) {
212 top_level,
213 compile,
214 install_artifact,
215 install_file,
216 install_dir,
217 remove_dir,
218 fail,
219 fmt,
220 translate_c,
221 write_file,
222 update_source_files,
223 run,
224 check_file,
225 check_object,
226 config_header,
227 objcopy,
228 options,
229 };
230
231 pub const TopLevel = struct {
232 description: String,
233 };
234};
235
236/// Points into `extra`, where the first element is number of deps,
237/// following elements is `Step.Index` per dep.
238pub const Deps = enum(u32) {
239 _,
22240};
23241
24242pub const Path = extern struct {
......@@ -27,8 +245,8 @@ pub const Path = extern struct {
27245
28246 pub const Base = enum(u8) {
29247 cwd,
30 global_cache,
31248 local_cache,
249 global_cache,
32250 build_root,
33251 };
34252
......@@ -40,6 +258,7 @@ pub const Path = extern struct {
40258 }
41259};
42260
261/// Points into `string_bytes`, null-terminated.
43262pub const String = enum(u32) {
44263 _,
45264
......@@ -49,24 +268,29 @@ pub const String = enum(u32) {
49268 }
50269};
51270
52pub const LoadError = Io.File.Reader.Error || Allocator.Error || error{EndOfStream};
271pub const LoadFileError = Io.File.Reader.Error || Allocator.Error || error{EndOfStream};
53272
54pub fn load(arena: Allocator, io: Io, file: Io.File) LoadError!Configuration {
273pub fn loadFile(arena: Allocator, io: Io, file: Io.File) LoadFileError!Configuration {
55274 var buffer: [2000]u8 = undefined;
56275 var fr = file.reader(io, &buffer);
57 const header = fr.interface.takeStruct(Header, .little) catch |err| switch (err) {
276 return load(arena, &fr.interface) catch |err| switch (err) {
58277 error.ReadFailed => return fr.err.?,
59278 else => |e| return e,
60279 };
280}
281
282pub const LoadError = Io.Reader.Error || Allocator.Error;
61283
284pub fn load(arena: Allocator, reader: *Io.Reader) LoadError!Configuration {
285 const header = try reader.takeStruct(Header, .little);
62286 var result: Configuration = .{
63287 .string_bytes = try arena.alloc(u8, header.string_bytes_len),
64288 .steps = try arena.alloc(Step, header.steps_len),
65289 .path_deps_sub = try arena.alloc(String, header.path_deps_len),
66290 .path_deps_base = try arena.alloc(Path.Base, header.path_deps_len),
67291 .unlazy_deps = try arena.alloc(String, header.unlazy_deps_len),
292 .extra = try arena.alloc(u32, header.extra_len),
68293 };
69
70294 var vecs = [_][]u8{
71295 result.string_bytes,
72296 @ptrCast(result.steps),
......@@ -74,10 +298,6 @@ pub fn load(arena: Allocator, io: Io, file: Io.File) LoadError!Configuration {
74298 @ptrCast(result.path_deps_sub),
75299 @ptrCast(result.unlazy_deps),
76300 };
77 fr.interface.readVecAll(&vecs) catch |err| switch (err) {
78 error.ReadFailed => return fr.err.?,
79 else => |e| return e,
80 };
81
301 try reader.readVecAll(&vecs);
82302 return result;
83303}
src/main.zig+117-38
......@@ -298,7 +298,11 @@ fn mainArgs(
298298 return process.exit(try llvmArMain(arena, args));
299299 } else if (mem.eql(u8, cmd, "build")) {
300300 dev.check(.build_command);
301 return cmdBuild(gpa, arena, io, cmd_args, environ_map);
301 var thread_safe_arena: std.heap.ThreadSafeAllocator = .{
302 .child_allocator = arena,
303 .io = io,
304 };
305 return cmdBuild(gpa, thread_safe_arena.allocator(), io, cmd_args, environ_map);
302306 } else if (mem.eql(u8, cmd, "clang") or
303307 mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as"))
304308 {
......@@ -4941,6 +4945,7 @@ test sanitizeExampleName {
49414945
49424946fn cmdBuild(
49434947 gpa: Allocator,
4948 /// Needs a thread-safe arena.
49444949 arena: Allocator,
49454950 io: Io,
49464951 args: []const []const u8,
......@@ -4973,28 +4978,34 @@ fn cmdBuild(
49734978 var debug_target: ?[]const u8 = null;
49744979 var debug_libc_paths_file: ?[]const u8 = null;
49754980
4981 const self_exe_path = try process.executablePathAlloc(io, arena);
4982 const default_seed = try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)});
4983
4984 try configure_argv.ensureUnusedCapacity(arena, 16);
4985
49764986 const argv_index_exe = configure_argv.items.len;
4977 _ = try configure_argv.addOne(arena);
4987 _ = configure_argv.addOneAssumeCapacity();
49784988
4979 const self_exe_path = try process.executablePathAlloc(io, arena);
4980 try configure_argv.append(arena, self_exe_path);
4989 configure_argv.appendAssumeCapacity("--zig");
4990 configure_argv.appendAssumeCapacity(self_exe_path);
49814991
4992 configure_argv.appendAssumeCapacity("--zig-lib-dir");
49824993 const argv_index_zig_lib_dir = configure_argv.items.len;
4983 _ = try configure_argv.addOne(arena);
4994 _ = configure_argv.addOneAssumeCapacity();
49844995
4996 configure_argv.appendAssumeCapacity("--build-root");
49854997 const argv_index_build_file = configure_argv.items.len;
4986 _ = try configure_argv.addOne(arena);
4998 _ = configure_argv.addOneAssumeCapacity();
49874999
5000 configure_argv.appendAssumeCapacity("--local-cache");
49885001 const argv_index_cache_dir = configure_argv.items.len;
4989 _ = try configure_argv.addOne(arena);
5002 _ = configure_argv.addOneAssumeCapacity();
49905003
5004 configure_argv.appendAssumeCapacity("--global-cache");
49915005 const argv_index_global_cache_dir = configure_argv.items.len;
4992 _ = try configure_argv.addOne(arena);
5006 _ = configure_argv.addOneAssumeCapacity();
49935007
4994 try configure_argv.appendSlice(arena, &.{
4995 "--seed",
4996 try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)}),
4997 });
5008 configure_argv.appendSliceAssumeCapacity(&.{ "--seed", default_seed });
49985009 const argv_index_seed = configure_argv.items.len - 1;
49995010
50005011 const argv_index_configuration_file = make_argv.items.len;
......@@ -5192,14 +5203,6 @@ fn cmdBuild(
51925203 );
51935204 try setThreadLimit(arena, thread_limit);
51945205
5195 // Kick off an optimized compilation of the make runner.
5196 var make_runner_task = io.async(compileMakeRunner, .{ io, .{
5197 .dirs = &dirs,
5198 .optimize = .ReleaseSafe,
5199 .parent_prog_node = root_prog_node,
5200 } });
5201 defer if (make_runner_task.cancel(io)) |mr| mr.deinit(io) else |_| {};
5202
52035206 // Cache lookup for configure options. If we get a match, we can skip
52045207 // execution of the configure script. If not, we get the file path to pass
52055208 // to the configure process.
......@@ -5255,6 +5258,19 @@ fn cmdBuild(
52555258 break :lci lci;
52565259 };
52575260
5261 // Kick off an optimized compilation of the make runner.
5262 var make_runner_task = io.async(compileMakeRunner, .{ gpa, arena, io, .{
5263 .dirs = &dirs,
5264 .environ_map = environ_map,
5265 .parent_prog_node = root_prog_node,
5266 .resolved_target = resolved_target,
5267 .libc_installation = libc_installation,
5268 .thread_limit = thread_limit,
5269 .self_exe_path = self_exe_path,
5270 .color = color,
5271 } });
5272 defer _ = make_runner_task.cancel(io) catch {};
5273
52585274 configure_argv.items[argv_index_zig_lib_dir] = dirs.zig_lib.path orelse cwd_path;
52595275 configure_argv.items[argv_index_build_file] = build_root.directory.path orelse cwd_path;
52605276 configure_argv.items[argv_index_global_cache_dir] = dirs.global_cache.path orelse cwd_path;
......@@ -5305,7 +5321,7 @@ fn cmdBuild(
53055321 .root_src_path = fs.path.basename(runner),
53065322 } else .{
53075323 .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"),
5308 .root_src_path = "build_runner.zig",
5324 .root_src_path = "configure_runner.zig",
53095325 };
53105326
53115327 const config = try Compilation.Config.resolve(.{
......@@ -5533,7 +5549,7 @@ fn cmdBuild(
55335549 const comp = Compilation.create(gpa, arena, io, &create_diag, .{
55345550 .libc_installation = libc_installation,
55355551 .dirs = dirs,
5536 .root_name = "build",
5552 .root_name = "configure",
55375553 .config = config,
55385554 .root_mod = root_mod,
55395555 .main_mod = build_mod,
......@@ -5554,7 +5570,7 @@ fn cmdBuild(
55545570 .environ_map = environ_map,
55555571 }) catch |err| switch (err) {
55565572 error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
5557 else => fatal("failed to create compilation: {t}", .{err}),
5573 else => |e| fatal("failed to create compilation: {t}", .{e}),
55585574 };
55595575 defer comp.destroy();
55605576
......@@ -5625,7 +5641,7 @@ fn cmdBuild(
56255641 // add them to `config_man` before obtaining the final digest.
56265642 // * If it contains a set of lazy packages that need to be
56275643 // fetched, we need to fetch those now and re-run configure.
5628 var configuration = std.zig.Configuration.load(arena, io, config_tmp_file) catch |err|
5644 var configuration = std.Build.Configuration.loadFile(arena, io, config_tmp_file) catch |err|
56295645 fatal("failed to load configuration file {f}: {t}", .{ config_tmp_path, err });
56305646
56315647 if (configuration.unlazy_deps.len != 0) {
......@@ -5661,7 +5677,7 @@ fn cmdBuild(
56615677 }
56625678
56635679 for (configuration.path_deps_base, configuration.path_deps_sub) |base, sub| {
5664 const conf_path: std.zig.Configuration.Path = .{ .base = base, .sub = sub };
5680 const conf_path: std.Build.Configuration.Path = .{ .base = base, .sub = sub };
56655681 try config_man.addPathPost(conf_path.toCachePath(&configuration, arena));
56665682 }
56675683
......@@ -5707,7 +5723,6 @@ fn cmdBuild(
57075723
57085724 const make_runner = make_runner_task.await(io) catch |err|
57095725 fatal("failed to compile maker: {t}", .{err});
5710 defer make_runner.deinit(io);
57115726
57125727 make_argv.items[0] = try make_runner.exe_path.toString(arena);
57135728 make_argv.items[argv_index_configuration_file] = try configuration_path.toString(arena);
......@@ -5748,22 +5763,86 @@ const MakeRunner = struct {
57485763 exe_path: Path,
57495764
57505765 const Options = struct {
5766 environ_map: *const process.Environ.Map,
57515767 dirs: *Compilation.Directories,
5752 optimize: std.builtin.OptimizeMode,
57535768 parent_prog_node: std.Progress.Node,
5769 resolved_target: Package.Module.ResolvedTarget,
5770 libc_installation: ?*const LibCInstallation,
5771 self_exe_path: []const u8,
5772 thread_limit: usize,
5773 color: Color,
57545774 };
5755
5756 fn deinit(mr: MakeRunner, io: Io) void {
5757 _ = mr;
5758 _ = io;
5759 @panic("TODO");
5760 }
57615775};
57625776
5763fn compileMakeRunner(io: Io, options: MakeRunner.Options) !MakeRunner {
5764 _ = io;
5765 _ = options;
5766 @panic("TODO");
5777fn compileMakeRunner(gpa: Allocator, arena: Allocator, io: Io, options: MakeRunner.Options) !MakeRunner {
5778 const compile_prog_node = options.parent_prog_node.start("Compile Maker", 0);
5779 defer compile_prog_node.end();
5780
5781 const optimize_mode: std.builtin.OptimizeMode = if (EnvVar.ZIG_DEBUG_CMD.isSet(options.environ_map))
5782 .Debug
5783 else
5784 .ReleaseSafe;
5785 const strip = optimize_mode != .Debug;
5786
5787 const main_mod_paths: Package.Module.CreateOptions.Paths = .{
5788 .root = try .fromRoot(arena, options.dirs.*, .zig_lib, "compiler"),
5789 .root_src_path = "maker.zig",
5790 };
5791
5792 const config = try Compilation.Config.resolve(.{
5793 .output_mode = .Exe,
5794 .root_strip = strip,
5795 .root_optimize_mode = optimize_mode,
5796 .resolved_target = options.resolved_target,
5797 .have_zcu = true,
5798 .emit_bin = true,
5799 .is_test = false,
5800 });
5801
5802 const root_mod = try Package.Module.create(arena, .{
5803 .paths = main_mod_paths,
5804 .fully_qualified_name = "root",
5805 .cc_argv = &.{},
5806 .inherited = .{
5807 .resolved_target = options.resolved_target,
5808 .optimize_mode = optimize_mode,
5809 .strip = strip,
5810 },
5811 .global = config,
5812 .parent = null,
5813 });
5814
5815 var create_diag: Compilation.CreateDiagnostic = undefined;
5816 const comp = Compilation.create(gpa, arena, io, &create_diag, .{
5817 .dirs = options.dirs.*,
5818 .root_name = "maker",
5819 .config = config,
5820 .root_mod = root_mod,
5821 .main_mod = root_mod,
5822 .emit_bin = .yes_cache,
5823 .self_exe_path = options.self_exe_path,
5824 .thread_limit = options.thread_limit,
5825 .cache_mode = .whole,
5826 .environ_map = options.environ_map,
5827 }) catch |err| switch (err) {
5828 error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
5829 error.Canceled => |e| return e,
5830 else => |e| fatal("failed to create compilation: {t}", .{e}),
5831 };
5832 defer comp.destroy();
5833
5834 try updateModule(comp, options.color, compile_prog_node);
5835
5836 const exe_path: Path = .{
5837 .root_dir = options.dirs.global_cache,
5838 .sub_path = try std.fmt.allocPrint(arena, "o/{s}/{s}", .{
5839 &Cache.binToHex(comp.digest.?), comp.emit_bin.?,
5840 }),
5841 };
5842
5843 return .{
5844 .exe_path = exe_path,
5845 };
57675846}
57685847
57695848const Fork = struct {
......@@ -5972,7 +6051,7 @@ fn jitCmdInner(
59726051 .environ_map = environ_map,
59736052 }) catch |err| switch (err) {
59746053 error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
5975 else => fatal("failed to create compilation: {s}", .{@errorName(err)}),
6054 else => fatal("failed to create compilation: {t}", .{err}),
59766055 };
59776056 defer comp.destroy();
59786057