Maker: update macos file watching code to new api

This commit is contained in:
Andrew Kelley
2026-05-23 14:10:40 -07:00
parent 05fbeb4ea7
commit b069a2eb21
3 changed files with 52 additions and 40 deletions
+1 -1
View File
@@ -623,7 +623,7 @@ pub fn main(init: process.Init.Minimal) !void {
// Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
if (!Watch.have_impl) unreachable;
try w.update(gpa, maker.step_stack.keys());
try w.update(maker.step_stack.keys());
// Wait until a file system notification arrives. Read all such events
// until the buffer is empty. Then wait for a debounce interval, resetting
+19 -16
View File
@@ -177,8 +177,9 @@ const Os = switch (builtin.os.tag) {
}
}
fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void {
fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
const maker = w.maker;
const gpa = maker.gpa;
// Add missing marks and note persisted ones.
for (steps) |step_index| {
@@ -465,8 +466,7 @@ const Os = switch (builtin.os.tag) {
};
};
fn init(cwd_path: []const u8) !Watch {
_ = cwd_path;
fn init(maker: *Maker) !Watch {
return .{
.dir_table = .{},
.dir_count = 0,
@@ -478,6 +478,7 @@ const Os = switch (builtin.os.tag) {
else => {},
},
.generation = 0,
.maker = maker,
};
}
@@ -546,7 +547,8 @@ const Os = switch (builtin.os.tag) {
return any_dirty;
}
fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void {
fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
const gpa = w.maker.gpa;
// Add missing marks and note persisted ones.
for (steps) |step| {
for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
@@ -677,8 +679,7 @@ const Os = switch (builtin.os.tag) {
const EV = std.c.EV;
const NOTE = std.c.NOTE;
fn init(cwd_path: []const u8) !Watch {
_ = cwd_path;
fn init(maker: *Maker) !Watch {
return .{
.dir_table = .{},
.dir_count = 0,
@@ -687,10 +688,12 @@ const Os = switch (builtin.os.tag) {
.handles = .empty,
},
.generation = 0,
.maker = maker,
};
}
fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void {
fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
const gpa = w.maker.gpa;
const handles = &w.os.handles;
for (steps) |step| {
for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
@@ -860,21 +863,21 @@ const Os = switch (builtin.os.tag) {
.macos => struct {
fse: FsEvents,
fn init(cwd_path: []const u8) !Watch {
fn init(maker: *Maker) !Watch {
return .{
.os = .{ .fse = try .init(cwd_path) },
.os = .{ .fse = try .init(maker.graph.cache.cwd) },
.dir_count = 0,
.dir_table = undefined,
.generation = undefined,
.maker = maker,
};
}
fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void {
try w.os.fse.setPaths(gpa, steps);
fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
try w.os.fse.setPaths(w.maker, steps);
w.dir_count = w.os.fse.watch_roots.len;
}
fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult {
_ = io;
return w.os.fse.wait(gpa, switch (timeout) {
fn wait(w: *Watch, timeout: Timeout) !WaitResult {
return w.os.fse.wait(w.maker, switch (timeout) {
.none => null,
.ms => |ms| @as(u64, ms) * std.time.ns_per_ms,
});
@@ -938,8 +941,8 @@ fn markStepSetDirty(maker: *Maker, step_set: *StepSet, any_dirty: bool) bool {
return any_dirty or this_any_dirty;
}
pub fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void {
return Os.update(w, gpa, steps);
pub fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
return Os.update(w, steps);
}
pub const Timeout = union(enum) {
+32 -23
View File
@@ -17,6 +17,7 @@
//! the logic that would avoid them is currently disabled, because the build system kind
//! of relies on them at the time of writing to avoid redundant work -- see the comment at
//! the top of `wait` for details.
const FsEvents = @This();
const enable_debug_logs = false;
@@ -30,7 +31,7 @@ paths_arena: std.heap.ArenaAllocator.State,
watch_roots: [][:0]const u8,
/// All of the paths being watched. Value is the set of steps which depend on the file/directory.
/// Keys and values are in `paths_arena`, but this map is allocated into the GPA.
watch_paths: std.StringArrayHashMapUnmanaged([]const *std.Build.Step),
watch_paths: std.array_hash_map.String([]const std.Build.Configuration.Step.Index),
/// The semaphore we use to block the thread calling `wait` until the callback determines a relevant
/// event has occurred. This is retained across `wait` calls for simplicity and efficiency.
@@ -118,19 +119,22 @@ pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void {
}
}
pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) !void {
pub fn setPaths(fse: *FsEvents, maker: *Maker, steps: []const std.Build.Configuration.Step.Index) !void {
const gpa = maker.gpa;
var paths_arena_instance = fse.paths_arena.promote(gpa);
defer fse.paths_arena = paths_arena_instance.state;
const paths_arena = paths_arena_instance.allocator();
var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty;
var need_dirs: std.array_hash_map.String(void) = .empty;
defer need_dirs.deinit(gpa);
fse.watch_paths.clearRetainingCapacity();
// We take `step` by pointer for a slight memory optimization in a moment.
for (steps) |*step| {
for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| {
// We take `step_index` by pointer for a slight memory optimization in a moment.
for (steps) |*step_index| {
const step = maker.stepByIndex(step_index.*);
for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| {
const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{
fse.cwd_path, path.root_dir.path orelse ".", path.sub_path,
});
@@ -143,14 +147,14 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
const gop = try fse.watch_paths.getOrPut(gpa, watch_path);
if (gop.found_existing) {
const old_steps = gop.value_ptr.*;
const new_steps = try paths_arena.alloc(*std.Build.Step, old_steps.len + 1);
const new_steps = try paths_arena.alloc(std.Build.Configuration.Step.Index, old_steps.len + 1);
@memcpy(new_steps[0..old_steps.len], old_steps);
new_steps[old_steps.len] = step.*;
new_steps[old_steps.len] = step_index.*;
gop.value_ptr.* = new_steps;
} else {
// This is why we captured `step` by pointer! We can avoid allocating a slice of one
// step in the arena in the common case where a file is referenced by only one step.
gop.value_ptr.* = step[0..1];
gop.value_ptr.* = step_index[0..1];
}
}
}
@@ -206,8 +210,9 @@ pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step)
}
}
pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!std.Build.Watch.WaitResult {
pub fn wait(fse: *FsEvents, maker: *Maker, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!Watch.WaitResult {
if (fse.watch_roots.len == 0) @panic("nothing to watch");
const gpa = maker.gpa;
const rs = fse.resolved_symbols;
@@ -253,7 +258,7 @@ pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory
const callback_ctx: EventCallbackCtx = .{
.fse = fse,
.gpa = gpa,
.maker = maker,
};
const event_stream = rs.FSEventStreamCreate(
null,
@@ -321,7 +326,7 @@ const cf_alloc_callbacks = struct {
const EventCallbackCtx = struct {
fse: *FsEvents,
gpa: Allocator,
maker: *Maker,
};
fn eventCallback(
@@ -333,8 +338,8 @@ fn eventCallback(
events_ids_ptr: [*]const FSEventStreamEventId,
) callconv(.c) void {
const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info));
const maker = ctx.maker;
const fse = ctx.fse;
const gpa = ctx.gpa;
const rs = fse.resolved_symbols;
const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr));
const events_paths = events_paths_ptr_casted[0..num_events];
@@ -349,17 +354,13 @@ fn eventCallback(
false => {
if (fse.watch_paths.get(event_path)) |steps| {
assert(steps.len > 0);
for (steps) |s| {
if (s.invalidateResult(gpa)) any_dirty = true;
}
if (invalidateSteps(maker, steps)) any_dirty = true;
}
if (std.fs.path.dirname(event_path)) |event_dirname| {
// Modifying '/foo/bar' triggers the watch on '/foo'.
if (fse.watch_paths.get(event_dirname)) |steps| {
assert(steps.len > 0);
for (steps) |s| {
if (s.invalidateResult(gpa)) any_dirty = true;
}
if (invalidateSteps(maker, steps)) any_dirty = true;
}
}
},
@@ -372,9 +373,7 @@ fn eventCallback(
const changed_path = std.fs.path.dirname(event_path) orelse event_path;
for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| {
if (dirStartsWith(watching_path, changed_path)) {
for (steps) |s| {
if (s.invalidateResult(gpa)) any_dirty = true;
}
if (invalidateSteps(maker, steps)) any_dirty = true;
}
}
},
@@ -392,6 +391,15 @@ fn dirStartsWith(path: []const u8, prefix: []const u8) bool {
return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar`
}
fn invalidateSteps(maker: *Maker, steps: []const std.Build.Configuration.Step.Index) bool {
var any_dirty = false;
for (steps) |step_index| {
const step = maker.stepByIndex(step_index);
if (maker.invalidateResult(step)) any_dirty = true;
}
return any_dirty;
}
const CFAllocatorRef = ?*const opaque {};
const CFArrayRef = *const opaque {};
const CFStringRef = *const opaque {};
@@ -476,4 +484,5 @@ const Io = std.Io;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const watch_log = std.log.scoped(.watch);
const FsEvents = @This();
const Maker = @import("../../Maker.zig");
const Watch = @import("../Watch.zig");