| 1 | //! The *mutable* state that `Maker` needs in order to process one node from |
| 2 | //! the build graph. |
| 3 | const Step = @This(); |
| 4 | |
| 5 | const builtin = @import("builtin"); |
| 6 | |
| 7 | const std = @import("std"); |
| 8 | const Allocator = std.mem.Allocator; |
| 9 | const Cache = std.Build.Cache; |
| 10 | const Io = std.Io; |
| 11 | const Dir = std.Io.Dir; |
| 12 | const LazyPath = std.Build.Configuration.LazyPath; |
| 13 | const Package = std.Build.Configuration.Package; |
| 14 | const Path = std.Build.Cache.Path; |
| 15 | const Configuration = std.Build.Configuration; |
| 16 | const assert = std.debug.assert; |
| 17 | |
| 18 | const WebServer = @import("WebServer.zig"); |
| 19 | const Maker = @import("../Maker.zig"); |
| 20 | |
| 21 | pub const CheckFile = @import("Step/CheckFile.zig"); |
| 22 | pub const Compile = @import("Step/Compile.zig"); |
| 23 | pub const ConfigHeader = @import("Step/ConfigHeader.zig"); |
| 24 | pub const FindProgram = @import("Step/FindProgram.zig"); |
| 25 | pub const Fmt = @import("Step/Fmt.zig"); |
| 26 | pub const InstallArtifact = @import("Step/InstallArtifact.zig"); |
| 27 | pub const InstallDir = @import("Step/InstallDir.zig"); |
| 28 | pub const InstallFile = @import("Step/InstallFile.zig"); |
| 29 | pub const ObjCopy = @import("Step/ObjCopy.zig"); |
| 30 | pub const Options = @import("Step/Options.zig"); |
| 31 | pub const Run = @import("Step/Run.zig"); |
| 32 | pub const TranslateC = @import("Step/TranslateC.zig"); |
| 33 | pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig"); |
| 34 | pub const WriteFile = @import("Step/WriteFile.zig"); |
| 35 | |
| 36 | /// Avoid false sharing. |
| 37 | _: void align(std.atomic.cache_line) = {}, |
| 38 | |
| 39 | /// Extra data for specific types of steps. |
| 40 | extended: Extended, |
| 41 | |
| 42 | /// This field is atomically accessed multi-threaded. |
| 43 | state: State = .precheck_unstarted, |
| 44 | |
| 45 | dependants: std.ArrayList(Configuration.Step.Index) = .empty, |
| 46 | /// Collects the set of files that retrigger this step to run. |
| 47 | /// |
| 48 | /// This is used by the build system's implementation of `--watch` but it can |
| 49 | /// also be potentially useful for IDEs to know what effects editing a |
| 50 | /// particular file has. |
| 51 | /// |
| 52 | /// Populated within `make`. Implementation may choose to clear and repopulate, |
| 53 | /// retain previous value, or update. |
| 54 | inputs: Inputs = .init, |
| 55 | pending_deps: u32 = undefined, |
| 56 | |
| 57 | /// Array list and internal memory owned by process arena. |
| 58 | result_error_msgs: std.ArrayList([]const u8) = .empty, |
| 59 | result_error_bundle: std.zig.ErrorBundle = .empty, |
| 60 | /// Owned by `Maker.gpa`. |
| 61 | result_stderr: []const u8 = "", |
| 62 | result_cached: bool = false, |
| 63 | /// Indicates error information is missing due to allocation failure. |
| 64 | result_oom: bool = false, |
| 65 | result_duration_ns: ?u64 = null, |
| 66 | /// 0 means unavailable or not reported. |
| 67 | result_peak_rss: usize = 0, |
| 68 | /// If the step is failed and this field is populated, this is the command which failed. |
| 69 | /// This field may be populated even if the step succeeded. |
| 70 | /// Memory owned by `Maker.gpa`. |
| 71 | result_failed_command: ?[]const u8 = null, |
| 72 | test_results: TestResults = .{}, |
| 73 | |
| 74 | comptime { |
| 75 | // Common cache line size is 128. This check prevents accidentally crossing |
| 76 | // an additional cache line. In the future it might be nice to try to fit |
| 77 | // this struct in 128 bytes or less. |
| 78 | if (std.atomic.cache_line <= 128) assert(@sizeOf(@This()) <= 128 * 3); |
| 79 | } |
| 80 | |
| 81 | pub const Extended = union(enum) { |
| 82 | check_file: CheckFile, |
| 83 | compile: Compile, |
| 84 | config_header: ConfigHeader, |
| 85 | fail: Fail, |
| 86 | find_program: FindProgram, |
| 87 | fmt: Fmt, |
| 88 | install_artifact: InstallArtifact, |
| 89 | install_dir: InstallDir, |
| 90 | install_file: InstallFile, |
| 91 | obj_copy: ObjCopy, |
| 92 | options: Options, |
| 93 | run: Run, |
| 94 | top_level: TopLevel, |
| 95 | translate_c: TranslateC, |
| 96 | update_source_files: UpdateSourceFiles, |
| 97 | write_file: WriteFile, |
| 98 | |
| 99 | pub fn init(tag: Configuration.Step.Tag) Extended { |
| 100 | return switch (tag) { |
| 101 | .check_file => .{ .check_file = .{} }, |
| 102 | .compile => .{ .compile = .{} }, |
| 103 | .config_header => .{ .config_header = .{} }, |
| 104 | .fail => .{ .fail = .{} }, |
| 105 | .find_program => .{ .find_program = .{} }, |
| 106 | .fmt => .{ .fmt = .{} }, |
| 107 | .install_artifact => .{ .install_artifact = .{} }, |
| 108 | .install_dir => .{ .install_dir = .{} }, |
| 109 | .install_file => .{ .install_file = .{} }, |
| 110 | .obj_copy => .{ .obj_copy = .{} }, |
| 111 | .options => .{ .options = .{} }, |
| 112 | .run => .{ .run = .{} }, |
| 113 | .top_level => .{ .top_level = .{} }, |
| 114 | .translate_c => .{ .translate_c = .{} }, |
| 115 | .update_source_files => .{ .update_source_files = .{} }, |
| 116 | .write_file => .{ .write_file = .{} }, |
| 117 | }; |
| 118 | } |
| 119 | |
| 120 | pub const TopLevel = struct { |
| 121 | pub fn make( |
| 122 | top_level: *TopLevel, |
| 123 | step_index: Configuration.Step.Index, |
| 124 | maker: *Maker, |
| 125 | progress_node: std.Progress.Node, |
| 126 | ) Step.ExtendedMakeError!void { |
| 127 | _ = top_level; |
| 128 | _ = step_index; |
| 129 | _ = maker; |
| 130 | _ = progress_node; |
| 131 | } |
| 132 | }; |
| 133 | |
| 134 | pub const Fail = struct { |
| 135 | pub fn make( |
| 136 | this: *@This(), |
| 137 | step_index: Configuration.Step.Index, |
| 138 | maker: *Maker, |
| 139 | progress_node: std.Progress.Node, |
| 140 | ) Step.ExtendedMakeError!void { |
| 141 | _ = this; |
| 142 | _ = progress_node; |
| 143 | const graph = maker.graph; |
| 144 | const arena = graph.arena; // TODO don't leak into the process arena |
| 145 | const conf = &maker.scanned_config.configuration; |
| 146 | const step = maker.stepByIndex(step_index); |
| 147 | const conf_step = step_index.ptr(conf); |
| 148 | const conf_fail = conf_step.extended.get(conf.extra).fail; |
| 149 | |
| 150 | try step.result_error_msgs.append(arena, conf_fail.msg.slice(conf)); |
| 151 | return error.MakeFailed; |
| 152 | } |
| 153 | }; |
| 154 | }; |
| 155 | |
| 156 | pub const State = enum { |
| 157 | precheck_unstarted, |
| 158 | precheck_started, |
| 159 | /// This is also used to indicate "dirty" steps that have been modified |
| 160 | /// after a previous build completed, in which case, the step may or may |
| 161 | /// not have been completed before. Either way, one or more of its direct |
| 162 | /// file system inputs have been modified, meaning that the step needs to |
| 163 | /// be re-evaluated. |
| 164 | precheck_done, |
| 165 | dependency_failure, |
| 166 | /// Handled exactly the same as `dependency_failure` except communicates |
| 167 | /// that the dependency didn't fail but rather was skipped. |
| 168 | dependency_skipped, |
| 169 | success, |
| 170 | failure, |
| 171 | /// This state indicates that the step did not complete, however, it also did not fail, |
| 172 | /// and it is safe to continue executing its dependencies. |
| 173 | skipped, |
| 174 | /// This step was skipped because it specified a max_rss that exceeded the runner's maximum. |
| 175 | /// It is not safe to run its dependencies. |
| 176 | skipped_oom, |
| 177 | }; |
| 178 | |
| 179 | pub const Inputs = struct { |
| 180 | table: Table, |
| 181 | |
| 182 | pub const init: Inputs = .{ |
| 183 | .table = .{}, |
| 184 | }; |
| 185 | |
| 186 | pub const Table = std.array_hash_map.Custom(Path, Files, Path.TableAdapter, false); |
| 187 | /// The special file name "." means any changes inside the directory. |
| 188 | pub const Files = std.ArrayList([]const u8); |
| 189 | |
| 190 | pub fn populated(inputs: *Inputs) bool { |
| 191 | return inputs.table.count() != 0; |
| 192 | } |
| 193 | |
| 194 | pub fn clear(inputs: *Inputs, gpa: Allocator) void { |
| 195 | for (inputs.table.values()) |*files| files.deinit(gpa); |
| 196 | inputs.table.clearRetainingCapacity(); |
| 197 | } |
| 198 | |
| 199 | pub fn deinit(inputs: *Inputs, gpa: Allocator) void { |
| 200 | clear(inputs, gpa); |
| 201 | inputs.table.deinit(gpa); |
| 202 | } |
| 203 | }; |
| 204 | |
| 205 | pub const TestResults = struct { |
| 206 | /// The total number of tests in the step. Every test has a "status" from the following: |
| 207 | /// * passed |
| 208 | /// * skipped |
| 209 | /// * failed cleanly |
| 210 | /// * crashed |
| 211 | /// * timed out |
| 212 | test_count: u32 = 0, |
| 213 | |
| 214 | /// The number of tests which were skipped (`error.SkipZigTest`). |
| 215 | skip_count: u32 = 0, |
| 216 | /// The number of tests which failed cleanly. |
| 217 | fail_count: u32 = 0, |
| 218 | /// The number of tests which terminated unexpectedly, i.e. crashed. |
| 219 | crash_count: u32 = 0, |
| 220 | /// The number of tests which timed out. |
| 221 | timeout_count: u32 = 0, |
| 222 | |
| 223 | /// The number of detected memory leaks. The associated test may still have passed; indeed, *all* |
| 224 | /// individual tests may have passed. However, the step as a whole fails if any test has leaks. |
| 225 | leak_count: u32 = 0, |
| 226 | /// The number of detected error logs. The associated test may still have passed; indeed, *all* |
| 227 | /// individual tests may have passed. However, the step as a whole fails if any test logs errors. |
| 228 | log_err_count: u32 = 0, |
| 229 | |
| 230 | pub fn isSuccess(tr: TestResults) bool { |
| 231 | // all steps are success or skip |
| 232 | return tr.fail_count == 0 and |
| 233 | tr.crash_count == 0 and |
| 234 | tr.timeout_count == 0 and |
| 235 | // no (otherwise successful) step leaked memory or logged errors |
| 236 | tr.leak_count == 0 and |
| 237 | tr.log_err_count == 0; |
| 238 | } |
| 239 | |
| 240 | /// Computes the number of tests which passed from the other values. |
| 241 | pub fn passCount(tr: TestResults) u32 { |
| 242 | return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count; |
| 243 | } |
| 244 | }; |
| 245 | |
| 246 | pub const MakeError = error{ |
| 247 | /// Indicates the error is already reported. |
| 248 | MakeFailed, |
| 249 | MakeSkipped, |
| 250 | } || Io.Cancelable; |
| 251 | |
| 252 | pub const ExtendedMakeError = MakeError || Allocator.Error; |
| 253 | |
| 254 | pub fn make( |
| 255 | step_index: Configuration.Step.Index, |
| 256 | maker: *Maker, |
| 257 | progress_node: std.Progress.Node, |
| 258 | ) MakeError!void { |
| 259 | const graph = maker.graph; |
| 260 | const arena = graph.arena; // TODO don't leak into the process arena |
| 261 | const io = graph.io; |
| 262 | const c = &maker.scanned_config.configuration; |
| 263 | const conf_step = step_index.ptr(c); |
| 264 | const s = maker.stepByIndex(step_index); |
| 265 | |
| 266 | var start_ts: ?Io.Timestamp = t: { |
| 267 | if (!graph.time_report) break :t null; |
| 268 | const flags = conf_step.flags(c); |
| 269 | switch (flags.tag) { |
| 270 | .compile => break :t null, |
| 271 | .run => { |
| 272 | const run_flags: Configuration.Step.Run.Flags = @bitCast(flags); |
| 273 | if (run_flags.stdio == .zig_test) break :t null; |
| 274 | }, |
| 275 | else => {}, |
| 276 | } |
| 277 | break :t Io.Clock.awake.now(io); |
| 278 | }; |
| 279 | const make_result = switch (s.extended) { |
| 280 | inline else => |*extended| extended.make(step_index, maker, progress_node), |
| 281 | }; |
| 282 | if (start_ts) |*ts| { |
| 283 | const duration = ts.untilNow(io, .awake); |
| 284 | maker.web_server.?.updateTimeReportGeneric(step_index, duration); |
| 285 | } |
| 286 | |
| 287 | make_result catch |err| switch (err) { |
| 288 | error.MakeFailed, error.MakeSkipped => |e| return e, |
| 289 | error.OutOfMemory => { |
| 290 | s.result_oom = true; |
| 291 | return error.MakeFailed; |
| 292 | }, |
| 293 | error.Canceled => |e| return e, |
| 294 | }; |
| 295 | |
| 296 | if (!s.test_results.isSuccess()) { |
| 297 | return error.MakeFailed; |
| 298 | } |
| 299 | |
| 300 | const max_rss = conf_step.max_rss.toBytes(); |
| 301 | if (max_rss != 0 and s.result_peak_rss > max_rss) { |
| 302 | if (std.fmt.allocPrint( |
| 303 | arena, |
| 304 | "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", |
| 305 | .{ s.result_peak_rss, max_rss }, |
| 306 | )) |msg| { |
| 307 | s.oomWrap(s.result_error_msgs.append(arena, msg)); |
| 308 | } else |_| s.result_oom = true; |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | /// Prepares the step for being re-evaluated. |
| 313 | pub fn reset(step: *Step, maker: *Maker) void { |
| 314 | assert(step.state == .precheck_done); |
| 315 | const gpa = maker.gpa; |
| 316 | |
| 317 | clearFailedCommand(step, gpa); |
| 318 | clearResultStderr(step, gpa); |
| 319 | step.result_error_msgs.clearRetainingCapacity(); |
| 320 | step.result_cached = false; |
| 321 | step.result_duration_ns = null; |
| 322 | step.result_peak_rss = 0; |
| 323 | step.test_results = .{}; |
| 324 | clearWatchInputs(step, maker); |
| 325 | clearErrorBundle(step, gpa); |
| 326 | } |
| 327 | |
| 328 | pub const CaptureChildProcessError = error{ |
| 329 | FileNotFound, |
| 330 | } || ExtendedMakeError; |
| 331 | |
| 332 | pub const CaptureChildProcessOptions = struct { |
| 333 | argv: []const []const u8, |
| 334 | progress_node: std.Progress.Node = .none, |
| 335 | environ_map: ?*const std.process.Environ.Map = null, |
| 336 | allow_failure: bool = false, |
| 337 | }; |
| 338 | |
| 339 | /// Populates `s.result_failed_command` unconditionally. |
| 340 | pub fn captureChildProcess( |
| 341 | s: *Step, |
| 342 | maker: *Maker, |
| 343 | allocator: Allocator, |
| 344 | options: CaptureChildProcessOptions, |
| 345 | ) !std.process.RunResult { |
| 346 | const gpa = maker.gpa; |
| 347 | const graph = maker.graph; |
| 348 | const io = graph.io; |
| 349 | |
| 350 | s.setFailedCommand(gpa, options.argv, .{}); |
| 351 | |
| 352 | try handleChildProcUnsupported(s, maker); |
| 353 | try graph.handleVerbose(null, null, options.argv); |
| 354 | |
| 355 | const result = std.process.run(allocator, io, .{ |
| 356 | .argv = options.argv, |
| 357 | .environ_map = options.environ_map orelse &graph.environ_map, |
| 358 | .progress_node = options.progress_node, |
| 359 | }) catch |err| { |
| 360 | switch (err) { |
| 361 | error.OutOfMemory, error.Canceled => |e| return e, |
| 362 | error.FileNotFound => |e| if (options.allow_failure) return e, |
| 363 | else => {}, |
| 364 | } |
| 365 | return s.fail(maker, "failed to run {s}: {t}", .{ options.argv[0], err }); |
| 366 | }; |
| 367 | |
| 368 | if (result.stderr.len > 0) try s.result_error_msgs.append(graph.arena, result.stderr); |
| 369 | |
| 370 | return result; |
| 371 | } |
| 372 | |
| 373 | pub fn clearErrorBundle(s: *Step, gpa: Allocator) void { |
| 374 | s.result_error_bundle.deinit(gpa); |
| 375 | s.result_error_bundle = .empty; |
| 376 | } |
| 377 | |
| 378 | pub fn clearFailedCommand(s: *Step, gpa: Allocator) void { |
| 379 | if (s.result_failed_command) |cmd| { |
| 380 | gpa.free(cmd); |
| 381 | s.result_failed_command = null; |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | pub fn setFailedCommand( |
| 386 | s: *Step, |
| 387 | gpa: Allocator, |
| 388 | argv: []const []const u8, |
| 389 | options: std.zig.AllocPrintCmdOptions, |
| 390 | ) void { |
| 391 | s.clearFailedCommand(gpa); |
| 392 | s.result_failed_command = std.zig.allocPrintCmd(gpa, argv, options) catch |err| switch (err) { |
| 393 | error.OutOfMemory => { |
| 394 | s.result_oom = true; |
| 395 | return; |
| 396 | }, |
| 397 | }; |
| 398 | } |
| 399 | |
| 400 | pub const FailError = error{ OutOfMemory, MakeFailed }; |
| 401 | |
| 402 | pub fn fail(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) FailError { |
| 403 | try step.addError(maker, fmt, args); |
| 404 | return error.MakeFailed; |
| 405 | } |
| 406 | |
| 407 | pub fn addError(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { |
| 408 | const graph = maker.graph; |
| 409 | const arena = graph.arena; // TODO don't leak into the process_arena |
| 410 | const msg = try std.fmt.allocPrint(arena, fmt, args); |
| 411 | try step.result_error_msgs.append(arena, msg); |
| 412 | } |
| 413 | |
| 414 | pub const ZigProcess = struct { |
| 415 | child: std.process.Child, |
| 416 | multi_reader_buffer: Io.File.MultiReader.Buffer(2), |
| 417 | multi_reader: Io.File.MultiReader, |
| 418 | progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn, |
| 419 | |
| 420 | pub const StreamEnum = enum { stdout, stderr }; |
| 421 | |
| 422 | pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void { |
| 423 | zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null; |
| 424 | } |
| 425 | |
| 426 | pub fn deinit(zp: *ZigProcess, io: Io) void { |
| 427 | zp.child.kill(io); |
| 428 | zp.multi_reader.deinit(); |
| 429 | zp.* = undefined; |
| 430 | } |
| 431 | }; |
| 432 | |
| 433 | /// Assumes that argv contains `--listen=-` and that the process being spawned |
| 434 | /// is the zig compiler - the same version that compiled the build runner. |
| 435 | /// |
| 436 | /// Populates `s.result_failed_command` on failure. |
| 437 | pub fn evalZigProcess( |
| 438 | step_index: Configuration.Step.Index, |
| 439 | maker: *Maker, |
| 440 | argv: []const []const u8, |
| 441 | prog_node: std.Progress.Node, |
| 442 | watch: bool, |
| 443 | ) (Step.ExtendedMakeError || error{NeedCompileErrorCheck})!?Path { |
| 444 | const s = maker.stepByIndex(step_index); |
| 445 | const gpa = maker.gpa; |
| 446 | const graph = maker.graph; |
| 447 | const io = graph.io; |
| 448 | |
| 449 | // If an error occurs, it's happened in this command: |
| 450 | errdefer s.setFailedCommand(gpa, argv, .{}); |
| 451 | |
| 452 | if (s.getZigProcess()) |zp| update: { |
| 453 | assert(watch); |
| 454 | if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index); |
| 455 | zp.progress_ipc_index = null; |
| 456 | var exited = false; |
| 457 | defer if (exited) { |
| 458 | s.extended.compile.zig_process = null; |
| 459 | zp.deinit(io); |
| 460 | gpa.destroy(zp); |
| 461 | } else zp.saveState(prog_node); |
| 462 | const result = zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) { |
| 463 | error.BrokenPipe, error.EndOfStream => |reason| { |
| 464 | // Process restart required. |
| 465 | std.log.info("{s} restart required: {t}", .{ argv[0], reason }); |
| 466 | _ = zp.child.wait(io) catch |e| return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e }); |
| 467 | exited = true; |
| 468 | break :update; |
| 469 | }, |
| 470 | error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e, |
| 471 | else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}), |
| 472 | }; |
| 473 | |
| 474 | if (s.result_error_bundle.errorMessageCount() > 0) |
| 475 | return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); |
| 476 | |
| 477 | if (s.result_error_msgs.items.len > 0 and result == null) { |
| 478 | // Crash detected. |
| 479 | const term = zp.child.wait(io) catch |e| { |
| 480 | return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e }); |
| 481 | }; |
| 482 | s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; |
| 483 | exited = true; |
| 484 | try handleChildProcessTerm(s, maker, term); |
| 485 | return error.MakeFailed; |
| 486 | } |
| 487 | |
| 488 | return result; |
| 489 | } |
| 490 | assert(argv.len != 0); |
| 491 | |
| 492 | try handleChildProcUnsupported(s, maker); |
| 493 | try graph.handleVerbose(null, null, argv); |
| 494 | |
| 495 | const zp = try gpa.create(ZigProcess); |
| 496 | defer if (!watch) gpa.destroy(zp); |
| 497 | |
| 498 | zp.child = std.process.spawn(io, .{ |
| 499 | .argv = argv, |
| 500 | .environ_map = &graph.environ_map, |
| 501 | .stdin = .pipe, |
| 502 | .stdout = .pipe, |
| 503 | .stderr = .pipe, |
| 504 | .request_resource_usage_statistics = true, |
| 505 | .progress_node = prog_node, |
| 506 | }) catch |err| return s.fail(maker, "failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); |
| 507 | |
| 508 | zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{ |
| 509 | zp.child.stdout.?, zp.child.stderr.?, |
| 510 | }); |
| 511 | if (watch) s.extended.compile.zig_process = zp; |
| 512 | defer if (!watch) zp.deinit(io); |
| 513 | |
| 514 | const result = result: { |
| 515 | defer if (watch) zp.saveState(prog_node); |
| 516 | break :result zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) { |
| 517 | error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e, |
| 518 | else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}), |
| 519 | }; |
| 520 | }; |
| 521 | |
| 522 | if (!watch) { |
| 523 | // Send EOF to stdin. |
| 524 | zp.child.stdin.?.close(io); |
| 525 | zp.child.stdin = null; |
| 526 | |
| 527 | const term = zp.child.wait(io) catch |err| { |
| 528 | return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], err }); |
| 529 | }; |
| 530 | s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; |
| 531 | |
| 532 | // Special handling for compile step that is expecting compile errors. |
| 533 | const conf = &maker.scanned_config.configuration; |
| 534 | if (term == .exited) switch (step_index.ptr(conf).extended.get(conf.extra)) { |
| 535 | .compile => |compile| if (compile.flags4.expect_errors != .none) { |
| 536 | // Note that the exit code may be 0 in this case due to the |
| 537 | // compiler server protocol. |
| 538 | return error.NeedCompileErrorCheck; |
| 539 | }, |
| 540 | else => {}, |
| 541 | }; |
| 542 | try handleChildProcessTerm(s, maker, term); |
| 543 | } |
| 544 | |
| 545 | if (s.result_error_bundle.errorMessageCount() > 0) { |
| 546 | return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); |
| 547 | } |
| 548 | |
| 549 | return result; |
| 550 | } |
| 551 | |
| 552 | fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *ZigProcess, watch: bool) !?Path { |
| 553 | const s = maker.stepByIndex(step_index); |
| 554 | const gpa = maker.gpa; |
| 555 | const graph = maker.graph; |
| 556 | const arena = graph.arena; // TODO don't leak into the process arena |
| 557 | const io = graph.io; |
| 558 | |
| 559 | const start_ts = Io.Clock.awake.now(io); |
| 560 | |
| 561 | try sendMessage(io, zp.child.stdin.?, .update); |
| 562 | if (!watch) try sendMessage(io, zp.child.stdin.?, .exit); |
| 563 | |
| 564 | var result: ?Path = null; |
| 565 | var eos_err: error{EndOfStream}!void = {}; |
| 566 | |
| 567 | var client: std.zig.Client = .{ |
| 568 | .in = zp.multi_reader.reader(0), |
| 569 | .out = undefined, |
| 570 | }; |
| 571 | |
| 572 | while (true) { |
| 573 | const header = client.receiveMessageWithMultiReader(&zp.multi_reader, .none) catch |err| switch (err) { |
| 574 | error.Timeout => unreachable, |
| 575 | error.EndOfStream => |e| { |
| 576 | if (client.in.bufferedLen() == 0) break; |
| 577 | // Better to report the crash with stderr below, but we set |
| 578 | // this in case the child exits successfully while violating |
| 579 | // this protocol. |
| 580 | eos_err = e; |
| 581 | break; |
| 582 | }, |
| 583 | else => |e| return e, |
| 584 | }; |
| 585 | const body = client.in.take(header.bytes_len) catch unreachable; |
| 586 | |
| 587 | switch (header.tag) { |
| 588 | .zig_version => { |
| 589 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) { |
| 590 | return s.fail( |
| 591 | maker, |
| 592 | "zig version mismatch build runner vs compiler: {q} vs {q}", |
| 593 | .{ builtin.zig_version_string, body }, |
| 594 | ); |
| 595 | } |
| 596 | }, |
| 597 | .error_bundle => { |
| 598 | s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); |
| 599 | // This message indicates the end of the update. |
| 600 | if (watch) break; |
| 601 | }, |
| 602 | .emit_digest => { |
| 603 | const EmitDigest = std.zig.Server.Message.EmitDigest; |
| 604 | const emit_digest: *align(1) const EmitDigest = @ptrCast(body); |
| 605 | s.result_cached = emit_digest.flags.cache_hit; |
| 606 | const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; |
| 607 | result = .{ |
| 608 | .root_dir = graph.local_cache_root, |
| 609 | .sub_path = try arena.dupe(u8, "o" ++ Dir.path.sep_str ++ Cache.binToHex(digest.*)), |
| 610 | }; |
| 611 | }, |
| 612 | .file_system_inputs => { |
| 613 | clearWatchInputs(s, maker); |
| 614 | const conf = &maker.scanned_config.configuration; |
| 615 | const conf_step = step_index.ptr(conf); |
| 616 | var it = std.mem.splitScalar(u8, body, 0); |
| 617 | while (it.next()) |prefixed_path| { |
| 618 | const prefix_index: std.zig.Server.Message.PathPrefix = @fromBackingInt(@intCast(prefixed_path[0] - 1)); |
| 619 | const sub_path = try arena.dupe(u8, prefixed_path[1..]); |
| 620 | const sub_path_dirname = Dir.path.dirname(sub_path) orelse ""; |
| 621 | switch (prefix_index) { |
| 622 | .cwd => { |
| 623 | const path: Path = .{ |
| 624 | .root_dir = .cwd(), |
| 625 | .sub_path = sub_path_dirname, |
| 626 | }; |
| 627 | try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); |
| 628 | }, |
| 629 | .zig_lib => zl: { |
| 630 | switch (conf_step.extended.get(conf.extra)) { |
| 631 | .compile => |compile| if (compile.zig_lib_dir.value) |zig_lib_dir| { |
| 632 | const resolved = try maker.resolveLazyPathIndex(arena, zig_lib_dir, step_index); |
| 633 | const appended = try resolved.join(arena, sub_path); |
| 634 | try addWatchInputPath(s, maker, appended); |
| 635 | break :zl; |
| 636 | }, |
| 637 | else => {}, |
| 638 | } |
| 639 | const path: Path = .{ |
| 640 | .root_dir = graph.zig_lib_directory, |
| 641 | .sub_path = sub_path_dirname, |
| 642 | }; |
| 643 | try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); |
| 644 | }, |
| 645 | .local_cache => { |
| 646 | const path: Path = .{ |
| 647 | .root_dir = graph.local_cache_root, |
| 648 | .sub_path = sub_path_dirname, |
| 649 | }; |
| 650 | try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); |
| 651 | }, |
| 652 | .global_cache => { |
| 653 | const path: Path = .{ |
| 654 | .root_dir = graph.global_cache_root, |
| 655 | .sub_path = sub_path_dirname, |
| 656 | }; |
| 657 | try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); |
| 658 | }, |
| 659 | .build_root => { |
| 660 | const path: Path = .{ |
| 661 | .root_dir = graph.build_root_directory, |
| 662 | .sub_path = sub_path_dirname, |
| 663 | }; |
| 664 | try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); |
| 665 | }, |
| 666 | } |
| 667 | } |
| 668 | }, |
| 669 | .time_report => if (maker.web_server) |ws| { |
| 670 | const TimeReport = std.zig.Server.Message.TimeReport; |
| 671 | const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]); |
| 672 | ws.updateTimeReportCompile(.{ |
| 673 | .compile_step = step_index, |
| 674 | .use_llvm = tr.flags.use_llvm, |
| 675 | .stats = tr.stats, |
| 676 | .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()), |
| 677 | .llvm_pass_timings_len = tr.llvm_pass_timings_len, |
| 678 | .files_len = tr.files_len, |
| 679 | .decls_len = tr.decls_len, |
| 680 | .trailing = body[@sizeOf(TimeReport)..], |
| 681 | }); |
| 682 | }, |
| 683 | else => {}, // ignore other messages |
| 684 | } |
| 685 | } |
| 686 | |
| 687 | s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()); |
| 688 | |
| 689 | const stderr_contents = zp.multi_reader.reader(1).buffered(); |
| 690 | if (stderr_contents.len > 0) { |
| 691 | try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents)); |
| 692 | } |
| 693 | |
| 694 | try eos_err; |
| 695 | |
| 696 | return result; |
| 697 | } |
| 698 | |
| 699 | pub fn getZigProcess(s: *Step) ?*ZigProcess { |
| 700 | return switch (s.extended) { |
| 701 | .compile => |*compile| compile.zig_process, |
| 702 | else => null, |
| 703 | }; |
| 704 | } |
| 705 | |
| 706 | fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { |
| 707 | const header: std.zig.Client.Message.Header = .{ |
| 708 | .tag = tag, |
| 709 | .bytes_len = 0, |
| 710 | }; |
| 711 | var w = file.writer(io, &.{}); |
| 712 | w.interface.writeStruct(header, .little) catch |err| switch (err) { |
| 713 | error.WriteFailed => return w.err.?, |
| 714 | }; |
| 715 | } |
| 716 | |
| 717 | pub inline fn handleChildProcUnsupported(s: *Step, maker: *Maker) FailError!void { |
| 718 | if (!std.process.can_spawn) |
| 719 | return s.fail(maker, "unable to spawn process: host cannot spawn child processes", .{}); |
| 720 | } |
| 721 | |
| 722 | pub fn handleChildProcessTerm(s: *Step, maker: *Maker, term: std.process.Child.Term) FailError!void { |
| 723 | if (!term.success()) return s.fail(maker, "process {f}", .{term}); |
| 724 | } |
| 725 | |
| 726 | /// Prefer `cacheHitWatched` unless you already added watch inputs |
| 727 | /// separately from using the cache system. |
| 728 | pub fn cacheHit(s: *Step, maker: *Maker, man: *Cache.Manifest, parent_progress_node: std.Progress.Node) !bool { |
| 729 | s.result_cached = man.hit(parent_progress_node) catch |err| return failWithCacheError(s, maker, man, err); |
| 730 | return s.result_cached; |
| 731 | } |
| 732 | |
| 733 | /// Clears previous watch inputs, if any, and then populates watch inputs from |
| 734 | /// the full set of files picked up by the cache manifest. |
| 735 | /// |
| 736 | /// Must be accompanied with `writeManifestAndWatch`. |
| 737 | pub fn cacheHitWatched(s: *Step, maker: *Maker, man: *Cache.Manifest, parent_progress_node: std.Progress.Node) !bool { |
| 738 | const is_hit = man.hit(parent_progress_node) catch |err| return failWithCacheError(s, maker, man, err); |
| 739 | s.result_cached = is_hit; |
| 740 | // The above call to hit() populates the manifest with files, so in case of |
| 741 | // a hit, we need to populate watch inputs. |
| 742 | if (is_hit) try setWatchInputsFromManifest(s, maker, man); |
| 743 | return is_hit; |
| 744 | } |
| 745 | |
| 746 | fn failWithCacheError( |
| 747 | s: *Step, |
| 748 | maker: *Maker, |
| 749 | man: *const Cache.Manifest, |
| 750 | err: Cache.Manifest.HitError, |
| 751 | ) error{ OutOfMemory, Canceled, MakeFailed } { |
| 752 | switch (err) { |
| 753 | error.CacheCheckFailed => switch (man.diagnostic) { |
| 754 | .none => unreachable, |
| 755 | .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail(maker, "failed checking cache: {t} {t}", .{ |
| 756 | man.diagnostic, e, |
| 757 | }), |
| 758 | .file_open, .file_stat, .file_read, .file_hash => |op| { |
| 759 | const pp = man.files.keys()[op.file_index].prefixed_path; |
| 760 | const prefix = man.cache.prefixes()[pp.prefix].path orelse ""; |
| 761 | return s.fail(maker, "failed checking cache: {s}{c}{s} {t} {t}", .{ |
| 762 | prefix, Dir.path.sep, pp.sub_path, man.diagnostic, op.err, |
| 763 | }); |
| 764 | }, |
| 765 | }, |
| 766 | error.OutOfMemory, error.Canceled => |e| return e, |
| 767 | error.InvalidFormat => return s.fail(maker, "failed checking cache: invalid manifest file format", .{}), |
| 768 | } |
| 769 | } |
| 770 | |
| 771 | /// Prefer `writeManifestAndWatch` unless you already added watch inputs |
| 772 | /// separately from using the cache system. |
| 773 | pub fn writeManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void { |
| 774 | if (s.test_results.isSuccess()) { |
| 775 | man.writeManifest() catch |err| switch (err) { |
| 776 | error.Canceled => |e| return e, |
| 777 | else => |e| try s.addError(maker, "failed writing cache manifest: {t}", .{e}), |
| 778 | }; |
| 779 | } |
| 780 | } |
| 781 | |
| 782 | /// Clears previous watch inputs, if any, and then populates watch inputs from |
| 783 | /// the full set of files picked up by the cache manifest. |
| 784 | /// |
| 785 | /// Must be accompanied with `cacheHitWatched`. |
| 786 | pub fn writeManifestAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !void { |
| 787 | try writeManifest(s, maker, man); |
| 788 | try setWatchInputsFromManifest(s, maker, man); |
| 789 | } |
| 790 | |
| 791 | pub fn setWatchInputsFromManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void { |
| 792 | return setWatchInputsFromManifestFiles(s, maker, &man.files, man.cache.prefixes()); |
| 793 | } |
| 794 | |
| 795 | pub fn setWatchInputsFromManifestFiles( |
| 796 | s: *Step, |
| 797 | maker: *Maker, |
| 798 | files: *const Cache.Manifest.Files, |
| 799 | prefixes: []const Cache.Directory, |
| 800 | ) !void { |
| 801 | const graph = maker.graph; |
| 802 | const arena = graph.arena; // TODO don't leak into process arena |
| 803 | clearWatchInputs(s, maker); |
| 804 | for (files.keys()) |file| { |
| 805 | // The file path data is freed when the cache manifest is cleaned up at the end of `make`. |
| 806 | const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path); |
| 807 | try addWatchInputFromPath(s, maker, .{ |
| 808 | .root_dir = prefixes[file.prefixed_path.prefix], |
| 809 | .sub_path = Dir.path.dirname(sub_path) orelse "", |
| 810 | }, Dir.path.basename(sub_path)); |
| 811 | } |
| 812 | } |
| 813 | |
| 814 | /// For steps that have a single input that never changes when re-running `make`. |
| 815 | pub fn singleUnchangingWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_path: LazyPath) Allocator.Error!void { |
| 816 | if (!step.inputs.populated()) try step.addWatchInput(maker, arena, lazy_path); |
| 817 | } |
| 818 | |
| 819 | pub fn clearWatchInputs(step: *Step, maker: *Maker) void { |
| 820 | step.inputs.clear(maker.gpa); |
| 821 | } |
| 822 | |
| 823 | /// Places a *file* dependency on the path. |
| 824 | pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: LazyPath) Allocator.Error!void { |
| 825 | const conf = &maker.scanned_config.configuration; |
| 826 | switch (lazy_file) { |
| 827 | .source_path => |source_path| { |
| 828 | const sub_path = source_path.sub_path.slice(conf); |
| 829 | const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path); |
| 830 | try addWatchInputPath(step, maker, pkg_path); |
| 831 | }, |
| 832 | .relative => |relative| { |
| 833 | const resolved_path = try maker.relativePath(arena, relative); |
| 834 | try addWatchInputPath(step, maker, resolved_path); |
| 835 | }, |
| 836 | // Nothing to watch because this dependency edge is modeled instead via `dependants`. |
| 837 | .generated => {}, |
| 838 | } |
| 839 | } |
| 840 | |
| 841 | /// Any changes inside the directory will trigger invalidation. |
| 842 | /// |
| 843 | /// See also `addDirectoryWatchInputFromPath` which takes a `Path` instead. |
| 844 | /// |
| 845 | /// Paths derived from this directory should also be manually added via |
| 846 | /// `addDirectoryWatchInputFromPath` if and only if this function returns |
| 847 | /// `true`. |
| 848 | pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPath) Allocator.Error!bool { |
| 849 | const graph = maker.graph; |
| 850 | const arena = graph.arena; // TODO don't leak into the process arena |
| 851 | switch (lazy_directory) { |
| 852 | .source_path => |source_path| { |
| 853 | const conf = &maker.scanned_config.configuration; |
| 854 | const sub_path = source_path.sub_path.slice(conf); |
| 855 | const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path); |
| 856 | try addDirectoryWatchInputFromPath(step, maker, pkg_path); |
| 857 | }, |
| 858 | .relative => |relative| { |
| 859 | const resolved_path = try maker.relativePath(arena, relative); |
| 860 | try addDirectoryWatchInputFromPath(step, maker, resolved_path); |
| 861 | }, |
| 862 | // Nothing to watch because this dependency edge is modeled instead via `dependants`. |
| 863 | .generated => return false, |
| 864 | } |
| 865 | return true; |
| 866 | } |
| 867 | |
| 868 | /// Any changes inside the directory will trigger invalidation. |
| 869 | /// |
| 870 | /// See also `addDirectoryWatchInput` which takes a `LazyPath` instead. |
| 871 | /// |
| 872 | /// This function should only be called when it has been verified that the |
| 873 | /// dependency on `path` is not already accounted for by a `Step` dependency. |
| 874 | /// In other words, before calling this function, first check that the |
| 875 | /// `LazyPath` which this `path` is derived from is not `generated`. |
| 876 | pub fn addDirectoryWatchInputFromPath(step: *Step, maker: *Maker, path: Path) !void { |
| 877 | return addWatchInputFromPath(step, maker, path, "."); |
| 878 | } |
| 879 | |
| 880 | fn addWatchInputPath(step: *Step, maker: *Maker, path: Path) Allocator.Error!void { |
| 881 | return addWatchInputFromPath(step, maker, .{ |
| 882 | .root_dir = path.root_dir, |
| 883 | .sub_path = Dir.path.dirname(path.sub_path) orelse "", |
| 884 | }, Dir.path.basename(path.sub_path)); |
| 885 | } |
| 886 | |
| 887 | fn addWatchInputFromPath(step: *Step, maker: *Maker, directory: Path, basename: []const u8) Allocator.Error!void { |
| 888 | const gpa = maker.gpa; |
| 889 | const gop = try step.inputs.table.getOrPut(gpa, directory); |
| 890 | if (!gop.found_existing) gop.value_ptr.* = .empty; |
| 891 | try gop.value_ptr.append(gpa, basename); |
| 892 | } |
| 893 | |
| 894 | fn oomWrap(s: *Step, result: error{OutOfMemory}!void) void { |
| 895 | result catch { |
| 896 | s.result_oom = true; |
| 897 | }; |
| 898 | } |
| 899 | |
| 900 | pub fn clearResultStderr(step: *Step, gpa: Allocator) void { |
| 901 | if (step.result_stderr.len != 0) { |
| 902 | gpa.free(step.result_stderr); |
| 903 | step.result_stderr = ""; |
| 904 | } |
| 905 | } |
| 906 | |
| 907 | pub fn setResultStderr(step: *Step, gpa: Allocator, bytes: []const u8) Allocator.Error!void { |
| 908 | takeResultStderr(step, gpa, try gpa.dupe(u8, bytes)); |
| 909 | } |
| 910 | |
| 911 | pub fn takeResultStderr(step: *Step, gpa: Allocator, owned: []const u8) void { |
| 912 | clearResultStderr(step, gpa); |
| 913 | step.result_stderr = owned; |
| 914 | } |