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