1const std = @import("std");
2const Io = std.Io;
3const Dir = std.Io.Dir;
4const Allocator = std.mem.Allocator;
5const Cache = std.Build.Cache;
6
7const usage =
8 \\Usage: incr-check <zig binary path> <input file> [options]
9 \\Options:
10 \\ --target triple-backend
11 \\ --quiet
12 \\ --zig-lib-dir /path/to/zig/lib
13 \\ --zig-cc-binary /path/to/zig
14 \\ -fqemu
15 \\ -fwine
16 \\ -fwasmtime
17 \\Debug Options:
18 \\ --preserve-tmp
19 \\ --debug-log foo
20;
21
22pub const std_options: std.Options = .{
23 .logFn = logImpl,
24};
25var log_cur_update: ?*const Case.Update = null;
26fn logImpl(
27 comptime level: std.log.Level,
28 comptime scope: @EnumLiteral(),
29 comptime format: []const u8,
30 args: anytype,
31) void {
32 const update = log_cur_update orelse {
33 return std.log.defaultLog(level, scope, format, args);
34 };
35 std.log.defaultLog(
36 level,
37 scope,
38 "['{s}'] " ++ format,
39 .{update.name} ++ args,
40 );
41}
42
43pub fn main(init: std.process.Init) !void {
44 const gpa = init.gpa;
45 const arena = init.arena.allocator();
46 const io = init.io;
47 const environ_map = init.environ_map;
48 const cwd_path = try std.process.currentPathAlloc(io, arena);
49
50 var opt_zig_exe: ?[]const u8 = null;
51 var opt_input_file_name: ?[]const u8 = null;
52 var opt_lib_dir: ?[]const u8 = null;
53 var opt_cc_zig: ?[]const u8 = null;
54 var opt_target: ?struct { std.Target.Query, Backend } = null;
55 var preserve_tmp = false;
56 var enable_qemu: bool = false;
57 var enable_wine: bool = false;
58 var enable_wasmtime: bool = false;
59 var enable_darling: bool = false;
60 var quiet: bool = false;
61
62 var debug_log_args: std.ArrayList([]const u8) = .empty;
63
64 var arg_it = try init.minimal.args.iterateAllocator(arena);
65 _ = arg_it.skip();
66 while (arg_it.next()) |arg| {
67 if (arg.len > 0 and arg[0] == '-') {
68 if (std.mem.eql(u8, arg, "--zig-lib-dir")) {
69 opt_lib_dir = arg_it.next() orelse badUsage("expected arg after --zig-lib-dir", .{});
70 } else if (std.mem.eql(u8, arg, "--target")) {
71 const str = arg_it.next() orelse badUsage("expected arg after --zig-cc-binary", .{});
72 opt_target = parseTargetQueryAndBackend(str, "");
73 } else if (std.mem.eql(u8, arg, "--quiet")) {
74 quiet = true;
75 } else if (std.mem.eql(u8, arg, "--debug-log")) {
76 try debug_log_args.append(
77 arena,
78 arg_it.next() orelse badUsage("expected arg after --debug-log", .{}),
79 );
80 } else if (std.mem.eql(u8, arg, "--preserve-tmp")) {
81 preserve_tmp = true;
82 } else if (std.mem.eql(u8, arg, "-fqemu")) {
83 enable_qemu = true;
84 } else if (std.mem.eql(u8, arg, "-fwine")) {
85 enable_wine = true;
86 } else if (std.mem.eql(u8, arg, "-fwasmtime")) {
87 enable_wasmtime = true;
88 } else if (std.mem.eql(u8, arg, "-fdarling")) {
89 enable_darling = true;
90 } else if (std.mem.eql(u8, arg, "--zig-cc-binary")) {
91 opt_cc_zig = arg_it.next() orelse badUsage("expected arg after --zig-cc-binary", .{});
92 } else {
93 badUsage("unknown option '{s}'", .{arg});
94 }
95 continue;
96 }
97 if (opt_zig_exe == null) {
98 opt_zig_exe = arg;
99 } else if (opt_input_file_name == null) {
100 opt_input_file_name = arg;
101 } else {
102 badUsage("unknown argument '{s}'\n{s}", .{ arg, usage });
103 }
104 }
105 const zig_exe = opt_zig_exe orelse badUsage("missing path to zig", .{});
106 const input_file_name = opt_input_file_name orelse badUsage("missing input file", .{});
107 const target_query, const backend = opt_target orelse badUsage("missing required option '--target'", .{});
108
109 if (backend == .cbe and opt_lib_dir == null) {
110 std.process.fatal("'--zig-lib-dir' required when using backend 'cbe'", .{});
111 }
112
113 const input_file_bytes = try Dir.cwd().readFileAlloc(io, input_file_name, arena, .limited(std.math.maxInt(u32)));
114 const case: Case = try .parse(arena, input_file_bytes);
115
116 for (case.skip_targets) |skip| {
117 if (target_query.eql(skip.query) and backend == skip.backend) {
118 if (!quiet) std.log.warn("skipping test because of a 'skip_target' match", .{});
119 return;
120 }
121 }
122
123 const target = try std.zig.system.resolveTargetQuery(io, target_query);
124
125 const prog_node = std.Progress.start(io, .{});
126 defer prog_node.end();
127
128 const rand_int = rand64(io);
129 const tmp_dir_path = "tmp_" ++ std.fmt.hex(rand_int);
130 var tmp_dir = try Dir.cwd().createDirPathOpen(io, tmp_dir_path, .{});
131 defer {
132 tmp_dir.close(io);
133 if (!preserve_tmp) {
134 Dir.cwd().deleteTree(io, tmp_dir_path) catch |err| {
135 std.log.warn("failed to delete tree '{s}': {t}", .{ tmp_dir_path, err });
136 };
137 }
138 }
139
140 // Convert paths to be relative to the cwd of the subprocess.
141 const resolved_zig_exe = try Dir.path.relative(arena, cwd_path, environ_map, tmp_dir_path, zig_exe);
142 const opt_resolved_lib_dir = if (opt_lib_dir) |lib_dir|
143 try Dir.path.relative(arena, cwd_path, environ_map, tmp_dir_path, lib_dir)
144 else
145 null;
146
147 const host = try std.zig.system.resolveTargetQuery(io, .{});
148
149 var child_args: std.ArrayList([]const u8) = .empty;
150 try child_args.appendSlice(arena, &.{
151 resolved_zig_exe,
152 "build-exe",
153 "-fincremental",
154 "-fno-ubsan-rt",
155 "-target",
156 try target_query.zigTriple(arena),
157 "--cache-dir",
158 ".local-cache",
159 "--global-cache-dir",
160 ".global-cache",
161 });
162 try child_args.append(arena, "--listen=-");
163
164 if (opt_resolved_lib_dir) |resolved_lib_dir| {
165 try child_args.appendSlice(arena, &.{ "--zig-lib-dir", resolved_lib_dir });
166 }
167 switch (backend) {
168 .sema => try child_args.append(arena, "-fno-emit-bin"),
169 .selfhosted => try child_args.appendSlice(arena, &.{ "-fno-llvm", "-fno-lld" }),
170 .llvm => try child_args.appendSlice(arena, &.{ "-fllvm", "-flld" }),
171 .cbe => try child_args.appendSlice(arena, &.{ "-ofmt=c", "-lc" }),
172 }
173 for (debug_log_args.items) |arg| {
174 try child_args.appendSlice(arena, &.{ "--debug-log", arg });
175 }
176 for (case.modules) |mod| {
177 try child_args.appendSlice(arena, &.{ "--dep", mod.name });
178 }
179 try child_args.append(arena, try std.fmt.allocPrint(arena, "-Mroot={s}", .{case.root_source_file}));
180 for (case.modules) |mod| {
181 try child_args.append(arena, try std.fmt.allocPrint(arena, "-M{s}={s}", .{ mod.name, mod.file }));
182 }
183
184 const zig_prog_node = prog_node.start("zig", 0);
185 defer zig_prog_node.end();
186
187 var cc_child_args: std.ArrayList([]const u8) = .empty;
188 if (backend == .cbe) {
189 const resolved_cc_zig_exe = if (opt_cc_zig) |cc_zig_exe|
190 try Dir.path.relative(arena, cwd_path, environ_map, tmp_dir_path, cc_zig_exe)
191 else
192 resolved_zig_exe;
193
194 try cc_child_args.appendSlice(arena, &.{
195 resolved_cc_zig_exe,
196 "cc",
197 "-target",
198 try target_query.zigTriple(arena),
199 "-I",
200 opt_resolved_lib_dir.?, // verified earlier
201 });
202
203 try cc_child_args.append(arena, "-o");
204 }
205
206 var child = try std.process.spawn(io, .{
207 .argv = child_args.items,
208 .stdin = .pipe,
209 .stdout = .pipe,
210 .stderr = .pipe,
211 .progress_node = zig_prog_node,
212 .cwd = .{ .path = tmp_dir_path },
213 });
214 defer child.kill(io);
215
216 const updates_prog_node = prog_node.start("updates", case.updates.len);
217 defer updates_prog_node.end();
218
219 var eval: Eval = .{
220 .arena = arena,
221 .io = io,
222 .case = case,
223 .host = host,
224 .target = target,
225 .backend = backend,
226 .tmp_dir = tmp_dir,
227 .tmp_dir_path = tmp_dir_path,
228 .child = &child,
229 .allow_compiler_stderr = debug_log_args.items.len != 0,
230 .quiet = quiet,
231 .preserve_tmp_on_fatal = preserve_tmp,
232 .cc_child_args = &cc_child_args,
233 .enable_qemu = enable_qemu,
234 .enable_wine = enable_wine,
235 .enable_wasmtime = enable_wasmtime,
236 .enable_darling = enable_darling,
237 };
238
239 var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
240 var multi_reader: Io.File.MultiReader = undefined;
241 multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? });
242 defer multi_reader.deinit();
243
244 var update_mtime = Io.Clock.real.now(io);
245 for (case.updates) |update| {
246 var update_prog_node = updates_prog_node.start(update.name, 0);
247 defer update_prog_node.end();
248
249 if (debug_log_args.items.len != 0) {
250 // Print a line separating the debug logs from the compiler in the stderr output.
251 std.log.scoped(.status).info("update: '{s}'", .{update.name});
252 }
253
254 log_cur_update = &update;
255 defer log_cur_update = null;
256
257 eval.write(update, update_mtime);
258 update_mtime = update_mtime.addDuration(.fromSeconds(5));
259 try eval.requestUpdate();
260 try eval.check(&multi_reader, update, update_prog_node);
261 }
262
263 try eval.end(&multi_reader);
264
265 waitChild(&child, &eval);
266}
267
268const Eval = struct {
269 arena: Allocator,
270 io: Io,
271 case: Case,
272 host: std.Target,
273 target: std.Target,
274 backend: Backend,
275 tmp_dir: Dir,
276 tmp_dir_path: []const u8,
277 child: *std.process.Child,
278 allow_compiler_stderr: bool,
279 quiet: bool,
280 preserve_tmp_on_fatal: bool,
281 /// When `backend == .cbe`, this contains the first few arguments to `zig cc` to build the generated binary.
282 /// The arguments `out.c in.c` must be appended before spawning the subprocess.
283 cc_child_args: *std.ArrayList([]const u8),
284
285 enable_qemu: bool,
286 enable_wine: bool,
287 enable_wasmtime: bool,
288 enable_darling: bool,
289
290 /// Currently this function assumes the previous updates have already been written.
291 fn write(eval: *Eval, update: Case.Update, mtime: Io.Timestamp) void {
292 const io = eval.io;
293 for (update.changes) |full_contents| {
294 var update_file = eval.tmp_dir.createFile(io, full_contents.name, .{}) catch |err| {
295 eval.fatal("failed to create update '{s}': {t}", .{ full_contents.name, err });
296 };
297 defer update_file.close(io);
298 update_file.writeStreamingAll(io, full_contents.bytes) catch |err| {
299 eval.fatal("failed to write update '{s}': {t}", .{ full_contents.name, err });
300 };
301 update_file.setTimestamps(io, .{ .modify_timestamp = .{ .new = mtime } }) catch |err| {
302 eval.fatal("failed to set mtime for '{s}': {t}", .{ full_contents.name, err });
303 };
304 }
305 for (update.deletes) |doomed_name| {
306 eval.tmp_dir.deleteFile(io, doomed_name) catch |err| {
307 eval.fatal("failed to delete '{s}': {t}", .{ doomed_name, err });
308 };
309 }
310 }
311
312 fn check(eval: *Eval, mr: *Io.File.MultiReader, update: Case.Update, prog_node: std.Progress.Node) !void {
313 const arena = eval.arena;
314 const stdout = mr.reader(0);
315 const stderr = mr.reader(1);
316
317 var client: std.zig.Client = .{
318 .in = stdout,
319 .out = undefined,
320 };
321
322 while (true) {
323 const header = client.receiveMessageWithMultiReader(mr, .none) catch |err| switch (err) {
324 error.Timeout => unreachable,
325 error.EndOfStream => break,
326 else => |e| return e,
327 };
328 const body = client.in.take(header.bytes_len) catch unreachable;
329
330 switch (header.tag) {
331 .error_bundle => {
332 const result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
333 if (stderr.bufferedLen() > 0) {
334 if (eval.allow_compiler_stderr) {
335 std.log.info("error_bundle stderr:\n{s}", .{stderr.buffered()});
336 } else {
337 eval.fatal("error_bundle unexpected stderr:\n{s}", .{stderr.buffered()});
338 }
339 stderr.tossBuffered();
340 }
341 if (result_error_bundle.errorMessageCount() != 0) {
342 try eval.checkErrorOutcome(update, result_error_bundle);
343 }
344 // This message indicates the end of the update.
345 return;
346 },
347 .emit_digest => {
348 var r: std.Io.Reader = .fixed(body);
349 _ = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch unreachable;
350
351 if (stderr.bufferedLen() > 0) {
352 if (eval.allow_compiler_stderr) {
353 std.log.info("emit_digest stderr:\n{s}", .{stderr.buffered()});
354 } else {
355 eval.fatal("emit_digest unexpected stderr:\n{s}", .{stderr.buffered()});
356 }
357 stderr.tossBuffered();
358 }
359 if (eval.backend == .sema) {
360 try eval.checkSuccessOutcome(update, null, prog_node);
361 continue;
362 }
363
364 const digest = r.takeArray(Cache.bin_digest_len) catch unreachable;
365 const result_dir = ".local-cache" ++ Dir.path.sep_str ++ "o" ++ Dir.path.sep_str ++ Cache.binToHex(digest.*);
366
367 const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{
368 .root_name = "root", // corresponds to the module name "root"
369 .cpu_arch = eval.target.cpu.arch,
370 .os_tag = eval.target.os.tag,
371 .ofmt = eval.target.ofmt,
372 .abi = eval.target.abi,
373 .output_mode = .Exe,
374 });
375 const bin_path = try Dir.path.join(arena, &.{ result_dir, bin_name });
376
377 try eval.checkSuccessOutcome(update, bin_path, prog_node);
378 },
379 else => {
380 // Ignore other messages.
381 },
382 }
383 }
384
385 const buffered_stderr = stderr.buffered();
386 if (buffered_stderr.len > 0) {
387 if (eval.allow_compiler_stderr) {
388 std.log.info("stderr:\n{s}", .{buffered_stderr});
389 } else {
390 eval.fatal("unexpected stderr:\n{s}", .{buffered_stderr});
391 }
392 }
393
394 waitChild(eval.child, eval);
395 eval.fatal("compiler failed to send terminating error_bundle", .{});
396 }
397
398 fn checkErrorOutcome(eval: *Eval, update: Case.Update, error_bundle: std.zig.ErrorBundle) !void {
399 const io = eval.io;
400 const expected = switch (update.outcome) {
401 .unknown => return,
402 .compile_errors => |ce| ce,
403 .stdout, .exit_code => {
404 try error_bundle.renderToStderr(io, .{}, .auto);
405 eval.fatal("unexpected compile errors", .{});
406 },
407 };
408
409 var expected_idx: usize = 0;
410
411 for (error_bundle.getMessages()) |err_idx| {
412 if (expected_idx == expected.errors.len) {
413 try error_bundle.renderToStderr(io, .{}, .auto);
414 eval.fatal("more errors than expected", .{});
415 }
416 try eval.checkOneError(error_bundle, expected.errors[expected_idx], false, err_idx);
417 expected_idx += 1;
418
419 for (error_bundle.getNotes(err_idx)) |note_idx| {
420 if (expected_idx == expected.errors.len) {
421 try error_bundle.renderToStderr(io, .{}, .auto);
422 eval.fatal("more error notes than expected", .{});
423 }
424 try eval.checkOneError(error_bundle, expected.errors[expected_idx], true, note_idx);
425 expected_idx += 1;
426 }
427 }
428
429 if (!std.mem.eql(u8, error_bundle.getCompileLogOutput(), expected.compile_log_output)) {
430 try error_bundle.renderToStderr(io, .{}, .auto);
431 eval.fatal("unexpected compile log output", .{});
432 }
433 }
434
435 fn checkOneError(
436 eval: *Eval,
437 eb: std.zig.ErrorBundle,
438 expected: Case.ExpectedError,
439 is_note: bool,
440 err_idx: std.zig.ErrorBundle.MessageIndex,
441 ) Allocator.Error!void {
442 const io = eval.io;
443 const err = eb.getErrorMessage(err_idx);
444 if (err.count != 1) @panic("TODO error message with count>1");
445 const msg = eb.nullTerminatedString(err.msg);
446 const matches = matches: {
447 if (expected.is_note != is_note) break :matches false;
448 if (!std.mem.eql(u8, expected.msg, msg)) break :matches false;
449 if (err.src_loc == .none) {
450 break :matches expected.src == null;
451 }
452 const expected_src = expected.src orelse break :matches false;
453 const src = eb.getSourceLocation(err.src_loc);
454 const raw_filename = eb.nullTerminatedString(src.src_path);
455 // We need to replace backslashes for consistency between platforms.
456 const filename = name: {
457 if (std.mem.findScalar(u8, raw_filename, '\\') == null) break :name raw_filename;
458 const copied = try eval.arena.dupe(u8, raw_filename);
459 std.mem.replaceScalar(u8, copied, '\\', '/');
460 break :name copied;
461 };
462 if (!std.mem.eql(u8, expected_src.filename, filename)) break :matches false;
463 if (expected_src.line != src.line + 1) break :matches false;
464 if (expected_src.column != src.column + 1) break :matches false;
465 break :matches true;
466 };
467 if (!matches) {
468 eb.renderToStderr(io, .{}, .auto) catch {};
469 eval.fatal("compile error did not match expected error", .{});
470 }
471 }
472
473 fn checkSuccessOutcome(eval: *Eval, update: Case.Update, opt_emitted_path: ?[]const u8, prog_node: std.Progress.Node) !void {
474 switch (update.outcome) {
475 .unknown => return,
476 .compile_errors => eval.fatal("expected compile errors but compilation incorrectly succeeded", .{}),
477 .stdout, .exit_code => {},
478 }
479 const emitted_path = opt_emitted_path orelse {
480 std.debug.assert(eval.backend == .sema);
481 return;
482 };
483 const io = eval.io;
484
485 const binary_path = switch (eval.backend) {
486 .sema => unreachable,
487 .selfhosted, .llvm => emitted_path,
488 .cbe => bin: {
489 const rand_int = rand64(io);
490 const out_bin_name = "./out_" ++ std.fmt.hex(rand_int);
491 try eval.buildCOutput(emitted_path, out_bin_name, prog_node);
492 break :bin out_bin_name;
493 },
494 };
495
496 var argv_buf: [2][]const u8 = undefined;
497 const argv: []const []const u8, const is_foreign: bool = sw: switch (std.zig.system.getExternalExecutor(
498 io,
499 &eval.target,
500 .{
501 .link_libc = eval.backend == .cbe,
502 .host_cpu_arch = eval.host.cpu.arch,
503 .host_os_tag = eval.host.os.tag,
504 },
505 )) {
506 .bad_dl, .bad_os_or_cpu => {
507 // This binary cannot be executed on this host.
508 if (!eval.quiet) {
509 std.log.warn("skipping execution because host '{s}' cannot execute binaries for foreign target '{s}'", .{
510 try eval.host.zigTriple(eval.arena),
511 try eval.target.zigTriple(eval.arena),
512 });
513 }
514 return;
515 },
516 .native, .rosetta => argv: {
517 argv_buf[0] = binary_path;
518 break :argv .{ argv_buf[0..1], false };
519 },
520 .qemu => |executor_cmd| argv: {
521 if (eval.enable_qemu) {
522 argv_buf[0] = executor_cmd;
523 argv_buf[1] = binary_path;
524 break :argv .{ argv_buf[0..2], true };
525 } else {
526 continue :sw .bad_os_or_cpu;
527 }
528 },
529 .wine => |executor_cmd| argv: {
530 if (eval.enable_wine) {
531 argv_buf[0] = executor_cmd;
532 argv_buf[1] = binary_path;
533 break :argv .{ argv_buf[0..2], true };
534 } else {
535 continue :sw .bad_os_or_cpu;
536 }
537 },
538 .wasmtime => |executor_cmd| argv: {
539 if (eval.enable_wasmtime) {
540 argv_buf[0] = executor_cmd;
541 argv_buf[1] = binary_path;
542 break :argv .{ argv_buf[0..2], true };
543 } else {
544 continue :sw .bad_os_or_cpu;
545 }
546 },
547 .darling => |executor_cmd| argv: {
548 if (eval.enable_darling) {
549 argv_buf[0] = executor_cmd;
550 argv_buf[1] = binary_path;
551 break :argv .{ argv_buf[0..2], true };
552 } else {
553 continue :sw .bad_os_or_cpu;
554 }
555 },
556 };
557
558 const run_prog_node = prog_node.start("run generated executable", 0);
559 defer run_prog_node.end();
560
561 const result = std.process.run(eval.arena, io, .{
562 .argv = argv,
563 .cwd = .{ .path = eval.tmp_dir_path },
564 }) catch |err| {
565 if (is_foreign) {
566 // Chances are the foreign executor isn't available. Skip this evaluation.
567 if (!eval.quiet) {
568 std.log.warn("skipping execution of '{s}' via executor for foreign target '{s}': {t}", .{
569 binary_path,
570 try eval.target.zigTriple(eval.arena),
571 err,
572 });
573 }
574 return;
575 }
576 eval.fatal("failed to run the generated executable '{s}': {t}", .{ binary_path, err });
577 };
578
579 // Some executors (looking at you, Wine) like throwing some stderr in, just for fun.
580 // Therefore, we'll ignore stderr when using a foreign executor.
581 if (!is_foreign and result.stderr.len != 0) {
582 std.log.err("generated executable '{s}' had unexpected stderr:\n{s}", .{
583 binary_path, result.stderr,
584 });
585 }
586
587 switch (result.term) {
588 .exited => |code| switch (update.outcome) {
589 .unknown, .compile_errors => unreachable,
590 .stdout => |expected_stdout| {
591 if (code != 0) {
592 eval.fatal("generated executable '{s}' failed with code {d}", .{ binary_path, code });
593 }
594 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
595 },
596 .exit_code => |expected_code| try std.testing.expectEqual(expected_code, code),
597 },
598 .signal => |sig| {
599 eval.fatal("generated executable '{s}' terminated with signal {t}", .{ binary_path, sig });
600 },
601 .stopped => |sig| {
602 eval.fatal("generated executable '{s}' stopped with signal {t}", .{ binary_path, sig });
603 },
604 .unknown => {
605 eval.fatal("generated executable '{s}' terminated unexpectedly", .{binary_path});
606 },
607 }
608
609 if (!is_foreign and result.stderr.len != 0) std.process.exit(1);
610 }
611
612 fn requestUpdate(eval: *Eval) !void {
613 const io = eval.io;
614
615 var w = eval.child.stdin.?.writerStreaming(io, &.{});
616 var client: std.zig.Client = .{
617 .in = undefined,
618 .out = &w.interface,
619 };
620 client.serveBodylessMessage(.update) catch |err| switch (err) {
621 error.WriteFailed => return w.err.?,
622 };
623 }
624
625 fn end(eval: *Eval, mr: *Io.File.MultiReader) !void {
626 requestExit(eval.child, eval);
627
628 var client: std.zig.Client = .{
629 .in = mr.reader(0),
630 .out = undefined,
631 };
632
633 while (true) {
634 const header = client.receiveMessageWithMultiReader(mr, .none) catch |err| switch (err) {
635 error.Timeout => unreachable,
636 error.EndOfStream => |e| {
637 if (client.in.bufferedLen() == 0) break;
638 return e;
639 },
640 else => |e| return e,
641 };
642 try client.in.discardAll(header.bytes_len);
643 }
644
645 const stderr = mr.reader(1).buffered();
646 if (stderr.len > 0) eval.fatal("unexpected stderr:\n{s}", .{stderr});
647 }
648
649 fn buildCOutput(eval: *Eval, c_path: []const u8, out_path: []const u8, prog_node: std.Progress.Node) !void {
650 std.debug.assert(eval.cc_child_args.items.len > 0);
651
652 const child_prog_node = prog_node.start("build cbe output", 0);
653 defer child_prog_node.end();
654
655 try eval.cc_child_args.appendSlice(eval.arena, &.{ out_path, c_path });
656 defer eval.cc_child_args.items.len -= 2;
657
658 const result = std.process.run(eval.arena, eval.io, .{
659 .argv = eval.cc_child_args.items,
660 .cwd = .{ .path = eval.tmp_dir_path },
661 .progress_node = child_prog_node,
662 }) catch |err| {
663 eval.fatal("failed to spawn zig cc for '{s}': {t}", .{ c_path, err });
664 };
665
666 if (result.term == .exited and result.term.exited == 0) return;
667
668 if (result.stderr.len != 0) {
669 std.log.err("zig cc stderr:\n{s}", .{result.stderr});
670 }
671 switch (result.term) {
672 .exited => |code| eval.fatal("zig cc for '{s}' failed with code {d}", .{ c_path, code }),
673 .signal => |sig| eval.fatal("zig cc for '{s}' terminated unexpectedly with signal {t}", .{ c_path, sig }),
674 .stopped => |sig| eval.fatal("zig cc for '{s}' stopped unexpectedly with signal {t}", .{ c_path, sig }),
675 .unknown => eval.fatal("zig cc for '{s}' terminated unexpectedly", .{c_path}),
676 }
677 }
678
679 fn fatal(eval: *Eval, comptime fmt: []const u8, args: anytype) noreturn {
680 const io = eval.io;
681 eval.tmp_dir.close(io);
682 if (!eval.preserve_tmp_on_fatal) {
683 // Kill the child since it holds an open handle to its CWD which is the tmp dir path
684 eval.child.kill(io);
685 Dir.cwd().deleteTree(io, eval.tmp_dir_path) catch |err| {
686 std.log.warn("failed to delete tree '{s}': {t}", .{ eval.tmp_dir_path, err });
687 };
688 }
689 std.process.fatal(fmt, args);
690 }
691};
692
693const Backend = enum {
694 /// Run semantic analysis only. Runtime output will not be tested, but we still verify
695 /// that compilation succeeds. Corresponds to `-fno-emit-bin`.
696 sema,
697 /// Use the self-hosted code generation backend for this target.
698 /// Corresponds to `-fno-llvm -fno-lld`.
699 selfhosted,
700 /// Use the LLVM backend.
701 /// Corresponds to `-fllvm -flld`.
702 llvm,
703 /// Use the C backend. The output is compiled with `zig cc`.
704 /// Corresponds to `-ofmt=c`.
705 cbe,
706};
707
708const Case = struct {
709 updates: []Update,
710 root_source_file: []const u8,
711 skip_targets: []const SkipTarget,
712 modules: []const Module,
713
714 const SkipTarget = struct {
715 query: std.Target.Query,
716 backend: Backend,
717 };
718
719 const Module = struct {
720 name: []const u8,
721 file: []const u8,
722 };
723
724 const Update = struct {
725 name: []const u8,
726 outcome: Outcome,
727 changes: []const FullContents = &.{},
728 deletes: []const []const u8 = &.{},
729 };
730
731 const FullContents = struct {
732 name: []const u8,
733 bytes: []const u8,
734 };
735
736 const Outcome = union(enum) {
737 unknown,
738 compile_errors: struct {
739 errors: []const ExpectedError,
740 compile_log_output: []const u8,
741 },
742 stdout: []const u8,
743 exit_code: u8,
744 };
745
746 const ExpectedError = struct {
747 is_note: bool,
748 msg: []const u8,
749 src: ?struct {
750 filename: []const u8,
751 line: u32,
752 column: u32,
753 },
754 };
755
756 fn parse(arena: Allocator, bytes: []const u8) !Case {
757 const fatal = std.process.fatal;
758
759 var skip_targets: std.ArrayList(SkipTarget) = .empty;
760 var modules: std.ArrayList(Module) = .empty;
761 var updates: std.ArrayList(Update) = .empty;
762 var changes: std.ArrayList(FullContents) = .empty;
763 var deletes: std.ArrayList([]const u8) = .empty;
764 var it = std.mem.splitScalar(u8, bytes, '\n');
765 var line_n: usize = 1;
766 var root_source_file: ?[]const u8 = null;
767 while (it.next()) |line| : (line_n += 1) {
768 if (std.mem.startsWith(u8, line, "#")) {
769 var line_it = std.mem.splitScalar(u8, line, '=');
770 const key = line_it.first()[1..];
771 const val = std.mem.trimEnd(u8, line_it.rest(), "\r"); // windows moment
772 if (val.len == 0) {
773 fatal("line {d}: missing value", .{line_n});
774 } else if (std.mem.eql(u8, key, "skip_target")) {
775 const query, const backend = parseTargetQueryAndBackend(
776 val,
777 try std.fmt.allocPrint(arena, "line {d}: ", .{line_n}),
778 );
779 try skip_targets.append(arena, .{
780 .query = query,
781 .backend = backend,
782 });
783 } else if (std.mem.eql(u8, key, "module")) {
784 const split_idx = std.mem.findScalar(u8, val, '=') orelse
785 fatal("line {d}: module does not include file", .{line_n});
786 const name = val[0..split_idx];
787 const file = val[split_idx + 1 ..];
788 try modules.append(arena, .{
789 .name = name,
790 .file = file,
791 });
792 } else if (std.mem.eql(u8, key, "update")) {
793 if (updates.items.len > 0) {
794 const last_update = &updates.items[updates.items.len - 1];
795 last_update.changes = try changes.toOwnedSlice(arena);
796 last_update.deletes = try deletes.toOwnedSlice(arena);
797 }
798 try updates.append(arena, .{
799 .name = val,
800 .outcome = .unknown,
801 });
802 } else if (std.mem.eql(u8, key, "file")) {
803 if (updates.items.len == 0) fatal("line {d}: file directive before update", .{line_n});
804
805 if (root_source_file == null)
806 root_source_file = val;
807
808 // Because Windows is so excellent, we need to convert CRLF to LF, so
809 // can't just slice into the input here. How delightful!
810 var src: std.ArrayList(u8) = .empty;
811
812 while (true) {
813 const next_line_raw = it.peek() orelse fatal("line {d}: unexpected EOF", .{line_n});
814 const next_line = std.mem.trimEnd(u8, next_line_raw, "\r");
815 if (std.mem.startsWith(u8, next_line, "#")) break;
816
817 _ = it.next();
818 line_n += 1;
819
820 try src.ensureUnusedCapacity(arena, next_line.len + 1);
821 src.appendSliceAssumeCapacity(next_line);
822 src.appendAssumeCapacity('\n');
823 }
824
825 try changes.append(arena, .{
826 .name = val,
827 .bytes = src.items,
828 });
829 } else if (std.mem.eql(u8, key, "rm_file")) {
830 if (updates.items.len == 0) fatal("line {d}: rm_file directive before update", .{line_n});
831 try deletes.append(arena, val);
832 } else if (std.mem.eql(u8, key, "expect_stdout")) {
833 if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
834 const last_update = &updates.items[updates.items.len - 1];
835 if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n});
836 last_update.outcome = .{
837 .stdout = std.zig.string_literal.parseAlloc(arena, val) catch |err| {
838 fatal("line {d}: bad string literal: {t}", .{ line_n, err });
839 },
840 };
841 } else if (std.mem.eql(u8, key, "expect_error")) {
842 if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n});
843 const last_update = &updates.items[updates.items.len - 1];
844 if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n});
845
846 var errors: std.ArrayList(ExpectedError) = .empty;
847 try errors.append(arena, parseExpectedError(val, line_n));
848 while (true) {
849 const next_line = it.peek() orelse break;
850 if (!std.mem.startsWith(u8, next_line, "#")) break;
851 var new_line_it = std.mem.splitScalar(u8, next_line, '=');
852 const new_key = new_line_it.first()[1..];
853 const new_val = std.mem.trimEnd(u8, new_line_it.rest(), "\r");
854 if (new_val.len == 0) break;
855 if (!std.mem.eql(u8, new_key, "expect_error")) break;
856
857 _ = it.next();
858 line_n += 1;
859 try errors.append(arena, parseExpectedError(new_val, line_n));
860 }
861
862 var compile_log_output: std.ArrayList(u8) = .empty;
863 while (true) {
864 const next_line = it.peek() orelse break;
865 if (!std.mem.startsWith(u8, next_line, "#")) break;
866 var new_line_it = std.mem.splitScalar(u8, next_line, '=');
867 const new_key = new_line_it.first()[1..];
868 const new_val = std.mem.trimEnd(u8, new_line_it.rest(), "\r");
869 if (new_val.len == 0) break;
870 if (!std.mem.eql(u8, new_key, "expect_compile_log")) break;
871
872 _ = it.next();
873 line_n += 1;
874 try compile_log_output.ensureUnusedCapacity(arena, new_val.len + 1);
875 compile_log_output.appendSliceAssumeCapacity(new_val);
876 compile_log_output.appendAssumeCapacity('\n');
877 }
878
879 last_update.outcome = .{ .compile_errors = .{
880 .errors = errors.items,
881 .compile_log_output = compile_log_output.items,
882 } };
883 } else if (std.mem.eql(u8, key, "expect_compile_log")) {
884 fatal("line {d}: 'expect_compile_log' must immediately follow 'expect_error'", .{line_n});
885 } else {
886 fatal("line {d}: unrecognized key '{s}'", .{ line_n, key });
887 }
888 }
889 }
890
891 if (changes.items.len > 0) {
892 const last_update = &updates.items[updates.items.len - 1];
893 last_update.changes = changes.items; // arena so no need for toOwnedSlice
894 last_update.deletes = deletes.items;
895 }
896
897 return .{
898 .updates = updates.items,
899 .root_source_file = root_source_file orelse fatal("missing root source file", .{}),
900 .skip_targets = skip_targets.items, // arena so no need for toOwnedSlice
901 .modules = modules.items,
902 };
903 }
904};
905
906fn requestExit(child: *std.process.Child, eval: *Eval) void {
907 if (child.stdin == null) return;
908 const io = eval.io;
909
910 var w = eval.child.stdin.?.writerStreaming(io, &.{});
911 var client: std.zig.Client = .{
912 .in = undefined,
913 .out = &w.interface,
914 };
915 client.serveBodylessMessage(.exit) catch |err| switch (err) {
916 error.WriteFailed => switch (w.err.?) {
917 error.BrokenPipe => {},
918 else => |e| eval.fatal("failed to send exit: {t}", .{e}),
919 },
920 };
921
922 // Send EOF to stdin.
923 child.stdin.?.close(io);
924 child.stdin = null;
925}
926
927fn waitChild(child: *std.process.Child, eval: *Eval) void {
928 const io = eval.io;
929 requestExit(child, eval);
930 const term = child.wait(io) catch |err| eval.fatal("child process failed: {t}", .{err});
931 switch (term) {
932 .exited => |code| if (code != 0) eval.fatal("compiler failed with code {d}", .{code}),
933 .signal => |sig| eval.fatal("compiler terminated with signal {t}", .{sig}),
934 .stopped => |sig| eval.fatal("compiler stopped unexpectedly with signal {t}", .{sig}),
935 .unknown => eval.fatal("compiler terminated unexpectedly", .{}),
936 }
937}
938
939fn parseExpectedError(str: []const u8, l: usize) Case.ExpectedError {
940 // #expect_error=foo.zig:1:2: error: the error message
941 // #expect_error=foo.zig:1:2: note: and a note
942
943 const fatal = std.process.fatal;
944
945 var it = std.mem.splitScalar(u8, str, ':');
946 const filename = it.first();
947 const line_str, const column_str = if (filename.len > 0) .{
948 it.next() orelse fatal("line {d}: incomplete error specification", .{l}),
949 it.next() orelse fatal("line {d}: incomplete error specification", .{l}),
950 } else .{ undefined, undefined };
951 const error_or_note_str = std.mem.trim(
952 u8,
953 it.next() orelse fatal("line {d}: incomplete error specification", .{l}),
954 " ",
955 );
956
957 const is_note = if (std.mem.eql(u8, error_or_note_str, "error"))
958 false
959 else if (std.mem.eql(u8, error_or_note_str, "note"))
960 true
961 else
962 fatal("line {d}: expeted 'error' or 'note', found '{s}'", .{ l, error_or_note_str });
963
964 const message = std.mem.trim(u8, it.rest(), " ");
965 if (message.len == 0) fatal("line {d}: empty error message", .{l});
966
967 return .{
968 .is_note = is_note,
969 .msg = message,
970 .src = if (filename.len == 0) null else .{
971 .filename = filename,
972 .line = std.fmt.parseInt(u32, line_str, 10) catch
973 fatal("line {d}: invalid line number '{s}'", .{ l, line_str }),
974 .column = std.fmt.parseInt(u32, column_str, 10) catch
975 fatal("line {d}: invalid column number '{s}'", .{ l, column_str }),
976 },
977 };
978}
979
980fn rand64(io: Io) u64 {
981 var x: u64 = undefined;
982 io.random(@ptrCast(&x));
983 return x;
984}
985
986/// Calls `std.process.fatal` on error. The error messages are prefixed with `err_prefix`.
987fn parseTargetQueryAndBackend(input_str: []const u8, err_prefix: []const u8) struct { std.Target.Query, Backend } {
988 const fatal = std.process.fatal;
989
990 const split_idx = std.mem.findScalarLast(u8, input_str, '-') orelse
991 fatal("{s}target does not include backend", .{err_prefix});
992
993 const query = input_str[0..split_idx];
994
995 const backend_str = input_str[split_idx + 1 ..];
996 const backend: Backend = std.meta.stringToEnum(Backend, backend_str) orelse
997 fatal("{s}invalid backend '{s}'", .{ err_prefix, backend_str });
998
999 const parsed_query = std.Build.parseTargetQuery(.{
1000 .arch_os_abi = query,
1001 .object_format = switch (backend) {
1002 .sema, .selfhosted, .llvm => null,
1003 .cbe => "c",
1004 },
1005 }) catch fatal("{s}invalid target query '{s}'", .{ err_prefix, query });
1006
1007 return .{ parsed_query, backend };
1008}
1009
1010fn badUsage(comptime fmt: []const u8, args: anytype) noreturn {
1011 std.log.err(fmt ++ "\n{s}", args ++ .{usage});
1012 std.process.exit(1);
1013}