diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 79585086b3..cf78ad320f 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -74,6 +74,7 @@ pub fn main() !void { .query = .{}, .result = try std.zig.system.resolveTargetQuery(.{}), }, + .watch = null, }; graph.cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() }); @@ -97,12 +98,12 @@ pub fn main() !void { var dir_list = std.Build.DirList{}; var summary: ?Summary = null; var max_rss: u64 = 0; - var skip_oom_steps: bool = false; + var skip_oom_steps = false; var color: Color = .auto; var seed: u32 = 0; - var prominent_compile_errors: bool = false; - var help_menu: bool = false; - var steps_menu: bool = false; + var prominent_compile_errors = false; + var help_menu = false; + var steps_menu = false; var output_tmp_nonce: ?[16]u8 = null; while (nextArg(args, &arg_idx)) |arg| { @@ -227,6 +228,10 @@ pub fn main() !void { builder.verbose_llvm_cpu_features = true; } else if (mem.eql(u8, arg, "--prominent-compile-errors")) { prominent_compile_errors = true; + } else if (mem.eql(u8, arg, "--watch")) { + const watch = try arena.create(std.Build.Watch); + watch.* = std.Build.Watch.init; + graph.watch = watch; } else if (mem.eql(u8, arg, "-fwine")) { builder.enable_wine = true; } else if (mem.eql(u8, arg, "-fno-wine")) { @@ -344,7 +349,7 @@ pub fn main() !void { .prominent_compile_errors = prominent_compile_errors, .claimed_rss = 0, - .summary = summary, + .summary = summary orelse if (graph.watch != null) .new else .failures, .ttyconf = ttyconf, .stderr = stderr, }; @@ -363,7 +368,10 @@ pub fn main() !void { &run, seed, ) catch |err| switch (err) { - error.UncleanExit => process.exit(1), + error.UncleanExit => { + if (graph.watch == null) + process.exit(1); + }, else => return err, }; } @@ -377,7 +385,7 @@ const Run = struct { prominent_compile_errors: bool, claimed_rss: usize, - summary: ?Summary, + summary: Summary, ttyconf: std.io.tty.Config, stderr: File, }; @@ -417,7 +425,7 @@ fn runStepNames( for (starting_steps) |s| { constructGraphAndCheckForDependencyLoop(b, s, &step_stack, rand) catch |err| switch (err) { - error.DependencyLoopDetected => return error.UncleanExit, + error.DependencyLoopDetected => return uncleanExit(), else => |e| return e, }; } @@ -442,7 +450,7 @@ fn runStepNames( if (run.max_rss_is_default) { std.debug.print("note: use --maxrss to override the default", .{}); } - return error.UncleanExit; + return uncleanExit(); } } @@ -524,13 +532,19 @@ fn runStepNames( // A proper command line application defaults to silently succeeding. // The user may request verbose mode if they have a different preference. - const failures_only = run.summary != .all and run.summary != .new; - if (failure_count == 0 and failures_only) return cleanExit(); + const failures_only = switch (run.summary) { + .failures, .none => true, + else => false, + }; + if (failure_count == 0 and failures_only) { + if (b.graph.watch != null) return; + return cleanExit(); + } const ttyconf = run.ttyconf; const stderr = run.stderr; - if (run.summary != Summary.none) { + if (run.summary != .none) { const total_count = success_count + failure_count + pending_count + skipped_count; ttyconf.setColor(stderr, .cyan) catch {}; stderr.writeAll("Build Summary:") catch {}; @@ -544,11 +558,6 @@ fn runStepNames( if (test_fail_count > 0) stderr.writer().print("; {d} failed", .{test_fail_count}) catch {}; if (test_leak_count > 0) stderr.writer().print("; {d} leaked", .{test_leak_count}) catch {}; - if (run.summary == null) { - ttyconf.setColor(stderr, .dim) catch {}; - stderr.writeAll(" (disable with --summary none)") catch {}; - ttyconf.setColor(stderr, .reset) catch {}; - } stderr.writeAll("\n") catch {}; // Print a fancy tree with build results. @@ -562,7 +571,7 @@ fn runStepNames( while (i > 0) { i -= 1; const step = b.top_level_steps.get(step_names[i]).?.step; - const found = switch (run.summary orelse .failures) { + const found = switch (run.summary) { .all, .none => unreachable, .failures => step.state != .success, .new => !step.result_cached, @@ -579,7 +588,10 @@ fn runStepNames( } } - if (failure_count == 0) return cleanExit(); + if (failure_count == 0) { + if (b.graph.watch != null) return; + return cleanExit(); + } // Finally, render compile errors at the bottom of the terminal. // We use a separate compile_error_steps array list because step_stack is destructively @@ -591,13 +603,24 @@ fn runStepNames( } } + if (b.graph.watch != null) return uncleanExit(); + // Signal to parent process that we have printed compile errors. The // parent process may choose to omit the "following command failed" // line in this case. process.exit(2); } - process.exit(1); + return uncleanExit(); +} + +fn uncleanExit() error{UncleanExit}!void { + if (builtin.mode == .Debug) { + return error.UncleanExit; + } else { + std.debug.lockStdErr(); + process.exit(1); + } } const PrintNode = struct { @@ -768,7 +791,7 @@ fn printTreeStep( step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void), ) !void { const first = step_stack.swapRemove(s); - const summary = run.summary orelse .failures; + const summary = run.summary; const skip = switch (summary) { .none => unreachable, .all => false, @@ -1124,6 +1147,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void { \\ --maxrss Limit memory usage (default is to use available memory) \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss \\ --fetch Exit after fetching dependency tree + \\ --watch Continuously rebuild when source files are modified \\ \\Project-Specific Options: \\ diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 0255245a3c..bdfd71891b 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -120,6 +120,61 @@ pub const Graph = struct { needed_lazy_dependencies: std.StringArrayHashMapUnmanaged(void) = .{}, /// Information about the native target. Computed before build() is invoked. host: ResolvedTarget, + /// When `--watch` is provided, collects the set of files that should be + /// watched and the state to required to poll the system for changes. + watch: ?*Watch, +}; + +pub const Watch = struct { + table: Table, + + pub const init: Watch = .{ + .table = .{}, + }; + + /// Key is the directory to watch which contains one or more files we are + /// interested in noticing changes to. + pub const Table = std.ArrayHashMapUnmanaged(Cache.Path, ReactionSet, TableContext, false); + + const Hash = std.hash.Wyhash; + + pub const TableContext = struct { + pub fn hash(self: TableContext, a: Cache.Path) u32 { + _ = self; + const seed: u32 = @bitCast(a.root_dir.handle.fd); + return @truncate(Hash.hash(seed, a.sub_path)); + } + pub fn eql(self: TableContext, a: Cache.Path, b: Cache.Path, b_index: usize) bool { + _ = self; + _ = b_index; + return a.eql(b); + } + }; + + pub const ReactionSet = std.ArrayHashMapUnmanaged(Match, void, Match.Context, false); + + pub const Match = struct { + /// Relative to the watched directory, the file path that triggers this + /// match. + basename: []const u8, + /// The step to re-run when file corresponding to `basename` is changed. + step: *Step, + + pub const Context = struct { + pub fn hash(self: Context, a: Match) u32 { + _ = self; + var hasher = Hash.init(0); + std.hash.autoHash(&hasher, a.step); + hasher.update(a.basename); + return @truncate(hasher.final()); + } + pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool { + _ = self; + _ = b_index; + return a.step == b.step and mem.eql(u8, a.basename, b.basename); + } + }; + }; }; const AvailableDeps = []const struct { []const u8, []const u8 }; diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig index 0813aba689..91fe298666 100644 --- a/lib/std/Build/Step.zig +++ b/lib/std/Build/Step.zig @@ -562,6 +562,52 @@ pub fn writeManifest(s: *Step, man: *std.Build.Cache.Manifest) !void { } } +fn oom(err: anytype) noreturn { + switch (err) { + error.OutOfMemory => @panic("out of memory"), + } +} + +pub fn addWatchInput(step: *Step, lazy_path: std.Build.LazyPath) void { + errdefer |err| oom(err); + const w = step.owner.graph.watch orelse return; + switch (lazy_path) { + .src_path => |src_path| try addWatchInputFromBuilder(step, w, src_path.owner, src_path.sub_path), + .dependency => |d| try addWatchInputFromBuilder(step, w, d.dependency.builder, d.sub_path), + .cwd_relative => |path_string| { + try addWatchInputFromPath(w, .{ + .root_dir = .{ + .path = null, + .handle = std.fs.cwd(), + }, + .sub_path = std.fs.path.dirname(path_string) orelse "", + }, .{ + .step = step, + .basename = std.fs.path.basename(path_string), + }); + }, + // Nothing to watch because this dependency edge is modeled instead via `dependants`. + .generated => {}, + } +} + +fn addWatchInputFromBuilder(step: *Step, w: *std.Build.Watch, builder: *std.Build, sub_path: []const u8) !void { + return addWatchInputFromPath(w, .{ + .root_dir = builder.build_root, + .sub_path = std.fs.path.dirname(sub_path) orelse "", + }, .{ + .step = step, + .basename = std.fs.path.basename(sub_path), + }); +} + +fn addWatchInputFromPath(w: *std.Build.Watch, path: std.Build.Cache.Path, match: std.Build.Watch.Match) !void { + const gpa = match.step.owner.allocator; + const gop = try w.table.getOrPut(gpa, path); + if (!gop.found_existing) gop.value_ptr.* = .{}; + try gop.value_ptr.put(gpa, match, {}); +} + test { _ = CheckFile; _ = CheckObject; diff --git a/lib/std/Build/Step/InstallFile.zig b/lib/std/Build/Step/InstallFile.zig index 8202a9d796..c7f1088c48 100644 --- a/lib/std/Build/Step/InstallFile.zig +++ b/lib/std/Build/Step/InstallFile.zig @@ -40,6 +40,7 @@ fn make(step: *Step, prog_node: std.Progress.Node) !void { _ = prog_node; const b = step.owner; const install_file: *InstallFile = @fieldParentPtr("step", step); + step.addWatchInput(install_file.source); const full_src_path = install_file.source.getPath2(b, step); const full_dest_path = b.getInstallPath(install_file.dir, install_file.dest_rel_path); const cwd = std.fs.cwd();