| ... | ... | @@ -0,0 +1,403 @@ |
| 1 | const std = @import("std"); |
| 2 | const fatal = std.process.fatal; |
| 3 | const Allocator = std.mem.Allocator; |
| 4 | |
| 5 | pub fn main() !void { |
| 6 | var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); |
| 7 | defer arena_instance.deinit(); |
| 8 | const arena = arena_instance.allocator(); |
| 9 | |
| 10 | const args = try std.process.argsAlloc(arena); |
| 11 | const zig_exe = args[1]; |
| 12 | const input_file_name = args[2]; |
| 13 | |
| 14 | const input_file_bytes = try std.fs.cwd().readFileAlloc(arena, input_file_name, std.math.maxInt(u32)); |
| 15 | const case = try Case.parse(arena, input_file_bytes); |
| 16 | |
| 17 | const prog_node = std.Progress.start(.{}); |
| 18 | defer prog_node.end(); |
| 19 | |
| 20 | const rand_int = std.crypto.random.int(u64); |
| 21 | const tmp_dir_path = "tmp_" ++ std.fmt.hex(rand_int); |
| 22 | const tmp_dir = try std.fs.cwd().makeOpenPath(tmp_dir_path, .{}); |
| 23 | |
| 24 | const child_prog_node = prog_node.start("zig build-exe", 0); |
| 25 | defer child_prog_node.end(); |
| 26 | |
| 27 | var child = std.process.Child.init(&.{ |
| 28 | // Convert incr-check-relative path to subprocess-relative path. |
| 29 | try std.fs.path.relative(arena, tmp_dir_path, zig_exe), |
| 30 | "build-exe", |
| 31 | case.root_source_file, |
| 32 | "-fno-llvm", |
| 33 | "-fno-lld", |
| 34 | "-fincremental", |
| 35 | "-target", |
| 36 | case.target_query, |
| 37 | "--cache-dir", |
| 38 | ".local-cache", |
| 39 | "--global-cache-dir", |
| 40 | ".global_cache", |
| 41 | "--listen=-", |
| 42 | }, arena); |
| 43 | |
| 44 | child.stdin_behavior = .Pipe; |
| 45 | child.stdout_behavior = .Pipe; |
| 46 | child.stderr_behavior = .Pipe; |
| 47 | child.progress_node = child_prog_node; |
| 48 | child.cwd_dir = tmp_dir; |
| 49 | child.cwd = tmp_dir_path; |
| 50 | |
| 51 | var eval: Eval = .{ |
| 52 | .arena = arena, |
| 53 | .case = case, |
| 54 | .tmp_dir = tmp_dir, |
| 55 | .tmp_dir_path = tmp_dir_path, |
| 56 | .child = &child, |
| 57 | }; |
| 58 | |
| 59 | try child.spawn(); |
| 60 | |
| 61 | var poller = std.io.poll(arena, Eval.StreamEnum, .{ |
| 62 | .stdout = child.stdout.?, |
| 63 | .stderr = child.stderr.?, |
| 64 | }); |
| 65 | defer poller.deinit(); |
| 66 | |
| 67 | for (case.updates) |update| { |
| 68 | eval.write(update); |
| 69 | try eval.requestUpdate(); |
| 70 | try eval.check(&poller, update); |
| 71 | } |
| 72 | |
| 73 | try eval.end(&poller); |
| 74 | |
| 75 | waitChild(&child); |
| 76 | } |
| 77 | |
| 78 | const Eval = struct { |
| 79 | arena: Allocator, |
| 80 | case: Case, |
| 81 | tmp_dir: std.fs.Dir, |
| 82 | tmp_dir_path: []const u8, |
| 83 | child: *std.process.Child, |
| 84 | |
| 85 | const StreamEnum = enum { stdout, stderr }; |
| 86 | const Poller = std.io.Poller(StreamEnum); |
| 87 | |
| 88 | /// Currently this function assumes the previous updates have already been written. |
| 89 | fn write(eval: *Eval, update: Case.Update) void { |
| 90 | for (update.changes) |full_contents| { |
| 91 | eval.tmp_dir.writeFile(.{ |
| 92 | .sub_path = full_contents.name, |
| 93 | .data = full_contents.bytes, |
| 94 | }) catch |err| { |
| 95 | fatal("failed to update '{s}': {s}", .{ full_contents.name, @errorName(err) }); |
| 96 | }; |
| 97 | } |
| 98 | for (update.deletes) |doomed_name| { |
| 99 | eval.tmp_dir.deleteFile(doomed_name) catch |err| { |
| 100 | fatal("failed to delete '{s}': {s}", .{ doomed_name, @errorName(err) }); |
| 101 | }; |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | fn check(eval: *Eval, poller: *Poller, update: Case.Update) !void { |
| 106 | const arena = eval.arena; |
| 107 | const Header = std.zig.Server.Message.Header; |
| 108 | const stdout = poller.fifo(.stdout); |
| 109 | const stderr = poller.fifo(.stderr); |
| 110 | |
| 111 | poll: while (true) { |
| 112 | while (stdout.readableLength() < @sizeOf(Header)) { |
| 113 | if (!(try poller.poll())) break :poll; |
| 114 | } |
| 115 | const header = stdout.reader().readStruct(Header) catch unreachable; |
| 116 | while (stdout.readableLength() < header.bytes_len) { |
| 117 | if (!(try poller.poll())) break :poll; |
| 118 | } |
| 119 | const body = stdout.readableSliceOfLen(header.bytes_len); |
| 120 | |
| 121 | switch (header.tag) { |
| 122 | .error_bundle => { |
| 123 | const EbHdr = std.zig.Server.Message.ErrorBundle; |
| 124 | const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body)); |
| 125 | const extra_bytes = |
| 126 | body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len]; |
| 127 | const string_bytes = |
| 128 | body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len]; |
| 129 | // TODO: use @ptrCast when the compiler supports it |
| 130 | const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes); |
| 131 | const extra_array = try arena.alloc(u32, unaligned_extra.len); |
| 132 | @memcpy(extra_array, unaligned_extra); |
| 133 | const result_error_bundle: std.zig.ErrorBundle = .{ |
| 134 | .string_bytes = try arena.dupe(u8, string_bytes), |
| 135 | .extra = extra_array, |
| 136 | }; |
| 137 | if (stderr.readableLength() > 0) { |
| 138 | const stderr_data = try stderr.toOwnedSlice(); |
| 139 | fatal("error_bundle included unexpected stderr:\n{s}", .{stderr_data}); |
| 140 | } |
| 141 | try eval.checkErrorOutcome(update, result_error_bundle); |
| 142 | // This message indicates the end of the update. |
| 143 | stdout.discard(body.len); |
| 144 | return; |
| 145 | }, |
| 146 | .emit_bin_path => { |
| 147 | const EbpHdr = std.zig.Server.Message.EmitBinPath; |
| 148 | const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body)); |
| 149 | _ = ebp_hdr; |
| 150 | const result_binary = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]); |
| 151 | if (stderr.readableLength() > 0) { |
| 152 | const stderr_data = try stderr.toOwnedSlice(); |
| 153 | fatal("emit_bin_path included unexpected stderr:\n{s}", .{stderr_data}); |
| 154 | } |
| 155 | try eval.checkSuccessOutcome(update, result_binary); |
| 156 | // This message indicates the end of the update. |
| 157 | stdout.discard(body.len); |
| 158 | return; |
| 159 | }, |
| 160 | else => { |
| 161 | // Ignore other messages. |
| 162 | stdout.discard(body.len); |
| 163 | }, |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | if (stderr.readableLength() > 0) { |
| 168 | const stderr_data = try stderr.toOwnedSlice(); |
| 169 | fatal("update '{s}' failed:\n{s}", .{ update.name, stderr_data }); |
| 170 | } |
| 171 | |
| 172 | waitChild(eval.child); |
| 173 | fatal("update '{s}': compiler failed to send error_bundle or emit_bin_path", .{update.name}); |
| 174 | } |
| 175 | |
| 176 | fn checkErrorOutcome(eval: *Eval, update: Case.Update, error_bundle: std.zig.ErrorBundle) !void { |
| 177 | _ = eval; |
| 178 | switch (update.outcome) { |
| 179 | .unknown => return, |
| 180 | .compile_errors => |expected_errors| { |
| 181 | for (expected_errors) |expected_error| { |
| 182 | _ = expected_error; |
| 183 | @panic("TODO check if the expected error matches the compile errors"); |
| 184 | } |
| 185 | }, |
| 186 | .stdout, .exit_code => { |
| 187 | const color: std.zig.Color = .auto; |
| 188 | error_bundle.renderToStdErr(color.renderOptions()); |
| 189 | fatal("update '{s}': unexpected compile errors", .{update.name}); |
| 190 | }, |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | fn checkSuccessOutcome(eval: *Eval, update: Case.Update, binary_path: []const u8) !void { |
| 195 | switch (update.outcome) { |
| 196 | .unknown => return, |
| 197 | .compile_errors => fatal("expected compile errors but compilation incorrectly succeeded", .{}), |
| 198 | .stdout, .exit_code => {}, |
| 199 | } |
| 200 | const result = std.process.Child.run(.{ |
| 201 | .allocator = eval.arena, |
| 202 | .argv = &.{binary_path}, |
| 203 | .cwd_dir = eval.tmp_dir, |
| 204 | .cwd = eval.tmp_dir_path, |
| 205 | }) catch |err| { |
| 206 | fatal("update '{s}': failed to run the generated executable '{s}': {s}", .{ |
| 207 | update.name, binary_path, @errorName(err), |
| 208 | }); |
| 209 | }; |
| 210 | if (result.stderr.len != 0) { |
| 211 | std.log.err("update '{s}': generated executable '{s}' had unexpected stderr:\n{s}", .{ |
| 212 | update.name, binary_path, result.stderr, |
| 213 | }); |
| 214 | } |
| 215 | switch (result.term) { |
| 216 | .Exited => |code| switch (update.outcome) { |
| 217 | .unknown, .compile_errors => unreachable, |
| 218 | .stdout => |expected_stdout| { |
| 219 | if (code != 0) { |
| 220 | fatal("update '{s}': generated executable '{s}' failed with code {d}", .{ |
| 221 | update.name, binary_path, code, |
| 222 | }); |
| 223 | } |
| 224 | try std.testing.expectEqualStrings(expected_stdout, result.stdout); |
| 225 | }, |
| 226 | .exit_code => |expected_code| try std.testing.expectEqual(expected_code, result.term.Exited), |
| 227 | }, |
| 228 | .Signal, .Stopped, .Unknown => { |
| 229 | fatal("update '{s}': generated executable '{s}' terminated unexpectedly", .{ |
| 230 | update.name, binary_path, |
| 231 | }); |
| 232 | }, |
| 233 | } |
| 234 | if (result.stderr.len != 0) std.process.exit(1); |
| 235 | } |
| 236 | |
| 237 | fn requestUpdate(eval: *Eval) !void { |
| 238 | const header: std.zig.Client.Message.Header = .{ |
| 239 | .tag = .update, |
| 240 | .bytes_len = 0, |
| 241 | }; |
| 242 | try eval.child.stdin.?.writeAll(std.mem.asBytes(&header)); |
| 243 | } |
| 244 | |
| 245 | fn end(eval: *Eval, poller: *Poller) !void { |
| 246 | requestExit(eval.child); |
| 247 | |
| 248 | const Header = std.zig.Server.Message.Header; |
| 249 | const stdout = poller.fifo(.stdout); |
| 250 | const stderr = poller.fifo(.stderr); |
| 251 | |
| 252 | poll: while (true) { |
| 253 | while (stdout.readableLength() < @sizeOf(Header)) { |
| 254 | if (!(try poller.poll())) break :poll; |
| 255 | } |
| 256 | const header = stdout.reader().readStruct(Header) catch unreachable; |
| 257 | while (stdout.readableLength() < header.bytes_len) { |
| 258 | if (!(try poller.poll())) break :poll; |
| 259 | } |
| 260 | const body = stdout.readableSliceOfLen(header.bytes_len); |
| 261 | stdout.discard(body.len); |
| 262 | } |
| 263 | |
| 264 | if (stderr.readableLength() > 0) { |
| 265 | const stderr_data = try stderr.toOwnedSlice(); |
| 266 | fatal("unexpected stderr:\n{s}", .{stderr_data}); |
| 267 | } |
| 268 | } |
| 269 | }; |
| 270 | |
| 271 | const Case = struct { |
| 272 | updates: []Update, |
| 273 | root_source_file: []const u8, |
| 274 | target_query: []const u8, |
| 275 | |
| 276 | const Update = struct { |
| 277 | name: []const u8, |
| 278 | outcome: Outcome, |
| 279 | changes: []const FullContents = &.{}, |
| 280 | deletes: []const []const u8 = &.{}, |
| 281 | }; |
| 282 | |
| 283 | const FullContents = struct { |
| 284 | name: []const u8, |
| 285 | bytes: []const u8, |
| 286 | }; |
| 287 | |
| 288 | const Outcome = union(enum) { |
| 289 | unknown, |
| 290 | compile_errors: []const ExpectedError, |
| 291 | stdout: []const u8, |
| 292 | exit_code: u8, |
| 293 | }; |
| 294 | |
| 295 | const ExpectedError = struct { |
| 296 | file_name: ?[]const u8 = null, |
| 297 | line: ?u32 = null, |
| 298 | column: ?u32 = null, |
| 299 | msg_exact: ?[]const u8 = null, |
| 300 | msg_substring: ?[]const u8 = null, |
| 301 | }; |
| 302 | |
| 303 | fn parse(arena: Allocator, bytes: []const u8) !Case { |
| 304 | var updates: std.ArrayListUnmanaged(Update) = .{}; |
| 305 | var changes: std.ArrayListUnmanaged(FullContents) = .{}; |
| 306 | var target_query: ?[]const u8 = null; |
| 307 | var it = std.mem.splitScalar(u8, bytes, '\n'); |
| 308 | var line_n: usize = 1; |
| 309 | var root_source_file: ?[]const u8 = null; |
| 310 | while (it.next()) |line| : (line_n += 1) { |
| 311 | if (std.mem.startsWith(u8, line, "#")) { |
| 312 | var line_it = std.mem.splitScalar(u8, line, '='); |
| 313 | const key = line_it.first()[1..]; |
| 314 | const val = line_it.rest(); |
| 315 | if (val.len == 0) { |
| 316 | fatal("line {d}: missing value", .{line_n}); |
| 317 | } else if (std.mem.eql(u8, key, "target")) { |
| 318 | if (target_query != null) fatal("line {d}: duplicate target", .{line_n}); |
| 319 | target_query = val; |
| 320 | } else if (std.mem.eql(u8, key, "update")) { |
| 321 | if (updates.items.len > 0) { |
| 322 | const last_update = &updates.items[updates.items.len - 1]; |
| 323 | last_update.changes = try changes.toOwnedSlice(arena); |
| 324 | } |
| 325 | try updates.append(arena, .{ |
| 326 | .name = val, |
| 327 | .outcome = .unknown, |
| 328 | }); |
| 329 | } else if (std.mem.eql(u8, key, "file")) { |
| 330 | if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n}); |
| 331 | |
| 332 | if (root_source_file == null) |
| 333 | root_source_file = val; |
| 334 | |
| 335 | const start_index = it.index.?; |
| 336 | const src = while (true) : (line_n += 1) { |
| 337 | const old = it; |
| 338 | const next_line = it.next() orelse fatal("line {d}: unexpected EOF", .{line_n}); |
| 339 | if (std.mem.startsWith(u8, next_line, "#")) { |
| 340 | const end_index = old.index.?; |
| 341 | const src = bytes[start_index..end_index]; |
| 342 | it = old; |
| 343 | break src; |
| 344 | } |
| 345 | }; |
| 346 | |
| 347 | try changes.append(arena, .{ |
| 348 | .name = val, |
| 349 | .bytes = src, |
| 350 | }); |
| 351 | } else if (std.mem.eql(u8, key, "expect_stdout")) { |
| 352 | if (updates.items.len == 0) fatal("line {d}: expect directive before update", .{line_n}); |
| 353 | const last_update = &updates.items[updates.items.len - 1]; |
| 354 | if (last_update.outcome != .unknown) fatal("line {d}: conflicting expect directive", .{line_n}); |
| 355 | last_update.outcome = .{ |
| 356 | .stdout = std.zig.string_literal.parseAlloc(arena, val) catch |err| { |
| 357 | fatal("line {d}: bad string literal: {s}", .{ line_n, @errorName(err) }); |
| 358 | }, |
| 359 | }; |
| 360 | } else { |
| 361 | fatal("line {d}: unrecognized key '{s}'", .{ line_n, key }); |
| 362 | } |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | if (changes.items.len > 0) { |
| 367 | const last_update = &updates.items[updates.items.len - 1]; |
| 368 | last_update.changes = try changes.toOwnedSlice(arena); |
| 369 | } |
| 370 | |
| 371 | return .{ |
| 372 | .updates = updates.items, |
| 373 | .root_source_file = root_source_file orelse fatal("missing root source file", .{}), |
| 374 | .target_query = target_query orelse fatal("missing target", .{}), |
| 375 | }; |
| 376 | } |
| 377 | }; |
| 378 | |
| 379 | fn requestExit(child: *std.process.Child) void { |
| 380 | if (child.stdin == null) return; |
| 381 | |
| 382 | const header: std.zig.Client.Message.Header = .{ |
| 383 | .tag = .exit, |
| 384 | .bytes_len = 0, |
| 385 | }; |
| 386 | child.stdin.?.writeAll(std.mem.asBytes(&header)) catch |err| switch (err) { |
| 387 | error.BrokenPipe => {}, |
| 388 | else => fatal("failed to send exit: {s}", .{@errorName(err)}), |
| 389 | }; |
| 390 | |
| 391 | // Send EOF to stdin. |
| 392 | child.stdin.?.close(); |
| 393 | child.stdin = null; |
| 394 | } |
| 395 | |
| 396 | fn waitChild(child: *std.process.Child) void { |
| 397 | requestExit(child); |
| 398 | const term = child.wait() catch |err| fatal("child process failed: {s}", .{@errorName(err)}); |
| 399 | switch (term) { |
| 400 | .Exited => |code| if (code != 0) fatal("compiler failed with code {d}", .{code}), |
| 401 | .Signal, .Stopped, .Unknown => fatal("compiler terminated unexpectedly", .{}), |
| 402 | } |
| 403 | } |