Commit 6e025fc2e2

Andrew Kelley <andrew@ziglang.org>
2024-07-05 21:24:32
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.
1 parent d2bec8f
Changed files (4)
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 <bytes>             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:
         \\
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();
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;
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 };