authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-12 14:20:20-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-07-12 14:20:20-07:00
log5efcc2e9e7c84893b9e418ca82d8d2d4366dde7c
tree5cebca8abfb571a36b8415950e9be9ece8555441
parentf77b43dad39a5140f3e39f32e98e9624368c16d6

build runner: refactor fs watch logic for OS abstraction

Makes the build runner compile successfully for non-linux targets; printing an error if you ask for --watch rather than making build scripts fail to compile.

2 files changed, 328 insertions(+), 255 deletions(-)

lib/compiler/build_runner.zig+31-120
......@@ -10,6 +10,7 @@ const File = std.fs.File;
1010const Step = std.Build.Step;
1111const Watch = std.Build.Watch;
1212const Allocator = std.mem.Allocator;
13const fatal = std.zig.fatal;
1314
1415pub const root = @import("@build");
1516pub const dependencies = @import("@dependencies");
......@@ -371,18 +372,7 @@ pub fn main() !void {
371372 else => return err,
372373 };
373374
374 var w = Watch.init;
375 if (watch) {
376 w.fan_fd = try std.posix.fanotify_init(.{
377 .CLASS = .NOTIF,
378 .CLOEXEC = true,
379 .NONBLOCK = true,
380 .REPORT_NAME = true,
381 .REPORT_DIR_FID = true,
382 .REPORT_FID = true,
383 .REPORT_TARGET_FID = true,
384 }, 0);
385 }
375 var w = if (watch) try Watch.init() else undefined;
386376
387377 try run.thread_pool.init(thread_pool_options);
388378 defer run.thread_pool.deinit();
......@@ -403,127 +393,53 @@ pub fn main() !void {
403393 };
404394 if (!watch) return cleanExit();
405395
406 // Add missing marks and note persisted ones.
407 for (run.step_stack.keys()) |step| {
408 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
409 const reaction_set = rs: {
410 const gop = try w.dir_table.getOrPut(gpa, path);
411 if (!gop.found_existing) {
412 const dir_handle = try Watch.getDirHandle(gpa, path);
413 // `dir_handle` may already be present in the table in
414 // the case that we have multiple Cache.Path instances
415 // that compare inequal but ultimately point to the same
416 // directory on the file system.
417 // In such case, we must revert adding this directory, but keep
418 // the additions to the step set.
419 const dh_gop = try w.handle_table.getOrPut(gpa, dir_handle);
420 if (dh_gop.found_existing) {
421 _ = w.dir_table.pop();
422 } else {
423 assert(dh_gop.index == gop.index);
424 dh_gop.value_ptr.* = .{};
425 std.posix.fanotify_mark(w.fan_fd, .{
426 .ADD = true,
427 .ONLYDIR = true,
428 }, Watch.fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| {
429 fatal("unable to watch {}: {s}", .{ path, @errorName(err) });
430 };
431 }
432 break :rs dh_gop.value_ptr;
433 }
434 break :rs &w.handle_table.values()[gop.index];
435 };
436 for (files.items) |basename| {
437 const gop = try reaction_set.getOrPut(gpa, basename);
438 if (!gop.found_existing) gop.value_ptr.* = .{};
439 try gop.value_ptr.put(gpa, step, w.generation);
440 }
441 }
396 switch (builtin.os.tag) {
397 .linux => {},
398 else => fatal("--watch not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
442399 }
443400
444 {
445 // Remove marks for files that are no longer inputs.
446 var i: usize = 0;
447 while (i < w.handle_table.entries.len) {
448 {
449 const reaction_set = &w.handle_table.values()[i];
450 var step_set_i: usize = 0;
451 while (step_set_i < reaction_set.entries.len) {
452 const step_set = &reaction_set.values()[step_set_i];
453 var dirent_i: usize = 0;
454 while (dirent_i < step_set.entries.len) {
455 const generations = step_set.values();
456 if (generations[dirent_i] == w.generation) {
457 dirent_i += 1;
458 continue;
459 }
460 step_set.swapRemoveAt(dirent_i);
461 }
462 if (step_set.entries.len > 0) {
463 step_set_i += 1;
464 continue;
465 }
466 reaction_set.swapRemoveAt(step_set_i);
467 }
468 if (reaction_set.entries.len > 0) {
469 i += 1;
470 continue;
471 }
472 }
473
474 const path = w.dir_table.keys()[i];
475
476 std.posix.fanotify_mark(w.fan_fd, .{
477 .REMOVE = true,
478 .ONLYDIR = true,
479 }, Watch.fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| switch (err) {
480 error.FileNotFound => {}, // Expected, harmless.
481 else => |e| std.log.warn("unable to unwatch '{}': {s}", .{ path, @errorName(e) }),
482 };
483
484 w.dir_table.swapRemoveAt(i);
485 w.handle_table.swapRemoveAt(i);
486 }
487 w.generation +%= 1;
488 }
401 try w.update(gpa, run.step_stack.keys());
489402
490403 // Wait until a file system notification arrives. Read all such events
491404 // until the buffer is empty. Then wait for a debounce interval, resetting
492405 // if any more events come in. After the debounce interval has passed,
493406 // trigger a rebuild on all steps with modified inputs, as well as their
494407 // recursive dependants.
495 var poll_fds: [1]std.posix.pollfd = .{
496 .{
497 .fd = w.fan_fd,
498 .events = std.posix.POLL.IN,
499 .revents = undefined,
500 },
501 };
502408 var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
503409 const caption = std.fmt.bufPrint(&caption_buf, "Watching {d} Directories", .{
504410 w.dir_table.entries.len,
505411 }) catch &caption_buf;
506412 var debouncing_node = main_progress_node.start(caption, 0);
507 var debouncing = false;
508 while (true) {
509 const timeout: i32 = if (debouncing) debounce_interval_ms else -1;
510 const events_len = try std.posix.poll(&poll_fds, timeout);
511 if (events_len == 0) {
413 var debounce_timeout: Watch.Timeout = .none;
414 while (true) switch (try w.wait(gpa, debounce_timeout)) {
415 .timeout => {
512416 debouncing_node.end();
513 Watch.markFailedStepsDirty(gpa, run.step_stack.keys());
417 markFailedStepsDirty(gpa, run.step_stack.keys());
514418 continue :rebuild;
515 }
516 if (try w.markDirtySteps(gpa)) {
517 if (!debouncing) {
518 debouncing = true;
519 debouncing_node.end();
520 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
521 }
522 }
523 }
419 },
420 .dirty => if (debounce_timeout == .none) {
421 debounce_timeout = .{ .ms = debounce_interval_ms };
422 debouncing_node.end();
423 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
424 },
425 .clean => {},
426 };
524427 }
525428}
526429
430fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
431 for (all_steps) |step| switch (step.state) {
432 .dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
433 else => continue,
434 };
435 // Now that all dirty steps have been found, the remaining steps that
436 // succeeded from last run shall be marked "cached".
437 for (all_steps) |step| switch (step.state) {
438 .success => step.result_cached = true,
439 else => continue,
440 };
441}
442
527443const Run = struct {
528444 max_rss: u64,
529445 max_rss_is_default: bool,
......@@ -1430,11 +1346,6 @@ fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
14301346 process.exit(1);
14311347}
14321348
1433fn fatal(comptime f: []const u8, args: anytype) noreturn {
1434 std.debug.print(f ++ "\n", args);
1435 process.exit(1);
1436}
1437
14381349fn validateSystemLibraryOptions(b: *std.Build) void {
14391350 var bad = false;
14401351 for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
lib/std/Build/Watch.zig+297-135
......@@ -1,41 +1,21 @@
1const builtin = @import("builtin");
12const std = @import("../std.zig");
23const Watch = @This();
34const Step = std.Build.Step;
45const Allocator = std.mem.Allocator;
56const assert = std.debug.assert;
7const fatal = std.zig.fatal;
68
79dir_table: DirTable,
8/// Keyed differently but indexes correspond 1:1 with `dir_table`.
9handle_table: HandleTable,
10fan_fd: std.posix.fd_t,
10os: Os,
1111generation: Generation,
1212
13pub const fan_mask: std.os.linux.fanotify.MarkMask = .{
14 .CLOSE_WRITE = true,
15 .CREATE = true,
16 .DELETE = true,
17 .DELETE_SELF = true,
18 .EVENT_ON_CHILD = true,
19 .MOVED_FROM = true,
20 .MOVED_TO = true,
21 .MOVE_SELF = true,
22 .ONDIR = true,
23};
24
25pub const init: Watch = .{
26 .dir_table = .{},
27 .handle_table = .{},
28 .fan_fd = -1,
29 .generation = 0,
30};
31
3213/// Key is the directory to watch which contains one or more files we are
3314/// interested in noticing changes to.
3415///
3516/// Value is generation.
3617const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false);
3718
38const HandleTable = std.ArrayHashMapUnmanaged(LinuxFileHandle, ReactionSet, LinuxFileHandle.Adapter, false);
3919/// Special key of "." means any changes in this directory trigger the steps.
4020const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet);
4121const StepSet = std.AutoArrayHashMapUnmanaged(*Step, Generation);
......@@ -45,6 +25,255 @@ const Generation = u8;
4525const Hash = std.hash.Wyhash;
4626const Cache = std.Build.Cache;
4727
28const Os = switch (builtin.os.tag) {
29 .linux => struct {
30 const posix = std.posix;
31
32 /// Keyed differently but indexes correspond 1:1 with `dir_table`.
33 handle_table: HandleTable,
34 poll_fds: [1]posix.pollfd,
35
36 const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, ReactionSet, FileHandle.Adapter, false);
37
38 const fan_mask: std.os.linux.fanotify.MarkMask = .{
39 .CLOSE_WRITE = true,
40 .CREATE = true,
41 .DELETE = true,
42 .DELETE_SELF = true,
43 .EVENT_ON_CHILD = true,
44 .MOVED_FROM = true,
45 .MOVED_TO = true,
46 .MOVE_SELF = true,
47 .ONDIR = true,
48 };
49
50 const FileHandle = struct {
51 handle: *align(1) std.os.linux.file_handle,
52
53 fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle {
54 const bytes = lfh.slice();
55 const new_ptr = try gpa.alignedAlloc(
56 u8,
57 @alignOf(std.os.linux.file_handle),
58 @sizeOf(std.os.linux.file_handle) + bytes.len,
59 );
60 const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
61 new_header.* = lfh.handle.*;
62 const new: FileHandle = .{ .handle = new_header };
63 @memcpy(new.slice(), lfh.slice());
64 return new;
65 }
66
67 fn destroy(lfh: FileHandle, gpa: Allocator) void {
68 const ptr: [*]u8 = @ptrCast(lfh.handle);
69 const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
70 return gpa.free(allocated_slice);
71 }
72
73 fn slice(lfh: FileHandle) []u8 {
74 const ptr: [*]u8 = &lfh.handle.f_handle;
75 return ptr[0..lfh.handle.handle_bytes];
76 }
77
78 const Adapter = struct {
79 pub fn hash(self: Adapter, a: FileHandle) u32 {
80 _ = self;
81 const unsigned_type: u32 = @bitCast(a.handle.handle_type);
82 return @truncate(Hash.hash(unsigned_type, a.slice()));
83 }
84 pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool {
85 _ = self;
86 _ = b_index;
87 return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
88 }
89 };
90 };
91
92 fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !FileHandle {
93 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
94 var mount_id: i32 = undefined;
95 var buf: [std.fs.max_path_bytes]u8 = undefined;
96 const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
97 path.sub_path,
98 }) catch return error.NameTooLong;
99 const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
100 stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
101 try posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, &mount_id, std.os.linux.AT.HANDLE_FID);
102 const stack_lfh: FileHandle = .{ .handle = stack_ptr };
103 return stack_lfh.clone(gpa);
104 }
105
106 fn markDirtySteps(w: *Watch, gpa: Allocator) !bool {
107 const fan_fd = w.os.getFanFd();
108 const fanotify = std.os.linux.fanotify;
109 const M = fanotify.event_metadata;
110 var events_buf: [256 + 4096]u8 = undefined;
111 var any_dirty = false;
112 while (true) {
113 var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) {
114 error.WouldBlock => return any_dirty,
115 else => |e| return e,
116 };
117 var meta: [*]align(1) M = @ptrCast(&events_buf);
118 while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
119 len -= meta[0].event_len;
120 meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
121 }) {
122 assert(meta[0].vers == M.VERSION);
123 if (meta[0].mask.Q_OVERFLOW) {
124 any_dirty = true;
125 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
126 markAllFilesDirty(w, gpa);
127 return true;
128 }
129 const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
130 switch (fid.hdr.info_type) {
131 .DFID_NAME => {
132 const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
133 const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
134 const file_name = std.mem.span(file_name_z);
135 const lfh: FileHandle = .{ .handle = file_handle };
136 if (w.os.handle_table.getPtr(lfh)) |reaction_set| {
137 if (reaction_set.getPtr(".")) |glob_set|
138 any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
139 if (reaction_set.getPtr(file_name)) |step_set|
140 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
141 }
142 },
143 else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
144 }
145 }
146 }
147 }
148
149 fn getFanFd(os: *const @This()) posix.fd_t {
150 return os.poll_fds[0].fd;
151 }
152
153 fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
154 const fan_fd = w.os.getFanFd();
155 // Add missing marks and note persisted ones.
156 for (steps) |step| {
157 for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
158 const reaction_set = rs: {
159 const gop = try w.dir_table.getOrPut(gpa, path);
160 if (!gop.found_existing) {
161 const dir_handle = try Os.getDirHandle(gpa, path);
162 // `dir_handle` may already be present in the table in
163 // the case that we have multiple Cache.Path instances
164 // that compare inequal but ultimately point to the same
165 // directory on the file system.
166 // In such case, we must revert adding this directory, but keep
167 // the additions to the step set.
168 const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle);
169 if (dh_gop.found_existing) {
170 _ = w.dir_table.pop();
171 } else {
172 assert(dh_gop.index == gop.index);
173 dh_gop.value_ptr.* = .{};
174 posix.fanotify_mark(fan_fd, .{
175 .ADD = true,
176 .ONLYDIR = true,
177 }, fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| {
178 fatal("unable to watch {}: {s}", .{ path, @errorName(err) });
179 };
180 }
181 break :rs dh_gop.value_ptr;
182 }
183 break :rs &w.os.handle_table.values()[gop.index];
184 };
185 for (files.items) |basename| {
186 const gop = try reaction_set.getOrPut(gpa, basename);
187 if (!gop.found_existing) gop.value_ptr.* = .{};
188 try gop.value_ptr.put(gpa, step, w.generation);
189 }
190 }
191 }
192
193 {
194 // Remove marks for files that are no longer inputs.
195 var i: usize = 0;
196 while (i < w.os.handle_table.entries.len) {
197 {
198 const reaction_set = &w.os.handle_table.values()[i];
199 var step_set_i: usize = 0;
200 while (step_set_i < reaction_set.entries.len) {
201 const step_set = &reaction_set.values()[step_set_i];
202 var dirent_i: usize = 0;
203 while (dirent_i < step_set.entries.len) {
204 const generations = step_set.values();
205 if (generations[dirent_i] == w.generation) {
206 dirent_i += 1;
207 continue;
208 }
209 step_set.swapRemoveAt(dirent_i);
210 }
211 if (step_set.entries.len > 0) {
212 step_set_i += 1;
213 continue;
214 }
215 reaction_set.swapRemoveAt(step_set_i);
216 }
217 if (reaction_set.entries.len > 0) {
218 i += 1;
219 continue;
220 }
221 }
222
223 const path = w.dir_table.keys()[i];
224
225 posix.fanotify_mark(fan_fd, .{
226 .REMOVE = true,
227 .ONLYDIR = true,
228 }, fan_mask, path.root_dir.handle.fd, path.subPathOrDot()) catch |err| switch (err) {
229 error.FileNotFound => {}, // Expected, harmless.
230 else => |e| std.log.warn("unable to unwatch '{}': {s}", .{ path, @errorName(e) }),
231 };
232
233 w.dir_table.swapRemoveAt(i);
234 w.os.handle_table.swapRemoveAt(i);
235 }
236 w.generation +%= 1;
237 }
238 }
239 },
240 else => void,
241};
242
243pub fn init() !Watch {
244 switch (builtin.os.tag) {
245 .linux => {
246 const fan_fd = try std.posix.fanotify_init(.{
247 .CLASS = .NOTIF,
248 .CLOEXEC = true,
249 .NONBLOCK = true,
250 .REPORT_NAME = true,
251 .REPORT_DIR_FID = true,
252 .REPORT_FID = true,
253 .REPORT_TARGET_FID = true,
254 }, 0);
255 return .{
256 .dir_table = .{},
257 .os = switch (builtin.os.tag) {
258 .linux => .{
259 .handle_table = .{},
260 .poll_fds = .{
261 .{
262 .fd = fan_fd,
263 .events = std.posix.POLL.IN,
264 .revents = undefined,
265 },
266 },
267 },
268 else => {},
269 },
270 .generation = 0,
271 };
272 },
273 else => @panic("unimplemented"),
274 }
275}
276
48277pub const Match = struct {
49278 /// Relative to the watched directory, the file path that triggers this
50279 /// match.
......@@ -68,119 +297,8 @@ pub const Match = struct {
68297 };
69298};
70299
71pub const LinuxFileHandle = struct {
72 handle: *align(1) std.os.linux.file_handle,
73
74 pub fn clone(lfh: LinuxFileHandle, gpa: Allocator) Allocator.Error!LinuxFileHandle {
75 const bytes = lfh.slice();
76 const new_ptr = try gpa.alignedAlloc(
77 u8,
78 @alignOf(std.os.linux.file_handle),
79 @sizeOf(std.os.linux.file_handle) + bytes.len,
80 );
81 const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr);
82 new_header.* = lfh.handle.*;
83 const new: LinuxFileHandle = .{ .handle = new_header };
84 @memcpy(new.slice(), lfh.slice());
85 return new;
86 }
87
88 pub fn destroy(lfh: LinuxFileHandle, gpa: Allocator) void {
89 const ptr: [*]u8 = @ptrCast(lfh.handle);
90 const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
91 return gpa.free(allocated_slice);
92 }
93
94 pub fn slice(lfh: LinuxFileHandle) []u8 {
95 const ptr: [*]u8 = &lfh.handle.f_handle;
96 return ptr[0..lfh.handle.handle_bytes];
97 }
98
99 pub const Adapter = struct {
100 pub fn hash(self: Adapter, a: LinuxFileHandle) u32 {
101 _ = self;
102 const unsigned_type: u32 = @bitCast(a.handle.handle_type);
103 return @truncate(Hash.hash(unsigned_type, a.slice()));
104 }
105 pub fn eql(self: Adapter, a: LinuxFileHandle, b: LinuxFileHandle, b_index: usize) bool {
106 _ = self;
107 _ = b_index;
108 return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice());
109 }
110 };
111};
112
113pub fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !LinuxFileHandle {
114 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
115 var mount_id: i32 = undefined;
116 var buf: [std.fs.max_path_bytes]u8 = undefined;
117 const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{
118 path.sub_path,
119 }) catch return error.NameTooLong;
120 const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer);
121 stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle);
122 try std.posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, &mount_id, std.os.linux.AT.HANDLE_FID);
123 const stack_lfh: LinuxFileHandle = .{ .handle = stack_ptr };
124 return stack_lfh.clone(gpa);
125}
126
127pub fn markDirtySteps(w: *Watch, gpa: Allocator) !bool {
128 const fanotify = std.os.linux.fanotify;
129 const M = fanotify.event_metadata;
130 var events_buf: [256 + 4096]u8 = undefined;
131 var any_dirty = false;
132 while (true) {
133 var len = std.posix.read(w.fan_fd, &events_buf) catch |err| switch (err) {
134 error.WouldBlock => return any_dirty,
135 else => |e| return e,
136 };
137 var meta: [*]align(1) M = @ptrCast(&events_buf);
138 while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({
139 len -= meta[0].event_len;
140 meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len);
141 }) {
142 assert(meta[0].vers == M.VERSION);
143 if (meta[0].mask.Q_OVERFLOW) {
144 any_dirty = true;
145 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
146 markAllFilesDirty(w, gpa);
147 return true;
148 }
149 const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
150 switch (fid.hdr.info_type) {
151 .DFID_NAME => {
152 const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle);
153 const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes);
154 const file_name = std.mem.span(file_name_z);
155 const lfh: Watch.LinuxFileHandle = .{ .handle = file_handle };
156 if (w.handle_table.getPtr(lfh)) |reaction_set| {
157 if (reaction_set.getPtr(".")) |glob_set|
158 any_dirty = markStepSetDirty(gpa, glob_set, any_dirty);
159 if (reaction_set.getPtr(file_name)) |step_set|
160 any_dirty = markStepSetDirty(gpa, step_set, any_dirty);
161 }
162 },
163 else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}),
164 }
165 }
166 }
167}
168
169pub fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
170 for (all_steps) |step| switch (step.state) {
171 .dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
172 else => continue,
173 };
174 // Now that all dirty steps have been found, the remaining steps that
175 // succeeded from last run shall be marked "cached".
176 for (all_steps) |step| switch (step.state) {
177 .success => step.result_cached = true,
178 else => continue,
179 };
180}
181
182300fn markAllFilesDirty(w: *Watch, gpa: Allocator) void {
183 for (w.handle_table.values()) |reaction_set| {
301 for (w.os.handle_table.values()) |reaction_set| {
184302 for (reaction_set.values()) |step_set| {
185303 for (step_set.keys()) |step| {
186304 step.recursiveReset(gpa);
......@@ -199,3 +317,47 @@ fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool {
199317 }
200318 return any_dirty or this_any_dirty;
201319}
320
321pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void {
322 switch (builtin.os.tag) {
323 .linux => return Os.update(w, gpa, steps),
324 else => @compileError("unimplemented"),
325 }
326}
327
328pub const Timeout = union(enum) {
329 none,
330 ms: u16,
331
332 pub fn to_i32_ms(t: Timeout) i32 {
333 return switch (t) {
334 .none => -1,
335 .ms => |ms| ms,
336 };
337 }
338};
339
340pub const WaitResult = enum {
341 timeout,
342 /// File system watching triggered on files that were marked as inputs to at least one Step.
343 /// Relevant steps have been marked dirty.
344 dirty,
345 /// File system watching triggered but none of the events were relevant to
346 /// what we are listening to. There is nothing to do.
347 clean,
348};
349
350pub fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult {
351 switch (builtin.os.tag) {
352 .linux => {
353 const events_len = try std.posix.poll(&w.os.poll_fds, timeout.to_i32_ms());
354 return if (events_len == 0)
355 .timeout
356 else if (try Os.markDirtySteps(w, gpa))
357 .dirty
358 else
359 .clean;
360 },
361 else => @compileError("unimplemented"),
362 }
363}