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