blob b58e524c (6773B) - Raw
1 const std = @import("../std.zig"); 2 const Watch = @This(); 3 const Step = std.Build.Step; 4 const Allocator = std.mem.Allocator; 5 const assert = std.debug.assert; 6 7 dir_table: DirTable, 8 /// Keyed differently but indexes correspond 1:1 with `dir_table`. 9 handle_table: HandleTable, 10 fan_fd: std.posix.fd_t, 11 generation: Generation, 12 13 pub const fan_mask: std.os.linux.fanotify.MarkMask = .{ 14 .CLOSE_WRITE = true, 15 .DELETE = true, 16 .MOVED_FROM = true, 17 .MOVED_TO = true, 18 .EVENT_ON_CHILD = true, 19 }; 20 21 pub const init: Watch = .{ 22 .dir_table = .{}, 23 .handle_table = .{}, 24 .fan_fd = -1, 25 .generation = 0, 26 }; 27 28 /// Key is the directory to watch which contains one or more files we are 29 /// interested in noticing changes to. 30 /// 31 /// Value is generation. 32 const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false); 33 34 const HandleTable = std.ArrayHashMapUnmanaged(LinuxFileHandle, ReactionSet, LinuxFileHandle.Adapter, false); 35 const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet); 36 const StepSet = std.AutoArrayHashMapUnmanaged(*Step, Generation); 37 38 const Generation = u8; 39 40 const Hash = std.hash.Wyhash; 41 const Cache = std.Build.Cache; 42 43 pub const Match = struct { 44 /// Relative to the watched directory, the file path that triggers this 45 /// match. 46 basename: []const u8, 47 /// The step to re-run when file corresponding to `basename` is changed. 48 step: *Step, 49 50 pub const Context = struct { 51 pub fn hash(self: Context, a: Match) u32 { 52 _ = self; 53 var hasher = Hash.init(0); 54 std.hash.autoHash(&hasher, a.step); 55 hasher.update(a.basename); 56 return @truncate(hasher.final()); 57 } 58 pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool { 59 _ = self; 60 _ = b_index; 61 return a.step == b.step and std.mem.eql(u8, a.basename, b.basename); 62 } 63 }; 64 }; 65 66 pub const LinuxFileHandle = struct { 67 handle: *align(1) std.os.linux.file_handle, 68 69 pub fn clone(lfh: LinuxFileHandle, gpa: Allocator) Allocator.Error!LinuxFileHandle { 70 const bytes = lfh.slice(); 71 const new_ptr = try gpa.alignedAlloc( 72 u8, 73 @alignOf(std.os.linux.file_handle), 74 @sizeOf(std.os.linux.file_handle) + bytes.len, 75 ); 76 const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr); 77 new_header.* = lfh.handle.*; 78 const new: LinuxFileHandle = .{ .handle = new_header }; 79 @memcpy(new.slice(), lfh.slice()); 80 return new; 81 } 82 83 pub fn destroy(lfh: LinuxFileHandle, gpa: Allocator) void { 84 const ptr: [*]u8 = @ptrCast(lfh.handle); 85 const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes]; 86 return gpa.free(allocated_slice); 87 } 88 89 pub fn slice(lfh: LinuxFileHandle) []u8 { 90 const ptr: [*]u8 = &lfh.handle.f_handle; 91 return ptr[0..lfh.handle.handle_bytes]; 92 } 93 94 pub const Adapter = struct { 95 pub fn hash(self: Adapter, a: LinuxFileHandle) u32 { 96 _ = self; 97 const unsigned_type: u32 = @bitCast(a.handle.handle_type); 98 return @truncate(Hash.hash(unsigned_type, a.slice())); 99 } 100 pub fn eql(self: Adapter, a: LinuxFileHandle, b: LinuxFileHandle, b_index: usize) bool { 101 _ = self; 102 _ = b_index; 103 return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice()); 104 } 105 }; 106 }; 107 108 pub fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path) !LinuxFileHandle { 109 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; 110 var mount_id: i32 = undefined; 111 var buf: [std.fs.max_path_bytes]u8 = undefined; 112 const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ 113 path.sub_path, 114 }) catch return error.NameTooLong; 115 const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); 116 stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); 117 try std.posix.name_to_handle_at(path.root_dir.handle.fd, adjusted_path, stack_ptr, &mount_id, std.os.linux.AT.HANDLE_FID); 118 const stack_lfh: LinuxFileHandle = .{ .handle = stack_ptr }; 119 return stack_lfh.clone(gpa); 120 } 121 122 pub fn markDirtySteps(w: *Watch, gpa: Allocator) !bool { 123 const fanotify = std.os.linux.fanotify; 124 const M = fanotify.event_metadata; 125 var events_buf: [256 + 4096]u8 = undefined; 126 var any_dirty = false; 127 while (true) { 128 var len = std.posix.read(w.fan_fd, &events_buf) catch |err| switch (err) { 129 error.WouldBlock => return any_dirty, 130 else => |e| return e, 131 }; 132 var meta: [*]align(1) M = @ptrCast(&events_buf); 133 while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ 134 len -= meta[0].event_len; 135 meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); 136 }) { 137 assert(meta[0].vers == M.VERSION); 138 const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); 139 switch (fid.hdr.info_type) { 140 .DFID_NAME => { 141 const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); 142 const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); 143 const file_name = std.mem.span(file_name_z); 144 const lfh: Watch.LinuxFileHandle = .{ .handle = file_handle }; 145 if (w.handle_table.getPtr(lfh)) |reaction_set| { 146 if (reaction_set.getPtr(file_name)) |step_set| { 147 for (step_set.keys()) |step| { 148 if (step.state != .precheck_done) { 149 step.recursiveReset(gpa); 150 any_dirty = true; 151 } 152 } 153 } 154 } 155 }, 156 else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}), 157 } 158 } 159 } 160 } 161 162 pub fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void { 163 for (all_steps) |step| switch (step.state) { 164 .dependency_failure, .failure, .skipped => step.recursiveReset(gpa), 165 else => continue, 166 }; 167 // Now that all dirty steps have been found, the remaining steps that 168 // succeeded from last run shall be marked "cached". 169 for (all_steps) |step| switch (step.state) { 170 .success => step.result_cached = true, 171 else => continue, 172 }; 173 }