authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-05 12:24:32-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-12 00:14:07-07:00
log6e025fc2e298c633ab36e9058a2cc610f57e4522
treedc58c61d22da365eaae0321de0af242f250b43c6
parentd2bec8f92f15ac16a0714ddc8282ab31dd5bb889

build system: add --watch flag and report source file in InstallFile

This direction is not quite right because it mutates shared state in a threaded context, so the next commit will need to fix this.

4 files changed, 147 insertions(+), 21 deletions(-)

lib/compiler/build_runner.zig+45-21
......@@ -74,6 +74,7 @@ pub fn main() !void {
7474 .query = .{},
7575 .result = try std.zig.system.resolveTargetQuery(.{}),
7676 },
77 .watch = null,
7778 };
7879
7980 graph.cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() });
......@@ -97,12 +98,12 @@ pub fn main() !void {
9798 var dir_list = std.Build.DirList{};
9899 var summary: ?Summary = null;
99100 var max_rss: u64 = 0;
100 var skip_oom_steps: bool = false;
101 var skip_oom_steps = false;
101102 var color: Color = .auto;
102103 var seed: u32 = 0;
103 var prominent_compile_errors: bool = false;
104 var help_menu: bool = false;
105 var steps_menu: bool = false;
104 var prominent_compile_errors = false;
105 var help_menu = false;
106 var steps_menu = false;
106107 var output_tmp_nonce: ?[16]u8 = null;
107108
108109 while (nextArg(args, &arg_idx)) |arg| {
......@@ -227,6 +228,10 @@ pub fn main() !void {
227228 builder.verbose_llvm_cpu_features = true;
228229 } else if (mem.eql(u8, arg, "--prominent-compile-errors")) {
229230 prominent_compile_errors = true;
231 } else if (mem.eql(u8, arg, "--watch")) {
232 const watch = try arena.create(std.Build.Watch);
233 watch.* = std.Build.Watch.init;
234 graph.watch = watch;
230235 } else if (mem.eql(u8, arg, "-fwine")) {
231236 builder.enable_wine = true;
232237 } else if (mem.eql(u8, arg, "-fno-wine")) {
......@@ -344,7 +349,7 @@ pub fn main() !void {
344349 .prominent_compile_errors = prominent_compile_errors,
345350
346351 .claimed_rss = 0,
347 .summary = summary,
352 .summary = summary orelse if (graph.watch != null) .new else .failures,
348353 .ttyconf = ttyconf,
349354 .stderr = stderr,
350355 };
......@@ -363,7 +368,10 @@ pub fn main() !void {
363368 &run,
364369 seed,
365370 ) catch |err| switch (err) {
366 error.UncleanExit => process.exit(1),
371 error.UncleanExit => {
372 if (graph.watch == null)
373 process.exit(1);
374 },
367375 else => return err,
368376 };
369377}
......@@ -377,7 +385,7 @@ const Run = struct {
377385 prominent_compile_errors: bool,
378386
379387 claimed_rss: usize,
380 summary: ?Summary,
388 summary: Summary,
381389 ttyconf: std.io.tty.Config,
382390 stderr: File,
383391};
......@@ -417,7 +425,7 @@ fn runStepNames(
417425
418426 for (starting_steps) |s| {
419427 constructGraphAndCheckForDependencyLoop(b, s, &step_stack, rand) catch |err| switch (err) {
420 error.DependencyLoopDetected => return error.UncleanExit,
428 error.DependencyLoopDetected => return uncleanExit(),
421429 else => |e| return e,
422430 };
423431 }
......@@ -442,7 +450,7 @@ fn runStepNames(
442450 if (run.max_rss_is_default) {
443451 std.debug.print("note: use --maxrss to override the default", .{});
444452 }
445 return error.UncleanExit;
453 return uncleanExit();
446454 }
447455 }
448456
......@@ -524,13 +532,19 @@ fn runStepNames(
524532
525533 // A proper command line application defaults to silently succeeding.
526534 // The user may request verbose mode if they have a different preference.
527 const failures_only = run.summary != .all and run.summary != .new;
528 if (failure_count == 0 and failures_only) return cleanExit();
535 const failures_only = switch (run.summary) {
536 .failures, .none => true,
537 else => false,
538 };
539 if (failure_count == 0 and failures_only) {
540 if (b.graph.watch != null) return;
541 return cleanExit();
542 }
529543
530544 const ttyconf = run.ttyconf;
531545 const stderr = run.stderr;
532546
533 if (run.summary != Summary.none) {
547 if (run.summary != .none) {
534548 const total_count = success_count + failure_count + pending_count + skipped_count;
535549 ttyconf.setColor(stderr, .cyan) catch {};
536550 stderr.writeAll("Build Summary:") catch {};
......@@ -544,11 +558,6 @@ fn runStepNames(
544558 if (test_fail_count > 0) stderr.writer().print("; {d} failed", .{test_fail_count}) catch {};
545559 if (test_leak_count > 0) stderr.writer().print("; {d} leaked", .{test_leak_count}) catch {};
546560
547 if (run.summary == null) {
548 ttyconf.setColor(stderr, .dim) catch {};
549 stderr.writeAll(" (disable with --summary none)") catch {};
550 ttyconf.setColor(stderr, .reset) catch {};
551 }
552561 stderr.writeAll("\n") catch {};
553562
554563 // Print a fancy tree with build results.
......@@ -562,7 +571,7 @@ fn runStepNames(
562571 while (i > 0) {
563572 i -= 1;
564573 const step = b.top_level_steps.get(step_names[i]).?.step;
565 const found = switch (run.summary orelse .failures) {
574 const found = switch (run.summary) {
566575 .all, .none => unreachable,
567576 .failures => step.state != .success,
568577 .new => !step.result_cached,
......@@ -579,7 +588,10 @@ fn runStepNames(
579588 }
580589 }
581590
582 if (failure_count == 0) return cleanExit();
591 if (failure_count == 0) {
592 if (b.graph.watch != null) return;
593 return cleanExit();
594 }
583595
584596 // Finally, render compile errors at the bottom of the terminal.
585597 // We use a separate compile_error_steps array list because step_stack is destructively
......@@ -591,13 +603,24 @@ fn runStepNames(
591603 }
592604 }
593605
606 if (b.graph.watch != null) return uncleanExit();
607
594608 // Signal to parent process that we have printed compile errors. The
595609 // parent process may choose to omit the "following command failed"
596610 // line in this case.
597611 process.exit(2);
598612 }
599613
600 process.exit(1);
614 return uncleanExit();
615}
616
617fn uncleanExit() error{UncleanExit}!void {
618 if (builtin.mode == .Debug) {
619 return error.UncleanExit;
620 } else {
621 std.debug.lockStdErr();
622 process.exit(1);
623 }
601624}
602625
603626const PrintNode = struct {
......@@ -768,7 +791,7 @@ fn printTreeStep(
768791 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
769792) !void {
770793 const first = step_stack.swapRemove(s);
771 const summary = run.summary orelse .failures;
794 const summary = run.summary;
772795 const skip = switch (summary) {
773796 .none => unreachable,
774797 .all => false,
......@@ -1124,6 +1147,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void {
11241147 \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
11251148 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
11261149 \\ --fetch Exit after fetching dependency tree
1150 \\ --watch Continuously rebuild when source files are modified
11271151 \\
11281152 \\Project-Specific Options:
11291153 \\
lib/std/Build.zig+55
......@@ -120,6 +120,61 @@ pub const Graph = struct {
120120 needed_lazy_dependencies: std.StringArrayHashMapUnmanaged(void) = .{},
121121 /// Information about the native target. Computed before build() is invoked.
122122 host: ResolvedTarget,
123 /// When `--watch` is provided, collects the set of files that should be
124 /// watched and the state to required to poll the system for changes.
125 watch: ?*Watch,
126};
127
128pub const Watch = struct {
129 table: Table,
130
131 pub const init: Watch = .{
132 .table = .{},
133 };
134
135 /// Key is the directory to watch which contains one or more files we are
136 /// interested in noticing changes to.
137 pub const Table = std.ArrayHashMapUnmanaged(Cache.Path, ReactionSet, TableContext, false);
138
139 const Hash = std.hash.Wyhash;
140
141 pub const TableContext = struct {
142 pub fn hash(self: TableContext, a: Cache.Path) u32 {
143 _ = self;
144 const seed: u32 = @bitCast(a.root_dir.handle.fd);
145 return @truncate(Hash.hash(seed, a.sub_path));
146 }
147 pub fn eql(self: TableContext, a: Cache.Path, b: Cache.Path, b_index: usize) bool {
148 _ = self;
149 _ = b_index;
150 return a.eql(b);
151 }
152 };
153
154 pub const ReactionSet = std.ArrayHashMapUnmanaged(Match, void, Match.Context, false);
155
156 pub const Match = struct {
157 /// Relative to the watched directory, the file path that triggers this
158 /// match.
159 basename: []const u8,
160 /// The step to re-run when file corresponding to `basename` is changed.
161 step: *Step,
162
163 pub const Context = struct {
164 pub fn hash(self: Context, a: Match) u32 {
165 _ = self;
166 var hasher = Hash.init(0);
167 std.hash.autoHash(&hasher, a.step);
168 hasher.update(a.basename);
169 return @truncate(hasher.final());
170 }
171 pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool {
172 _ = self;
173 _ = b_index;
174 return a.step == b.step and mem.eql(u8, a.basename, b.basename);
175 }
176 };
177 };
123178};
124179
125180const AvailableDeps = []const struct { []const u8, []const u8 };
lib/std/Build/Step.zig+46
......@@ -562,6 +562,52 @@ pub fn writeManifest(s: *Step, man: *std.Build.Cache.Manifest) !void {
562562 }
563563}
564564
565fn oom(err: anytype) noreturn {
566 switch (err) {
567 error.OutOfMemory => @panic("out of memory"),
568 }
569}
570
571pub fn addWatchInput(step: *Step, lazy_path: std.Build.LazyPath) void {
572 errdefer |err| oom(err);
573 const w = step.owner.graph.watch orelse return;
574 switch (lazy_path) {
575 .src_path => |src_path| try addWatchInputFromBuilder(step, w, src_path.owner, src_path.sub_path),
576 .dependency => |d| try addWatchInputFromBuilder(step, w, d.dependency.builder, d.sub_path),
577 .cwd_relative => |path_string| {
578 try addWatchInputFromPath(w, .{
579 .root_dir = .{
580 .path = null,
581 .handle = std.fs.cwd(),
582 },
583 .sub_path = std.fs.path.dirname(path_string) orelse "",
584 }, .{
585 .step = step,
586 .basename = std.fs.path.basename(path_string),
587 });
588 },
589 // Nothing to watch because this dependency edge is modeled instead via `dependants`.
590 .generated => {},
591 }
592}
593
594fn addWatchInputFromBuilder(step: *Step, w: *std.Build.Watch, builder: *std.Build, sub_path: []const u8) !void {
595 return addWatchInputFromPath(w, .{
596 .root_dir = builder.build_root,
597 .sub_path = std.fs.path.dirname(sub_path) orelse "",
598 }, .{
599 .step = step,
600 .basename = std.fs.path.basename(sub_path),
601 });
602}
603
604fn addWatchInputFromPath(w: *std.Build.Watch, path: std.Build.Cache.Path, match: std.Build.Watch.Match) !void {
605 const gpa = match.step.owner.allocator;
606 const gop = try w.table.getOrPut(gpa, path);
607 if (!gop.found_existing) gop.value_ptr.* = .{};
608 try gop.value_ptr.put(gpa, match, {});
609}
610
565611test {
566612 _ = CheckFile;
567613 _ = CheckObject;
lib/std/Build/Step/InstallFile.zig+1
......@@ -40,6 +40,7 @@ fn make(step: *Step, prog_node: std.Progress.Node) !void {
4040 _ = prog_node;
4141 const b = step.owner;
4242 const install_file: *InstallFile = @fieldParentPtr("step", step);
43 step.addWatchInput(install_file.source);
4344 const full_src_path = install_file.source.getPath2(b, step);
4445 const full_dest_path = b.getInstallPath(install_file.dir, install_file.dest_rel_path);
4546 const cwd = std.fs.cwd();