diff --git a/home/develop/langs/__zig_watch.patch b/home/develop/langs/__zig_watch.patch new file mode 100644 index 0000000..61039e5 --- /dev/null +++ b/home/develop/langs/__zig_watch.patch @@ -0,0 +1,227 @@ +From d465c56d8c0d0b95ff189f60ea86e244d7626241 Mon Sep 17 00:00:00 2001 +From: Kevin Boulain +Date: Sat, 7 Jun 2025 18:58:24 +0200 +Subject: [PATCH] std.Build.Watch: key fanotify file descriptors by mount id +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Since marks are limited to a single filesystem. +Context: https://github.com/ziglang/zig/issues/20670 +Original pull request: https://github.com/ziglang/zig/pull/20672 + +Co-authored-by: Maciej 'vesim' KuliƄski +--- + lib/std/Build/Watch.zig | 103 ++++++++++++++++++++++------------------ + 1 file changed, 56 insertions(+), 47 deletions(-) + +diff --git a/lib/std/Build/Watch.zig b/lib/std/Build/Watch.zig +index 13540a2b36cf..ade31f628e72 100644 +--- a/lib/std/Build/Watch.zig ++++ b/lib/std/Build/Watch.zig +@@ -33,9 +33,12 @@ const Os = switch (builtin.os.tag) { + + /// Keyed differently but indexes correspond 1:1 with `dir_table`. + handle_table: HandleTable, +- poll_fds: [1]posix.pollfd, ++ /// fanotify file descriptors are keyed by mount id since marks ++ /// are limited to a single filesystem. ++ poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd), + +- const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, ReactionSet, FileHandle.Adapter, false); ++ const MountId = i32; ++ const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false); + + const fan_mask: std.os.linux.fanotify.MarkMask = .{ + .CLOSE_WRITE = true, +@@ -92,30 +95,12 @@ const Os = switch (builtin.os.tag) { + }; + + fn init() !Watch { +- const fan_fd = std.posix.fanotify_init(.{ +- .CLASS = .NOTIF, +- .CLOEXEC = true, +- .NONBLOCK = true, +- .REPORT_NAME = true, +- .REPORT_DIR_FID = true, +- .REPORT_FID = true, +- .REPORT_TARGET_FID = true, +- }, 0) catch |err| switch (err) { +- error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}), +- else => |e| return e, +- }; + return .{ + .dir_table = .{}, + .os = switch (builtin.os.tag) { + .linux => .{ + .handle_table = .{}, +- .poll_fds = .{ +- .{ +- .fd = fan_fd, +- .events = std.posix.POLL.IN, +- .revents = undefined, +- }, +- }, ++ .poll_fds = .{}, + }, + else => {}, + }, +@@ -123,22 +108,20 @@ const Os = switch (builtin.os.tag) { + }; + } + +- fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !FileHandle { ++ fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle { + var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; +- var mount_id: i32 = undefined; + var buf: [std.fs.max_path_bytes]u8 = undefined; + const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ + path.sub_path, + }) catch return error.NameTooLong; + const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); + stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); +- try posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, &mount_id, std.os.linux.AT.HANDLE_FID); ++ try posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID); + const stack_lfh: FileHandle = .{ .handle = stack_ptr }; + return stack_lfh.clone(gpa); + } + +- fn markDirtySteps(w: *Watch, gpa: Allocator) !bool { +- const fan_fd = w.os.getFanFd(); ++ fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool { + const fanotify = std.os.linux.fanotify; + const M = fanotify.event_metadata; + var events_buf: [256 + 4096]u8 = undefined; +@@ -167,10 +150,10 @@ const Os = switch (builtin.os.tag) { + 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: FileHandle = .{ .handle = file_handle }; +- if (w.os.handle_table.getPtr(lfh)) |reaction_set| { +- if (reaction_set.getPtr(".")) |glob_set| ++ if (w.os.handle_table.getPtr(lfh)) |value| { ++ if (value.reaction_set.getPtr(".")) |glob_set| + any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); +- if (reaction_set.getPtr(file_name)) |step_set| ++ if (value.reaction_set.getPtr(file_name)) |step_set| + any_dirty = markStepSetDirty(gpa, step_set, any_dirty); + } + }, +@@ -180,19 +163,38 @@ const Os = switch (builtin.os.tag) { + } + } + +- fn getFanFd(os: *const @This()) posix.fd_t { +- return os.poll_fds[0].fd; +- } +- + fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { +- const fan_fd = w.os.getFanFd(); + // Add missing marks and note persisted ones. + for (steps) |step| { + for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { + const reaction_set = rs: { + const gop = try w.dir_table.getOrPut(gpa, path); + if (!gop.found_existing) { +- const dir_handle = try Os.getDirHandle(gpa, path); ++ var mount_id: MountId = undefined; ++ const dir_handle = try Os.getDirHandle(gpa, path, &mount_id); ++ const fan_fd = blk: { ++ const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id); ++ if (!fd_gop.found_existing) { ++ const fan_fd = std.posix.fanotify_init(.{ ++ .CLASS = .NOTIF, ++ .CLOEXEC = true, ++ .NONBLOCK = true, ++ .REPORT_NAME = true, ++ .REPORT_DIR_FID = true, ++ .REPORT_FID = true, ++ .REPORT_TARGET_FID = true, ++ }, 0) catch |err| switch (err) { ++ error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}), ++ else => |e| return e, ++ }; ++ fd_gop.value_ptr.* = .{ ++ .fd = fan_fd, ++ .events = std.posix.POLL.IN, ++ .revents = undefined, ++ }; ++ } ++ break :blk fd_gop.value_ptr.*.fd; ++ }; + // `dir_handle` may already be present in the table in + // the case that we have multiple Cache.Path instances + // that compare inequal but ultimately point to the same +@@ -204,7 +206,7 @@ const Os = switch (builtin.os.tag) { + _ = w.dir_table.pop(); + } else { + assert(dh_gop.index == gop.index); +- dh_gop.value_ptr.* = .{}; ++ dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} }; + posix.fanotify_mark(fan_fd, .{ + .ADD = true, + .ONLYDIR = true, +@@ -212,9 +214,9 @@ const Os = switch (builtin.os.tag) { + fatal("unable to watch {}: {s}", .{ path, @errorName(err) }); + }; + } +- break :rs dh_gop.value_ptr; ++ break :rs &dh_gop.value_ptr.reaction_set; + } +- break :rs &w.os.handle_table.values()[gop.index]; ++ break :rs &w.os.handle_table.values()[gop.index].reaction_set; + }; + for (files.items) |basename| { + const gop = try reaction_set.getOrPut(gpa, basename); +@@ -229,7 +231,7 @@ const Os = switch (builtin.os.tag) { + var i: usize = 0; + while (i < w.os.handle_table.entries.len) { + { +- const reaction_set = &w.os.handle_table.values()[i]; ++ const reaction_set = &w.os.handle_table.values()[i].reaction_set; + var step_set_i: usize = 0; + while (step_set_i < reaction_set.entries.len) { + const step_set = &reaction_set.values()[step_set_i]; +@@ -256,6 +258,8 @@ const Os = switch (builtin.os.tag) { + + const path = w.dir_table.keys()[i]; + ++ const mount_id = w.os.handle_table.values()[i].mount_id; ++ const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd; + posix.fanotify_mark(fan_fd, .{ + .REMOVE = true, + .ONLYDIR = true, +@@ -272,13 +276,14 @@ const Os = switch (builtin.os.tag) { + } + + fn wait(w: *Watch, gpa: Allocator, timeout: Timeout) !WaitResult { +- const events_len = try std.posix.poll(&w.os.poll_fds, timeout.to_i32_ms()); +- return if (events_len == 0) +- .timeout +- else if (try Os.markDirtySteps(w, gpa)) +- .dirty +- else +- .clean; ++ const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms()); ++ if (events_len == 0) ++ return .timeout; ++ for (w.os.poll_fds.values()) |poll_fd| { ++ if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try Os.markDirtySteps(w, gpa, poll_fd.fd)) ++ return .dirty; ++ } ++ return .clean; + } + }, + .windows => struct { +@@ -838,7 +843,11 @@ pub const Match = struct { + }; + + fn markAllFilesDirty(w: *Watch, gpa: Allocator) void { +- for (w.os.handle_table.values()) |reaction_set| { ++ for (w.os.handle_table.values()) |value| { ++ const reaction_set = switch (builtin.os.tag) { ++ .linux => value.reaction_set, ++ else => value, ++ }; + for (reaction_set.values()) |step_set| { + for (step_set.keys()) |step| { + step.recursiveReset(gpa); diff --git a/home/develop/langs/zig.nix b/home/develop/langs/zig.nix index 7edaf8e..82e643c 100644 --- a/home/develop/langs/zig.nix +++ b/home/develop/langs/zig.nix @@ -1,3 +1,8 @@ {pkgs, ...}: { - home.packages = with pkgs; [zig zls]; + home.packages = let + zig_patched = pkgs.zig.overrideAttrs {patches = [./__zig_watch.patch];}; + in [ + zig_patched + (pkgs.zls.override {zig_0_14 = zig_patched;}) + ]; }