Commit 001ff7b3b2

Andrew Kelley <andrew@ziglang.org>
2024-07-10 05:05:37
std.Build.Watch: make dirty steps invalidate each other
and make failed steps always be invalidated and make steps that don't need to be reevaluated marked as cached
1 parent 6f89824
Changed files (3)
lib
lib/compiler/build_runner.zig
@@ -500,9 +500,10 @@ pub fn main() !void {
             const events_len = try std.posix.poll(&poll_fds, timeout);
             if (events_len == 0) {
                 debouncing_node.end();
+                Watch.markFailedStepsDirty(gpa, run.step_stack.keys());
                 continue :rebuild;
             }
-            if (try markDirtySteps(&w)) {
+            if (try w.markDirtySteps(gpa)) {
                 if (!debouncing) {
                     debouncing = true;
                     debouncing_node.end();
@@ -513,44 +514,6 @@ pub fn main() !void {
     }
 }
 
-fn markDirtySteps(w: *Watch) !bool {
-    const fanotify = std.os.linux.fanotify;
-    const M = fanotify.event_metadata;
-    var events_buf: [256 + 4096]u8 = undefined;
-    var any_dirty = false;
-    while (true) {
-        var len = std.posix.read(w.fan_fd, &events_buf) catch |err| switch (err) {
-            error.WouldBlock => return any_dirty,
-            else => |e| return e,
-        };
-        var meta: [*]align(1) M = @ptrCast(&events_buf);
-        while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
-            len -= meta[0].event_len;
-            meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
-        }) {
-            assert(meta[0].vers == M.VERSION);
-            const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
-            switch (fid.hdr.info_type) {
-                .DFID_NAME => {
-                    const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
-                    const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
-                    const file_name = mem.span(file_name_z);
-                    const lfh: Watch.LinuxFileHandle = .{ .handle = file_handle };
-                    if (w.handle_table.getPtr(lfh)) |reaction_set| {
-                        if (reaction_set.getPtr(file_name)) |step_set| {
-                            for (step_set.keys()) |step| {
-                                step.state = .precheck_done;
-                                any_dirty = true;
-                            }
-                        }
-                    }
-                },
-                else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
-            }
-        }
-    }
-}
-
 const Run = struct {
     max_rss: u64,
     max_rss_is_default: bool,
@@ -1319,7 +1282,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void {
         \\  --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
-        \\  --debounce <ms>              Delay before rebuilding after watched file detection
+        \\  --debounce <ms>              Delay before rebuilding after changed file detected
         \\
         \\Project-Specific Options:
         \\
lib/std/Build/Step.zig
@@ -637,6 +637,31 @@ fn addWatchInputFromPath(step: *Step, path: Build.Cache.Path, basename: []const
     try gop.value_ptr.append(gpa, basename);
 }
 
+fn reset(step: *Step, gpa: Allocator) void {
+    assert(step.state == .precheck_done);
+
+    step.result_error_msgs.clearRetainingCapacity();
+    step.result_stderr = "";
+    step.result_cached = false;
+    step.result_duration_ns = null;
+    step.result_peak_rss = 0;
+    step.test_results = .{};
+
+    step.result_error_bundle.deinit(gpa);
+    step.result_error_bundle = std.zig.ErrorBundle.empty;
+}
+
+/// Implementation detail of file watching. Prepares the step for being re-evaluated.
+pub fn recursiveReset(step: *Step, gpa: Allocator) void {
+    assert(step.state != .precheck_done);
+    step.state = .precheck_done;
+    step.reset(gpa);
+    for (step.dependants.items) |dep| {
+        if (dep.state == .precheck_done) continue;
+        dep.recursiveReset(gpa);
+    }
+}
+
 test {
     _ = CheckFile;
     _ = CheckObject;
lib/std/Build/Watch.zig
@@ -2,6 +2,7 @@ const std = @import("../std.zig");
 const Watch = @This();
 const Step = std.Build.Step;
 const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
 
 dir_table: DirTable,
 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
@@ -117,3 +118,56 @@ pub fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !LinuxFileHandle
     const stack_lfh: LinuxFileHandle = .{ .handle = stack_ptr };
     return stack_lfh.clone(gpa);
 }
+
+pub fn markDirtySteps(w: *Watch, gpa: Allocator) !bool {
+    const fanotify = std.os.linux.fanotify;
+    const M = fanotify.event_metadata;
+    var events_buf: [256 + 4096]u8 = undefined;
+    var any_dirty = false;
+    while (true) {
+        var len = std.posix.read(w.fan_fd, &events_buf) catch |err| switch (err) {
+            error.WouldBlock => return any_dirty,
+            else => |e| return e,
+        };
+        var meta: [*]align(1) M = @ptrCast(&events_buf);
+        while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
+            len -= meta[0].event_len;
+            meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
+        }) {
+            assert(meta[0].vers == M.VERSION);
+            const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
+            switch (fid.hdr.info_type) {
+                .DFID_NAME => {
+                    const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
+                    const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
+                    const file_name = std.mem.span(file_name_z);
+                    const lfh: Watch.LinuxFileHandle = .{ .handle = file_handle };
+                    if (w.handle_table.getPtr(lfh)) |reaction_set| {
+                        if (reaction_set.getPtr(file_name)) |step_set| {
+                            for (step_set.keys()) |step| {
+                                if (step.state != .precheck_done) {
+                                    step.recursiveReset(gpa);
+                                    any_dirty = true;
+                                }
+                            }
+                        }
+                    }
+                },
+                else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
+            }
+        }
+    }
+}
+
+pub fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
+    for (all_steps) |step| switch (step.state) {
+        .dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
+        else => continue,
+    };
+    // Now that all dirty steps have been found, the remaining steps that
+    // succeeded from last run shall be marked "cached".
+    for (all_steps) |step| switch (step.state) {
+        .success => step.result_cached = true,
+        else => continue,
+    };
+}