authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-08-14 08:39:05+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-08-14 08:39:05+02:00
log4e5b5356094a63a13955c757cb2713f120fa920e
tree80e4b93f3f7159780bba5bbe9fd13b0544ebc46c
parent613c03321a0970cce3a5d04ede04ab4a24ac1dbb
parentbc1f280a77ce118300d3f486932c5768911b0e6f

Merge pull request 'Maker: detect modifications to configurer and recompile it' (#36485) from reconfigure into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/36485

6 files changed, 259 insertions(+), 113 deletions(-)

lib/compiler/Maker.zig+113-45
......@@ -44,6 +44,10 @@ gpa: Allocator,
4444graph: *Graph,
4545install_paths: InstallPaths,
4646scanned_config: *const ScannedConfig,
47/// Includes an extra auto-generated placeholder Step at the end that indicates
48/// configure must be rerun. It is done this way so that the hot path of file
49/// system watching does not need to make any special cases, and to avoid more
50/// OS-specific logic in file system watching implementation.
4751steps: []Step,
4852generated_files: []Path,
4953run_args: ?[]const []const u8,
......@@ -221,7 +225,7 @@ pub fn main(init: process.Init.Minimal) !void {
221225 var skip_oom_steps = false;
222226 var test_timeout_ns: ?u64 = null;
223227 var color: Color = .settingFromEnvironment(&graph.environ_map);
224 var watch = false;
228 var watch_flag = false;
225229 var fuzz: ?Fuzz.Mode = null;
226230 var debounce_interval_ms: u16 = 50;
227231 var listen: bool = false;
......@@ -470,7 +474,7 @@ pub fn main(init: process.Init.Minimal) !void {
470474 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
471475 graph.verbose_llvm_ir = true;
472476 } else if (mem.eql(u8, arg, "--watch")) {
473 watch = true;
477 watch_flag = true;
474478 } else if (mem.eql(u8, arg, "--time-report")) {
475479 graph.time_report = true;
476480 if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
......@@ -570,7 +574,7 @@ pub fn main(init: process.Init.Minimal) !void {
570574 }
571575
572576 const early_exit_mode = fetch_only or help_menu or steps_menu or print_configuration != .none;
573 const server_mode = !early_exit_mode and (watch or webui_listen != null or fuzz != null or listen);
577 const server_mode = !early_exit_mode and (watch_flag or webui_listen != null or fuzz != null or listen);
574578
575579 process.raiseFileDescriptorLimit();
576580
......@@ -697,7 +701,7 @@ pub fn main(init: process.Init.Minimal) !void {
697701 var protocol_server_allocation: AvoidableServer = undefined;
698702 const protocol_server: ?*AvoidableServer = if (listen) s: {
699703 if (builtin.single_threaded) fatal("--listen is not yet supported on single-threaded hosts", .{});
700 if (watch) fatal("using '--watch' and '--listen' together is not supported", .{});
704 if (watch_flag) fatal("using '--watch' and '--listen' together is not supported", .{});
701705 if (fuzz != null) fatal("using '--fuzz' and '--listen' together is not supported", .{});
702706 if (step_names.items.len > 0) fatal("build steps must be provided over the protocol instead of using CLI arguments", .{});
703707 protocol_server_allocation = .{
......@@ -708,7 +712,12 @@ pub fn main(init: process.Init.Minimal) !void {
708712 break :s &protocol_server_allocation;
709713 } else null;
710714
711 while (true) {
715 configure: while (true) {
716 // Set of files that, if modified, imply that recompiling and rerunning
717 // configurer is needed.
718 var configure_source_files: Cache.Manifest.Files = .empty;
719 defer Cache.Manifest.freeFiles(gpa, &configure_source_files);
720
712721 // If this fails, we can still start the server and wait for user
713722 // to request a rebuild. If it returns error.FailedButCacheIntact
714723 // we can even still do file system watching and automatically
......@@ -730,6 +739,7 @@ pub fn main(init: process.Init.Minimal) !void {
730739 .fetch_only = fetch_only,
731740 .print_configuration = print_configuration,
732741 .forks = forks.items,
742 .src_files = &configure_source_files,
733743 })) |scanned_config| {
734744 if (help_menu) {
735745 scanned_config.printUsage(&graph, initStdoutWriter(io)) catch |err| switch (err) {
......@@ -766,7 +776,9 @@ pub fn main(init: process.Init.Minimal) !void {
766776 .include = install_include_path,
767777 },
768778
769 .steps = try arena.alloc(Step, scanned_config.configuration.steps.len),
779 // Extra step at the end which is the autogenerated placeholder
780 // step which indicates that we need to reconfigure.
781 .steps = try arena.alloc(Step, scanned_config.configuration.steps.len + 1),
770782 .generated_files = try arena.alloc(Path, scanned_config.configuration.generated_files_len),
771783 .run_args = run_args,
772784
......@@ -776,7 +788,7 @@ pub fn main(init: process.Init.Minimal) !void {
776788 .skip_oom_steps = skip_oom_steps,
777789 .unit_test_timeout_ns = test_timeout_ns,
778790
779 .watch = watch,
791 .watch = watch_flag,
780792 .web_server = web_server,
781793 .protocol_server = protocol_server,
782794 .protocol_server_mutex = .init,
......@@ -789,7 +801,7 @@ pub fn main(init: process.Init.Minimal) !void {
789801 .multiline_errors = multiline_errors,
790802 .summary = summary orelse if (listen)
791803 .none
792 else if (watch or webui_listen != null)
804 else if (watch_flag or webui_listen != null)
793805 .new
794806 else
795807 .failures,
......@@ -808,7 +820,8 @@ pub fn main(init: process.Init.Minimal) !void {
808820 if (protocol_server) |s| {
809821 try s.serveStringMessage(.bsp_configuration, try arena.print("{f}", .{scanned_config.path}));
810822
811 var w: ?Watch = null;
823 var watch: ?Watch = null;
824 defer if (watch) |*w| w.deinit();
812825
813826 const Event = union(enum) {
814827 message: Reader.Error!Client.Message.Header,
......@@ -843,7 +856,7 @@ pub fn main(init: process.Init.Minimal) !void {
843856 try select.concurrent(.message, Server.receiveMessage, .{s});
844857
845858 maker.watch = body.flags.watch;
846 maker.prepare(steps) catch |err| switch (err) {
859 maker.prepare(steps, &configure_source_files) catch |err| switch (err) {
847860 error.DependencyLoopDetected, error.InsufficientMemory => {
848861 // TODO handle DependencyLoopDetected as error.FailedButCacheIntact
849862 // and handle InsufficientMemory as error.AlreadyReported
......@@ -857,10 +870,13 @@ pub fn main(init: process.Init.Minimal) !void {
857870
858871 if (body.flags.watch) {
859872 if (!Watch.have_impl) unreachable;
860 if (w == null) w = try .init(&maker);
873 if (watch == null) watch = try .init(&maker);
861874
862 try w.?.update(maker.step_stack.keys());
863 try select.concurrent(.fs_event, Watch.wait, .{ &w.?, if (in_debounce) .{ .ms = debounce_interval_ms } else .none });
875 try updateWatch(&maker, &watch.?);
876 try select.concurrent(.fs_event, Watch.wait, .{
877 &watch.?,
878 if (in_debounce) .{ .ms = debounce_interval_ms } else .none,
879 });
864880 }
865881
866882 continue :loop try select.await();
......@@ -870,7 +886,13 @@ pub fn main(init: process.Init.Minimal) !void {
870886 },
871887 .fs_event => |payload| {
872888 if (!Watch.have_impl) unreachable;
873 switch (try payload) {
889 switch (payload catch |err| switch (err) {
890 error.MustReconfigure => {
891 try io.sleep(.fromMilliseconds(debounce_interval_ms), .awake);
892 continue :configure;
893 },
894 else => |e| fatal("file watching failed: {t}", .{e}),
895 }) {
874896 .timeout => {
875897 assert(in_debounce);
876898 markFailedStepsDirty(&maker);
......@@ -880,7 +902,10 @@ pub fn main(init: process.Init.Minimal) !void {
880902 .dirty => in_debounce = true,
881903 .clean => {},
882904 }
883 try select.concurrent(.fs_event, Watch.wait, .{ &w.?, if (in_debounce) .{ .ms = debounce_interval_ms } else .none });
905 try select.concurrent(.fs_event, Watch.wait, .{
906 &watch.?,
907 if (in_debounce) .{ .ms = debounce_interval_ms } else .none,
908 });
884909 continue :loop try select.await();
885910 },
886911 }
......@@ -889,7 +914,7 @@ pub fn main(init: process.Init.Minimal) !void {
889914 const initial_steps = try maker.resolveTopLevelSteps(step_names.items);
890915 defer gpa.free(initial_steps);
891916
892 maker.prepare(initial_steps) catch |err| switch (err) {
917 maker.prepare(initial_steps, &configure_source_files) catch |err| switch (err) {
893918 error.DependencyLoopDetected, error.InsufficientMemory => {
894919 // TODO handle DependencyLoopDetected as error.FailedButCacheIntact
895920 // and handle InsufficientMemory as error.AlreadyReported
......@@ -900,10 +925,11 @@ pub fn main(init: process.Init.Minimal) !void {
900925 };
901926
902927 var w: Watch = w: {
903 if (!watch) break :w undefined;
928 if (!watch_flag) break :w undefined;
904929 if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{native_os});
905930 break :w try .init(&maker);
906931 };
932 defer w.deinit();
907933
908934 if (web_server) |ws| try ws.updateConfiguration(&maker);
909935
......@@ -918,7 +944,7 @@ pub fn main(init: process.Init.Minimal) !void {
918944
919945 if (web_server) |ws| {
920946 const c = &scanned_config.configuration;
921 assert(!watch); // fatal error after CLI parsing
947 assert(!watch_flag); // fatal error after CLI parsing
922948 while (true) switch (try ws.wait()) {
923949 .rebuild => {
924950 for (maker.step_stack.keys()) |step_index| {
......@@ -938,7 +964,7 @@ pub fn main(init: process.Init.Minimal) !void {
938964 // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
939965 if (!Watch.have_impl) unreachable;
940966
941 try w.update(maker.step_stack.keys());
967 try updateWatch(&maker, &w);
942968
943969 // Wait until a file system notification arrives. Read all such events
944970 // until the buffer is empty. Then wait for a debounce interval, resetting
......@@ -950,21 +976,34 @@ pub fn main(init: process.Init.Minimal) !void {
950976 w.dir_count, countSubProcesses(&maker),
951977 }) catch &caption_buf;
952978 var debouncing_node = main_progress_node.start(caption, 0);
979 defer debouncing_node.end();
953980 var in_debounce = false;
954 while (true) switch (try w.wait(if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) {
955 .timeout => {
956 assert(in_debounce);
957 debouncing_node.end();
958 markFailedStepsDirty(&maker);
959 continue :rebuild;
960 },
961 .dirty => if (!in_debounce) {
962 in_debounce = true;
963 debouncing_node.end();
964 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
965 },
966 .clean => {},
967 };
981 while (true) {
982 const timeout: Watch.Timeout = if (in_debounce) .{ .ms = debounce_interval_ms } else .none;
983 switch (w.wait(timeout) catch |err| switch (err) {
984 error.MustReconfigure => {
985 debouncing_node.end();
986 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
987 try io.sleep(.fromMilliseconds(debounce_interval_ms), .awake);
988 continue :configure;
989 },
990 else => |e| fatal("file watching failed: {t}", .{e}),
991 }) {
992 .timeout => {
993 assert(in_debounce);
994 debouncing_node.end();
995 debouncing_node = .none;
996 markFailedStepsDirty(&maker);
997 continue :rebuild;
998 },
999 .dirty => if (!in_debounce) {
1000 in_debounce = true;
1001 debouncing_node.end();
1002 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
1003 },
1004 .clean => {},
1005 }
1006 }
9681007 }
9691008 } else |err| {
9701009 const can_fs_watch = switch (err) {
......@@ -982,7 +1021,7 @@ pub fn main(init: process.Init.Minimal) !void {
9821021 if (protocol_server != null) {
9831022 fatal("(zig build system) TODO send error messages to client when build.zig compilation fails", .{});
9841023 }
985 if (watch and can_fs_watch) {
1024 if (watch_flag and can_fs_watch) {
9861025 fatal("(zig build system) TODO set up fs watching even when build.zig compilation fails", .{});
9871026 } else {
9881027 fatal("(zig build system) TODO stay running and wait for user to request rebuild even when build.zig compilation fails", .{});
......@@ -991,6 +1030,15 @@ pub fn main(init: process.Init.Minimal) !void {
9911030 }
9921031}
9931032
1033/// Temporarily adds the reconfigure pseudostep to step_stack, calls
1034/// `Watch.update`, and then pops it again.
1035fn updateWatch(maker: *Maker, watch: *Watch) !void {
1036 const step_stack = &maker.step_stack;
1037 try step_stack.putNoClobber(maker.gpa, @fromBackingInt(@intCast(maker.steps.len - 1)), {});
1038 defer _ = step_stack.pop().?;
1039 try watch.update(step_stack.keys());
1040}
1041
9941042const ConfigureOptions = struct {
9951043 configure_argv: [][]const u8,
9961044 conf_argv_index_build_root: usize,
......@@ -1008,6 +1056,7 @@ const ConfigureOptions = struct {
10081056 fetch_only: bool,
10091057 print_configuration: PrintConfiguration,
10101058 forks: []Fork,
1059 src_files: *Cache.Manifest.Files,
10111060};
10121061
10131062fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {
......@@ -1362,14 +1411,14 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {
13621411
13631412 if (config_man) |man| {
13641413 if (try man.hit(compile_prog_node)) {
1414 log.debug("configuration cache hit", .{});
13651415 const digest = man.final();
1366 break :cp .{
1367 .{
1368 .root_dir = graph.local_cache_root,
1369 .sub_path = try arena.print("c/{s}", .{&digest}),
1370 },
1371 man.toOwnedLock(),
1416 const path: Path = .{
1417 .root_dir = graph.local_cache_root,
1418 .sub_path = try arena.print("c/{s}", .{&digest}),
13721419 };
1420 options.src_files.* = man.takeFiles();
1421 break :cp .{ path, man.toOwnedLock() };
13731422 }
13741423 }
13751424 const configure_exe_path: Path = if (std.zig.buildExeSubprocess(gpa, io, .{
......@@ -1505,6 +1554,7 @@ fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {
15051554 });
15061555 };
15071556 man.writeManifest() catch |err| log.warn("failed to write cache manifest: {t}", .{err});
1557 options.src_files.* = man.takeFiles();
15081558 break :cp .{ final_path, man.toOwnedLock() };
15091559 }
15101560 };
......@@ -2119,7 +2169,13 @@ fn markFailedStepsDirty(maker: *Maker) void {
21192169 for (all_steps) |step_index| {
21202170 const step = maker.stepByIndex(step_index);
21212171 switch (step.state) {
2122 .dependency_failure, .dependency_skipped, .failure, .skipped => _ = maker.invalidateResult(step),
2172 .dependency_failure,
2173 .dependency_skipped,
2174 .failure,
2175 .skipped,
2176 => _ = maker.invalidateResult(step) catch |err| switch (err) {
2177 error.MustReconfigure => unreachable,
2178 },
21232179 else => continue,
21242180 }
21252181 }
......@@ -2173,7 +2229,11 @@ fn resolveTopLevelSteps(maker: *Maker, step_names: []const []const u8) ![]const
21732229 return try gpa.dupe(Configuration.Step.Index, result.keys());
21742230}
21752231
2176fn prepare(maker: *Maker, step_indices: []const Configuration.Step.Index) !void {
2232fn prepare(
2233 maker: *Maker,
2234 step_indices: []const Configuration.Step.Index,
2235 configure_source_files: *const Cache.Manifest.Files,
2236) !void {
21772237 const gpa = maker.gpa;
21782238 const graph = maker.graph;
21792239 const arena = graph.arena;
......@@ -2182,10 +2242,17 @@ fn prepare(maker: *Maker, step_indices: []const Configuration.Step.Index) !void
21822242 const step_stack = &maker.step_stack;
21832243 const c = &maker.scanned_config.configuration;
21842244
2185 for (maker.steps, 0..) |*step, step_index_usize| {
2245 // The last element is a reserved special pseudostep which contains the
2246 // watch inputs for the configurer executable.
2247 for (maker.steps[0 .. maker.steps.len - 1], 0..) |*step, step_index_usize| {
21862248 const step_index: Configuration.Step.Index = @fromBackingInt(@intCast(step_index_usize));
21872249 step.* = .{ .extended = .init(step_index.ptr(c).flags(c).tag) };
21882250 }
2251 {
2252 const last_step = &maker.steps[maker.steps.len - 1];
2253 last_step.* = .{ .extended = .init(.top_level) };
2254 try last_step.setWatchInputsFromManifestFiles(maker, configure_source_files, graph.cache.prefixes());
2255 }
21892256
21902257 try initial_steps.ensureUnusedCapacity(gpa, step_indices.len);
21912258 try step_stack.ensureUnusedCapacity(gpa, step_indices.len);
......@@ -3045,14 +3112,15 @@ fn constructGraphAndCheckForDependencyLoop(
30453112/// When file watching, prepares the step for being re-evaluated. Returns
30463113/// `true` if the step was newly invalidated, `false` if it was already
30473114/// invalidated.
3048pub fn invalidateResult(maker: *Maker, step: *Step) bool {
3115pub fn invalidateResult(maker: *Maker, step: *Step) error{MustReconfigure}!bool {
3116 if (step == &maker.steps[maker.steps.len - 1]) return error.MustReconfigure;
30493117 if (step.state == .precheck_done) return false;
30503118 assert(step.pending_deps == 0);
30513119 step.state = .precheck_done;
30523120 step.reset(maker);
30533121 for (step.dependants.items) |dependant_index| {
30543122 const dependant = maker.stepByIndex(dependant_index);
3055 _ = invalidateResult(maker, dependant);
3123 _ = try invalidateResult(maker, dependant);
30563124 dependant.pending_deps += 1;
30573125 }
30583126 return true;
lib/compiler/Maker/Step.zig+11-3
......@@ -781,12 +781,20 @@ pub fn writeManifestAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !voi
781781 try setWatchInputsFromManifest(s, maker, man);
782782}
783783
784fn setWatchInputsFromManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
784pub fn setWatchInputsFromManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void {
785 return setWatchInputsFromManifestFiles(s, maker, &man.files, man.cache.prefixes());
786}
787
788pub fn setWatchInputsFromManifestFiles(
789 s: *Step,
790 maker: *Maker,
791 files: *const Cache.Manifest.Files,
792 prefixes: []const Cache.Directory,
793) !void {
785794 const graph = maker.graph;
786795 const arena = graph.arena; // TODO don't leak into process arena
787 const prefixes = man.cache.prefixes();
788796 clearWatchInputs(s, maker);
789 for (man.files.keys()) |file| {
797 for (files.keys()) |file| {
790798 // The file path data is freed when the cache manifest is cleaned up at the end of `make`.
791799 const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path);
792800 try addWatchInputFromPath(s, maker, .{
lib/compiler/Maker/Watch.zig+84-46
......@@ -49,7 +49,10 @@ const Os = switch (builtin.os.tag) {
4949 poll_fds: std.array_hash_map.Auto(MountId, posix.pollfd),
5050
5151 const MountId = i32;
52 const HandleTable = std.array_hash_map.Custom(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false);
52 const HandleTable = std.array_hash_map.Custom(FileHandle, struct {
53 mount_id: MountId,
54 reaction_set: ReactionSet,
55 }, FileHandle.Adapter, false);
5356
5457 const fan_mask: std.os.linux.fanotify.MarkMask = .{
5558 .CLOSE_WRITE = true,
......@@ -81,7 +84,7 @@ const Os = switch (builtin.os.tag) {
8184 }
8285
8386 fn destroy(lfh: FileHandle, gpa: Allocator) void {
84 const ptr: [*]u8 = @ptrCast(lfh.handle);
87 const ptr: [*]align(@alignOf(std.os.linux.file_handle)) u8 = @ptrCast(@alignCast(lfh.handle));
8588 const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes];
8689 return gpa.free(allocated_slice);
8790 }
......@@ -121,6 +124,24 @@ const Os = switch (builtin.os.tag) {
121124 };
122125 }
123126
127 fn deinit(w: *Watch) void {
128 const gpa = w.maker.gpa;
129
130 for (w.os.handle_table.keys(), w.os.handle_table.values()) |fh, *reaction| {
131 fh.destroy(gpa);
132 reaction.reaction_set.deinit(gpa);
133 }
134 w.os.handle_table.deinit(gpa);
135
136 for (w.os.poll_fds.values()) |pollfd| {
137 Io.Threaded.closeFd(pollfd.fd);
138 }
139 w.os.poll_fds.deinit(gpa);
140
141 w.dir_table.deinit(gpa);
142 w.* = undefined;
143 }
144
124145 fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle {
125146 var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined;
126147 var buf: [std.fs.max_path_bytes]u8 = undefined;
......@@ -152,10 +173,8 @@ const Os = switch (builtin.os.tag) {
152173 }) {
153174 assert(meta[0].vers == M.VERSION);
154175 if (meta[0].mask.Q_OVERFLOW) {
155 any_dirty = true;
156 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
157 markAllFilesDirty(w);
158 return true;
176 std.log.warn("file system watch queue overflowed; reconfiguring", .{});
177 return error.MustReconfigure;
159178 }
160179 const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1);
161180 switch (fid.hdr.info_type) {
......@@ -166,9 +185,9 @@ const Os = switch (builtin.os.tag) {
166185 const lfh: FileHandle = .{ .handle = file_handle };
167186 if (w.os.handle_table.getPtr(lfh)) |value| {
168187 if (value.reaction_set.getPtr(".")) |glob_set|
169 any_dirty = markStepSetDirty(maker, glob_set, any_dirty);
188 any_dirty = try markStepSetDirty(maker, glob_set, any_dirty);
170189 if (value.reaction_set.getPtr(file_name)) |step_set|
171 any_dirty = markStepSetDirty(maker, step_set, any_dirty);
190 any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
172191 }
173192 },
174193 else => |t| std.log.warn("unexpected fanotify event '{t}'", .{t}),
......@@ -304,8 +323,11 @@ const Os = switch (builtin.os.tag) {
304323 if (events_len == 0)
305324 return .timeout;
306325 for (w.os.poll_fds.values()) |poll_fd| {
307 if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try markDirtySteps(w, poll_fd.fd))
326 if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and
327 try markDirtySteps(w, poll_fd.fd))
328 {
308329 return .dirty;
330 }
309331 }
310332 return .clean;
311333 }
......@@ -361,7 +383,7 @@ const Os = switch (builtin.os.tag) {
361383 }
362384 }
363385
364 fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void {
386 fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(Io.Threaded.apc_align) callconv(.winapi) void {
365387 const w: *Watch = @ptrCast(@alignCast(apc_context));
366388 const dir: *Directory = @fieldParentPtr("iosb", iosb);
367389 assert(iosb.u.Status != .PENDING);
......@@ -481,6 +503,18 @@ const Os = switch (builtin.os.tag) {
481503 };
482504 }
483505
506 fn deinit(w: *Watch) void {
507 const gpa = w.maker.gpa;
508
509 for (w.os.handle_table.keys()) |dir| {
510 dir.deinit(gpa, w);
511 }
512 w.os.handle_table.deinit(gpa);
513
514 w.dir_table.deinit(gpa);
515 w.* = undefined;
516 }
517
484518 fn getFileId(handle: windows.HANDLE) !FileId {
485519 var file_id: FileId = undefined;
486520 var io_status: windows.IO_STATUS_BLOCK = undefined;
......@@ -521,10 +555,8 @@ const Os = switch (builtin.os.tag) {
521555 var any_dirty = false;
522556 const bytes_returned = dir.iosb.Information;
523557 if (bytes_returned == 0) {
524 std.log.warn("file system watch queue overflowed; falling back to fstat", .{});
525 markAllFilesDirty(w);
526 try dir.startListening(w);
527 return true;
558 std.log.warn("file system watch queue overflowed; reconfiguring", .{});
559 return error.MustReconfigure;
528560 }
529561 var file_name_buf: [std.fs.max_path_bytes]u8 = undefined;
530562 var offset: usize = 0;
......@@ -532,9 +564,9 @@ const Os = switch (builtin.os.tag) {
532564 const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset]));
533565 const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())];
534566 if (dir.reaction_set.getPtr(".")) |glob_set|
535 any_dirty = markStepSetDirty(maker, glob_set, any_dirty);
567 any_dirty = try markStepSetDirty(maker, glob_set, any_dirty);
536568 if (dir.reaction_set.getPtr(file_name)) |step_set|
537 any_dirty = markStepSetDirty(maker, step_set, any_dirty);
569 any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
538570 if (notify.NextEntryOffset == 0)
539571 break;
540572
......@@ -693,6 +725,21 @@ const Os = switch (builtin.os.tag) {
693725 };
694726 }
695727
728 fn deinit(w: *Watch) void {
729 const gpa = w.maker.gpa;
730
731 for (w.os.handles.items(.rs), w.os.handles.items(.dir_fd)) |*rs, dir_fd| {
732 rs.deinit(gpa);
733 Io.Threaded.closeFd(dir_fd);
734 }
735 w.os.handles.deinit(gpa);
736
737 Io.Threaded.closeFd(w.os.kq_fd);
738
739 w.dir_table.deinit(gpa);
740 w.* = undefined;
741 }
742
696743 fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
697744 const maker = w.maker;
698745 const gpa = maker.gpa;
......@@ -711,7 +758,7 @@ const Os = switch (builtin.os.tag) {
711758 fatal("failed to open directory {f}: {t}", .{ path, err });
712759 };
713760 // Empirically the dir has to stay open or else no events are triggered.
714 errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd);
761 errdefer if (!skip_open_dir) Io.Threaded.closeFd(dir_fd);
715762 const changes = [1]posix.Kevent{.{
716763 .ident = @bitCast(@as(isize, dir_fd)),
717764 .filter = std.c.EVFILT.VNODE,
......@@ -811,7 +858,7 @@ const Os = switch (builtin.os.tag) {
811858 };
812859 const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes;
813860 _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null);
814 if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd);
861 if (path.sub_path.len != 0) Io.Threaded.closeFd(dir_fd);
815862
816863 w.dir_table.swapRemoveAt(i);
817864 handles.swapRemove(i);
......@@ -828,12 +875,12 @@ const Os = switch (builtin.os.tag) {
828875 var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer));
829876 if (n == 0) return .timeout;
830877 const reaction_sets = w.os.handles.items(.rs);
831 var any_dirty = markDirtySteps(maker, reaction_sets, event_buffer[0..n], false);
878 var any_dirty = try markDirtySteps(maker, reaction_sets, event_buffer[0..n], false);
832879 timespec_buffer = .{ .sec = 0, .nsec = 0 };
833880 while (n == event_buffer.len) {
834881 n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer);
835882 if (n == 0) break;
836 any_dirty = markDirtySteps(maker, reaction_sets, event_buffer[0..n], any_dirty);
883 any_dirty = try markDirtySteps(maker, reaction_sets, event_buffer[0..n], any_dirty);
837884 }
838885 return if (any_dirty) .dirty else .clean;
839886 }
......@@ -843,7 +890,7 @@ const Os = switch (builtin.os.tag) {
843890 reaction_sets: []ReactionSet,
844891 events: []const std.c.Kevent,
845892 start_any_dirty: bool,
846 ) bool {
893 ) !bool {
847894 var any_dirty = start_any_dirty;
848895 for (events) |event| {
849896 const index: usize = @intCast(event.udata);
......@@ -851,13 +898,13 @@ const Os = switch (builtin.os.tag) {
851898 // If we knew the basename of the changed file, here we would
852899 // mark only the step set dirty, and possibly the glob set:
853900 //if (reaction_set.getPtr(".")) |glob_set|
854 // any_dirty = markStepSetDirty(maker, glob_set, any_dirty);
901 // any_dirty = try markStepSetDirty(maker, glob_set, any_dirty);
855902 //if (reaction_set.getPtr(file_name)) |step_set|
856 // any_dirty = markStepSetDirty(maker, step_set, any_dirty);
903 // any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
857904 // However we don't know the file name so just mark all the
858905 // sets dirty for this directory.
859906 for (reaction_set.values()) |*step_set| {
860 any_dirty = markStepSetDirty(maker, step_set, any_dirty);
907 any_dirty = try markStepSetDirty(maker, step_set, any_dirty);
861908 }
862909 }
863910 return any_dirty;
......@@ -875,6 +922,12 @@ const Os = switch (builtin.os.tag) {
875922 .maker = maker,
876923 };
877924 }
925 fn deinit(w: *Watch) void {
926 const gpa = w.maker.gpa;
927 const io = w.maker.graph.io;
928 w.os.fse.deinit(gpa, io);
929 w.* = undefined;
930 }
878931 fn update(w: *Watch, steps: []const Configuration.Step.Index) !void {
879932 try w.os.fse.setPaths(w.maker, steps);
880933 w.dir_count = w.os.fse.watch_roots.len;
......@@ -915,31 +968,11 @@ pub const Match = struct {
915968 };
916969};
917970
918fn markAllFilesDirty(w: *Watch) void {
919 const maker = w.maker;
920
921 for (switch (builtin.os.tag) {
922 .windows => w.os.handle_table.keys(),
923 else => w.os.handle_table.values(),
924 }) |item| {
925 const reaction_set = switch (builtin.os.tag) {
926 .linux, .windows => item.reaction_set,
927 else => item,
928 };
929 for (reaction_set.values()) |step_set| {
930 for (step_set.keys()) |step_index| {
931 const step = maker.stepByIndex(step_index);
932 _ = maker.invalidateResult(step);
933 }
934 }
935 }
936}
937
938fn markStepSetDirty(maker: *Maker, step_set: *StepSet, any_dirty: bool) bool {
971fn markStepSetDirty(maker: *Maker, step_set: *StepSet, any_dirty: bool) error{MustReconfigure}!bool {
939972 var this_any_dirty = false;
940973 for (step_set.keys()) |step_index| {
941974 const step = maker.stepByIndex(step_index);
942 if (maker.invalidateResult(step)) this_any_dirty = true;
975 if (try maker.invalidateResult(step)) this_any_dirty = true;
943976 }
944977 return any_dirty or this_any_dirty;
945978}
......@@ -984,6 +1017,11 @@ pub const WaitResult = enum {
9841017 clean,
9851018};
9861019
1020/// May return `error.MustReconfigure`.
9871021pub fn wait(w: *Watch, timeout: Timeout) !WaitResult {
9881022 return Os.wait(w, timeout);
9891023}
1024
1025pub fn deinit(w: *Watch) void {
1026 Os.deinit(w);
1027}
lib/compiler/Maker/Watch/FsEvents.zig+28-8
......@@ -46,6 +46,8 @@ since_event: FSEventStreamEventId,
4646
4747cwd_path: []const u8,
4848
49must_reconfigure: bool,
50
4951/// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols
5052/// is not present, `init` will close the framework and return an error.
5153const ResolvedSymbols = struct {
......@@ -104,13 +106,15 @@ pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreService
104106 // to notice any changes which happened during said work.
105107 .since_event = resolved_symbols.FSEventsGetCurrentEventId(),
106108 .cwd_path = cwd_path,
109 .must_reconfigure = false,
107110 };
108111}
109112
110113pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void {
114 _ = io;
111115 fse.waiting_semaphore.as_object().release();
112116 fse.dispatch_queue.as_object().release();
113 fse.core_services.close(io);
117 fse.core_services.close();
114118
115119 gpa.free(fse.watch_roots);
116120 fse.watch_paths.deinit(gpa);
......@@ -211,7 +215,7 @@ pub fn setPaths(fse: *FsEvents, maker: *Maker, steps: []const std.Build.Configur
211215 }
212216}
213217
214pub fn wait(fse: *FsEvents, maker: *Maker, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!Watch.WaitResult {
218pub fn wait(fse: *FsEvents, maker: *Maker, timeout_ns: ?u64) error{ OutOfMemory, StartFailed, MustReconfigure }!Watch.WaitResult {
215219 if (fse.watch_roots.len == 0) @panic("nothing to watch");
216220 const gpa = maker.gpa;
217221
......@@ -285,6 +289,7 @@ pub fn wait(fse: *FsEvents, maker: *Maker, timeout_ns: ?u64) error{ OutOfMemory,
285289 const ns = timeout_ns orelse break :timeout .FOREVER;
286290 break :timeout .time(.NOW, @intCast(ns));
287291 });
292 if (fse.must_reconfigure) return error.MustReconfigure;
288293 return switch (result) {
289294 0 => .dirty,
290295 else => .timeout,
......@@ -355,13 +360,23 @@ fn eventCallback(
355360 false => {
356361 if (fse.watch_paths.get(event_path)) |steps| {
357362 assert(steps.len > 0);
358 if (invalidateSteps(maker, steps)) any_dirty = true;
363 if (invalidateSteps(maker, steps) catch |err| switch (err) {
364 error.MustReconfigure => {
365 fse.must_reconfigure = true;
366 break;
367 },
368 }) any_dirty = true;
359369 }
360370 if (std.fs.path.dirname(event_path)) |event_dirname| {
361371 // Modifying '/foo/bar' triggers the watch on '/foo'.
362372 if (fse.watch_paths.get(event_dirname)) |steps| {
363373 assert(steps.len > 0);
364 if (invalidateSteps(maker, steps)) any_dirty = true;
374 if (invalidateSteps(maker, steps) catch |err| switch (err) {
375 error.MustReconfigure => {
376 fse.must_reconfigure = true;
377 break;
378 },
379 }) any_dirty = true;
365380 }
366381 }
367382 },
......@@ -374,13 +389,18 @@ fn eventCallback(
374389 const changed_path = std.fs.path.dirname(event_path) orelse event_path;
375390 for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| {
376391 if (dirStartsWith(watching_path, changed_path)) {
377 if (invalidateSteps(maker, steps)) any_dirty = true;
392 if (invalidateSteps(maker, steps) catch |err| switch (err) {
393 error.MustReconfigure => {
394 fse.must_reconfigure = true;
395 break;
396 },
397 }) any_dirty = true;
378398 }
379399 }
380400 },
381401 }
382402 }
383 if (any_dirty) {
403 if (any_dirty or fse.must_reconfigure) {
384404 fse.since_event = rs.FSEventStreamGetLatestEventId(stream);
385405 _ = fse.waiting_semaphore.signal();
386406 }
......@@ -392,11 +412,11 @@ fn dirStartsWith(path: []const u8, prefix: []const u8) bool {
392412 return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar`
393413}
394414
395fn invalidateSteps(maker: *Maker, steps: []const std.Build.Configuration.Step.Index) bool {
415fn invalidateSteps(maker: *Maker, steps: []const std.Build.Configuration.Step.Index) !bool {
396416 var any_dirty = false;
397417 for (steps) |step_index| {
398418 const step = maker.stepByIndex(step_index);
399 if (maker.invalidateResult(step)) any_dirty = true;
419 if (try maker.invalidateResult(step)) any_dirty = true;
400420 }
401421 return any_dirty;
402422}
lib/std/Build/Cache.zig+22-11
......@@ -1,7 +1,7 @@
1//! Manages `zig-cache` directories.
2//! This is not a general-purpose cache. It is designed to be fast and simple,
3//! not to withstand attacks using specially-crafted input.
4
1//! Tracks metadata of file inputs associated with Zig compiler and build
2//! system artifacts in order to determine whether those artifacts must be
3//! produced again, or may be retrieved from the cache directory on the
4//! filesystem.
55const Cache = @This();
66const builtin = @import("builtin");
77
......@@ -1236,19 +1236,32 @@ pub const Manifest = struct {
12361236
12371237 /// Obtain only the data needed to maintain a lock on the manifest file.
12381238 /// The `Manifest` remains safe to deinit.
1239 ///
12391240 /// Don't forget to call `writeManifest` before this!
12401241 pub fn toOwnedLock(self: *Manifest) Lock {
12411242 defer self.manifest_file = null;
12421243 return .{ .manifest_file = self.manifest_file.? };
12431244 }
12441245
1246 pub fn takeFiles(man: *Manifest) Files {
1247 defer man.files = .empty;
1248 return man.files;
1249 }
1250
1251 pub fn freeFiles(gpa: Allocator, files: *Files) void {
1252 for (files.keys()) |*file| file.deinit(gpa);
1253 files.deinit(gpa);
1254 }
1255
12451256 /// Releases the manifest file and frees any memory the Manifest was using.
12461257 /// `Manifest.hit` must be called first.
1258 ///
12471259 /// Don't forget to call `writeManifest` before this!
1248 pub fn deinit(self: *Manifest) void {
1249 const io = self.cache.io;
1260 pub fn deinit(man: *Manifest) void {
1261 const io = man.cache.io;
1262 const gpa = man.cache.gpa;
12501263
1251 if (self.manifest_file) |file| {
1264 if (man.manifest_file) |file| {
12521265 if (builtin.os.tag == .windows) {
12531266 // See Lock.release for why this is required on Windows
12541267 file.unlock(io);
......@@ -1256,10 +1269,8 @@ pub const Manifest = struct {
12561269
12571270 file.close(io);
12581271 }
1259 for (self.files.keys()) |*file| {
1260 file.deinit(self.cache.gpa);
1261 }
1262 self.files.deinit(self.cache.gpa);
1272 freeFiles(gpa, &man.files);
1273 man.* = undefined;
12631274 }
12641275
12651276 pub fn populateFileSystemInputs(man: *Manifest, buf: *std.ArrayList(u8)) Allocator.Error!void {
src/main.zig+1
......@@ -5134,6 +5134,7 @@ fn jitCmdInner(
51345134 }
51355135
51365136 if (process.can_replace and options.capture == null) {
5137 _ = try io.lockStderr(&.{}, .no_color);
51375138 const err = process.replace(io, .{ .argv = child_argv.items, .environ_map = environ_map });
51385139 const cmd = try std.mem.join(arena, " ", child_argv.items);
51395140 fatal("the following command failed to execve with {t}:\n{s}", .{ err, cmd });