| author | |
| committer | |
| log | b3d162d6bfe82d84d55edd58016347a420732fe2 |
| tree | f4b64b73de31b3d662d99d1f466444f4d7c9242f |
| parent | ef050483dff9f4fe57107b4c3ddba9a2692bdef4 |
23 files changed, 9347 insertions(+), 9347 deletions(-)
lib/compiler/Maker.zig created+1848| ... | @@ -0,0 +1,1848 @@ | ||
| 1 | const Maker = @This(); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | |||
| 4 | const std = @import("std"); | ||
| 5 | const Allocator = std.mem.Allocator; | ||
| 6 | const Cache = std.Build.Cache; | ||
| 7 | const Configuration = std.Build.Configuration; | ||
| 8 | const File = std.Io.File; | ||
| 9 | const Io = std.Io; | ||
| 10 | const Path = std.Build.Cache.Path; | ||
| 11 | const Writer = std.Io.Writer; | ||
| 12 | const assert = std.debug.assert; | ||
| 13 | const fatal = std.process.fatal; | ||
| 14 | const fmt = std.fmt; | ||
| 15 | const log = std.log; | ||
| 16 | const mem = std.mem; | ||
| 17 | const process = std.process; | ||
| 18 | |||
| 19 | const Fuzz = @import("Maker/Fuzz.zig"); | ||
| 20 | const Graph = @import("Maker/Graph.zig"); | ||
| 21 | const Step = @import("Maker/Step.zig"); | ||
| 22 | const Watch = @import("Maker/Watch.zig"); | ||
| 23 | const WebServer = @import("Maker/WebServer.zig"); | ||
| 24 | |||
| 25 | pub const std_options: std.Options = .{ | ||
| 26 | .side_channels_mitigations = .none, | ||
| 27 | .http_disable_tls = true, | ||
| 28 | }; | ||
| 29 | |||
| 30 | gpa: Allocator, | ||
| 31 | graph: *Graph, | ||
| 32 | install_paths: InstallPaths, | ||
| 33 | scanned_config: *const ScannedConfig, | ||
| 34 | steps: []Step, | ||
| 35 | |||
| 36 | available_rss: usize, | ||
| 37 | max_rss_is_default: bool, | ||
| 38 | max_rss_mutex: Io.Mutex, | ||
| 39 | skip_oom_steps: bool, | ||
| 40 | unit_test_timeout_ns: ?u64, | ||
| 41 | watch: bool, | ||
| 42 | web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn, | ||
| 43 | /// Allocated into `gpa`. | ||
| 44 | memory_blocked_steps: std.ArrayList(Configuration.Step.Index), | ||
| 45 | /// Allocated into `gpa`. | ||
| 46 | step_stack: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void), | ||
| 47 | |||
| 48 | error_style: ErrorStyle, | ||
| 49 | multiline_errors: MultilineErrors, | ||
| 50 | summary: Summary, | ||
| 51 | |||
| 52 | pub fn main(init: process.Init.Minimal) !void { | ||
| 53 | // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not | ||
| 54 | // always the case. So, we do need a true gpa for some things. | ||
| 55 | var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{}); | ||
| 56 | defer _ = safe_gpa_state.deinit(); | ||
| 57 | const gpa = safe_gpa_state.allocator(); | ||
| 58 | |||
| 59 | var threaded: std.Io.Threaded = .init(gpa, .{ | ||
| 60 | .environ = init.environ, | ||
| 61 | .argv0 = .init(init.args), | ||
| 62 | }); | ||
| 63 | defer threaded.deinit(); | ||
| 64 | const io = threaded.io(); | ||
| 65 | |||
| 66 | // ...but we'll back our arena by `std.heap.page_allocator` for efficiency. | ||
| 67 | var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); | ||
| 68 | defer arena_instance.deinit(); | ||
| 69 | const arena = arena_instance.allocator(); | ||
| 70 | |||
| 71 | const args = try init.args.toSlice(arena); | ||
| 72 | |||
| 73 | // skip my own exe name | ||
| 74 | var arg_idx: usize = 1; | ||
| 75 | |||
| 76 | const zig_exe = expectArgOrFatal(args, &arg_idx, "--zig"); | ||
| 77 | const zig_lib_dir = expectArgOrFatal(args, &arg_idx, "--zig-lib-dir"); | ||
| 78 | const build_root = expectArgOrFatal(args, &arg_idx, "--build-root"); | ||
| 79 | const local_cache_root = expectArgOrFatal(args, &arg_idx, "--local-cache"); | ||
| 80 | const global_cache_root = expectArgOrFatal(args, &arg_idx, "--global-cache"); | ||
| 81 | const configure_path = expectArgOrFatal(args, &arg_idx, "--configuration"); | ||
| 82 | |||
| 83 | const cwd: Io.Dir = .cwd(); | ||
| 84 | |||
| 85 | const zig_lib_directory: Cache.Directory = .{ | ||
| 86 | .path = zig_lib_dir, | ||
| 87 | .handle = try cwd.openDir(io, zig_lib_dir, .{}), | ||
| 88 | }; | ||
| 89 | |||
| 90 | const build_root_directory: Cache.Directory = .{ | ||
| 91 | .path = build_root, | ||
| 92 | .handle = try cwd.openDir(io, build_root, .{}), | ||
| 93 | }; | ||
| 94 | |||
| 95 | const local_cache_directory: Cache.Directory = .{ | ||
| 96 | .path = local_cache_root, | ||
| 97 | .handle = try cwd.createDirPathOpen(io, local_cache_root, .{}), | ||
| 98 | }; | ||
| 99 | |||
| 100 | const global_cache_directory: Cache.Directory = .{ | ||
| 101 | .path = global_cache_root, | ||
| 102 | .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}), | ||
| 103 | }; | ||
| 104 | |||
| 105 | var graph: Graph = .{ | ||
| 106 | .io = io, | ||
| 107 | .arena = arena, | ||
| 108 | .cache = .{ | ||
| 109 | .io = io, | ||
| 110 | .gpa = gpa, | ||
| 111 | .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}), | ||
| 112 | .cwd = try process.currentPathAlloc(io, arena), | ||
| 113 | }, | ||
| 114 | .zig_exe = zig_exe, | ||
| 115 | .environ_map = try init.environ.createMap(arena), | ||
| 116 | .global_cache_root = global_cache_directory, | ||
| 117 | .zig_lib_directory = zig_lib_directory, | ||
| 118 | }; | ||
| 119 | |||
| 120 | graph.cache.addPrefix(.{ .path = null, .handle = cwd }); | ||
| 121 | graph.cache.addPrefix(build_root_directory); | ||
| 122 | graph.cache.addPrefix(local_cache_directory); | ||
| 123 | graph.cache.addPrefix(global_cache_directory); | ||
| 124 | graph.cache.hash.addBytes(builtin.zig_version_string); | ||
| 125 | |||
| 126 | var step_names: std.ArrayList([]const u8) = .empty; | ||
| 127 | var debug_log_scopes: std.ArrayList([]const u8) = .empty; | ||
| 128 | var help_menu = false; | ||
| 129 | var steps_menu = false; | ||
| 130 | var print_configuration = false; | ||
| 131 | var override_install_prefix: ?[]const u8 = null; | ||
| 132 | var override_lib_dir: ?[]const u8 = null; | ||
| 133 | var override_bin_dir: ?[]const u8 = null; | ||
| 134 | var override_include_dir: ?[]const u8 = null; | ||
| 135 | var error_style: ErrorStyle = .verbose; | ||
| 136 | var multiline_errors: MultilineErrors = .indent; | ||
| 137 | var summary: ?Summary = null; | ||
| 138 | var max_rss: u64 = 0; | ||
| 139 | var skip_oom_steps = false; | ||
| 140 | var test_timeout_ns: ?u64 = null; | ||
| 141 | var color: Color = .auto; | ||
| 142 | var watch = false; | ||
| 143 | var fuzz: ?Fuzz.Mode = null; | ||
| 144 | var debounce_interval_ms: u16 = 50; | ||
| 145 | var webui_listen: ?Io.net.IpAddress = null; | ||
| 146 | var verbose = false; | ||
| 147 | var sysroot: ?[]const u8 = null; | ||
| 148 | var search_prefixes: std.ArrayList([]const u8) = .empty; | ||
| 149 | var libc_file: ?[]const u8 = null; | ||
| 150 | var debug_pkg_config: bool = false; | ||
| 151 | // After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md, | ||
| 152 | // this will be the directory $glibc-build-dir/install/glibcs | ||
| 153 | // Given the example of the aarch64 target, this is the directory | ||
| 154 | // that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`. | ||
| 155 | // Also works for dynamic musl. | ||
| 156 | var libc_runtimes_dir: ?[]const u8 = null; | ||
| 157 | var enable_wine = false; | ||
| 158 | var enable_qemu = false; | ||
| 159 | var enable_wasmtime = false; | ||
| 160 | var enable_darling = false; | ||
| 161 | var enable_rosetta = false; | ||
| 162 | var reference_trace: ?u32 = null; | ||
| 163 | var run_args: ?[]const []const u8 = null; | ||
| 164 | |||
| 165 | if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| { | ||
| 166 | if (std.meta.stringToEnum(ErrorStyle, str)) |style| { | ||
| 167 | error_style = style; | ||
| 168 | } | ||
| 169 | } | ||
| 170 | |||
| 171 | if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| { | ||
| 172 | if (std.meta.stringToEnum(MultilineErrors, str)) |style| { | ||
| 173 | multiline_errors = style; | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | while (nextArg(args, &arg_idx)) |arg| { | ||
| 178 | if (mem.startsWith(u8, arg, "-")) { | ||
| 179 | if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { | ||
| 180 | help_menu = true; | ||
| 181 | } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) { | ||
| 182 | steps_menu = true; | ||
| 183 | } else if (mem.eql(u8, arg, "--print-configuration")) { | ||
| 184 | print_configuration = true; | ||
| 185 | } else if (mem.eql(u8, arg, "--verbose")) { | ||
| 186 | verbose = true; | ||
| 187 | } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) { | ||
| 188 | override_install_prefix = nextArgOrFatal(args, &arg_idx); | ||
| 189 | } else if (mem.eql(u8, arg, "--prefix-lib-dir")) { | ||
| 190 | override_lib_dir = nextArgOrFatal(args, &arg_idx); | ||
| 191 | } else if (mem.eql(u8, arg, "--prefix-exe-dir")) { | ||
| 192 | override_bin_dir = nextArgOrFatal(args, &arg_idx); | ||
| 193 | } else if (mem.eql(u8, arg, "--prefix-include-dir")) { | ||
| 194 | override_include_dir = nextArgOrFatal(args, &arg_idx); | ||
| 195 | } else if (mem.eql(u8, arg, "--sysroot")) { | ||
| 196 | sysroot = nextArgOrFatal(args, &arg_idx); | ||
| 197 | } else if (mem.eql(u8, arg, "--maxrss")) { | ||
| 198 | const max_rss_text = nextArgOrFatal(args, &arg_idx); | ||
| 199 | max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| | ||
| 200 | fatal("invalid byte size: '{s}': {t}", .{ max_rss_text, err }); | ||
| 201 | } else if (mem.eql(u8, arg, "--skip-oom-steps")) { | ||
| 202 | skip_oom_steps = true; | ||
| 203 | } else if (mem.eql(u8, arg, "--test-timeout")) { | ||
| 204 | const units: []const struct { []const u8, u64 } = &.{ | ||
| 205 | .{ "ns", 1 }, | ||
| 206 | .{ "nanosecond", 1 }, | ||
| 207 | .{ "us", std.time.ns_per_us }, | ||
| 208 | .{ "microsecond", std.time.ns_per_us }, | ||
| 209 | .{ "ms", std.time.ns_per_ms }, | ||
| 210 | .{ "millisecond", std.time.ns_per_ms }, | ||
| 211 | .{ "s", std.time.ns_per_s }, | ||
| 212 | .{ "second", std.time.ns_per_s }, | ||
| 213 | .{ "m", std.time.ns_per_min }, | ||
| 214 | .{ "minute", std.time.ns_per_min }, | ||
| 215 | .{ "h", std.time.ns_per_hour }, | ||
| 216 | .{ "hour", std.time.ns_per_hour }, | ||
| 217 | }; | ||
| 218 | const timeout_str = nextArgOrFatal(args, &arg_idx); | ||
| 219 | const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal( | ||
| 220 | "invalid timeout '{s}': expected unit (ns, us, ms, s, m, h)", | ||
| 221 | .{timeout_str}, | ||
| 222 | ); | ||
| 223 | const num_str = timeout_str[0 .. num_end_idx + 1]; | ||
| 224 | const unit_str = timeout_str[num_end_idx + 1 ..]; | ||
| 225 | const unit_factor: f64 = for (units) |unit_and_factor| { | ||
| 226 | if (std.mem.eql(u8, unit_str, unit_and_factor[0])) { | ||
| 227 | break @floatFromInt(unit_and_factor[1]); | ||
| 228 | } | ||
| 229 | } else fatal( | ||
| 230 | "invalid timeout '{s}': invalid unit '{s}' (expected ns, us, ms, s, m, h)", | ||
| 231 | .{ timeout_str, unit_str }, | ||
| 232 | ); | ||
| 233 | const num_parsed = std.fmt.parseFloat(f64, num_str) catch |err| fatal( | ||
| 234 | "invalid timeout '{s}': invalid number '{s}' ({t})", | ||
| 235 | .{ timeout_str, num_str, err }, | ||
| 236 | ); | ||
| 237 | test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed); | ||
| 238 | } else if (mem.eql(u8, arg, "--search-prefix")) { | ||
| 239 | try search_prefixes.append(arena, nextArgOrFatal(args, &arg_idx)); | ||
| 240 | } else if (mem.eql(u8, arg, "--libc")) { | ||
| 241 | libc_file = nextArgOrFatal(args, &arg_idx); | ||
| 242 | } else if (mem.eql(u8, arg, "--color")) { | ||
| 243 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 244 | fatalWithHint("expected [auto|on|off] after '{s}'", .{arg}); | ||
| 245 | color = std.meta.stringToEnum(Color, next_arg) orelse { | ||
| 246 | fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{ | ||
| 247 | arg, next_arg, | ||
| 248 | }); | ||
| 249 | }; | ||
| 250 | } else if (mem.eql(u8, arg, "--error-style")) { | ||
| 251 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 252 | fatalWithHint("expected style after '{s}'", .{arg}); | ||
| 253 | error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse { | ||
| 254 | fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg }); | ||
| 255 | }; | ||
| 256 | } else if (mem.eql(u8, arg, "--multiline-errors")) { | ||
| 257 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 258 | fatalWithHint("expected style after '{s}'", .{arg}); | ||
| 259 | multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse { | ||
| 260 | fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg }); | ||
| 261 | }; | ||
| 262 | } else if (mem.eql(u8, arg, "--summary")) { | ||
| 263 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 264 | fatalWithHint("expected [all|new|failures|line|none] after '{s}'", .{arg}); | ||
| 265 | summary = std.meta.stringToEnum(Summary, next_arg) orelse { | ||
| 266 | fatalWithHint("expected [all|new|failures|line|none] after '{s}', found '{s}'", .{ | ||
| 267 | arg, next_arg, | ||
| 268 | }); | ||
| 269 | }; | ||
| 270 | } else if (mem.eql(u8, arg, "--seed")) { | ||
| 271 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 272 | fatalWithHint("expected u32 after '{s}'", .{arg}); | ||
| 273 | graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| { | ||
| 274 | fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {t}", .{ next_arg, err }); | ||
| 275 | }; | ||
| 276 | } else if (mem.eql(u8, arg, "--debounce")) { | ||
| 277 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 278 | fatalWithHint("expected u16 after '{s}'", .{arg}); | ||
| 279 | debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| { | ||
| 280 | fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {t}\n", .{ | ||
| 281 | next_arg, err, | ||
| 282 | }); | ||
| 283 | }; | ||
| 284 | } else if (mem.eql(u8, arg, "--webui")) { | ||
| 285 | if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; | ||
| 286 | } else if (mem.startsWith(u8, arg, "--webui=")) { | ||
| 287 | const addr_str = arg["--webui=".len..]; | ||
| 288 | if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{}); | ||
| 289 | webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| { | ||
| 290 | fatal("invalid web UI address '{s}': {t}", .{ addr_str, err }); | ||
| 291 | }; | ||
| 292 | } else if (mem.eql(u8, arg, "--debug-log")) { | ||
| 293 | const next_arg = nextArgOrFatal(args, &arg_idx); | ||
| 294 | try debug_log_scopes.append(arena, next_arg); | ||
| 295 | } else if (mem.eql(u8, arg, "--debug-pkg-config")) { | ||
| 296 | debug_pkg_config = true; | ||
| 297 | } else if (mem.eql(u8, arg, "--debug-rt")) { | ||
| 298 | graph.debug_compiler_runtime_libs = .Debug; | ||
| 299 | } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| { | ||
| 300 | graph.debug_compiler_runtime_libs = std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse | ||
| 301 | fatal("unrecognized optimization mode: {s}", .{rest}); | ||
| 302 | } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) { | ||
| 303 | // --glibc-runtimes was the old name of the flag; kept for compatibility for now. | ||
| 304 | libc_runtimes_dir = nextArgOrFatal(args, &arg_idx); | ||
| 305 | } else if (mem.eql(u8, arg, "--watch")) { | ||
| 306 | watch = true; | ||
| 307 | } else if (mem.eql(u8, arg, "--time-report")) { | ||
| 308 | graph.time_report = true; | ||
| 309 | if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; | ||
| 310 | } else if (mem.eql(u8, arg, "--fuzz")) { | ||
| 311 | fuzz = .{ .forever = undefined }; | ||
| 312 | if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; | ||
| 313 | } else if (mem.startsWith(u8, arg, "--fuzz=")) { | ||
| 314 | const value = arg["--fuzz=".len..]; | ||
| 315 | if (value.len == 0) fatal("missing argument to --fuzz", .{}); | ||
| 316 | |||
| 317 | const unit: u8 = value[value.len - 1]; | ||
| 318 | const digits = switch (unit) { | ||
| 319 | '0'...'9' => value, | ||
| 320 | 'K', 'M', 'G' => value[0 .. value.len - 1], | ||
| 321 | else => fatal( | ||
| 322 | "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]", | ||
| 323 | .{}, | ||
| 324 | ), | ||
| 325 | }; | ||
| 326 | |||
| 327 | const amount = std.fmt.parseInt(u64, digits, 10) catch { | ||
| 328 | fatal( | ||
| 329 | "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]", | ||
| 330 | .{}, | ||
| 331 | ); | ||
| 332 | }; | ||
| 333 | |||
| 334 | const normalized_amount = std.math.mul(u64, amount, switch (unit) { | ||
| 335 | else => unreachable, | ||
| 336 | '0'...'9' => 1, | ||
| 337 | 'K' => 1000, | ||
| 338 | 'M' => 1_000_000, | ||
| 339 | 'G' => 1_000_000_000, | ||
| 340 | }) catch fatal("fuzzing limit amount overflows u64", .{}); | ||
| 341 | |||
| 342 | fuzz = .{ | ||
| 343 | .limit = .{ | ||
| 344 | .amount = normalized_amount, | ||
| 345 | }, | ||
| 346 | }; | ||
| 347 | } else if (mem.eql(u8, arg, "-fincremental")) { | ||
| 348 | graph.incremental = true; | ||
| 349 | } else if (mem.eql(u8, arg, "-fno-incremental")) { | ||
| 350 | graph.incremental = false; | ||
| 351 | } else if (mem.eql(u8, arg, "-fwine")) { | ||
| 352 | enable_wine = true; | ||
| 353 | } else if (mem.eql(u8, arg, "-fno-wine")) { | ||
| 354 | enable_wine = false; | ||
| 355 | } else if (mem.eql(u8, arg, "-fqemu")) { | ||
| 356 | enable_qemu = true; | ||
| 357 | } else if (mem.eql(u8, arg, "-fno-qemu")) { | ||
| 358 | enable_qemu = false; | ||
| 359 | } else if (mem.eql(u8, arg, "-fwasmtime")) { | ||
| 360 | enable_wasmtime = true; | ||
| 361 | } else if (mem.eql(u8, arg, "-fno-wasmtime")) { | ||
| 362 | enable_wasmtime = false; | ||
| 363 | } else if (mem.eql(u8, arg, "-frosetta")) { | ||
| 364 | enable_rosetta = true; | ||
| 365 | } else if (mem.eql(u8, arg, "-fno-rosetta")) { | ||
| 366 | enable_rosetta = false; | ||
| 367 | } else if (mem.eql(u8, arg, "-fdarling")) { | ||
| 368 | enable_darling = true; | ||
| 369 | } else if (mem.eql(u8, arg, "-fno-darling")) { | ||
| 370 | enable_darling = false; | ||
| 371 | } else if (mem.eql(u8, arg, "-fallow-so-scripts")) { | ||
| 372 | graph.allow_so_scripts = true; | ||
| 373 | } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) { | ||
| 374 | graph.allow_so_scripts = false; | ||
| 375 | } else if (mem.eql(u8, arg, "-freference-trace")) { | ||
| 376 | reference_trace = 256; | ||
| 377 | } else if (mem.startsWith(u8, arg, "-freference-trace=")) { | ||
| 378 | const num = arg["-freference-trace=".len..]; | ||
| 379 | reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| { | ||
| 380 | std.debug.print("unable to parse reference_trace count '{s}': {t}", .{ num, err }); | ||
| 381 | process.exit(1); | ||
| 382 | }; | ||
| 383 | } else if (mem.eql(u8, arg, "-fno-reference-trace")) { | ||
| 384 | reference_trace = null; | ||
| 385 | } else if (mem.cutPrefix(u8, arg, "-j")) |text| { | ||
| 386 | const n = std.fmt.parseUnsigned(u32, text, 10) catch |err| | ||
| 387 | fatal("unable to parse jobs count '{s}': {t}", .{ text, err }); | ||
| 388 | if (n < 1) fatal("number of jobs must be at least 1", .{}); | ||
| 389 | threaded.setAsyncLimit(.limited(n)); | ||
| 390 | graph.max_jobs = n; | ||
| 391 | } else if (mem.eql(u8, arg, "--")) { | ||
| 392 | run_args = argsRest(args, arg_idx); | ||
| 393 | break; | ||
| 394 | } else { | ||
| 395 | fatalWithHint("unrecognized argument: '{s}'", .{arg}); | ||
| 396 | } | ||
| 397 | } else { | ||
| 398 | try step_names.append(arena, arg); | ||
| 399 | } | ||
| 400 | } | ||
| 401 | |||
| 402 | const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map); | ||
| 403 | const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map); | ||
| 404 | |||
| 405 | graph.stderr_mode = switch (color) { | ||
| 406 | .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE), | ||
| 407 | .on => .escape_codes, | ||
| 408 | .off => .no_color, | ||
| 409 | }; | ||
| 410 | |||
| 411 | const scanned_config: ScannedConfig = sc: { | ||
| 412 | const configuration = c: { | ||
| 413 | var file = cwd.openFile(io, configure_path, .{}) catch |err| | ||
| 414 | fatal("failed to open configuration file {s}: {t}", .{ configure_path, err }); | ||
| 415 | defer file.close(io); | ||
| 416 | break :c Configuration.loadFile(arena, io, file) catch |err| | ||
| 417 | fatal("failed to load configuration file {s}: {t}", .{ configure_path, err }); | ||
| 418 | }; | ||
| 419 | var top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index) = .empty; | ||
| 420 | for (configuration.steps, 0..) |*conf_step, step_index_usize| { | ||
| 421 | const step_index: Configuration.Step.Index = @enumFromInt(step_index_usize); | ||
| 422 | const flags: Configuration.Step.Flags = @bitCast(configuration.extra[conf_step.extra_index]); | ||
| 423 | if (flags.tag == .top_level) { | ||
| 424 | const name = step_index.ptr(&configuration).name.slice(&configuration); | ||
| 425 | try top_level_steps.put(arena, name, step_index); | ||
| 426 | } | ||
| 427 | } | ||
| 428 | break :sc .{ | ||
| 429 | .configuration = configuration, | ||
| 430 | .top_level_steps = top_level_steps, | ||
| 431 | }; | ||
| 432 | }; | ||
| 433 | |||
| 434 | if (help_menu) { | ||
| 435 | var w = initStdoutWriter(io); | ||
| 436 | scanned_config.printUsage(&graph, w) catch |err| switch (err) { | ||
| 437 | error.WriteFailed => return stdout_writer_allocation.err.?, | ||
| 438 | else => |e| return e, | ||
| 439 | }; | ||
| 440 | w.flush() catch return stdout_writer_allocation.err.?; | ||
| 441 | return; | ||
| 442 | } else if (steps_menu) { | ||
| 443 | var w = initStdoutWriter(io); | ||
| 444 | scanned_config.printSteps(&graph, w) catch |err| switch (err) { | ||
| 445 | error.WriteFailed => return stdout_writer_allocation.err.?, | ||
| 446 | else => |e| return e, | ||
| 447 | }; | ||
| 448 | w.flush() catch return stdout_writer_allocation.err.?; | ||
| 449 | return; | ||
| 450 | } else if (print_configuration) { | ||
| 451 | var w = initStdoutWriter(io); | ||
| 452 | scanned_config.print(w) catch return stdout_writer_allocation.err.?; | ||
| 453 | w.flush() catch return stdout_writer_allocation.err.?; | ||
| 454 | return; | ||
| 455 | } | ||
| 456 | |||
| 457 | if (webui_listen != null) { | ||
| 458 | if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{}); | ||
| 459 | if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{}); | ||
| 460 | } | ||
| 461 | |||
| 462 | const main_progress_node = std.Progress.start(io, .{ | ||
| 463 | .disable_printing = (color == .off), | ||
| 464 | }); | ||
| 465 | defer main_progress_node.end(); | ||
| 466 | |||
| 467 | const install_prefix_path: Path = if (graph.environ_map.get("DESTDIR")) |dest_dir| .{ | ||
| 468 | .root_dir = .cwd(), | ||
| 469 | .sub_path = try Io.Dir.path.join(arena, &.{ dest_dir, override_install_prefix orelse "/usr" }), | ||
| 470 | } else if (override_install_prefix) |cwd_relative| .{ | ||
| 471 | .root_dir = .cwd(), | ||
| 472 | .sub_path = cwd_relative, | ||
| 473 | } else .{ | ||
| 474 | .root_dir = build_root_directory, | ||
| 475 | .sub_path = "zig-out", | ||
| 476 | }; | ||
| 477 | |||
| 478 | const install_lib_path: Path = if (override_lib_dir) |cwd_relative| .{ | ||
| 479 | .root_dir = .cwd(), | ||
| 480 | .sub_path = cwd_relative, | ||
| 481 | } else try install_prefix_path.join(arena, "lib"); | ||
| 482 | |||
| 483 | const install_bin_path: Path = if (override_bin_dir) |cwd_relative| .{ | ||
| 484 | .root_dir = .cwd(), | ||
| 485 | .sub_path = cwd_relative, | ||
| 486 | } else try install_prefix_path.join(arena, "bin"); | ||
| 487 | |||
| 488 | const install_include_path: Path = if (override_include_dir) |cwd_relative| .{ | ||
| 489 | .root_dir = .cwd(), | ||
| 490 | .sub_path = cwd_relative, | ||
| 491 | } else try install_prefix_path.join(arena, "include"); | ||
| 492 | |||
| 493 | var maker: Maker = .{ | ||
| 494 | .gpa = gpa, | ||
| 495 | .graph = &graph, | ||
| 496 | .scanned_config = &scanned_config, | ||
| 497 | .install_paths = .{ | ||
| 498 | .prefix = install_prefix_path, | ||
| 499 | .lib = install_lib_path, | ||
| 500 | .bin = install_bin_path, | ||
| 501 | .include = install_include_path, | ||
| 502 | }, | ||
| 503 | .steps = try arena.alloc(Step, scanned_config.configuration.steps.len), | ||
| 504 | |||
| 505 | .available_rss = max_rss, | ||
| 506 | .max_rss_is_default = false, | ||
| 507 | .max_rss_mutex = .init, | ||
| 508 | .skip_oom_steps = skip_oom_steps, | ||
| 509 | .unit_test_timeout_ns = test_timeout_ns, | ||
| 510 | |||
| 511 | .watch = watch, | ||
| 512 | .web_server = undefined, // set after `prepare` | ||
| 513 | .memory_blocked_steps = .empty, | ||
| 514 | .step_stack = .empty, | ||
| 515 | |||
| 516 | .error_style = error_style, | ||
| 517 | .multiline_errors = multiline_errors, | ||
| 518 | .summary = summary orelse if (watch or webui_listen != null) .line else .failures, | ||
| 519 | }; | ||
| 520 | defer { | ||
| 521 | maker.memory_blocked_steps.deinit(gpa); | ||
| 522 | maker.step_stack.deinit(gpa); | ||
| 523 | } | ||
| 524 | |||
| 525 | if (maker.available_rss == 0) { | ||
| 526 | maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64); | ||
| 527 | maker.max_rss_is_default = true; | ||
| 528 | } | ||
| 529 | |||
| 530 | maker.prepare(step_names.items) catch |err| switch (err) { | ||
| 531 | error.DependencyLoopDetected, error.InsufficientMemory => { | ||
| 532 | _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; | ||
| 533 | process.exit(1); | ||
| 534 | }, | ||
| 535 | else => |e| return e, | ||
| 536 | }; | ||
| 537 | |||
| 538 | var w: Watch = w: { | ||
| 539 | if (!watch) break :w undefined; | ||
| 540 | if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag}); | ||
| 541 | break :w try .init(graph.cache.cwd, &scanned_config.configuration, maker.steps); | ||
| 542 | }; | ||
| 543 | |||
| 544 | const now = Io.Clock.Timestamp.now(io, .awake); | ||
| 545 | |||
| 546 | maker.web_server = if (webui_listen) |listen_address| ws: { | ||
| 547 | if (builtin.single_threaded) unreachable; // `fatal` above | ||
| 548 | break :ws .init(.{ | ||
| 549 | .gpa = gpa, | ||
| 550 | .graph = &graph, | ||
| 551 | .all_steps = maker.step_stack.keys(), | ||
| 552 | .root_prog_node = main_progress_node, | ||
| 553 | .watch = watch, | ||
| 554 | .listen_address = listen_address, | ||
| 555 | .base_timestamp = now, | ||
| 556 | .configuration = &scanned_config.configuration, | ||
| 557 | }); | ||
| 558 | } else null; | ||
| 559 | |||
| 560 | if (maker.web_server) |*ws| { | ||
| 561 | ws.start() catch |err| fatal("failed to start web server: {t}", .{err}); | ||
| 562 | } | ||
| 563 | |||
| 564 | rebuild: while (true) : (if (maker.error_style.clearOnUpdate()) { | ||
| 565 | const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); | ||
| 566 | defer io.unlockStderr(); | ||
| 567 | try stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H"); | ||
| 568 | }) { | ||
| 569 | if (maker.web_server) |*ws| ws.startBuild(); | ||
| 570 | |||
| 571 | try maker.makeStepNames(step_names.items, main_progress_node, fuzz); | ||
| 572 | |||
| 573 | if (maker.web_server) |*web_server| { | ||
| 574 | if (fuzz) |mode| if (mode != .forever) fatal( | ||
| 575 | "error: limited fuzzing is not implemented yet for --webui", | ||
| 576 | .{}, | ||
| 577 | ); | ||
| 578 | |||
| 579 | web_server.finishBuild(.{ .fuzz = fuzz != null }); | ||
| 580 | } | ||
| 581 | |||
| 582 | if (maker.web_server) |*ws| { | ||
| 583 | const c = &scanned_config.configuration; | ||
| 584 | assert(!watch); // fatal error after CLI parsing | ||
| 585 | while (true) switch (try ws.wait()) { | ||
| 586 | .rebuild => { | ||
| 587 | for (maker.step_stack.keys()) |step_index| { | ||
| 588 | const step = maker.stepByIndex(step_index); | ||
| 589 | step.state = .precheck_done; | ||
| 590 | const deps = step_index.ptr(c).deps.slice(c); | ||
| 591 | step.pending_deps = @intCast(deps.len); | ||
| 592 | step.reset(gpa); | ||
| 593 | } | ||
| 594 | continue :rebuild; | ||
| 595 | }, | ||
| 596 | }; | ||
| 597 | } | ||
| 598 | |||
| 599 | // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`. | ||
| 600 | if (!Watch.have_impl) unreachable; | ||
| 601 | |||
| 602 | try w.update(gpa, maker.step_stack.keys()); | ||
| 603 | |||
| 604 | // Wait until a file system notification arrives. Read all such events | ||
| 605 | // until the buffer is empty. Then wait for a debounce interval, resetting | ||
| 606 | // if any more events come in. After the debounce interval has passed, | ||
| 607 | // trigger a rebuild on all steps with modified inputs, as well as their | ||
| 608 | // recursive dependants. | ||
| 609 | var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined; | ||
| 610 | const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{ | ||
| 611 | w.dir_count, countSubProcesses(maker.steps, maker.step_stack.keys()), | ||
| 612 | }) catch &caption_buf; | ||
| 613 | var debouncing_node = main_progress_node.start(caption, 0); | ||
| 614 | var in_debounce = false; | ||
| 615 | while (true) switch (try w.wait(gpa, io, if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) { | ||
| 616 | .timeout => { | ||
| 617 | assert(in_debounce); | ||
| 618 | debouncing_node.end(); | ||
| 619 | markFailedStepsDirty(gpa, maker.steps, maker.step_stack.keys()); | ||
| 620 | continue :rebuild; | ||
| 621 | }, | ||
| 622 | .dirty => if (!in_debounce) { | ||
| 623 | in_debounce = true; | ||
| 624 | debouncing_node.end(); | ||
| 625 | debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0); | ||
| 626 | }, | ||
| 627 | .clean => {}, | ||
| 628 | }; | ||
| 629 | } | ||
| 630 | } | ||
| 631 | |||
| 632 | fn markFailedStepsDirty(gpa: Allocator, make_steps: []Step, all_steps: []const Configuration.Step.Index) void { | ||
| 633 | for (all_steps) |step_index| { | ||
| 634 | const step = &make_steps[@intFromEnum(step_index)]; | ||
| 635 | switch (step.state) { | ||
| 636 | .dependency_failure, .failure, .skipped => _ = step.invalidateResult(gpa), | ||
| 637 | else => continue, | ||
| 638 | } | ||
| 639 | } | ||
| 640 | // Now that all dirty steps have been found, the remaining steps that | ||
| 641 | // succeeded from last run shall be marked "cached". | ||
| 642 | for (all_steps) |step_index| { | ||
| 643 | const step = &make_steps[@intFromEnum(step_index)]; | ||
| 644 | switch (step.state) { | ||
| 645 | .success => step.result_cached = true, | ||
| 646 | else => continue, | ||
| 647 | } | ||
| 648 | } | ||
| 649 | } | ||
| 650 | |||
| 651 | fn countSubProcesses(make_steps: []Step, all_steps: []const Configuration.Step.Index) usize { | ||
| 652 | var count: usize = 0; | ||
| 653 | for (all_steps) |step_index| { | ||
| 654 | const s = &make_steps[@intFromEnum(step_index)]; | ||
| 655 | count += @intFromBool(s.getZigProcess() != null); | ||
| 656 | } | ||
| 657 | return count; | ||
| 658 | } | ||
| 659 | |||
| 660 | const InstallPaths = struct { | ||
| 661 | prefix: Path, | ||
| 662 | lib: Path, | ||
| 663 | bin: Path, | ||
| 664 | include: Path, | ||
| 665 | }; | ||
| 666 | |||
| 667 | fn stepByIndex(maker: *const Maker, i: Configuration.Step.Index) *Step { | ||
| 668 | return &maker.steps[@intFromEnum(i)]; | ||
| 669 | } | ||
| 670 | |||
| 671 | fn prepare(maker: *Maker, step_names: []const []const u8) !void { | ||
| 672 | const gpa = maker.gpa; | ||
| 673 | const graph = maker.graph; | ||
| 674 | const arena = graph.arena; | ||
| 675 | const seed: u32 = graph.random_seed; | ||
| 676 | const step_stack = &maker.step_stack; | ||
| 677 | const c = &maker.scanned_config.configuration; | ||
| 678 | |||
| 679 | @memset(maker.steps, .{}); | ||
| 680 | |||
| 681 | if (step_names.len == 0) { | ||
| 682 | try step_stack.put(gpa, c.default_step, {}); | ||
| 683 | } else { | ||
| 684 | try step_stack.ensureUnusedCapacity(gpa, step_names.len); | ||
| 685 | for (0..step_names.len) |i| { | ||
| 686 | const step_name = step_names[step_names.len - i - 1]; | ||
| 687 | const s = maker.scanned_config.top_level_steps.get(step_name) orelse { | ||
| 688 | log.info("to list available steps: zig build -l", .{}); | ||
| 689 | fatal("no such step: {s}", .{step_name}); | ||
| 690 | }; | ||
| 691 | step_stack.putAssumeCapacity(s, {}); | ||
| 692 | } | ||
| 693 | } | ||
| 694 | |||
| 695 | const starting_steps = try arena.dupe(Configuration.Step.Index, step_stack.keys()); | ||
| 696 | |||
| 697 | var rng = std.Random.DefaultPrng.init(seed); | ||
| 698 | const rand = rng.random(); | ||
| 699 | rand.shuffle(Configuration.Step.Index, starting_steps); | ||
| 700 | |||
| 701 | for (starting_steps) |s| { | ||
| 702 | try constructGraphAndCheckForDependencyLoop(gpa, c, maker.steps, s, &maker.step_stack, rand); | ||
| 703 | } | ||
| 704 | |||
| 705 | { | ||
| 706 | // Check that we have enough memory to complete the build. | ||
| 707 | var any_problems = false; | ||
| 708 | var max_needed: usize = 0; | ||
| 709 | for (step_stack.keys()) |step_index| { | ||
| 710 | const make_step = maker.stepByIndex(step_index); | ||
| 711 | const conf_step = step_index.ptr(c); | ||
| 712 | const max_rss = conf_step.max_rss.toBytes(); | ||
| 713 | if (max_rss == 0) continue; | ||
| 714 | max_needed = @max(max_needed, max_rss); | ||
| 715 | if (max_rss > maker.available_rss) { | ||
| 716 | if (maker.skip_oom_steps) { | ||
| 717 | make_step.state = .skipped_oom; | ||
| 718 | for (make_step.dependants.items) |dependant| { | ||
| 719 | maker.stepByIndex(dependant).pending_deps -= 1; | ||
| 720 | } | ||
| 721 | } else { | ||
| 722 | log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{ | ||
| 723 | conf_step.owner.depPrefixSlice(c), | ||
| 724 | conf_step.name.slice(c), | ||
| 725 | max_rss, | ||
| 726 | maker.available_rss, | ||
| 727 | }); | ||
| 728 | any_problems = true; | ||
| 729 | } | ||
| 730 | } | ||
| 731 | } | ||
| 732 | if (any_problems) { | ||
| 733 | if (maker.max_rss_is_default) { | ||
| 734 | std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{ | ||
| 735 | max_needed, | ||
| 736 | }); | ||
| 737 | } | ||
| 738 | return error.InsufficientMemory; | ||
| 739 | } | ||
| 740 | } | ||
| 741 | } | ||
| 742 | |||
| 743 | fn makeStepNames( | ||
| 744 | maker: *Maker, | ||
| 745 | step_names: []const []const u8, | ||
| 746 | parent_prog_node: std.Progress.Node, | ||
| 747 | fuzz: ?Fuzz.Mode, | ||
| 748 | ) !void { | ||
| 749 | const graph = maker.graph; | ||
| 750 | const gpa = maker.gpa; | ||
| 751 | const io = graph.io; | ||
| 752 | const step_stack = &maker.step_stack; | ||
| 753 | const top_level_steps = &maker.scanned_config.top_level_steps; | ||
| 754 | const c = &maker.scanned_config.configuration; | ||
| 755 | |||
| 756 | { | ||
| 757 | // Collect the initial set of tasks (those with no outstanding dependencies) into a buffer, | ||
| 758 | // then spawn them. The buffer is so that we don't race with `makeStep` and end up thinking | ||
| 759 | // a step is initial when it actually became ready due to an earlier initial step. | ||
| 760 | var initial_set: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 761 | defer initial_set.deinit(gpa); | ||
| 762 | try initial_set.ensureUnusedCapacity(gpa, step_stack.count()); | ||
| 763 | for (step_stack.keys()) |step_index| { | ||
| 764 | const s = maker.stepByIndex(step_index); | ||
| 765 | if (s.state == .precheck_done and s.pending_deps == 0) { | ||
| 766 | initial_set.appendAssumeCapacity(step_index); | ||
| 767 | } | ||
| 768 | } | ||
| 769 | |||
| 770 | const step_prog = parent_prog_node.start("steps", step_stack.count()); | ||
| 771 | defer step_prog.end(); | ||
| 772 | |||
| 773 | var group: Io.Group = .init; | ||
| 774 | defer group.cancel(io); | ||
| 775 | // Start working on all of the initial steps... | ||
| 776 | for (initial_set.items) |step_index| try stepReady(maker, &group, step_index, step_prog); | ||
| 777 | // ...and `makeStep` will trigger every other step when their last dependency finishes. | ||
| 778 | try group.await(io); | ||
| 779 | } | ||
| 780 | |||
| 781 | assert(maker.memory_blocked_steps.items.len == 0); | ||
| 782 | |||
| 783 | var test_pass_count: usize = 0; | ||
| 784 | var test_skip_count: usize = 0; | ||
| 785 | var test_fail_count: usize = 0; | ||
| 786 | var test_crash_count: usize = 0; | ||
| 787 | var test_timeout_count: usize = 0; | ||
| 788 | |||
| 789 | var test_count: usize = 0; | ||
| 790 | |||
| 791 | var success_count: usize = 0; | ||
| 792 | var skipped_count: usize = 0; | ||
| 793 | var failure_count: usize = 0; | ||
| 794 | var pending_count: usize = 0; | ||
| 795 | var total_compile_errors: usize = 0; | ||
| 796 | |||
| 797 | var cleanup_task = io.async(cleanTmpFiles, .{ io, step_stack.keys() }); | ||
| 798 | defer cleanup_task.await(io); | ||
| 799 | |||
| 800 | for (step_stack.keys()) |step_index| { | ||
| 801 | const make_step = maker.stepByIndex(step_index); | ||
| 802 | test_pass_count += make_step.test_results.passCount(); | ||
| 803 | test_skip_count += make_step.test_results.skip_count; | ||
| 804 | test_fail_count += make_step.test_results.fail_count; | ||
| 805 | test_crash_count += make_step.test_results.crash_count; | ||
| 806 | test_timeout_count += make_step.test_results.timeout_count; | ||
| 807 | |||
| 808 | test_count += make_step.test_results.test_count; | ||
| 809 | |||
| 810 | switch (make_step.state) { | ||
| 811 | .precheck_unstarted => unreachable, | ||
| 812 | .precheck_started => unreachable, | ||
| 813 | .precheck_done => unreachable, | ||
| 814 | .dependency_failure => pending_count += 1, | ||
| 815 | .success => success_count += 1, | ||
| 816 | .skipped, .skipped_oom => skipped_count += 1, | ||
| 817 | .failure => { | ||
| 818 | failure_count += 1; | ||
| 819 | const compile_errors_len = make_step.result_error_bundle.errorMessageCount(); | ||
| 820 | if (compile_errors_len > 0) { | ||
| 821 | total_compile_errors += compile_errors_len; | ||
| 822 | } | ||
| 823 | }, | ||
| 824 | } | ||
| 825 | } | ||
| 826 | |||
| 827 | if (fuzz) |mode| blk: { | ||
| 828 | switch (builtin.os.tag) { | ||
| 829 | // Current implementation depends on two things that need to be ported to Windows: | ||
| 830 | // * Memory-mapping to share data between the fuzzer and build runner. | ||
| 831 | // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving | ||
| 832 | // many addresses to source locations). | ||
| 833 | .windows => fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}), | ||
| 834 | else => {}, | ||
| 835 | } | ||
| 836 | if (@bitSizeOf(usize) != 64) { | ||
| 837 | // Current implementation depends on posix.mmap()'s second parameter, `length: usize`, | ||
| 838 | // being compatible with file system's u64 return value. This is not the case | ||
| 839 | // on 32-bit platforms. | ||
| 840 | // Affects or affected by issues #5185, #22523, and #22464. | ||
| 841 | fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); | ||
| 842 | } | ||
| 843 | |||
| 844 | switch (mode) { | ||
| 845 | .forever => break :blk, | ||
| 846 | .limit => {}, | ||
| 847 | } | ||
| 848 | |||
| 849 | assert(mode == .limit); | ||
| 850 | var f = Fuzz.init( | ||
| 851 | gpa, | ||
| 852 | io, | ||
| 853 | step_stack.keys(), | ||
| 854 | parent_prog_node, | ||
| 855 | mode, | ||
| 856 | ) catch |err| fatal("failed to start fuzzer: {t}", .{err}); | ||
| 857 | defer f.deinit(); | ||
| 858 | |||
| 859 | f.start(); | ||
| 860 | try f.waitAndPrintReport(); | ||
| 861 | } | ||
| 862 | |||
| 863 | // Every test has a state | ||
| 864 | assert(test_pass_count + test_skip_count + test_fail_count + test_crash_count + test_timeout_count == test_count); | ||
| 865 | |||
| 866 | if (failure_count == 0) { | ||
| 867 | std.Progress.setStatus(.success); | ||
| 868 | } else { | ||
| 869 | std.Progress.setStatus(.failure); | ||
| 870 | } | ||
| 871 | |||
| 872 | summary: { | ||
| 873 | switch (maker.summary) { | ||
| 874 | .all, .new, .line => {}, | ||
| 875 | .failures => if (failure_count == 0) break :summary, | ||
| 876 | .none => break :summary, | ||
| 877 | } | ||
| 878 | |||
| 879 | const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); | ||
| 880 | defer io.unlockStderr(); | ||
| 881 | const t = stderr.terminal(); | ||
| 882 | const w = &stderr.file_writer.interface; | ||
| 883 | |||
| 884 | const total_count = success_count + failure_count + pending_count + skipped_count; | ||
| 885 | t.setColor(.cyan) catch {}; | ||
| 886 | t.setColor(.bold) catch {}; | ||
| 887 | w.writeAll("Build Summary: ") catch {}; | ||
| 888 | t.setColor(.reset) catch {}; | ||
| 889 | w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {}; | ||
| 890 | { | ||
| 891 | t.setColor(.dim) catch {}; | ||
| 892 | var first = true; | ||
| 893 | if (skipped_count > 0) { | ||
| 894 | w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {}; | ||
| 895 | first = false; | ||
| 896 | } | ||
| 897 | if (failure_count > 0) { | ||
| 898 | w.print("{s}{d} failed", .{ if (first) " (" else ", ", failure_count }) catch {}; | ||
| 899 | first = false; | ||
| 900 | } | ||
| 901 | if (!first) w.writeByte(')') catch {}; | ||
| 902 | t.setColor(.reset) catch {}; | ||
| 903 | } | ||
| 904 | |||
| 905 | if (test_count > 0) { | ||
| 906 | w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {}; | ||
| 907 | t.setColor(.dim) catch {}; | ||
| 908 | var first = true; | ||
| 909 | if (test_skip_count > 0) { | ||
| 910 | w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {}; | ||
| 911 | first = false; | ||
| 912 | } | ||
| 913 | if (test_fail_count > 0) { | ||
| 914 | w.print("{s}{d} failed", .{ if (first) " (" else ", ", test_fail_count }) catch {}; | ||
| 915 | first = false; | ||
| 916 | } | ||
| 917 | if (test_crash_count > 0) { | ||
| 918 | w.print("{s}{d} crashed", .{ if (first) " (" else ", ", test_crash_count }) catch {}; | ||
| 919 | first = false; | ||
| 920 | } | ||
| 921 | if (test_timeout_count > 0) { | ||
| 922 | w.print("{s}{d} timed out", .{ if (first) " (" else ", ", test_timeout_count }) catch {}; | ||
| 923 | first = false; | ||
| 924 | } | ||
| 925 | if (!first) w.writeByte(')') catch {}; | ||
| 926 | t.setColor(.reset) catch {}; | ||
| 927 | } | ||
| 928 | |||
| 929 | w.writeAll("\n") catch {}; | ||
| 930 | |||
| 931 | if (maker.summary == .line) break :summary; | ||
| 932 | |||
| 933 | // Print a fancy tree with build results. | ||
| 934 | var step_stack_copy = try step_stack.clone(gpa); | ||
| 935 | defer step_stack_copy.deinit(gpa); | ||
| 936 | |||
| 937 | var print_node: PrintNode = .{ .parent = null }; | ||
| 938 | if (step_names.len == 0) { | ||
| 939 | print_node.last = true; | ||
| 940 | printTreeStep(maker, c.default_step, t, &print_node, &step_stack_copy) catch |err| switch (err) { | ||
| 941 | error.Canceled => |e| return e, | ||
| 942 | else => {}, | ||
| 943 | }; | ||
| 944 | } else { | ||
| 945 | const last_index = if (maker.summary == .all) top_level_steps.count() else blk: { | ||
| 946 | var i: usize = step_names.len; | ||
| 947 | while (i > 0) { | ||
| 948 | i -= 1; | ||
| 949 | const step_index = top_level_steps.get(step_names[i]).?; | ||
| 950 | const step = maker.stepByIndex(step_index); | ||
| 951 | const found = switch (maker.summary) { | ||
| 952 | .all, .line, .none => unreachable, | ||
| 953 | .failures => step.state != .success, | ||
| 954 | .new => !step.result_cached, | ||
| 955 | }; | ||
| 956 | if (found) break :blk i; | ||
| 957 | } | ||
| 958 | break :blk top_level_steps.count(); | ||
| 959 | }; | ||
| 960 | for (step_names, 0..) |step_name, i| { | ||
| 961 | const step_index = top_level_steps.get(step_name).?; | ||
| 962 | print_node.last = i + 1 == last_index; | ||
| 963 | printTreeStep(maker, step_index, t, &print_node, &step_stack_copy) catch |err| switch (err) { | ||
| 964 | error.Canceled => |e| return e, | ||
| 965 | else => {}, | ||
| 966 | }; | ||
| 967 | } | ||
| 968 | } | ||
| 969 | w.writeByte('\n') catch {}; | ||
| 970 | } | ||
| 971 | |||
| 972 | if (maker.watch or maker.web_server != null) return; | ||
| 973 | |||
| 974 | // Perhaps in the future there could be an Advanced Options flag such as | ||
| 975 | // --debug-build-runner-leaks which would make this code return instead of | ||
| 976 | // calling exit. | ||
| 977 | |||
| 978 | const code: u8 = code: { | ||
| 979 | if (failure_count == 0) break :code 0; // success | ||
| 980 | if (maker.error_style.verboseContext()) break :code 1; // failure; print build command | ||
| 981 | break :code 2; // failure; do not print build command | ||
| 982 | }; | ||
| 983 | _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; | ||
| 984 | process.exit(code); | ||
| 985 | } | ||
| 986 | |||
| 987 | fn stepReady( | ||
| 988 | maker: *Maker, | ||
| 989 | group: *Io.Group, | ||
| 990 | step_index: Configuration.Step.Index, | ||
| 991 | root_prog_node: std.Progress.Node, | ||
| 992 | ) Io.Cancelable!void { | ||
| 993 | const graph = maker.graph; | ||
| 994 | const io = graph.io; | ||
| 995 | const c = &maker.scanned_config.configuration; | ||
| 996 | const max_rss = step_index.ptr(c).max_rss.toBytes(); | ||
| 997 | if (max_rss != 0) { | ||
| 998 | try maker.max_rss_mutex.lock(io); | ||
| 999 | defer maker.max_rss_mutex.unlock(io); | ||
| 1000 | if (maker.available_rss < max_rss) { | ||
| 1001 | // Running this step right now could possibly exceed the allotted RSS. | ||
| 1002 | maker.memory_blocked_steps.append(maker.gpa, step_index) catch | ||
| 1003 | @panic("TODO eliminate memory allocation here"); | ||
| 1004 | return; | ||
| 1005 | } | ||
| 1006 | maker.available_rss -= max_rss; | ||
| 1007 | } | ||
| 1008 | group.async(io, makeStep, .{ maker, group, step_index, root_prog_node }); | ||
| 1009 | } | ||
| 1010 | |||
| 1011 | /// Runs the "make" function of the single step `s`, updates its state, and then spawns newly-ready | ||
| 1012 | /// dependant steps in `group`. If `s` makes an RSS claim (i.e. `s.max_rss != 0`), the caller must | ||
| 1013 | /// have already subtracted this value from `maker.available_rss`. This function will release the RSS | ||
| 1014 | /// claim (i.e. add `s.max_rss` back into `maker.available_rss`) and queue any viable memory-blocked | ||
| 1015 | /// steps after "make" completes for `s`. | ||
| 1016 | fn makeStep( | ||
| 1017 | maker: *Maker, | ||
| 1018 | group: *Io.Group, | ||
| 1019 | step_index: Configuration.Step.Index, | ||
| 1020 | root_prog_node: std.Progress.Node, | ||
| 1021 | ) Io.Cancelable!void { | ||
| 1022 | const graph = maker.graph; | ||
| 1023 | const io = graph.io; | ||
| 1024 | const gpa = maker.gpa; | ||
| 1025 | const c = &maker.scanned_config.configuration; | ||
| 1026 | const conf_step = step_index.ptr(c); | ||
| 1027 | const step_name = conf_step.name.slice(c); | ||
| 1028 | const deps = conf_step.deps.slice(c); | ||
| 1029 | const make_step = maker.stepByIndex(step_index); | ||
| 1030 | |||
| 1031 | { | ||
| 1032 | const step_prog_node = root_prog_node.start(step_name, 0); | ||
| 1033 | defer step_prog_node.end(); | ||
| 1034 | |||
| 1035 | if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .wip); | ||
| 1036 | |||
| 1037 | const new_state: Step.State = for (deps) |dep_index| { | ||
| 1038 | const dep_make_step = maker.stepByIndex(dep_index); | ||
| 1039 | switch (@atomicLoad(Step.State, &dep_make_step.state, .monotonic)) { | ||
| 1040 | .precheck_unstarted => unreachable, | ||
| 1041 | .precheck_started => unreachable, | ||
| 1042 | .precheck_done => unreachable, | ||
| 1043 | |||
| 1044 | .failure, | ||
| 1045 | .dependency_failure, | ||
| 1046 | .skipped_oom, | ||
| 1047 | => break .dependency_failure, | ||
| 1048 | |||
| 1049 | .success, .skipped => {}, | ||
| 1050 | } | ||
| 1051 | } else if (make_step.make(.{ | ||
| 1052 | .progress_node = step_prog_node, | ||
| 1053 | .watch = maker.watch, | ||
| 1054 | .web_server = if (maker.web_server) |*ws| ws else null, | ||
| 1055 | .unit_test_timeout_ns = maker.unit_test_timeout_ns, | ||
| 1056 | .gpa = gpa, | ||
| 1057 | })) state: { | ||
| 1058 | break :state .success; | ||
| 1059 | } else |err| switch (err) { | ||
| 1060 | error.MakeFailed => .failure, | ||
| 1061 | error.MakeSkipped => .skipped, | ||
| 1062 | }; | ||
| 1063 | |||
| 1064 | @atomicStore(Step.State, &make_step.state, new_state, .monotonic); | ||
| 1065 | |||
| 1066 | switch (new_state) { | ||
| 1067 | .precheck_unstarted => unreachable, | ||
| 1068 | .precheck_started => unreachable, | ||
| 1069 | .precheck_done => unreachable, | ||
| 1070 | |||
| 1071 | .failure, | ||
| 1072 | .dependency_failure, | ||
| 1073 | .skipped_oom, | ||
| 1074 | => { | ||
| 1075 | if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .failure); | ||
| 1076 | std.Progress.setStatus(.failure_working); | ||
| 1077 | }, | ||
| 1078 | |||
| 1079 | .success, | ||
| 1080 | .skipped, | ||
| 1081 | => { | ||
| 1082 | if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .success); | ||
| 1083 | }, | ||
| 1084 | } | ||
| 1085 | } | ||
| 1086 | |||
| 1087 | // No matter the result, we want to display error/warning messages. | ||
| 1088 | if (make_step.result_error_bundle.errorMessageCount() > 0 or | ||
| 1089 | make_step.result_error_msgs.items.len > 0 or | ||
| 1090 | make_step.result_stderr.len > 0) | ||
| 1091 | { | ||
| 1092 | const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); | ||
| 1093 | defer io.unlockStderr(); | ||
| 1094 | printErrorMessages(gpa, c, maker.steps, step_index, .{}, stderr.terminal(), maker.error_style, maker.multiline_errors) catch |err| switch (err) { | ||
| 1095 | error.Canceled => |e| return e, | ||
| 1096 | error.WriteFailed => switch (stderr.file_writer.err.?) { | ||
| 1097 | error.Canceled => |e| return e, | ||
| 1098 | else => {}, | ||
| 1099 | }, | ||
| 1100 | else => {}, | ||
| 1101 | }; | ||
| 1102 | } | ||
| 1103 | |||
| 1104 | const max_rss = conf_step.max_rss.toBytes(); | ||
| 1105 | if (max_rss != 0) { | ||
| 1106 | var dispatch_set: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 1107 | defer dispatch_set.deinit(gpa); | ||
| 1108 | |||
| 1109 | // Release our RSS claim and kick off some blocked steps if possible. We use `dispatch_set` | ||
| 1110 | // as a staging buffer to avoid recursing into `makeStep` while `maker.max_rss_mutex` is held. | ||
| 1111 | { | ||
| 1112 | try maker.max_rss_mutex.lock(io); | ||
| 1113 | defer maker.max_rss_mutex.unlock(io); | ||
| 1114 | maker.available_rss += max_rss; | ||
| 1115 | dispatch_set.ensureUnusedCapacity(gpa, maker.memory_blocked_steps.items.len) catch | ||
| 1116 | @panic("TODO eliminate memory allocation here"); | ||
| 1117 | while (maker.memory_blocked_steps.getLast()) |candidate_index| { | ||
| 1118 | const candidate_max_rss = candidate_index.ptr(c).max_rss.toBytes(); | ||
| 1119 | if (maker.available_rss < candidate_max_rss) break; | ||
| 1120 | assert(maker.memory_blocked_steps.pop() == candidate_index); | ||
| 1121 | dispatch_set.appendAssumeCapacity(candidate_index); | ||
| 1122 | } | ||
| 1123 | } | ||
| 1124 | for (dispatch_set.items) |candidate| { | ||
| 1125 | group.async(io, makeStep, .{ maker, group, candidate, root_prog_node }); | ||
| 1126 | } | ||
| 1127 | } | ||
| 1128 | |||
| 1129 | for (make_step.dependants.items) |dependant_index| { | ||
| 1130 | const dependant = maker.stepByIndex(dependant_index); | ||
| 1131 | // `.acq_rel` synchronizes with itself to ensure all dependencies' final states are visible when this hits 0. | ||
| 1132 | if (@atomicRmw(u32, &dependant.pending_deps, .Sub, 1, .acq_rel) == 1) { | ||
| 1133 | try stepReady(maker, group, dependant_index, root_prog_node); | ||
| 1134 | } | ||
| 1135 | } | ||
| 1136 | } | ||
| 1137 | |||
| 1138 | fn printTreeStep( | ||
| 1139 | maker: *const Maker, | ||
| 1140 | step_index: Configuration.Step.Index, | ||
| 1141 | stderr: Io.Terminal, | ||
| 1142 | parent_node: *PrintNode, | ||
| 1143 | step_stack: *std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void), | ||
| 1144 | ) !void { | ||
| 1145 | const writer = stderr.writer; | ||
| 1146 | const first = step_stack.swapRemove(step_index); | ||
| 1147 | const summary = maker.summary; | ||
| 1148 | const c = &maker.scanned_config.configuration; | ||
| 1149 | const conf_step = step_index.ptr(c); | ||
| 1150 | const make_step = maker.stepByIndex(step_index); | ||
| 1151 | const skip = switch (summary) { | ||
| 1152 | .none, .line => unreachable, | ||
| 1153 | .all => false, | ||
| 1154 | .new => make_step.result_cached, | ||
| 1155 | .failures => make_step.state == .success, | ||
| 1156 | }; | ||
| 1157 | if (skip) return; | ||
| 1158 | try printPrefix(parent_node, stderr); | ||
| 1159 | |||
| 1160 | if (parent_node.parent != null) { | ||
| 1161 | if (parent_node.last) { | ||
| 1162 | try printChildNodePrefix(stderr); | ||
| 1163 | } else { | ||
| 1164 | try writer.writeAll(switch (stderr.mode) { | ||
| 1165 | .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ | ||
| 1166 | else => "+- ", | ||
| 1167 | }); | ||
| 1168 | } | ||
| 1169 | } | ||
| 1170 | |||
| 1171 | if (!first) try stderr.setColor(.dim); | ||
| 1172 | |||
| 1173 | // dep_prefix omitted here because it is redundant with the tree. | ||
| 1174 | try writer.writeAll(conf_step.name.slice(c)); | ||
| 1175 | |||
| 1176 | const deps = conf_step.deps.slice(c); | ||
| 1177 | |||
| 1178 | if (first) { | ||
| 1179 | try printStepStatus(maker, step_index, stderr); | ||
| 1180 | |||
| 1181 | const last_index = if (summary == .all) deps.len -| 1 else blk: { | ||
| 1182 | var i: usize = deps.len; | ||
| 1183 | while (i > 0) { | ||
| 1184 | i -= 1; | ||
| 1185 | |||
| 1186 | const dep_index = deps[i]; | ||
| 1187 | const dep = maker.stepByIndex(dep_index); | ||
| 1188 | const found = switch (summary) { | ||
| 1189 | .all, .line, .none => unreachable, | ||
| 1190 | .failures => dep.state != .success, | ||
| 1191 | .new => !dep.result_cached, | ||
| 1192 | }; | ||
| 1193 | if (found) break :blk i; | ||
| 1194 | } | ||
| 1195 | break :blk deps.len -| 1; | ||
| 1196 | }; | ||
| 1197 | for (deps, 0..) |dep, i| { | ||
| 1198 | var print_node: PrintNode = .{ | ||
| 1199 | .parent = parent_node, | ||
| 1200 | .last = i == last_index, | ||
| 1201 | }; | ||
| 1202 | try printTreeStep(maker, dep, stderr, &print_node, step_stack); | ||
| 1203 | } | ||
| 1204 | } else { | ||
| 1205 | if (deps.len == 0) { | ||
| 1206 | try writer.writeAll(" (reused)\n"); | ||
| 1207 | } else { | ||
| 1208 | try writer.print(" (+{d} more reused dependencies)\n", .{deps.len}); | ||
| 1209 | } | ||
| 1210 | try stderr.setColor(.reset); | ||
| 1211 | } | ||
| 1212 | } | ||
| 1213 | |||
| 1214 | fn printStepStatus(maker: *const Maker, step_index: Configuration.Step.Index, stderr: Io.Terminal) !void { | ||
| 1215 | const s = maker.stepByIndex(step_index); | ||
| 1216 | const writer = stderr.writer; | ||
| 1217 | switch (s.state) { | ||
| 1218 | .precheck_unstarted => unreachable, | ||
| 1219 | .precheck_started => unreachable, | ||
| 1220 | .precheck_done => unreachable, | ||
| 1221 | |||
| 1222 | .dependency_failure => { | ||
| 1223 | try stderr.setColor(.dim); | ||
| 1224 | try writer.writeAll(" transitive failure\n"); | ||
| 1225 | try stderr.setColor(.reset); | ||
| 1226 | }, | ||
| 1227 | |||
| 1228 | .success => { | ||
| 1229 | try stderr.setColor(.green); | ||
| 1230 | if (s.result_cached) { | ||
| 1231 | try writer.writeAll(" cached"); | ||
| 1232 | } else if (s.test_results.test_count > 0) { | ||
| 1233 | const pass_count = s.test_results.passCount(); | ||
| 1234 | assert(s.test_results.test_count == pass_count + s.test_results.skip_count); | ||
| 1235 | try writer.print(" {d} pass", .{pass_count}); | ||
| 1236 | if (s.test_results.skip_count > 0) { | ||
| 1237 | try stderr.setColor(.reset); | ||
| 1238 | try writer.writeAll(", "); | ||
| 1239 | try stderr.setColor(.yellow); | ||
| 1240 | try writer.print("{d} skip", .{s.test_results.skip_count}); | ||
| 1241 | } | ||
| 1242 | try stderr.setColor(.reset); | ||
| 1243 | try writer.print(" ({d} total)", .{s.test_results.test_count}); | ||
| 1244 | } else { | ||
| 1245 | try writer.writeAll(" success"); | ||
| 1246 | } | ||
| 1247 | try stderr.setColor(.reset); | ||
| 1248 | if (s.result_duration_ns) |ns| { | ||
| 1249 | try stderr.setColor(.dim); | ||
| 1250 | if (ns >= std.time.ns_per_min) { | ||
| 1251 | try writer.print(" {d}m", .{ns / std.time.ns_per_min}); | ||
| 1252 | } else if (ns >= std.time.ns_per_s) { | ||
| 1253 | try writer.print(" {d}s", .{ns / std.time.ns_per_s}); | ||
| 1254 | } else if (ns >= std.time.ns_per_ms) { | ||
| 1255 | try writer.print(" {d}ms", .{ns / std.time.ns_per_ms}); | ||
| 1256 | } else if (ns >= std.time.ns_per_us) { | ||
| 1257 | try writer.print(" {d}us", .{ns / std.time.ns_per_us}); | ||
| 1258 | } else { | ||
| 1259 | try writer.print(" {d}ns", .{ns}); | ||
| 1260 | } | ||
| 1261 | try stderr.setColor(.reset); | ||
| 1262 | } | ||
| 1263 | if (s.result_peak_rss != 0) { | ||
| 1264 | const rss = s.result_peak_rss; | ||
| 1265 | try stderr.setColor(.dim); | ||
| 1266 | if (rss >= 1000_000_000) { | ||
| 1267 | try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000}); | ||
| 1268 | } else if (rss >= 1000_000) { | ||
| 1269 | try writer.print(" MaxRSS:{d}M", .{rss / 1000_000}); | ||
| 1270 | } else if (rss >= 1000) { | ||
| 1271 | try writer.print(" MaxRSS:{d}K", .{rss / 1000}); | ||
| 1272 | } else { | ||
| 1273 | try writer.print(" MaxRSS:{d}B", .{rss}); | ||
| 1274 | } | ||
| 1275 | try stderr.setColor(.reset); | ||
| 1276 | } | ||
| 1277 | try writer.writeAll("\n"); | ||
| 1278 | }, | ||
| 1279 | .skipped => { | ||
| 1280 | try stderr.setColor(.yellow); | ||
| 1281 | try writer.writeAll(" skipped\n"); | ||
| 1282 | try stderr.setColor(.reset); | ||
| 1283 | }, | ||
| 1284 | .skipped_oom => { | ||
| 1285 | const c = &maker.scanned_config.configuration; | ||
| 1286 | const max_rss = step_index.ptr(c).max_rss.toBytes(); | ||
| 1287 | try stderr.setColor(.yellow); | ||
| 1288 | try writer.writeAll(" skipped (not enough memory)"); | ||
| 1289 | try stderr.setColor(.dim); | ||
| 1290 | try writer.print(" upper bound of {d} exceeded runner limit ({d})\n", .{ | ||
| 1291 | max_rss, maker.available_rss, | ||
| 1292 | }); | ||
| 1293 | try stderr.setColor(.reset); | ||
| 1294 | }, | ||
| 1295 | .failure => { | ||
| 1296 | try printStepFailure(maker.steps, step_index, stderr, false); | ||
| 1297 | try stderr.setColor(.reset); | ||
| 1298 | }, | ||
| 1299 | } | ||
| 1300 | } | ||
| 1301 | |||
| 1302 | fn printStepFailure( | ||
| 1303 | make_steps: []Step, | ||
| 1304 | step_index: Configuration.Step.Index, | ||
| 1305 | stderr: Io.Terminal, | ||
| 1306 | dim: bool, | ||
| 1307 | ) !void { | ||
| 1308 | const w = stderr.writer; | ||
| 1309 | const s = &make_steps[@intFromEnum(step_index)]; | ||
| 1310 | if (s.result_error_bundle.errorMessageCount() > 0) { | ||
| 1311 | try stderr.setColor(.red); | ||
| 1312 | try w.print(" {d} errors\n", .{ | ||
| 1313 | s.result_error_bundle.errorMessageCount(), | ||
| 1314 | }); | ||
| 1315 | } else if (!s.test_results.isSuccess()) { | ||
| 1316 | // These first values include all of the test "statuses". Every test is either passsed, | ||
| 1317 | // skipped, failed, crashed, or timed out. | ||
| 1318 | try stderr.setColor(.green); | ||
| 1319 | try w.print(" {d} pass", .{s.test_results.passCount()}); | ||
| 1320 | try stderr.setColor(.reset); | ||
| 1321 | if (dim) try stderr.setColor(.dim); | ||
| 1322 | if (s.test_results.skip_count > 0) { | ||
| 1323 | try w.writeAll(", "); | ||
| 1324 | try stderr.setColor(.yellow); | ||
| 1325 | try w.print("{d} skip", .{s.test_results.skip_count}); | ||
| 1326 | try stderr.setColor(.reset); | ||
| 1327 | if (dim) try stderr.setColor(.dim); | ||
| 1328 | } | ||
| 1329 | if (s.test_results.fail_count > 0) { | ||
| 1330 | try w.writeAll(", "); | ||
| 1331 | try stderr.setColor(.red); | ||
| 1332 | try w.print("{d} fail", .{s.test_results.fail_count}); | ||
| 1333 | try stderr.setColor(.reset); | ||
| 1334 | if (dim) try stderr.setColor(.dim); | ||
| 1335 | } | ||
| 1336 | if (s.test_results.crash_count > 0) { | ||
| 1337 | try w.writeAll(", "); | ||
| 1338 | try stderr.setColor(.red); | ||
| 1339 | try w.print("{d} crash", .{s.test_results.crash_count}); | ||
| 1340 | try stderr.setColor(.reset); | ||
| 1341 | if (dim) try stderr.setColor(.dim); | ||
| 1342 | } | ||
| 1343 | if (s.test_results.timeout_count > 0) { | ||
| 1344 | try w.writeAll(", "); | ||
| 1345 | try stderr.setColor(.red); | ||
| 1346 | try w.print("{d} timeout", .{s.test_results.timeout_count}); | ||
| 1347 | try stderr.setColor(.reset); | ||
| 1348 | if (dim) try stderr.setColor(.dim); | ||
| 1349 | } | ||
| 1350 | try w.print(" ({d} total)", .{s.test_results.test_count}); | ||
| 1351 | |||
| 1352 | // Memory leaks are intentionally written after the total, because is isn't a test *status*, | ||
| 1353 | // but just a flag that any tests -- even passed ones -- can have. We also use a different | ||
| 1354 | // separator, so it looks like: | ||
| 1355 | // 2 pass, 1 skip, 2 fail (5 total); 2 leaks | ||
| 1356 | if (s.test_results.leak_count > 0) { | ||
| 1357 | try w.writeAll("; "); | ||
| 1358 | try stderr.setColor(.red); | ||
| 1359 | try w.print("{d} leaks", .{s.test_results.leak_count}); | ||
| 1360 | try stderr.setColor(.reset); | ||
| 1361 | if (dim) try stderr.setColor(.dim); | ||
| 1362 | } | ||
| 1363 | |||
| 1364 | // It's usually not helpful to know how many error logs there were because they tend to | ||
| 1365 | // just come with other errors (e.g. crashes and leaks print stack traces, and clean | ||
| 1366 | // failures print error traces). So only mention them if they're the only thing causing | ||
| 1367 | // the failure. | ||
| 1368 | const show_err_logs: bool = show: { | ||
| 1369 | var alt_results = s.test_results; | ||
| 1370 | alt_results.log_err_count = 0; | ||
| 1371 | break :show alt_results.isSuccess(); | ||
| 1372 | }; | ||
| 1373 | if (show_err_logs) { | ||
| 1374 | try w.writeAll("; "); | ||
| 1375 | try stderr.setColor(.red); | ||
| 1376 | try w.print("{d} error logs", .{s.test_results.log_err_count}); | ||
| 1377 | try stderr.setColor(.reset); | ||
| 1378 | if (dim) try stderr.setColor(.dim); | ||
| 1379 | } | ||
| 1380 | |||
| 1381 | try w.writeAll("\n"); | ||
| 1382 | } else if (s.result_error_msgs.items.len > 0) { | ||
| 1383 | try stderr.setColor(.red); | ||
| 1384 | try w.writeAll(" failure\n"); | ||
| 1385 | } else { | ||
| 1386 | assert(s.result_stderr.len > 0); | ||
| 1387 | try stderr.setColor(.red); | ||
| 1388 | try w.writeAll(" w\n"); | ||
| 1389 | } | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | const PrintNode = struct { | ||
| 1393 | parent: ?*PrintNode, | ||
| 1394 | last: bool = false, | ||
| 1395 | }; | ||
| 1396 | |||
| 1397 | fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void { | ||
| 1398 | const parent = node.parent orelse return; | ||
| 1399 | const writer = stderr.writer; | ||
| 1400 | if (parent.parent == null) return; | ||
| 1401 | try printPrefix(parent, stderr); | ||
| 1402 | if (parent.last) { | ||
| 1403 | try writer.writeAll(" "); | ||
| 1404 | } else { | ||
| 1405 | try writer.writeAll(switch (stderr.mode) { | ||
| 1406 | .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ | ||
| 1407 | else => "| ", | ||
| 1408 | }); | ||
| 1409 | } | ||
| 1410 | } | ||
| 1411 | |||
| 1412 | fn printChildNodePrefix(stderr: Io.Terminal) !void { | ||
| 1413 | try stderr.writer.writeAll(switch (stderr.mode) { | ||
| 1414 | .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─ | ||
| 1415 | else => "+- ", | ||
| 1416 | }); | ||
| 1417 | } | ||
| 1418 | |||
| 1419 | /// Traverse the dependency graph depth-first and make it undirected by having | ||
| 1420 | /// steps know their dependants (they only know dependencies at start). | ||
| 1421 | /// Along the way, check that there is no dependency loop, and record the steps | ||
| 1422 | /// in traversal order in `step_stack`. | ||
| 1423 | /// Each step has its dependencies traversed in random order, this accomplishes | ||
| 1424 | /// two things: | ||
| 1425 | /// - `step_stack` will be in randomized-depth-first order, so the build runner | ||
| 1426 | /// spawns initial steps in a random order | ||
| 1427 | /// - each step's `dependants` list is also filled in a random order, so that | ||
| 1428 | /// when it finishes executing in `makeStep`, it spawns next steps to run in | ||
| 1429 | /// random order | ||
| 1430 | fn constructGraphAndCheckForDependencyLoop( | ||
| 1431 | gpa: Allocator, | ||
| 1432 | c: *const Configuration, | ||
| 1433 | steps: []Step, | ||
| 1434 | step_index: Configuration.Step.Index, | ||
| 1435 | step_stack: *std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void), | ||
| 1436 | rand: std.Random, | ||
| 1437 | ) error{ DependencyLoopDetected, OutOfMemory }!void { | ||
| 1438 | const make_step: *Step = &steps[@intFromEnum(step_index)]; | ||
| 1439 | switch (make_step.state) { | ||
| 1440 | .precheck_started => { | ||
| 1441 | log.err("dependency loop detected: {s}", .{step_index.ptr(c).name.slice(c)}); | ||
| 1442 | return error.DependencyLoopDetected; | ||
| 1443 | }, | ||
| 1444 | .precheck_unstarted => { | ||
| 1445 | make_step.state = .precheck_started; | ||
| 1446 | |||
| 1447 | const step = step_index.ptr(c); | ||
| 1448 | const dependencies = step.deps.slice(c); | ||
| 1449 | try step_stack.ensureUnusedCapacity(gpa, dependencies.len); | ||
| 1450 | |||
| 1451 | // We dupe to avoid shuffling the steps in the summary, it depends | ||
| 1452 | // on dependencies' order. | ||
| 1453 | const deps = try gpa.dupe(Configuration.Step.Index, dependencies); | ||
| 1454 | defer gpa.free(deps); | ||
| 1455 | |||
| 1456 | rand.shuffle(Configuration.Step.Index, deps); | ||
| 1457 | |||
| 1458 | for (deps) |dep| { | ||
| 1459 | const dep_step: *Step = &steps[@intFromEnum(dep)]; | ||
| 1460 | try step_stack.put(gpa, dep, {}); | ||
| 1461 | try dep_step.dependants.append(gpa, step_index); | ||
| 1462 | constructGraphAndCheckForDependencyLoop(gpa, c, steps, dep, step_stack, rand) catch |err| switch (err) { | ||
| 1463 | error.DependencyLoopDetected => { | ||
| 1464 | log.info("needed by: {s}", .{step_index.ptr(c).name.slice(c)}); | ||
| 1465 | return err; | ||
| 1466 | }, | ||
| 1467 | else => return err, | ||
| 1468 | }; | ||
| 1469 | } | ||
| 1470 | |||
| 1471 | make_step.state = .precheck_done; | ||
| 1472 | make_step.pending_deps = @intCast(dependencies.len); | ||
| 1473 | }, | ||
| 1474 | .precheck_done => {}, | ||
| 1475 | |||
| 1476 | // These don't happen until we actually run the step graph. | ||
| 1477 | .dependency_failure => unreachable, | ||
| 1478 | .success => unreachable, | ||
| 1479 | .failure => unreachable, | ||
| 1480 | .skipped => unreachable, | ||
| 1481 | .skipped_oom => unreachable, | ||
| 1482 | } | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | pub fn printErrorMessages( | ||
| 1486 | gpa: Allocator, | ||
| 1487 | c: *const Configuration, | ||
| 1488 | make_steps: []Step, | ||
| 1489 | failing_step_index: Configuration.Step.Index, | ||
| 1490 | options: std.zig.ErrorBundle.RenderOptions, | ||
| 1491 | stderr: Io.Terminal, | ||
| 1492 | error_style: ErrorStyle, | ||
| 1493 | multiline_errors: MultilineErrors, | ||
| 1494 | ) !void { | ||
| 1495 | const writer = stderr.writer; | ||
| 1496 | if (error_style.verboseContext()) { | ||
| 1497 | // Provide context for where these error messages are coming from by | ||
| 1498 | // printing the corresponding Step subtree. | ||
| 1499 | var step_stack: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 1500 | defer step_stack.deinit(gpa); | ||
| 1501 | try step_stack.append(gpa, failing_step_index); | ||
| 1502 | while (true) { | ||
| 1503 | const last_step = &make_steps[@intFromEnum(step_stack.items[step_stack.items.len - 1])]; | ||
| 1504 | if (last_step.dependants.items.len == 0) break; | ||
| 1505 | try step_stack.append(gpa, last_step.dependants.items[0]); | ||
| 1506 | } | ||
| 1507 | |||
| 1508 | // Now, `step_stack` has the subtree that we want to print, in reverse order. | ||
| 1509 | try stderr.setColor(.dim); | ||
| 1510 | var indent: usize = 0; | ||
| 1511 | while (step_stack.pop()) |step_index| : (indent += 1) { | ||
| 1512 | if (indent > 0) { | ||
| 1513 | try writer.splatByteAll(' ', (indent - 1) * 3); | ||
| 1514 | try printChildNodePrefix(stderr); | ||
| 1515 | } | ||
| 1516 | |||
| 1517 | try writer.writeAll(step_index.ptr(c).name.slice(c)); | ||
| 1518 | |||
| 1519 | if (step_index == failing_step_index) { | ||
| 1520 | try printStepFailure(make_steps, step_index, stderr, true); | ||
| 1521 | } else { | ||
| 1522 | try writer.writeAll("\n"); | ||
| 1523 | } | ||
| 1524 | } | ||
| 1525 | try stderr.setColor(.reset); | ||
| 1526 | } else { | ||
| 1527 | // Just print the failing step itself. | ||
| 1528 | try stderr.setColor(.dim); | ||
| 1529 | try writer.writeAll(failing_step_index.ptr(c).name.slice(c)); | ||
| 1530 | try printStepFailure(make_steps, failing_step_index, stderr, true); | ||
| 1531 | try stderr.setColor(.reset); | ||
| 1532 | } | ||
| 1533 | |||
| 1534 | const failing_step = &make_steps[@intFromEnum(failing_step_index)]; | ||
| 1535 | |||
| 1536 | if (failing_step.result_stderr.len > 0) { | ||
| 1537 | try writer.writeAll(failing_step.result_stderr); | ||
| 1538 | if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) { | ||
| 1539 | try writer.writeAll("\n"); | ||
| 1540 | } | ||
| 1541 | } | ||
| 1542 | |||
| 1543 | try failing_step.result_error_bundle.renderToTerminal(options, stderr); | ||
| 1544 | |||
| 1545 | for (failing_step.result_error_msgs.items) |msg| { | ||
| 1546 | try stderr.setColor(.red); | ||
| 1547 | try writer.writeAll("error:"); | ||
| 1548 | try stderr.setColor(.reset); | ||
| 1549 | if (std.mem.indexOfScalar(u8, msg, '\n') == null) { | ||
| 1550 | try writer.print(" {s}\n", .{msg}); | ||
| 1551 | } else switch (multiline_errors) { | ||
| 1552 | .indent => { | ||
| 1553 | var it = std.mem.splitScalar(u8, msg, '\n'); | ||
| 1554 | try writer.print(" {s}\n", .{it.first()}); | ||
| 1555 | while (it.next()) |line| { | ||
| 1556 | try writer.print(" {s}\n", .{line}); | ||
| 1557 | } | ||
| 1558 | }, | ||
| 1559 | .newline => try writer.print("\n{s}\n", .{msg}), | ||
| 1560 | .none => try writer.print(" {s}\n", .{msg}), | ||
| 1561 | } | ||
| 1562 | } | ||
| 1563 | |||
| 1564 | if (error_style.verboseContext()) { | ||
| 1565 | if (failing_step.result_failed_command) |cmd_str| { | ||
| 1566 | try stderr.setColor(.red); | ||
| 1567 | try writer.writeAll("failed command: "); | ||
| 1568 | try stderr.setColor(.reset); | ||
| 1569 | try writer.writeAll(cmd_str); | ||
| 1570 | try writer.writeByte('\n'); | ||
| 1571 | } | ||
| 1572 | } | ||
| 1573 | |||
| 1574 | try writer.writeByte('\n'); | ||
| 1575 | } | ||
| 1576 | |||
| 1577 | fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 { | ||
| 1578 | if (idx.* >= args.len) return null; | ||
| 1579 | defer idx.* += 1; | ||
| 1580 | return args[idx.*]; | ||
| 1581 | } | ||
| 1582 | |||
| 1583 | fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 { | ||
| 1584 | return nextArg(args, idx) orelse { | ||
| 1585 | fatalWithHint("expected argument after {q}", .{args[idx.* - 1]}); | ||
| 1586 | }; | ||
| 1587 | } | ||
| 1588 | |||
| 1589 | fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 { | ||
| 1590 | const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first}); | ||
| 1591 | if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg }); | ||
| 1592 | const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first}); | ||
| 1593 | return arg; | ||
| 1594 | } | ||
| 1595 | |||
| 1596 | fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 { | ||
| 1597 | if (idx >= args.len) return null; | ||
| 1598 | return args[idx..]; | ||
| 1599 | } | ||
| 1600 | |||
| 1601 | const Color = std.zig.Color; | ||
| 1602 | const ErrorStyle = enum { | ||
| 1603 | verbose, | ||
| 1604 | minimal, | ||
| 1605 | verbose_clear, | ||
| 1606 | minimal_clear, | ||
| 1607 | fn verboseContext(s: ErrorStyle) bool { | ||
| 1608 | return switch (s) { | ||
| 1609 | .verbose, .verbose_clear => true, | ||
| 1610 | .minimal, .minimal_clear => false, | ||
| 1611 | }; | ||
| 1612 | } | ||
| 1613 | fn clearOnUpdate(s: ErrorStyle) bool { | ||
| 1614 | return switch (s) { | ||
| 1615 | .verbose, .minimal => false, | ||
| 1616 | .verbose_clear, .minimal_clear => true, | ||
| 1617 | }; | ||
| 1618 | } | ||
| 1619 | }; | ||
| 1620 | const MultilineErrors = enum { indent, newline, none }; | ||
| 1621 | const Summary = enum { all, new, failures, line, none }; | ||
| 1622 | |||
| 1623 | fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn { | ||
| 1624 | log.info("to access the help menu: zig build -h", .{}); | ||
| 1625 | fatal(f, args); | ||
| 1626 | } | ||
| 1627 | |||
| 1628 | fn cleanTmpFiles(io: Io, steps: []const Configuration.Step.Index) void { | ||
| 1629 | for (steps) |step_index| { | ||
| 1630 | if (true) @panic("TODO"); | ||
| 1631 | const wf = step_index.cast(std.Build.Step.WriteFile) orelse continue; | ||
| 1632 | if (wf.mode != .tmp) continue; | ||
| 1633 | const path = wf.generated_directory.path orelse continue; | ||
| 1634 | Io.Dir.cwd().deleteTree(io, path) catch |err| { | ||
| 1635 | log.warn("failed to delete {s}: {t}", .{ path, err }); | ||
| 1636 | }; | ||
| 1637 | } | ||
| 1638 | } | ||
| 1639 | |||
| 1640 | var stdio_buffer_allocation: [256]u8 = undefined; | ||
| 1641 | var stdout_writer_allocation: Io.File.Writer = undefined; | ||
| 1642 | |||
| 1643 | fn initStdoutWriter(io: Io) *Writer { | ||
| 1644 | stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation); | ||
| 1645 | return &stdout_writer_allocation.interface; | ||
| 1646 | } | ||
| 1647 | |||
| 1648 | const ScannedConfig = struct { | ||
| 1649 | configuration: Configuration, | ||
| 1650 | top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index), | ||
| 1651 | |||
| 1652 | fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void { | ||
| 1653 | const c = &sc.configuration; | ||
| 1654 | var serializer: std.zon.Serializer = .{ .writer = w }; | ||
| 1655 | var s = try serializer.beginStruct(.{}); | ||
| 1656 | |||
| 1657 | try s.field("default_step", @intFromEnum(c.default_step), .{}); | ||
| 1658 | { | ||
| 1659 | var ss = try s.beginStructField("top_level_steps", .{}); | ||
| 1660 | for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step| { | ||
| 1661 | try ss.field(name, @intFromEnum(step), .{}); | ||
| 1662 | } | ||
| 1663 | try ss.end(); | ||
| 1664 | } | ||
| 1665 | |||
| 1666 | try s.end(); | ||
| 1667 | } | ||
| 1668 | |||
| 1669 | fn printSteps(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { | ||
| 1670 | const arena = graph.arena; | ||
| 1671 | const c = &sc.configuration; | ||
| 1672 | for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step_index| { | ||
| 1673 | const step = step_index.ptr(c); | ||
| 1674 | const decorated_name = if (step_index == c.default_step) | ||
| 1675 | try fmt.allocPrint(arena, "{s} (default)", .{name}) | ||
| 1676 | else | ||
| 1677 | name; | ||
| 1678 | const top_level = c.extraData(Configuration.Step.TopLevel, step.extra_index); | ||
| 1679 | const description = top_level.description.slice(c); | ||
| 1680 | try w.print(" {s:<28} {s}\n", .{ decorated_name, description }); | ||
| 1681 | } | ||
| 1682 | } | ||
| 1683 | |||
| 1684 | fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { | ||
| 1685 | const arena = graph.arena; | ||
| 1686 | |||
| 1687 | try w.print( | ||
| 1688 | \\Usage: {s} build [steps] [options] | ||
| 1689 | \\ | ||
| 1690 | \\Steps: | ||
| 1691 | \\ | ||
| 1692 | , .{graph.zig_exe}); | ||
| 1693 | try printSteps(sc, graph, w); | ||
| 1694 | try w.writeAll( | ||
| 1695 | \\ | ||
| 1696 | \\Project-Specific Options: | ||
| 1697 | \\ | ||
| 1698 | ); | ||
| 1699 | |||
| 1700 | const available_options = sc.configuration.available_options; | ||
| 1701 | if (available_options.len == 0) { | ||
| 1702 | try w.print(" (none)\n", .{}); | ||
| 1703 | } else { | ||
| 1704 | for (available_options) |option| { | ||
| 1705 | const name = option.name.slice(&sc.configuration); | ||
| 1706 | const description = option.description.slice(&sc.configuration); | ||
| 1707 | const help = try fmt.allocPrint(arena, " -D{s}=[{t}]", .{ name, option.type }); | ||
| 1708 | try w.print("{s:<30} {s}\n", .{ help, description }); | ||
| 1709 | if (option.enum_options.slice(&sc.configuration)) |enum_options| { | ||
| 1710 | const padding: [33]u8 = @splat(' '); | ||
| 1711 | try w.writeAll(padding ++ "Supported Values:\n"); | ||
| 1712 | for (enum_options) |enum_option_index| { | ||
| 1713 | const enum_option = enum_option_index.slice(&sc.configuration); | ||
| 1714 | try w.print(padding ++ " {s}\n", .{enum_option}); | ||
| 1715 | } | ||
| 1716 | } | ||
| 1717 | } | ||
| 1718 | } | ||
| 1719 | |||
| 1720 | try w.writeAll( | ||
| 1721 | \\ | ||
| 1722 | \\System Integration Options: | ||
| 1723 | \\ --search-prefix [path] Add a path to look for binaries, libraries, headers | ||
| 1724 | \\ --sysroot [path] Set the system root directory (usually /) | ||
| 1725 | \\ --libc [file] Provide a file which specifies libc paths | ||
| 1726 | \\ | ||
| 1727 | \\ --system [pkgdir] Disable package fetching; enable all integrations | ||
| 1728 | \\ -fsys=[name] Enable a system integration | ||
| 1729 | \\ -fno-sys=[name] Disable a system integration | ||
| 1730 | \\ | ||
| 1731 | \\ -fdarling, -fno-darling Integration with system-installed Darling to | ||
| 1732 | \\ execute macOS programs on Linux hosts | ||
| 1733 | \\ (default: no) | ||
| 1734 | \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute | ||
| 1735 | \\ foreign-architecture programs on Linux hosts | ||
| 1736 | \\ (default: no) | ||
| 1737 | \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc | ||
| 1738 | \\ (e.g. glibc or musl) built for multiple foreign | ||
| 1739 | \\ architectures, allowing execution of non-native | ||
| 1740 | \\ programs that link with libc. | ||
| 1741 | \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on | ||
| 1742 | \\ ARM64 macOS hosts. (default: no) | ||
| 1743 | \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to | ||
| 1744 | \\ execute WASI binaries. (default: no) | ||
| 1745 | \\ -fwine, -fno-wine Integration with system-installed Wine to execute | ||
| 1746 | \\ Windows programs on Linux hosts. (default: no) | ||
| 1747 | \\ | ||
| 1748 | \\ Available System Integrations: Enabled: | ||
| 1749 | \\ | ||
| 1750 | ); | ||
| 1751 | if (sc.configuration.system_integrations.len == 0) { | ||
| 1752 | try w.writeAll(" (none) -\n"); | ||
| 1753 | } else { | ||
| 1754 | for (sc.configuration.system_integrations) |system_integration| { | ||
| 1755 | const name = system_integration.name.slice(&sc.configuration); | ||
| 1756 | const status = switch (system_integration.status) { | ||
| 1757 | .disabled => "no", | ||
| 1758 | .enabled => "yes", | ||
| 1759 | }; | ||
| 1760 | try w.print(" {s:<43} {s}\n", .{ name, status }); | ||
| 1761 | } | ||
| 1762 | } | ||
| 1763 | |||
| 1764 | try w.writeAll( | ||
| 1765 | \\ | ||
| 1766 | \\General Options: | ||
| 1767 | \\ -h, --help Print this help to stdout and exit | ||
| 1768 | \\ -l, --list-steps Print available steps to stdout and exit | ||
| 1769 | \\ | ||
| 1770 | \\ -p, --prefix [path] Where to install files (default: zig-out) | ||
| 1771 | \\ --prefix-lib-dir [path] Where to install libraries | ||
| 1772 | \\ --prefix-exe-dir [path] Where to install executables | ||
| 1773 | \\ --prefix-include-dir [path] Where to install C header files | ||
| 1774 | \\ --release[=mode] Request release mode, optionally specifying a | ||
| 1775 | \\ preferred optimization mode: fast, safe, small | ||
| 1776 | \\ | ||
| 1777 | \\ --verbose Print commands before executing them | ||
| 1778 | \\ --color [auto|off|on] Enable or disable colored error messages | ||
| 1779 | \\ --error-style [style] Control how build errors are printed | ||
| 1780 | \\ verbose (Default) Report errors with full context | ||
| 1781 | \\ minimal Report errors after summary, excluding context like command lines | ||
| 1782 | \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update | ||
| 1783 | \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update | ||
| 1784 | \\ --multiline-errors [style] Control how multi-line error messages are printed | ||
| 1785 | \\ indent (Default) Indent non-initial lines to align with initial line | ||
| 1786 | \\ newline Include a leading newline so that the error message is on its own lines | ||
| 1787 | \\ none Print as usual so the first line is misaligned | ||
| 1788 | \\ --summary [mode] Control the printing of the build summary | ||
| 1789 | \\ all Print the build summary in its entirety | ||
| 1790 | \\ new Omit cached steps | ||
| 1791 | \\ failures (Default if short-lived) Only print failed steps | ||
| 1792 | \\ line (Default if long-lived) Only print the single-line summary | ||
| 1793 | \\ none Do not print the build summary | ||
| 1794 | \\ -j<N> Limit concurrent jobs (default is to use all CPU cores) | ||
| 1795 | \\ --maxrss <bytes> Limit memory usage (default is to use available memory) | ||
| 1796 | \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss | ||
| 1797 | \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded. | ||
| 1798 | \\ The timeout must include a unit: ns, us, ms, s, m, h | ||
| 1799 | \\ --watch Continuously rebuild when source files are modified | ||
| 1800 | \\ --debounce <ms> Delay before rebuilding after changed file detected | ||
| 1801 | \\ --webui[=ip] Enable the web interface on the given IP address | ||
| 1802 | \\ --fuzz[=limit] Continuously search for unit test failures with an optional | ||
| 1803 | \\ limit to the max number of iterations. The argument supports | ||
| 1804 | \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies | ||
| 1805 | \\ '--webui' when no limit is specified. | ||
| 1806 | \\ --time-report Force full rebuild and provide detailed information on | ||
| 1807 | \\ compilation time of Zig source code (implies '--webui') | ||
| 1808 | \\ -fincremental Enable incremental compilation | ||
| 1809 | \\ -fno-incremental Disable incremental compilation | ||
| 1810 | \\ | ||
| 1811 | \\Package Management Options: | ||
| 1812 | \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit | ||
| 1813 | \\ needed (Default) Lazy dependencies are fetched as needed | ||
| 1814 | \\ all Lazy dependencies are always fetched | ||
| 1815 | \\ --fork=[path] Override one or more projects from dependency tree | ||
| 1816 | \\ | ||
| 1817 | \\Advanced Options: | ||
| 1818 | \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error | ||
| 1819 | \\ -fno-reference-trace Disable reference trace | ||
| 1820 | \\ -fallow-so-scripts Allows .so files to be GNU ld scripts | ||
| 1821 | \\ -fno-allow-so-scripts (default) .so files must be ELF files | ||
| 1822 | \\ --build-file [file] Override path to build.zig | ||
| 1823 | \\ --cache-dir [path] Override path to local Zig cache directory | ||
| 1824 | \\ --global-cache-dir [path] Override path to global Zig cache directory | ||
| 1825 | \\ --zig-lib-dir [arg] Override path to Zig lib directory | ||
| 1826 | \\ --build-runner [file] Override path to build runner | ||
| 1827 | \\ --seed [integer] For shuffling dependency traversal order (default: random) | ||
| 1828 | \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries | ||
| 1829 | \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) | ||
| 1830 | \\ sha1, tree 20-byte cryptographic hash (ELF, WASM) | ||
| 1831 | \\ md5 16-byte cryptographic hash (ELF) | ||
| 1832 | \\ uuid 16-byte random UUID (ELF, WASM) | ||
| 1833 | \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM) | ||
| 1834 | \\ none (default) No build ID | ||
| 1835 | \\ --debug-log [scope] Enable debugging the compiler | ||
| 1836 | \\ --debug-pkg-config Fail if unknown pkg-config flags encountered | ||
| 1837 | \\ --debug-rt Debug compiler runtime libraries | ||
| 1838 | \\ --verbose-link Enable compiler debug output for linking | ||
| 1839 | \\ --verbose-air Enable compiler debug output for Zig AIR | ||
| 1840 | \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR | ||
| 1841 | \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC | ||
| 1842 | \\ --verbose-cimport Enable compiler debug output for C imports | ||
| 1843 | \\ --verbose-cc Enable compiler debug output for C compilation | ||
| 1844 | \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features | ||
| 1845 | \\ | ||
| 1846 | ); | ||
| 1847 | } | ||
| 1848 | }; | ||
lib/compiler/Maker/Fuzz.zig created+606| ... | @@ -0,0 +1,606 @@ | ||
| 1 | const Fuzz = @This(); | ||
| 2 | |||
| 3 | const std = @import("std"); | ||
| 4 | const Allocator = std.mem.Allocator; | ||
| 5 | const Build = std.Build; | ||
| 6 | const Cache = std.Build.Cache; | ||
| 7 | const Coverage = std.debug.Coverage; | ||
| 8 | const Configuration = std.Build.Configuration; | ||
| 9 | const Io = std.Io; | ||
| 10 | const abi = std.Build.abi.fuzz; | ||
| 11 | const assert = std.debug.assert; | ||
| 12 | const fatal = std.process.fatal; | ||
| 13 | const log = std.log; | ||
| 14 | |||
| 15 | const Maker = @import("../Maker.zig"); | ||
| 16 | const WebServer = @import("WebServer.zig"); | ||
| 17 | |||
| 18 | gpa: Allocator, | ||
| 19 | io: Io, | ||
| 20 | mode: Mode, | ||
| 21 | |||
| 22 | /// Allocated into `gpa`. | ||
| 23 | run_steps: []const Configuration.Step.Index, | ||
| 24 | |||
| 25 | group: Io.Group, | ||
| 26 | root_prog_node: std.Progress.Node, | ||
| 27 | prog_node: std.Progress.Node, | ||
| 28 | |||
| 29 | /// Protects `coverage_files`. | ||
| 30 | coverage_mutex: Io.Mutex, | ||
| 31 | coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap), | ||
| 32 | |||
| 33 | queue_mutex: Io.Mutex, | ||
| 34 | queue_cond: Io.Condition, | ||
| 35 | msg_queue: std.ArrayList(Msg), | ||
| 36 | |||
| 37 | pub const Mode = union(enum) { | ||
| 38 | forever: struct { ws: *WebServer }, | ||
| 39 | limit: Limited, | ||
| 40 | |||
| 41 | pub const Limited = struct { | ||
| 42 | amount: u64, | ||
| 43 | }; | ||
| 44 | }; | ||
| 45 | |||
| 46 | const Msg = union(enum) { | ||
| 47 | coverage: struct { | ||
| 48 | id: u64, | ||
| 49 | cumulative: struct { | ||
| 50 | runs: u64, | ||
| 51 | unique: u64, | ||
| 52 | coverage: u64, | ||
| 53 | }, | ||
| 54 | run: Configuration.Step.Index, | ||
| 55 | }, | ||
| 56 | entry_point: struct { | ||
| 57 | coverage_id: u64, | ||
| 58 | addr: u64, | ||
| 59 | }, | ||
| 60 | }; | ||
| 61 | |||
| 62 | const CoverageMap = struct { | ||
| 63 | mapped_memory: []align(std.heap.page_size_min) const u8, | ||
| 64 | coverage: Coverage, | ||
| 65 | source_locations: []Coverage.SourceLocation, | ||
| 66 | /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested. | ||
| 67 | entry_points: std.ArrayList(u32), | ||
| 68 | start_timestamp: i64, | ||
| 69 | start_n_runs: u64, | ||
| 70 | |||
| 71 | fn deinit(cm: *CoverageMap, gpa: Allocator) void { | ||
| 72 | std.posix.munmap(cm.mapped_memory); | ||
| 73 | cm.coverage.deinit(gpa); | ||
| 74 | cm.* = undefined; | ||
| 75 | } | ||
| 76 | }; | ||
| 77 | |||
| 78 | pub fn init( | ||
| 79 | gpa: Allocator, | ||
| 80 | io: Io, | ||
| 81 | all_steps: []const Configuration.Step.Index, | ||
| 82 | root_prog_node: std.Progress.Node, | ||
| 83 | mode: Mode, | ||
| 84 | ) error{ OutOfMemory, Canceled }!Fuzz { | ||
| 85 | const run_steps: []const Configuration.Step.Index = steps: { | ||
| 86 | var steps: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 87 | defer steps.deinit(gpa); | ||
| 88 | const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0); | ||
| 89 | defer rebuild_node.end(); | ||
| 90 | var rebuild_group: Io.Group = .init; | ||
| 91 | defer rebuild_group.cancel(io); | ||
| 92 | |||
| 93 | for (all_steps) |step| { | ||
| 94 | if (true) @panic("TODO"); | ||
| 95 | const run = step.cast(std.Build.Step.Run) orelse continue; | ||
| 96 | if (run.producer == null) continue; | ||
| 97 | if (run.fuzz_tests.items.len == 0) continue; | ||
| 98 | try steps.append(gpa, run); | ||
| 99 | rebuild_group.async(io, rebuildTestsWorkerRun, .{ run, gpa, rebuild_node }); | ||
| 100 | } | ||
| 101 | |||
| 102 | if (steps.items.len == 0) fatal("no fuzz tests found", .{}); | ||
| 103 | rebuild_node.setEstimatedTotalItems(steps.items.len); | ||
| 104 | const run_steps = try gpa.dupe(Configuration.Step.Index, steps.items); | ||
| 105 | try rebuild_group.await(io); | ||
| 106 | break :steps run_steps; | ||
| 107 | }; | ||
| 108 | errdefer gpa.free(run_steps); | ||
| 109 | |||
| 110 | for (run_steps) |run_step_index| { | ||
| 111 | if (true) @panic("TODO"); | ||
| 112 | assert(run_step_index.fuzz_tests.items.len > 0); | ||
| 113 | if (run_step_index.rebuilt_executable == null) | ||
| 114 | fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{}); | ||
| 115 | } | ||
| 116 | |||
| 117 | return .{ | ||
| 118 | .gpa = gpa, | ||
| 119 | .io = io, | ||
| 120 | .mode = mode, | ||
| 121 | .run_steps = run_steps, | ||
| 122 | .group = .init, | ||
| 123 | .root_prog_node = root_prog_node, | ||
| 124 | .prog_node = .none, | ||
| 125 | .coverage_files = .empty, | ||
| 126 | .coverage_mutex = .init, | ||
| 127 | .queue_mutex = .init, | ||
| 128 | .queue_cond = .init, | ||
| 129 | .msg_queue = .empty, | ||
| 130 | }; | ||
| 131 | } | ||
| 132 | |||
| 133 | pub fn start(fuzz: *Fuzz) void { | ||
| 134 | const io = fuzz.io; | ||
| 135 | fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0); | ||
| 136 | |||
| 137 | if (fuzz.mode == .forever) { | ||
| 138 | // For polling messages and sending updates to subscribers. | ||
| 139 | fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err| | ||
| 140 | fatal("unable to spawn coverage task: {t}", .{err}); | ||
| 141 | } | ||
| 142 | |||
| 143 | if (true) @panic("TODO"); | ||
| 144 | |||
| 145 | for (fuzz.run_steps) |run| { | ||
| 146 | assert(run.rebuilt_executable != null); | ||
| 147 | fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run }); | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | pub fn deinit(fuzz: *Fuzz) void { | ||
| 152 | const io = fuzz.io; | ||
| 153 | fuzz.group.cancel(io); | ||
| 154 | fuzz.prog_node.end(); | ||
| 155 | fuzz.gpa.free(fuzz.run_steps); | ||
| 156 | } | ||
| 157 | |||
| 158 | fn rebuildTestsWorkerRun(run: Configuration.Step.Index, gpa: Allocator, parent_prog_node: std.Progress.Node) void { | ||
| 159 | rebuildTestsWorkerRunFallible(run, gpa, parent_prog_node) catch |err| { | ||
| 160 | const compile = run.producer.?; | ||
| 161 | log.err("step '{s}': failed to rebuild in fuzz mode: {t}", .{ compile.step.name, err }); | ||
| 162 | }; | ||
| 163 | } | ||
| 164 | |||
| 165 | fn rebuildTestsWorkerRunFallible(run: Configuration.Step.Index, gpa: Allocator, parent_prog_node: std.Progress.Node) !void { | ||
| 166 | const graph = run.step.owner.graph; | ||
| 167 | const io = graph.io; | ||
| 168 | const compile = run.producer.?; | ||
| 169 | const prog_node = parent_prog_node.start(compile.step.name, 0); | ||
| 170 | defer prog_node.end(); | ||
| 171 | |||
| 172 | const result = compile.rebuildInFuzzMode(gpa, prog_node); | ||
| 173 | |||
| 174 | const show_compile_errors = compile.step.result_error_bundle.errorMessageCount() > 0; | ||
| 175 | const show_error_msgs = compile.step.result_error_msgs.items.len > 0; | ||
| 176 | const show_stderr = compile.step.result_stderr.len > 0; | ||
| 177 | |||
| 178 | if (show_error_msgs or show_compile_errors or show_stderr) { | ||
| 179 | var buf: [256]u8 = undefined; | ||
| 180 | const stderr = try io.lockStderr(&buf, graph.stderr_mode); | ||
| 181 | defer io.unlockStderr(); | ||
| 182 | Maker.printErrorMessages(gpa, &compile.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; | ||
| 183 | } | ||
| 184 | |||
| 185 | const rebuilt_bin_path = result catch |err| switch (err) { | ||
| 186 | error.MakeFailed => return, | ||
| 187 | else => |other| return other, | ||
| 188 | }; | ||
| 189 | run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename); | ||
| 190 | } | ||
| 191 | |||
| 192 | fn fuzzWorkerRun(fuzz: *Fuzz, run: Configuration.Step.Index) void { | ||
| 193 | const owner = run.step.owner; | ||
| 194 | const gpa = owner.allocator; | ||
| 195 | const graph = owner.graph; | ||
| 196 | const io = graph.io; | ||
| 197 | |||
| 198 | run.rerunInFuzzMode(fuzz, fuzz.prog_node) catch |err| switch (err) { | ||
| 199 | error.MakeFailed => { | ||
| 200 | var buf: [256]u8 = undefined; | ||
| 201 | const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) { | ||
| 202 | error.Canceled => return, | ||
| 203 | }; | ||
| 204 | defer io.unlockStderr(); | ||
| 205 | Maker.printErrorMessages(gpa, &run.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; | ||
| 206 | return; | ||
| 207 | }, | ||
| 208 | else => { | ||
| 209 | log.err("step '{s}': failed to rerun in fuzz mode: {t}", .{ run.step.name, err }); | ||
| 210 | return; | ||
| 211 | }, | ||
| 212 | }; | ||
| 213 | } | ||
| 214 | |||
| 215 | pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void { | ||
| 216 | if (true) @panic("TODO"); | ||
| 217 | assert(fuzz.mode == .forever); | ||
| 218 | |||
| 219 | var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa); | ||
| 220 | defer arena_state.deinit(); | ||
| 221 | const arena = arena_state.allocator(); | ||
| 222 | |||
| 223 | const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false); | ||
| 224 | var dedup_table: DedupTable = .empty; | ||
| 225 | defer dedup_table.deinit(fuzz.gpa); | ||
| 226 | |||
| 227 | for (fuzz.run_steps) |run_step| { | ||
| 228 | const compile_inputs = run_step.producer.?.step.inputs.table; | ||
| 229 | for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| { | ||
| 230 | try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len); | ||
| 231 | for (file_list.items) |sub_path| { | ||
| 232 | if (!std.mem.endsWith(u8, sub_path, ".zig")) continue; | ||
| 233 | const joined_path = try dir_path.join(arena, sub_path); | ||
| 234 | dedup_table.putAssumeCapacity(joined_path, {}); | ||
| 235 | } | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | const deduped_paths = dedup_table.keys(); | ||
| 240 | const SortContext = struct { | ||
| 241 | pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool { | ||
| 242 | _ = this; | ||
| 243 | return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) { | ||
| 244 | .lt => true, | ||
| 245 | .gt => false, | ||
| 246 | .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path), | ||
| 247 | }; | ||
| 248 | } | ||
| 249 | }; | ||
| 250 | std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan); | ||
| 251 | return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths); | ||
| 252 | } | ||
| 253 | |||
| 254 | pub const Previous = struct { | ||
| 255 | unique_runs: usize, | ||
| 256 | entry_points: usize, | ||
| 257 | sent_source_index: bool, | ||
| 258 | pub const init: Previous = .{ | ||
| 259 | .unique_runs = 0, | ||
| 260 | .entry_points = 0, | ||
| 261 | .sent_source_index = false, | ||
| 262 | }; | ||
| 263 | }; | ||
| 264 | pub fn sendUpdate( | ||
| 265 | fuzz: *Fuzz, | ||
| 266 | socket: *std.http.Server.WebSocket, | ||
| 267 | prev: *Previous, | ||
| 268 | ) !void { | ||
| 269 | const io = fuzz.io; | ||
| 270 | |||
| 271 | try fuzz.coverage_mutex.lock(io); | ||
| 272 | defer fuzz.coverage_mutex.unlock(io); | ||
| 273 | |||
| 274 | const coverage_maps = fuzz.coverage_files.values(); | ||
| 275 | if (coverage_maps.len == 0) return; | ||
| 276 | // TODO: handle multiple fuzz steps in the WebSocket packets | ||
| 277 | const coverage_map = &coverage_maps[0]; | ||
| 278 | const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); | ||
| 279 | // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the | ||
| 280 | // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the | ||
| 281 | // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass | ||
| 282 | // this data straight to the socket with sendfile... | ||
| 283 | const seen_pcs = cov_header.seenBits(); | ||
| 284 | const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic); | ||
| 285 | const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic); | ||
| 286 | { | ||
| 287 | if (!prev.sent_source_index) { | ||
| 288 | prev.sent_source_index = true; | ||
| 289 | // We need to send initial context. | ||
| 290 | const header: abi.SourceIndexHeader = .{ | ||
| 291 | .directories_len = @intCast(coverage_map.coverage.directories.entries.len), | ||
| 292 | .files_len = @intCast(coverage_map.coverage.files.entries.len), | ||
| 293 | .source_locations_len = @intCast(coverage_map.source_locations.len), | ||
| 294 | .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len), | ||
| 295 | .start_timestamp = coverage_map.start_timestamp, | ||
| 296 | .start_n_runs = coverage_map.start_n_runs, | ||
| 297 | }; | ||
| 298 | var iovecs: [5][]const u8 = .{ | ||
| 299 | @ptrCast(&header), | ||
| 300 | @ptrCast(coverage_map.coverage.directories.keys()), | ||
| 301 | @ptrCast(coverage_map.coverage.files.keys()), | ||
| 302 | @ptrCast(coverage_map.source_locations), | ||
| 303 | coverage_map.coverage.string_bytes.items, | ||
| 304 | }; | ||
| 305 | try socket.writeMessageVec(&iovecs, .binary); | ||
| 306 | } | ||
| 307 | |||
| 308 | const header: abi.CoverageUpdateHeader = .{ | ||
| 309 | .n_runs = n_runs, | ||
| 310 | .unique_runs = unique_runs, | ||
| 311 | }; | ||
| 312 | var iovecs: [2][]const u8 = .{ | ||
| 313 | @ptrCast(&header), | ||
| 314 | @ptrCast(seen_pcs), | ||
| 315 | }; | ||
| 316 | try socket.writeMessageVec(&iovecs, .binary); | ||
| 317 | |||
| 318 | prev.unique_runs = unique_runs; | ||
| 319 | } | ||
| 320 | |||
| 321 | if (prev.entry_points != coverage_map.entry_points.items.len) { | ||
| 322 | const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len)); | ||
| 323 | var iovecs: [2][]const u8 = .{ | ||
| 324 | @ptrCast(&header), | ||
| 325 | @ptrCast(coverage_map.entry_points.items), | ||
| 326 | }; | ||
| 327 | try socket.writeMessageVec(&iovecs, .binary); | ||
| 328 | |||
| 329 | prev.entry_points = coverage_map.entry_points.items.len; | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | fn coverageRun(fuzz: *Fuzz) void { | ||
| 334 | coverageRunCancelable(fuzz) catch |err| switch (err) { | ||
| 335 | error.Canceled => return, | ||
| 336 | }; | ||
| 337 | } | ||
| 338 | |||
| 339 | fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void { | ||
| 340 | const io = fuzz.io; | ||
| 341 | |||
| 342 | try fuzz.queue_mutex.lock(io); | ||
| 343 | defer fuzz.queue_mutex.unlock(io); | ||
| 344 | |||
| 345 | while (true) { | ||
| 346 | try fuzz.queue_cond.wait(io, &fuzz.queue_mutex); | ||
| 347 | for (fuzz.msg_queue.items) |msg| switch (msg) { | ||
| 348 | .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) { | ||
| 349 | error.AlreadyReported => continue, | ||
| 350 | error.Canceled => return, | ||
| 351 | else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), | ||
| 352 | }, | ||
| 353 | .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) { | ||
| 354 | error.AlreadyReported => continue, | ||
| 355 | error.Canceled => return, | ||
| 356 | else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), | ||
| 357 | }, | ||
| 358 | }; | ||
| 359 | fuzz.msg_queue.clearRetainingCapacity(); | ||
| 360 | } | ||
| 361 | } | ||
| 362 | fn prepareTables(fuzz: *Fuzz, run_step_index: Configuration.Step.Index, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void { | ||
| 363 | if (true) @panic("TODO"); | ||
| 364 | assert(fuzz.mode == .forever); | ||
| 365 | const ws = fuzz.mode.forever.ws; | ||
| 366 | const gpa = fuzz.gpa; | ||
| 367 | const io = fuzz.io; | ||
| 368 | |||
| 369 | try fuzz.coverage_mutex.lock(io); | ||
| 370 | defer fuzz.coverage_mutex.unlock(io); | ||
| 371 | |||
| 372 | const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id); | ||
| 373 | if (gop.found_existing) { | ||
| 374 | // We are fuzzing the same executable with multiple threads. | ||
| 375 | // Perhaps the same unit test; perhaps a different one. In any | ||
| 376 | // case, since the coverage file is the same, we only have to | ||
| 377 | // notice changes to that one file in order to learn coverage for | ||
| 378 | // this particular executable. | ||
| 379 | return; | ||
| 380 | } | ||
| 381 | errdefer _ = fuzz.coverage_files.pop(); | ||
| 382 | |||
| 383 | gop.value_ptr.* = .{ | ||
| 384 | .coverage = std.debug.Coverage.init, | ||
| 385 | .mapped_memory = undefined, // populated below | ||
| 386 | .source_locations = undefined, // populated below | ||
| 387 | .entry_points = .empty, | ||
| 388 | .start_timestamp = ws.now(), | ||
| 389 | .start_n_runs = undefined, // populated below | ||
| 390 | }; | ||
| 391 | errdefer gop.value_ptr.coverage.deinit(gpa); | ||
| 392 | |||
| 393 | const rebuilt_exe_path = run_step_index.rebuilt_executable.?; | ||
| 394 | const target = run_step_index.producer.?.rootModuleTarget(); | ||
| 395 | var debug_info = std.debug.Info.load( | ||
| 396 | gpa, | ||
| 397 | io, | ||
| 398 | rebuilt_exe_path, | ||
| 399 | &gop.value_ptr.coverage, | ||
| 400 | target.ofmt, | ||
| 401 | target.cpu.arch, | ||
| 402 | ) catch |err| { | ||
| 403 | log.err("step '{s}': failed to load debug information for '{f}': {t}", .{ | ||
| 404 | run_step_index.step.name, rebuilt_exe_path, err, | ||
| 405 | }); | ||
| 406 | return error.AlreadyReported; | ||
| 407 | }; | ||
| 408 | defer debug_info.deinit(gpa); | ||
| 409 | |||
| 410 | const coverage_file_path: Build.Cache.Path = .{ | ||
| 411 | .root_dir = run_step_index.step.owner.cache_root, | ||
| 412 | .sub_path = "v/" ++ std.fmt.hex(coverage_id), | ||
| 413 | }; | ||
| 414 | var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { | ||
| 415 | log.err("step '{s}': failed to load coverage file '{f}': {t}", .{ | ||
| 416 | run_step_index.step.name, coverage_file_path, err, | ||
| 417 | }); | ||
| 418 | return error.AlreadyReported; | ||
| 419 | }; | ||
| 420 | defer coverage_file.close(io); | ||
| 421 | |||
| 422 | const file_size = coverage_file.length(io) catch |err| { | ||
| 423 | log.err("unable to check len of coverage file '{f}': {t}", .{ coverage_file_path, err }); | ||
| 424 | return error.AlreadyReported; | ||
| 425 | }; | ||
| 426 | |||
| 427 | const mapped_memory = std.posix.mmap( | ||
| 428 | null, | ||
| 429 | file_size, | ||
| 430 | .{ .READ = true }, | ||
| 431 | .{ .TYPE = .SHARED }, | ||
| 432 | coverage_file.handle, | ||
| 433 | 0, | ||
| 434 | ) catch |err| { | ||
| 435 | log.err("failed to map coverage file '{f}': {t}", .{ coverage_file_path, err }); | ||
| 436 | return error.AlreadyReported; | ||
| 437 | }; | ||
| 438 | gop.value_ptr.mapped_memory = mapped_memory; | ||
| 439 | |||
| 440 | const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); | ||
| 441 | const pcs = header.pcAddrs(); | ||
| 442 | const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len); | ||
| 443 | errdefer gpa.free(source_locations); | ||
| 444 | |||
| 445 | // Unfortunately the PCs array that LLVM gives us from the 8-bit PC | ||
| 446 | // counters feature is not sorted. | ||
| 447 | var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty; | ||
| 448 | defer sorted_pcs.deinit(gpa); | ||
| 449 | try sorted_pcs.resize(gpa, pcs.len); | ||
| 450 | @memcpy(sorted_pcs.items(.pc), pcs); | ||
| 451 | for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i); | ||
| 452 | sorted_pcs.sortUnstable(struct { | ||
| 453 | addrs: []const u64, | ||
| 454 | |||
| 455 | pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { | ||
| 456 | return ctx.addrs[a_index] < ctx.addrs[b_index]; | ||
| 457 | } | ||
| 458 | }{ .addrs = sorted_pcs.items(.pc) }); | ||
| 459 | |||
| 460 | debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| { | ||
| 461 | log.err("failed to resolve addresses to source locations: {t}", .{err}); | ||
| 462 | return error.AlreadyReported; | ||
| 463 | }; | ||
| 464 | |||
| 465 | for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl; | ||
| 466 | gop.value_ptr.source_locations = source_locations; | ||
| 467 | gop.value_ptr.start_n_runs = header.n_runs; | ||
| 468 | |||
| 469 | ws.notifyUpdate(); | ||
| 470 | } | ||
| 471 | |||
| 472 | fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void { | ||
| 473 | const io = fuzz.io; | ||
| 474 | |||
| 475 | try fuzz.coverage_mutex.lock(io); | ||
| 476 | defer fuzz.coverage_mutex.unlock(io); | ||
| 477 | |||
| 478 | const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?; | ||
| 479 | const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); | ||
| 480 | const pcs = header.pcAddrs(); | ||
| 481 | |||
| 482 | // Since this pcs list is unsorted, we must linear scan for the best index. | ||
| 483 | const index = i: { | ||
| 484 | var best: usize = 0; | ||
| 485 | for (pcs[1..], 1..) |elem_addr, i| { | ||
| 486 | if (elem_addr == addr) break :i i; | ||
| 487 | if (elem_addr > addr) continue; | ||
| 488 | if (elem_addr > pcs[best]) best = i; | ||
| 489 | } | ||
| 490 | break :i best; | ||
| 491 | }; | ||
| 492 | if (index >= pcs.len) { | ||
| 493 | log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{ | ||
| 494 | addr, pcs[0], pcs[pcs.len - 1], | ||
| 495 | }); | ||
| 496 | return error.AlreadyReported; | ||
| 497 | } | ||
| 498 | if (false) { | ||
| 499 | const sl = coverage_map.source_locations[index]; | ||
| 500 | const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename); | ||
| 501 | if (pcs.len == 1) { | ||
| 502 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{ | ||
| 503 | addr, file_name, sl.line, sl.column, | ||
| 504 | }); | ||
| 505 | } else if (index == 0) { | ||
| 506 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{ | ||
| 507 | addr, file_name, sl.line, sl.column, pcs[index + 1], | ||
| 508 | }); | ||
| 509 | } else if (index == pcs.len - 1) { | ||
| 510 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{ | ||
| 511 | addr, file_name, sl.line, sl.column, index, pcs[index - 1], | ||
| 512 | }); | ||
| 513 | } else { | ||
| 514 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{ | ||
| 515 | addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1], | ||
| 516 | }); | ||
| 517 | } | ||
| 518 | } | ||
| 519 | try coverage_map.entry_points.append(fuzz.gpa, @intCast(index)); | ||
| 520 | } | ||
| 521 | |||
| 522 | pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void { | ||
| 523 | if (true) @panic("TODO"); | ||
| 524 | assert(fuzz.mode == .limit); | ||
| 525 | const io = fuzz.io; | ||
| 526 | |||
| 527 | try fuzz.group.await(io); | ||
| 528 | fuzz.group = .init; | ||
| 529 | |||
| 530 | std.debug.print("======= FUZZING REPORT =======\n", .{}); | ||
| 531 | for (fuzz.msg_queue.items) |msg| { | ||
| 532 | if (msg != .coverage) continue; | ||
| 533 | |||
| 534 | const cov = msg.coverage; | ||
| 535 | const coverage_file_path: std.Build.Cache.Path = .{ | ||
| 536 | .root_dir = cov.run.step.owner.cache_root, | ||
| 537 | .sub_path = "v/" ++ std.fmt.hex(cov.id), | ||
| 538 | }; | ||
| 539 | var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { | ||
| 540 | fatal("step '{s}': failed to load coverage file '{f}': {t}", .{ | ||
| 541 | cov.run.step.name, coverage_file_path, err, | ||
| 542 | }); | ||
| 543 | }; | ||
| 544 | defer coverage_file.close(io); | ||
| 545 | |||
| 546 | const fuzz_abi = std.Build.abi.fuzz; | ||
| 547 | var rbuf: [0x1000]u8 = undefined; | ||
| 548 | var r = coverage_file.reader(io, &rbuf); | ||
| 549 | |||
| 550 | var header: fuzz_abi.SeenPcsHeader = undefined; | ||
| 551 | r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { | ||
| 552 | fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{ | ||
| 553 | cov.run.step.name, coverage_file_path, err, | ||
| 554 | }); | ||
| 555 | }; | ||
| 556 | |||
| 557 | if (header.pcs_len == 0) { | ||
| 558 | fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{ | ||
| 559 | cov.run.step.name, coverage_file_path, | ||
| 560 | }); | ||
| 561 | } | ||
| 562 | |||
| 563 | var seen_count: usize = 0; | ||
| 564 | const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len); | ||
| 565 | for (0..chunk_count) |_| { | ||
| 566 | const seen = r.interface.takeInt(usize, .little) catch |err| { | ||
| 567 | fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{ | ||
| 568 | cov.run.step.name, coverage_file_path, err, | ||
| 569 | }); | ||
| 570 | }; | ||
| 571 | seen_count += @popCount(seen); | ||
| 572 | } | ||
| 573 | |||
| 574 | const seen_f: f64 = @floatFromInt(seen_count); | ||
| 575 | const total_f: f64 = @floatFromInt(header.pcs_len); | ||
| 576 | const ratio = seen_f / total_f; | ||
| 577 | std.debug.print( | ||
| 578 | \\Step: {s} | ||
| 579 | \\Fuzz test: "{s}" ({x}) | ||
| 580 | \\Runs: {} -> {} | ||
| 581 | \\Unique runs: {} -> {} | ||
| 582 | \\Coverage: {}/{} -> {}/{} ({:.02}%) | ||
| 583 | \\ | ||
| 584 | , .{ | ||
| 585 | cov.run.step.name, | ||
| 586 | cov.run.fuzz_tests.items[0], | ||
| 587 | cov.id, | ||
| 588 | cov.cumulative.runs, | ||
| 589 | header.n_runs, | ||
| 590 | cov.cumulative.unique, | ||
| 591 | header.unique_runs, | ||
| 592 | cov.cumulative.coverage, | ||
| 593 | header.pcs_len, | ||
| 594 | seen_count, | ||
| 595 | header.pcs_len, | ||
| 596 | ratio * 100, | ||
| 597 | }); | ||
| 598 | |||
| 599 | std.debug.print("------------------------------\n", .{}); | ||
| 600 | } | ||
| 601 | std.debug.print( | ||
| 602 | \\Values are accumulated across multiple runs when preserving the cache. | ||
| 603 | \\============================== | ||
| 604 | \\ | ||
| 605 | , .{}); | ||
| 606 | } | ||
lib/compiler/Maker/Graph.zig created+25| ... | @@ -0,0 +1,25 @@ | ||
| 1 | //! Shared maker state among all steps. | ||
| 2 | const Graph = @This(); | ||
| 3 | |||
| 4 | const std = @import("std"); | ||
| 5 | const Io = std.Io; | ||
| 6 | const Allocator = std.mem.Allocator; | ||
| 7 | const Configuration = std.Build.Configuration; | ||
| 8 | |||
| 9 | io: Io, | ||
| 10 | /// Process lifetime. | ||
| 11 | arena: Allocator, | ||
| 12 | cache: std.Build.Cache, | ||
| 13 | zig_exe: []const u8, | ||
| 14 | environ_map: std.process.Environ.Map, | ||
| 15 | global_cache_root: std.Build.Cache.Directory, | ||
| 16 | zig_lib_directory: std.Build.Cache.Directory, | ||
| 17 | |||
| 18 | debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null, | ||
| 19 | incremental: ?bool = null, | ||
| 20 | random_seed: u32 = 0, | ||
| 21 | allow_so_scripts: ?bool = null, | ||
| 22 | time_report: bool = false, | ||
| 23 | /// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also | ||
| 24 | /// respects the '--color' flag. | ||
| 25 | stderr_mode: ?Io.Terminal.Mode = null, | ||
lib/compiler/Maker/Step.zig created+840| ... | @@ -0,0 +1,840 @@ | ||
| 1 | //! The state that maker needs in order to process a step. | ||
| 2 | const Step = @This(); | ||
| 3 | |||
| 4 | const builtin = @import("builtin"); | ||
| 5 | |||
| 6 | const std = @import("std"); | ||
| 7 | const Allocator = std.mem.Allocator; | ||
| 8 | const Cache = std.Build.Cache; | ||
| 9 | const Io = std.Io; | ||
| 10 | const LazyPath = std.Build.Configuration.LazyPath; | ||
| 11 | const Package = std.Build.Configuration.Package; | ||
| 12 | const Path = std.Build.Cache.Path; | ||
| 13 | const Configuration = std.Build.Configuration; | ||
| 14 | const assert = std.debug.assert; | ||
| 15 | |||
| 16 | const WebServer = @import("WebServer.zig"); | ||
| 17 | |||
| 18 | pub const Compile = void; // @import("Step/Compile.zig"); | ||
| 19 | pub const Run = void; // @import("Step/Run.zig"); | ||
| 20 | |||
| 21 | /// Avoid false sharing. | ||
| 22 | _: void align(std.atomic.cache_line) = {}, | ||
| 23 | |||
| 24 | state: State = .precheck_unstarted, | ||
| 25 | dependants: std.ArrayList(Configuration.Step.Index) = .empty, | ||
| 26 | /// Collects the set of files that retrigger this step to run. | ||
| 27 | /// | ||
| 28 | /// This is used by the build system's implementation of `--watch` but it can | ||
| 29 | /// also be potentially useful for IDEs to know what effects editing a | ||
| 30 | /// particular file has. | ||
| 31 | /// | ||
| 32 | /// Populated within `make`. Implementation may choose to clear and repopulate, | ||
| 33 | /// retain previous value, or update. | ||
| 34 | inputs: Inputs = .init, | ||
| 35 | pending_deps: u32 = undefined, | ||
| 36 | |||
| 37 | result_error_msgs: std.ArrayList([]const u8) = .empty, | ||
| 38 | result_error_bundle: std.zig.ErrorBundle = .empty, | ||
| 39 | result_stderr: []const u8 = "", | ||
| 40 | result_cached: bool = false, | ||
| 41 | result_duration_ns: ?u64 = null, | ||
| 42 | /// 0 means unavailable or not reported. | ||
| 43 | result_peak_rss: usize = 0, | ||
| 44 | /// If the step is failed and this field is populated, this is the command which failed. | ||
| 45 | /// This field may be populated even if the step succeeded. | ||
| 46 | result_failed_command: ?[]const u8 = null, | ||
| 47 | test_results: TestResults = .{}, | ||
| 48 | |||
| 49 | pub const State = enum { | ||
| 50 | precheck_unstarted, | ||
| 51 | precheck_started, | ||
| 52 | /// This is also used to indicate "dirty" steps that have been modified | ||
| 53 | /// after a previous build completed, in which case, the step may or may | ||
| 54 | /// not have been completed before. Either way, one or more of its direct | ||
| 55 | /// file system inputs have been modified, meaning that the step needs to | ||
| 56 | /// be re-evaluated. | ||
| 57 | precheck_done, | ||
| 58 | dependency_failure, | ||
| 59 | success, | ||
| 60 | failure, | ||
| 61 | /// This state indicates that the step did not complete, however, it also did not fail, | ||
| 62 | /// and it is safe to continue executing its dependencies. | ||
| 63 | skipped, | ||
| 64 | /// This step was skipped because it specified a max_rss that exceeded the runner's maximum. | ||
| 65 | /// It is not safe to run its dependencies. | ||
| 66 | skipped_oom, | ||
| 67 | }; | ||
| 68 | |||
| 69 | pub const Inputs = struct { | ||
| 70 | table: Table, | ||
| 71 | |||
| 72 | pub const init: Inputs = .{ | ||
| 73 | .table = .{}, | ||
| 74 | }; | ||
| 75 | |||
| 76 | pub const Table = std.ArrayHashMapUnmanaged(Cache.Path, Files, Cache.Path.TableAdapter, false); | ||
| 77 | /// The special file name "." means any changes inside the directory. | ||
| 78 | pub const Files = std.ArrayList([]const u8); | ||
| 79 | |||
| 80 | pub fn populated(inputs: *Inputs) bool { | ||
| 81 | return inputs.table.count() != 0; | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn clear(inputs: *Inputs, gpa: Allocator) void { | ||
| 85 | for (inputs.table.values()) |*files| files.deinit(gpa); | ||
| 86 | inputs.table.clearRetainingCapacity(); | ||
| 87 | } | ||
| 88 | }; | ||
| 89 | |||
| 90 | pub const TestResults = struct { | ||
| 91 | /// The total number of tests in the step. Every test has a "status" from the following: | ||
| 92 | /// * passed | ||
| 93 | /// * skipped | ||
| 94 | /// * failed cleanly | ||
| 95 | /// * crashed | ||
| 96 | /// * timed out | ||
| 97 | test_count: u32 = 0, | ||
| 98 | |||
| 99 | /// The number of tests which were skipped (`error.SkipZigTest`). | ||
| 100 | skip_count: u32 = 0, | ||
| 101 | /// The number of tests which failed cleanly. | ||
| 102 | fail_count: u32 = 0, | ||
| 103 | /// The number of tests which terminated unexpectedly, i.e. crashed. | ||
| 104 | crash_count: u32 = 0, | ||
| 105 | /// The number of tests which timed out. | ||
| 106 | timeout_count: u32 = 0, | ||
| 107 | |||
| 108 | /// The number of detected memory leaks. The associated test may still have passed; indeed, *all* | ||
| 109 | /// individual tests may have passed. However, the step as a whole fails if any test has leaks. | ||
| 110 | leak_count: u32 = 0, | ||
| 111 | /// The number of detected error logs. The associated test may still have passed; indeed, *all* | ||
| 112 | /// individual tests may have passed. However, the step as a whole fails if any test logs errors. | ||
| 113 | log_err_count: u32 = 0, | ||
| 114 | |||
| 115 | pub fn isSuccess(tr: TestResults) bool { | ||
| 116 | // all steps are success or skip | ||
| 117 | return tr.fail_count == 0 and | ||
| 118 | tr.crash_count == 0 and | ||
| 119 | tr.timeout_count == 0 and | ||
| 120 | // no (otherwise successful) step leaked memory or logged errors | ||
| 121 | tr.leak_count == 0 and | ||
| 122 | tr.log_err_count == 0; | ||
| 123 | } | ||
| 124 | |||
| 125 | /// Computes the number of tests which passed from the other values. | ||
| 126 | pub fn passCount(tr: TestResults) u32 { | ||
| 127 | return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count; | ||
| 128 | } | ||
| 129 | }; | ||
| 130 | |||
| 131 | pub const MakeOptions = struct { | ||
| 132 | progress_node: std.Progress.Node, | ||
| 133 | watch: bool, | ||
| 134 | web_server: ?*WebServer, | ||
| 135 | /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds. | ||
| 136 | unit_test_timeout_ns: ?u64, | ||
| 137 | /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`. | ||
| 138 | gpa: Allocator, | ||
| 139 | }; | ||
| 140 | |||
| 141 | pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void; | ||
| 142 | |||
| 143 | /// If the Step's `make` function reports `error.MakeFailed`, it indicates they | ||
| 144 | /// have already reported the error. Otherwise, we add a simple error report | ||
| 145 | /// here. | ||
| 146 | pub fn make(s: *Step, options: MakeOptions) error{ MakeFailed, MakeSkipped }!void { | ||
| 147 | if (true) @panic("TODO Step.make"); | ||
| 148 | const arena = s.owner.allocator; | ||
| 149 | const graph = s.owner.graph; | ||
| 150 | const io = graph.io; | ||
| 151 | |||
| 152 | var start_ts: ?Io.Timestamp = t: { | ||
| 153 | if (!graph.time_report) break :t null; | ||
| 154 | if (s.id == .compile) break :t null; | ||
| 155 | if (s.id == .run and s.cast(Run).?.stdio == .zig_test) break :t null; | ||
| 156 | break :t Io.Clock.awake.now(io); | ||
| 157 | }; | ||
| 158 | const make_result = s.makeFn(s, options); | ||
| 159 | if (start_ts) |*ts| { | ||
| 160 | const duration = ts.untilNow(io, .awake); | ||
| 161 | options.web_server.?.updateTimeReportGeneric(s, duration); | ||
| 162 | } | ||
| 163 | |||
| 164 | make_result catch |err| switch (err) { | ||
| 165 | error.MakeFailed, error.MakeSkipped => |e| return e, | ||
| 166 | else => { | ||
| 167 | s.result_error_msgs.append(arena, @errorName(err)) catch @panic("OOM"); | ||
| 168 | return error.MakeFailed; | ||
| 169 | }, | ||
| 170 | }; | ||
| 171 | |||
| 172 | if (!s.test_results.isSuccess()) { | ||
| 173 | return error.MakeFailed; | ||
| 174 | } | ||
| 175 | |||
| 176 | if (s.max_rss != 0 and s.result_peak_rss > s.max_rss) { | ||
| 177 | const msg = std.fmt.allocPrint(arena, "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", .{ | ||
| 178 | s.result_peak_rss, s.max_rss, | ||
| 179 | }) catch @panic("OOM"); | ||
| 180 | s.result_error_msgs.append(arena, msg) catch @panic("OOM"); | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | /// Implementation detail of file watching. Prepares the step for being re-evaluated. | ||
| 185 | /// Returns `true` if the step was newly invalidated, `false` if it was already invalidated. | ||
| 186 | pub fn invalidateResult(step: *Step, gpa: Allocator) bool { | ||
| 187 | if (true) @panic("TODO Step.invalidateResult"); | ||
| 188 | if (step.state == .precheck_done) return false; | ||
| 189 | assert(step.pending_deps == 0); | ||
| 190 | step.state = .precheck_done; | ||
| 191 | step.reset(gpa); | ||
| 192 | for (step.dependants.items) |dependant| { | ||
| 193 | _ = dependant.invalidateResult(gpa); | ||
| 194 | dependant.pending_deps += 1; | ||
| 195 | } | ||
| 196 | return true; | ||
| 197 | } | ||
| 198 | |||
| 199 | /// Implementation detail of file watching and forced rebuilds. Prepares the step for being re-evaluated. | ||
| 200 | pub fn reset(step: *Step, gpa: Allocator) void { | ||
| 201 | assert(step.state == .precheck_done); | ||
| 202 | |||
| 203 | if (step.result_failed_command) |cmd| gpa.free(cmd); | ||
| 204 | |||
| 205 | step.result_error_msgs.clearRetainingCapacity(); | ||
| 206 | step.result_stderr = ""; | ||
| 207 | step.result_cached = false; | ||
| 208 | step.result_duration_ns = null; | ||
| 209 | step.result_peak_rss = 0; | ||
| 210 | step.result_failed_command = null; | ||
| 211 | step.test_results = .{}; | ||
| 212 | step.clearWatchInputs(); | ||
| 213 | |||
| 214 | step.result_error_bundle.deinit(gpa); | ||
| 215 | step.result_error_bundle = std.zig.ErrorBundle.empty; | ||
| 216 | } | ||
| 217 | |||
| 218 | /// Populates `s.result_failed_command`. | ||
| 219 | pub fn captureChildProcess( | ||
| 220 | s: *Step, | ||
| 221 | gpa: Allocator, | ||
| 222 | progress_node: std.Progress.Node, | ||
| 223 | argv: []const []const u8, | ||
| 224 | ) !std.process.RunResult { | ||
| 225 | const graph = s.owner.graph; | ||
| 226 | const arena = graph.arena; | ||
| 227 | const io = graph.io; | ||
| 228 | |||
| 229 | // If an error occurs, it's happened in this command: | ||
| 230 | assert(s.result_failed_command == null); | ||
| 231 | s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv); | ||
| 232 | |||
| 233 | try handleChildProcUnsupported(s); | ||
| 234 | try handleVerbose(s, .inherit, argv); | ||
| 235 | |||
| 236 | const result = std.process.run(arena, io, .{ | ||
| 237 | .argv = argv, | ||
| 238 | .environ_map = &graph.environ_map, | ||
| 239 | .progress_node = progress_node, | ||
| 240 | }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err }); | ||
| 241 | |||
| 242 | if (result.stderr.len > 0) { | ||
| 243 | try s.result_error_msgs.append(arena, result.stderr); | ||
| 244 | } | ||
| 245 | |||
| 246 | return result; | ||
| 247 | } | ||
| 248 | |||
| 249 | pub fn fail(step: *Step, comptime fmt: []const u8, args: anytype) error{ OutOfMemory, MakeFailed } { | ||
| 250 | try step.addError(fmt, args); | ||
| 251 | return error.MakeFailed; | ||
| 252 | } | ||
| 253 | |||
| 254 | pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { | ||
| 255 | const arena = step.owner.allocator; | ||
| 256 | const msg = try std.fmt.allocPrint(arena, fmt, args); | ||
| 257 | try step.result_error_msgs.append(arena, msg); | ||
| 258 | } | ||
| 259 | |||
| 260 | pub const ZigProcess = struct { | ||
| 261 | child: std.process.Child, | ||
| 262 | multi_reader_buffer: Io.File.MultiReader.Buffer(2), | ||
| 263 | multi_reader: Io.File.MultiReader, | ||
| 264 | progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn, | ||
| 265 | |||
| 266 | pub const StreamEnum = enum { stdout, stderr }; | ||
| 267 | |||
| 268 | pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void { | ||
| 269 | zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null; | ||
| 270 | } | ||
| 271 | |||
| 272 | pub fn deinit(zp: *ZigProcess, io: Io) void { | ||
| 273 | zp.child.kill(io); | ||
| 274 | zp.multi_reader.deinit(); | ||
| 275 | zp.* = undefined; | ||
| 276 | } | ||
| 277 | }; | ||
| 278 | |||
| 279 | /// Assumes that argv contains `--listen=-` and that the process being spawned | ||
| 280 | /// is the zig compiler - the same version that compiled the build runner. | ||
| 281 | /// Populates `s.result_failed_command`. | ||
| 282 | pub fn evalZigProcess( | ||
| 283 | s: *Step, | ||
| 284 | argv: []const []const u8, | ||
| 285 | prog_node: std.Progress.Node, | ||
| 286 | watch: bool, | ||
| 287 | web_server: ?*WebServer, | ||
| 288 | gpa: Allocator, | ||
| 289 | ) !?Cache.Path { | ||
| 290 | const b = s.owner; | ||
| 291 | const io = b.graph.io; | ||
| 292 | |||
| 293 | // If an error occurs, it's happened in this command: | ||
| 294 | assert(s.result_failed_command == null); | ||
| 295 | s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv); | ||
| 296 | |||
| 297 | if (s.getZigProcess()) |zp| update: { | ||
| 298 | assert(watch); | ||
| 299 | if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index); | ||
| 300 | zp.progress_ipc_index = null; | ||
| 301 | var exited = false; | ||
| 302 | defer if (exited) { | ||
| 303 | s.cast(Compile).?.zig_process = null; | ||
| 304 | zp.deinit(io); | ||
| 305 | gpa.destroy(zp); | ||
| 306 | } else zp.saveState(prog_node); | ||
| 307 | const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) { | ||
| 308 | error.BrokenPipe, error.EndOfStream => |reason| { | ||
| 309 | std.log.info("{s} restart required: {t}", .{ argv[0], reason }); | ||
| 310 | // Process restart required. | ||
| 311 | const term = zp.child.wait(io) catch |e| { | ||
| 312 | return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); | ||
| 313 | }; | ||
| 314 | _ = term; | ||
| 315 | exited = true; | ||
| 316 | break :update; | ||
| 317 | }, | ||
| 318 | else => |e| return e, | ||
| 319 | }; | ||
| 320 | |||
| 321 | if (s.result_error_bundle.errorMessageCount() > 0) { | ||
| 322 | return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); | ||
| 323 | } | ||
| 324 | |||
| 325 | if (s.result_error_msgs.items.len > 0 and result == null) { | ||
| 326 | // Crash detected. | ||
| 327 | const term = zp.child.wait(io) catch |e| { | ||
| 328 | return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); | ||
| 329 | }; | ||
| 330 | s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 331 | exited = true; | ||
| 332 | try handleChildProcessTerm(s, term); | ||
| 333 | return error.MakeFailed; | ||
| 334 | } | ||
| 335 | |||
| 336 | return result; | ||
| 337 | } | ||
| 338 | assert(argv.len != 0); | ||
| 339 | |||
| 340 | try handleChildProcUnsupported(s); | ||
| 341 | try handleVerbose(s, .inherit, argv); | ||
| 342 | |||
| 343 | const zp = try gpa.create(ZigProcess); | ||
| 344 | defer if (!watch) gpa.destroy(zp); | ||
| 345 | |||
| 346 | zp.child = std.process.spawn(io, .{ | ||
| 347 | .argv = argv, | ||
| 348 | .environ_map = &b.graph.environ_map, | ||
| 349 | .stdin = .pipe, | ||
| 350 | .stdout = .pipe, | ||
| 351 | .stderr = .pipe, | ||
| 352 | .request_resource_usage_statistics = true, | ||
| 353 | .progress_node = prog_node, | ||
| 354 | }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); | ||
| 355 | |||
| 356 | zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{ | ||
| 357 | zp.child.stdout.?, zp.child.stderr.?, | ||
| 358 | }); | ||
| 359 | if (watch) s.cast(Compile).?.zig_process = zp; | ||
| 360 | defer if (!watch) zp.deinit(io); | ||
| 361 | |||
| 362 | const result = result: { | ||
| 363 | defer if (watch) zp.saveState(prog_node); | ||
| 364 | break :result try zigProcessUpdate(s, zp, watch, web_server, gpa); | ||
| 365 | }; | ||
| 366 | |||
| 367 | if (!watch) { | ||
| 368 | // Send EOF to stdin. | ||
| 369 | zp.child.stdin.?.close(io); | ||
| 370 | zp.child.stdin = null; | ||
| 371 | |||
| 372 | const term = zp.child.wait(io) catch |err| { | ||
| 373 | return s.fail("unable to wait for {s}: {t}", .{ argv[0], err }); | ||
| 374 | }; | ||
| 375 | s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 376 | |||
| 377 | // Special handling for Compile step that is expecting compile errors. | ||
| 378 | if (s.cast(Compile)) |compile| switch (term) { | ||
| 379 | .exited => { | ||
| 380 | // Note that the exit code may be 0 in this case due to the | ||
| 381 | // compiler server protocol. | ||
| 382 | if (compile.expect_errors != null) { | ||
| 383 | return error.NeedCompileErrorCheck; | ||
| 384 | } | ||
| 385 | }, | ||
| 386 | else => {}, | ||
| 387 | }; | ||
| 388 | |||
| 389 | try handleChildProcessTerm(s, term); | ||
| 390 | } | ||
| 391 | |||
| 392 | if (s.result_error_bundle.errorMessageCount() > 0) { | ||
| 393 | return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); | ||
| 394 | } | ||
| 395 | |||
| 396 | return result; | ||
| 397 | } | ||
| 398 | |||
| 399 | /// Wrapper around `Io.Dir.updateFile` that handles verbose and error output. | ||
| 400 | pub fn installFile(s: *Step, src_lazy_path: LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus { | ||
| 401 | const b = s.owner; | ||
| 402 | const io = b.graph.io; | ||
| 403 | const src_path = src_lazy_path.getPath3(b, s); | ||
| 404 | try handleVerbose(s, .inherit, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path }); | ||
| 405 | return Io.Dir.updateFile(src_path.root_dir.handle, io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err| | ||
| 406 | return s.fail("unable to update file from '{f}' to '{s}': {t}", .{ src_path, dest_path, err }); | ||
| 407 | } | ||
| 408 | |||
| 409 | /// Wrapper around `Io.Dir.createDirPathStatus` that handles verbose and error output. | ||
| 410 | pub fn installDir(s: *Step, dest_path: []const u8) !Io.Dir.CreatePathStatus { | ||
| 411 | const b = s.owner; | ||
| 412 | const io = b.graph.io; | ||
| 413 | try handleVerbose(s, .inherit, &.{ "install", "-d", dest_path }); | ||
| 414 | return Io.Dir.cwd().createDirPathStatus(io, dest_path, .default_dir) catch |err| | ||
| 415 | return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err }); | ||
| 416 | } | ||
| 417 | |||
| 418 | fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*WebServer, gpa: Allocator) !?Path { | ||
| 419 | const b = s.owner; | ||
| 420 | const arena = b.allocator; | ||
| 421 | const io = b.graph.io; | ||
| 422 | |||
| 423 | const start_ts = Io.Clock.awake.now(io); | ||
| 424 | |||
| 425 | try sendMessage(io, zp.child.stdin.?, .update); | ||
| 426 | if (!watch) try sendMessage(io, zp.child.stdin.?, .exit); | ||
| 427 | |||
| 428 | var result: ?Path = null; | ||
| 429 | var eos_err: error{EndOfStream}!void = {}; | ||
| 430 | |||
| 431 | const stdout = zp.multi_reader.fileReader(0); | ||
| 432 | |||
| 433 | while (true) { | ||
| 434 | const Header = std.zig.Server.Message.Header; | ||
| 435 | const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) { | ||
| 436 | error.EndOfStream => break, | ||
| 437 | error.ReadFailed => return stdout.err.?, | ||
| 438 | }; | ||
| 439 | const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) { | ||
| 440 | error.EndOfStream => |e| { | ||
| 441 | // Better to report the crash with stderr below, but we set | ||
| 442 | // this in case the child exits successfully while violating | ||
| 443 | // this protocol. | ||
| 444 | eos_err = e; | ||
| 445 | break; | ||
| 446 | }, | ||
| 447 | error.ReadFailed => return stdout.err.?, | ||
| 448 | }; | ||
| 449 | switch (header.tag) { | ||
| 450 | .zig_version => { | ||
| 451 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) { | ||
| 452 | return s.fail( | ||
| 453 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", | ||
| 454 | .{ builtin.zig_version_string, body }, | ||
| 455 | ); | ||
| 456 | } | ||
| 457 | }, | ||
| 458 | .error_bundle => { | ||
| 459 | s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); | ||
| 460 | // This message indicates the end of the update. | ||
| 461 | if (watch) break; | ||
| 462 | }, | ||
| 463 | .emit_digest => { | ||
| 464 | const EmitDigest = std.zig.Server.Message.EmitDigest; | ||
| 465 | const emit_digest: *align(1) const EmitDigest = @ptrCast(body); | ||
| 466 | s.result_cached = emit_digest.flags.cache_hit; | ||
| 467 | const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; | ||
| 468 | result = .{ | ||
| 469 | .root_dir = b.cache_root, | ||
| 470 | .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)), | ||
| 471 | }; | ||
| 472 | }, | ||
| 473 | .file_system_inputs => { | ||
| 474 | s.clearWatchInputs(); | ||
| 475 | var it = std.mem.splitScalar(u8, body, 0); | ||
| 476 | while (it.next()) |prefixed_path| { | ||
| 477 | const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1); | ||
| 478 | const sub_path = try arena.dupe(u8, prefixed_path[1..]); | ||
| 479 | const sub_path_dirname = std.fs.path.dirname(sub_path) orelse ""; | ||
| 480 | switch (prefix_index) { | ||
| 481 | .cwd => { | ||
| 482 | const path: Cache.Path = .{ | ||
| 483 | .root_dir = Cache.Directory.cwd(), | ||
| 484 | .sub_path = sub_path_dirname, | ||
| 485 | }; | ||
| 486 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 487 | }, | ||
| 488 | .zig_lib => zl: { | ||
| 489 | if (s.cast(Step.Compile)) |compile| { | ||
| 490 | if (compile.zig_lib_dir) |zig_lib_dir| { | ||
| 491 | const lp = try zig_lib_dir.join(arena, sub_path); | ||
| 492 | try addWatchInput(s, lp); | ||
| 493 | break :zl; | ||
| 494 | } | ||
| 495 | } | ||
| 496 | const path: Cache.Path = .{ | ||
| 497 | .root_dir = s.owner.graph.zig_lib_directory, | ||
| 498 | .sub_path = sub_path_dirname, | ||
| 499 | }; | ||
| 500 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 501 | }, | ||
| 502 | .local_cache => { | ||
| 503 | const path: Cache.Path = .{ | ||
| 504 | .root_dir = b.cache_root, | ||
| 505 | .sub_path = sub_path_dirname, | ||
| 506 | }; | ||
| 507 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 508 | }, | ||
| 509 | .global_cache => { | ||
| 510 | const path: Cache.Path = .{ | ||
| 511 | .root_dir = s.owner.graph.global_cache_root, | ||
| 512 | .sub_path = sub_path_dirname, | ||
| 513 | }; | ||
| 514 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 515 | }, | ||
| 516 | } | ||
| 517 | } | ||
| 518 | }, | ||
| 519 | .time_report => if (web_server) |ws| { | ||
| 520 | const TimeReport = std.zig.Server.Message.TimeReport; | ||
| 521 | const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]); | ||
| 522 | ws.updateTimeReportCompile(.{ | ||
| 523 | .compile = s.cast(Step.Compile).?, | ||
| 524 | .use_llvm = tr.flags.use_llvm, | ||
| 525 | .stats = tr.stats, | ||
| 526 | .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()), | ||
| 527 | .llvm_pass_timings_len = tr.llvm_pass_timings_len, | ||
| 528 | .files_len = tr.files_len, | ||
| 529 | .decls_len = tr.decls_len, | ||
| 530 | .trailing = body[@sizeOf(TimeReport)..], | ||
| 531 | }); | ||
| 532 | }, | ||
| 533 | else => {}, // ignore other messages | ||
| 534 | } | ||
| 535 | } | ||
| 536 | |||
| 537 | s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()); | ||
| 538 | |||
| 539 | const stderr_contents = zp.multi_reader.reader(1).buffered(); | ||
| 540 | if (stderr_contents.len > 0) { | ||
| 541 | try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents)); | ||
| 542 | } | ||
| 543 | |||
| 544 | try eos_err; | ||
| 545 | |||
| 546 | return result; | ||
| 547 | } | ||
| 548 | |||
| 549 | pub fn getZigProcess(s: *Step) ?*ZigProcess { | ||
| 550 | if (true) @panic("TODO getZigProcess"); | ||
| 551 | return switch (s.id) { | ||
| 552 | .compile => s.cast(Compile).?.zig_process, | ||
| 553 | else => null, | ||
| 554 | }; | ||
| 555 | } | ||
| 556 | |||
| 557 | fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { | ||
| 558 | const header: std.zig.Client.Message.Header = .{ | ||
| 559 | .tag = tag, | ||
| 560 | .bytes_len = 0, | ||
| 561 | }; | ||
| 562 | var w = file.writer(io, &.{}); | ||
| 563 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 564 | error.WriteFailed => return w.err.?, | ||
| 565 | }; | ||
| 566 | } | ||
| 567 | |||
| 568 | pub fn handleVerbose( | ||
| 569 | s: *Step, | ||
| 570 | arena: Allocator, | ||
| 571 | cwd: std.process.Child.Cwd, | ||
| 572 | opt_env: ?*const std.process.Environ.Map, | ||
| 573 | argv: []const []const u8, | ||
| 574 | ) error{OutOfMemory}!void { | ||
| 575 | if (!s.verbose) return; | ||
| 576 | const graph = s.graph; | ||
| 577 | // Intention of verbose is to print all sub-process command lines to | ||
| 578 | // stderr before spawning them. | ||
| 579 | const text = try allocPrintCmd(arena, cwd, if (opt_env) |env| .{ | ||
| 580 | .child = env, | ||
| 581 | .parent = &graph.environ_map, | ||
| 582 | } else null, argv); | ||
| 583 | std.log.scoped(.verbose).info("{s}", .{text}); | ||
| 584 | } | ||
| 585 | |||
| 586 | /// Asserts that the caller has already populated `s.result_failed_command`. | ||
| 587 | pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void { | ||
| 588 | if (!std.process.can_spawn) { | ||
| 589 | return s.fail("unable to spawn process: host cannot spawn child processes", .{}); | ||
| 590 | } | ||
| 591 | } | ||
| 592 | |||
| 593 | /// Asserts that the caller has already populated `s.result_failed_command`. | ||
| 594 | pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void { | ||
| 595 | assert(s.result_failed_command != null); | ||
| 596 | return switch (term) { | ||
| 597 | .exited => |code| if (code != 0) s.fail("process exited with error code {d}", .{code}), | ||
| 598 | .signal => |sig| s.fail("process terminated with signal {t}", .{sig}), | ||
| 599 | .stopped => |sig| s.fail("process stopped with signal {t}", .{sig}), | ||
| 600 | .unknown => s.fail("process terminated unexpectedly", .{}), | ||
| 601 | }; | ||
| 602 | } | ||
| 603 | |||
| 604 | /// Prefer `cacheHitAndWatch` unless you already added watch inputs | ||
| 605 | /// separately from using the cache system. | ||
| 606 | pub fn cacheHit(s: *Step, man: *Cache.Manifest) !bool { | ||
| 607 | s.result_cached = man.hit() catch |err| return failWithCacheError(s, man, err); | ||
| 608 | return s.result_cached; | ||
| 609 | } | ||
| 610 | |||
| 611 | /// Clears previous watch inputs, if any, and then populates watch inputs from | ||
| 612 | /// the full set of files picked up by the cache manifest. | ||
| 613 | /// | ||
| 614 | /// Must be accompanied with `writeManifestAndWatch`. | ||
| 615 | pub fn cacheHitAndWatch(s: *Step, man: *Cache.Manifest) !bool { | ||
| 616 | const is_hit = man.hit() catch |err| return failWithCacheError(s, man, err); | ||
| 617 | s.result_cached = is_hit; | ||
| 618 | // The above call to hit() populates the manifest with files, so in case of | ||
| 619 | // a hit, we need to populate watch inputs. | ||
| 620 | if (is_hit) try setWatchInputsFromManifest(s, man); | ||
| 621 | return is_hit; | ||
| 622 | } | ||
| 623 | |||
| 624 | fn failWithCacheError( | ||
| 625 | s: *Step, | ||
| 626 | man: *const Cache.Manifest, | ||
| 627 | err: Cache.Manifest.HitError, | ||
| 628 | ) error{ OutOfMemory, Canceled, MakeFailed } { | ||
| 629 | switch (err) { | ||
| 630 | error.CacheCheckFailed => switch (man.diagnostic) { | ||
| 631 | .none => unreachable, | ||
| 632 | .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {t} {t}", .{ | ||
| 633 | man.diagnostic, e, | ||
| 634 | }), | ||
| 635 | .file_open, .file_stat, .file_read, .file_hash => |op| { | ||
| 636 | const pp = man.files.keys()[op.file_index].prefixed_path; | ||
| 637 | const prefix = man.cache.prefixes()[pp.prefix].path orelse ""; | ||
| 638 | return s.fail("failed to check cache: '{s}{c}{s}' {t} {t}", .{ | ||
| 639 | prefix, std.fs.path.sep, pp.sub_path, man.diagnostic, op.err, | ||
| 640 | }); | ||
| 641 | }, | ||
| 642 | }, | ||
| 643 | error.OutOfMemory, error.Canceled => |e| return e, | ||
| 644 | error.InvalidFormat => return s.fail("failed to check cache: invalid manifest file format", .{}), | ||
| 645 | } | ||
| 646 | } | ||
| 647 | |||
| 648 | /// Prefer `writeManifestAndWatch` unless you already added watch inputs | ||
| 649 | /// separately from using the cache system. | ||
| 650 | pub fn writeManifest(s: *Step, man: *Cache.Manifest) !void { | ||
| 651 | if (s.test_results.isSuccess()) { | ||
| 652 | man.writeManifest() catch |err| { | ||
| 653 | try s.addError("unable to write cache manifest: {t}", .{err}); | ||
| 654 | }; | ||
| 655 | } | ||
| 656 | } | ||
| 657 | |||
| 658 | /// Clears previous watch inputs, if any, and then populates watch inputs from | ||
| 659 | /// the full set of files picked up by the cache manifest. | ||
| 660 | /// | ||
| 661 | /// Must be accompanied with `cacheHitAndWatch`. | ||
| 662 | pub fn writeManifestAndWatch(s: *Step, man: *Cache.Manifest) !void { | ||
| 663 | try writeManifest(s, man); | ||
| 664 | try setWatchInputsFromManifest(s, man); | ||
| 665 | } | ||
| 666 | |||
| 667 | fn setWatchInputsFromManifest(s: *Step, man: *Cache.Manifest) !void { | ||
| 668 | const arena = s.owner.allocator; | ||
| 669 | const prefixes = man.cache.prefixes(); | ||
| 670 | clearWatchInputs(s); | ||
| 671 | for (man.files.keys()) |file| { | ||
| 672 | // The file path data is freed when the cache manifest is cleaned up at the end of `make`. | ||
| 673 | const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path); | ||
| 674 | try addWatchInputFromPath(s, .{ | ||
| 675 | .root_dir = prefixes[file.prefixed_path.prefix], | ||
| 676 | .sub_path = std.fs.path.dirname(sub_path) orelse "", | ||
| 677 | }, std.fs.path.basename(sub_path)); | ||
| 678 | } | ||
| 679 | } | ||
| 680 | |||
| 681 | /// For steps that have a single input that never changes when re-running `make`. | ||
| 682 | pub fn singleUnchangingWatchInput(step: *Step, lazy_path: LazyPath) Allocator.Error!void { | ||
| 683 | if (!step.inputs.populated()) try step.addWatchInput(lazy_path); | ||
| 684 | } | ||
| 685 | |||
| 686 | pub fn clearWatchInputs(step: *Step) void { | ||
| 687 | const gpa = step.owner.allocator; | ||
| 688 | step.inputs.clear(gpa); | ||
| 689 | } | ||
| 690 | |||
| 691 | /// Places a *file* dependency on the path. | ||
| 692 | pub fn addWatchInput(step: *Step, lazy_file: LazyPath) Allocator.Error!void { | ||
| 693 | switch (lazy_file) { | ||
| 694 | .src_path => |src_path| try addWatchInputFromBuilder(step, src_path.owner, src_path.sub_path), | ||
| 695 | .dependency => |d| try addWatchInputFromBuilder(step, d.dependency.builder, d.sub_path), | ||
| 696 | .cwd_relative => |path_string| { | ||
| 697 | try addWatchInputFromPath(step, .{ | ||
| 698 | .root_dir = .{ | ||
| 699 | .path = null, | ||
| 700 | .handle = Io.Dir.cwd(), | ||
| 701 | }, | ||
| 702 | .sub_path = std.fs.path.dirname(path_string) orelse "", | ||
| 703 | }, std.fs.path.basename(path_string)); | ||
| 704 | }, | ||
| 705 | // Nothing to watch because this dependency edge is modeled instead via `dependants`. | ||
| 706 | .generated => {}, | ||
| 707 | } | ||
| 708 | } | ||
| 709 | |||
| 710 | /// Any changes inside the directory will trigger invalidation. | ||
| 711 | /// | ||
| 712 | /// See also `addDirectoryWatchInputFromPath` which takes a `Cache.Path` instead. | ||
| 713 | /// | ||
| 714 | /// Paths derived from this directory should also be manually added via | ||
| 715 | /// `addDirectoryWatchInputFromPath` if and only if this function returns | ||
| 716 | /// `true`. | ||
| 717 | pub fn addDirectoryWatchInput(step: *Step, lazy_directory: LazyPath) Allocator.Error!bool { | ||
| 718 | switch (lazy_directory) { | ||
| 719 | .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path), | ||
| 720 | .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path), | ||
| 721 | .cwd_relative => |path_string| { | ||
| 722 | try addDirectoryWatchInputFromPath(step, .{ | ||
| 723 | .root_dir = .{ | ||
| 724 | .path = null, | ||
| 725 | .handle = Io.Dir.cwd(), | ||
| 726 | }, | ||
| 727 | .sub_path = path_string, | ||
| 728 | }); | ||
| 729 | }, | ||
| 730 | // Nothing to watch because this dependency edge is modeled instead via `dependants`. | ||
| 731 | .generated => return false, | ||
| 732 | } | ||
| 733 | return true; | ||
| 734 | } | ||
| 735 | |||
| 736 | /// Any changes inside the directory will trigger invalidation. | ||
| 737 | /// | ||
| 738 | /// See also `addDirectoryWatchInput` which takes a `LazyPath` instead. | ||
| 739 | /// | ||
| 740 | /// This function should only be called when it has been verified that the | ||
| 741 | /// dependency on `path` is not already accounted for by a `Step` dependency. | ||
| 742 | /// In other words, before calling this function, first check that the | ||
| 743 | /// `LazyPath` which this `path` is derived from is not `generated`. | ||
| 744 | pub fn addDirectoryWatchInputFromPath(step: *Step, path: Cache.Path) !void { | ||
| 745 | return addWatchInputFromPath(step, path, "."); | ||
| 746 | } | ||
| 747 | |||
| 748 | fn addWatchInputFromBuilder(step: *Step, package: Package, sub_path: []const u8) !void { | ||
| 749 | return addWatchInputFromPath(step, .{ | ||
| 750 | .root_dir = package.build_root, | ||
| 751 | .sub_path = std.fs.path.dirname(sub_path) orelse "", | ||
| 752 | }, std.fs.path.basename(sub_path)); | ||
| 753 | } | ||
| 754 | |||
| 755 | fn addDirectoryWatchInputFromBuilder(step: *Step, package: Package, sub_path: []const u8) !void { | ||
| 756 | return addDirectoryWatchInputFromPath(step, .{ | ||
| 757 | .root_dir = package.build_root, | ||
| 758 | .sub_path = sub_path, | ||
| 759 | }); | ||
| 760 | } | ||
| 761 | |||
| 762 | fn addWatchInputFromPath(step: *Step, path: Cache.Path, basename: []const u8) !void { | ||
| 763 | const gpa = step.owner.allocator; | ||
| 764 | const gop = try step.inputs.table.getOrPut(gpa, path); | ||
| 765 | if (!gop.found_existing) gop.value_ptr.* = .empty; | ||
| 766 | try gop.value_ptr.append(gpa, basename); | ||
| 767 | } | ||
| 768 | |||
| 769 | pub fn allocPrintCmd( | ||
| 770 | gpa: Allocator, | ||
| 771 | cwd: std.process.Child.Cwd, | ||
| 772 | opt_env: ?struct { | ||
| 773 | child: *const std.process.Environ.Map, | ||
| 774 | parent: *const std.process.Environ.Map, | ||
| 775 | }, | ||
| 776 | argv: []const []const u8, | ||
| 777 | ) Allocator.Error![]u8 { | ||
| 778 | const shell = struct { | ||
| 779 | fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void { | ||
| 780 | for (string) |c| { | ||
| 781 | if (switch (c) { | ||
| 782 | else => true, | ||
| 783 | '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false, | ||
| 784 | '=' => is_argv0, | ||
| 785 | }) break; | ||
| 786 | } else return writer.writeAll(string); | ||
| 787 | |||
| 788 | try writer.writeByte('"'); | ||
| 789 | for (string) |c| { | ||
| 790 | if (switch (c) { | ||
| 791 | std.ascii.control_code.nul => break, | ||
| 792 | '!', '"', '$', '\\', '`' => true, | ||
| 793 | else => !std.ascii.isPrint(c), | ||
| 794 | }) try writer.writeByte('\\'); | ||
| 795 | switch (c) { | ||
| 796 | std.ascii.control_code.nul => unreachable, | ||
| 797 | std.ascii.control_code.bel => try writer.writeByte('a'), | ||
| 798 | std.ascii.control_code.bs => try writer.writeByte('b'), | ||
| 799 | std.ascii.control_code.ht => try writer.writeByte('t'), | ||
| 800 | std.ascii.control_code.lf => try writer.writeByte('n'), | ||
| 801 | std.ascii.control_code.vt => try writer.writeByte('v'), | ||
| 802 | std.ascii.control_code.ff => try writer.writeByte('f'), | ||
| 803 | std.ascii.control_code.cr => try writer.writeByte('r'), | ||
| 804 | std.ascii.control_code.esc => try writer.writeByte('E'), | ||
| 805 | ' '...'~' => try writer.writeByte(c), | ||
| 806 | else => try writer.print("{o:0>3}", .{c}), | ||
| 807 | } | ||
| 808 | } | ||
| 809 | try writer.writeByte('"'); | ||
| 810 | } | ||
| 811 | }; | ||
| 812 | |||
| 813 | var aw: Io.Writer.Allocating = .init(gpa); | ||
| 814 | defer aw.deinit(); | ||
| 815 | const writer = &aw.writer; | ||
| 816 | switch (cwd) { | ||
| 817 | .inherit => {}, | ||
| 818 | .path => |path| writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory, | ||
| 819 | .dir => @panic("TODO"), | ||
| 820 | } | ||
| 821 | if (opt_env) |env| { | ||
| 822 | var it = env.child.iterator(); | ||
| 823 | while (it.next()) |entry| { | ||
| 824 | const key = entry.key_ptr.*; | ||
| 825 | const value = entry.value_ptr.*; | ||
| 826 | if (env.parent.get(key)) |process_value| { | ||
| 827 | if (std.mem.eql(u8, value, process_value)) continue; | ||
| 828 | } | ||
| 829 | writer.print("{s}=", .{key}) catch return error.OutOfMemory; | ||
| 830 | shell.escape(writer, value, false) catch return error.OutOfMemory; | ||
| 831 | writer.writeByte(' ') catch return error.OutOfMemory; | ||
| 832 | } | ||
| 833 | } | ||
| 834 | shell.escape(writer, argv[0], true) catch return error.OutOfMemory; | ||
| 835 | for (argv[1..]) |arg| { | ||
| 836 | writer.writeByte(' ') catch return error.OutOfMemory; | ||
| 837 | shell.escape(writer, arg, false) catch return error.OutOfMemory; | ||
| 838 | } | ||
| 839 | return aw.toOwnedSlice(); | ||
| 840 | } | ||
lib/compiler/Maker/Step/Compile.zig created+1200| ... | @@ -0,0 +1,1200 @@ | ||
| 1 | /// Populated during the make phase when there is a long-lived compiler process. | ||
| 2 | /// Managed by the build runner, not user build script. | ||
| 3 | zig_process: ?*Step.ZigProcess, | ||
| 4 | |||
| 5 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 6 | const b = step.owner; | ||
| 7 | const compile: *Compile = @fieldParentPtr("step", step); | ||
| 8 | |||
| 9 | const zig_args = try getZigArgs(compile, false); | ||
| 10 | |||
| 11 | const maybe_output_dir = step.evalZigProcess( | ||
| 12 | zig_args, | ||
| 13 | options.progress_node, | ||
| 14 | (b.graph.incremental == true) and (options.watch or options.web_server != null), | ||
| 15 | options.web_server, | ||
| 16 | options.gpa, | ||
| 17 | ) catch |err| switch (err) { | ||
| 18 | error.NeedCompileErrorCheck => { | ||
| 19 | assert(compile.expect_errors != null); | ||
| 20 | try checkCompileErrors(compile); | ||
| 21 | return; | ||
| 22 | }, | ||
| 23 | else => |e| return e, | ||
| 24 | }; | ||
| 25 | |||
| 26 | // Update generated files | ||
| 27 | if (maybe_output_dir) |output_dir| { | ||
| 28 | if (compile.emit_directory) |lp| { | ||
| 29 | lp.path = b.fmt("{f}", .{output_dir}); | ||
| 30 | } | ||
| 31 | |||
| 32 | // zig fmt: off | ||
| 33 | if (compile.generated_bin) |lp| lp.path = compile.outputPath(output_dir, .bin); | ||
| 34 | if (compile.generated_pdb) |lp| lp.path = compile.outputPath(output_dir, .pdb); | ||
| 35 | // hack for stage2_x86_64 + coff | ||
| 36 | if (compile.generated_compiler_rt_dyn_lib) |lp| lp.path = compile.outputPath(output_dir, .compiler_rt_dyn_lib); | ||
| 37 | if (compile.generated_implib) |lp| lp.path = compile.outputPath(output_dir, .implib); | ||
| 38 | if (compile.generated_h) |lp| lp.path = compile.outputPath(output_dir, .h); | ||
| 39 | if (compile.generated_docs) |lp| lp.path = compile.outputPath(output_dir, .docs); | ||
| 40 | if (compile.generated_asm) |lp| lp.path = compile.outputPath(output_dir, .@"asm"); | ||
| 41 | if (compile.generated_llvm_ir) |lp| lp.path = compile.outputPath(output_dir, .llvm_ir); | ||
| 42 | if (compile.generated_llvm_bc) |lp| lp.path = compile.outputPath(output_dir, .llvm_bc); | ||
| 43 | // zig fmt: on | ||
| 44 | } | ||
| 45 | |||
| 46 | if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic and | ||
| 47 | compile.version != null and compile.generated_bin != null and | ||
| 48 | std.Build.wantSharedLibSymLinks(compile.rootModuleTarget())) | ||
| 49 | { | ||
| 50 | try doAtomicSymLinks( | ||
| 51 | step, | ||
| 52 | compile.getEmittedBin().getPath2(b, step), | ||
| 53 | compile.major_only_filename.?, | ||
| 54 | compile.name_only_filename.?, | ||
| 55 | ); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { | ||
| 60 | const step = &compile.step; | ||
| 61 | const b = step.owner; | ||
| 62 | const arena = b.allocator; | ||
| 63 | |||
| 64 | var zig_args = std.array_list.Managed([]const u8).init(arena); | ||
| 65 | defer zig_args.deinit(); | ||
| 66 | |||
| 67 | try zig_args.append(b.graph.zig_exe); | ||
| 68 | |||
| 69 | const cmd = switch (compile.kind) { | ||
| 70 | .lib => "build-lib", | ||
| 71 | .exe => "build-exe", | ||
| 72 | .obj => "build-obj", | ||
| 73 | .@"test" => "test", | ||
| 74 | .test_obj => "test-obj", | ||
| 75 | }; | ||
| 76 | try zig_args.append(cmd); | ||
| 77 | |||
| 78 | if (b.reference_trace) |some| { | ||
| 79 | try zig_args.append(try std.fmt.allocPrint(arena, "-freference-trace={d}", .{some})); | ||
| 80 | } | ||
| 81 | try addFlag(&zig_args, "allow-so-scripts", compile.allow_so_scripts orelse b.graph.allow_so_scripts); | ||
| 82 | |||
| 83 | try addFlag(&zig_args, "llvm", compile.use_llvm); | ||
| 84 | try addFlag(&zig_args, "lld", compile.use_lld); | ||
| 85 | try addFlag(&zig_args, "new-linker", compile.use_new_linker); | ||
| 86 | |||
| 87 | if (compile.root_module.resolved_target.?.query.ofmt) |ofmt| { | ||
| 88 | try zig_args.append(try std.fmt.allocPrint(arena, "-ofmt={s}", .{@tagName(ofmt)})); | ||
| 89 | } | ||
| 90 | |||
| 91 | switch (compile.entry) { | ||
| 92 | .default => {}, | ||
| 93 | .disabled => try zig_args.append("-fno-entry"), | ||
| 94 | .enabled => try zig_args.append("-fentry"), | ||
| 95 | .symbol_name => |entry_name| { | ||
| 96 | try zig_args.append(try std.fmt.allocPrint(arena, "-fentry={s}", .{entry_name})); | ||
| 97 | }, | ||
| 98 | } | ||
| 99 | |||
| 100 | { | ||
| 101 | for (compile.force_undefined_symbols.keys()) |symbol_name| { | ||
| 102 | try zig_args.append("--force_undefined"); | ||
| 103 | try zig_args.append(symbol_name.*); | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | if (compile.stack_size) |stack_size| { | ||
| 108 | try zig_args.append("--stack"); | ||
| 109 | try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size})); | ||
| 110 | } | ||
| 111 | |||
| 112 | if (fuzz) { | ||
| 113 | try zig_args.append("-ffuzz"); | ||
| 114 | } | ||
| 115 | |||
| 116 | { | ||
| 117 | // Stores system libraries that have already been seen for at least one | ||
| 118 | // module, along with any arguments that need to be passed to the | ||
| 119 | // compiler for each module individually. | ||
| 120 | var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty; | ||
| 121 | var frameworks: std.StringArrayHashMapUnmanaged(Module.LinkFrameworkOptions) = .empty; | ||
| 122 | |||
| 123 | var prev_has_cflags = false; | ||
| 124 | var prev_has_rcflags = false; | ||
| 125 | var prev_search_strategy: Module.SystemLib.SearchStrategy = .paths_first; | ||
| 126 | var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic; | ||
| 127 | // Track the number of positional arguments so that a nice error can be | ||
| 128 | // emitted if there is nothing to link. | ||
| 129 | var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null); | ||
| 130 | |||
| 131 | // Fully recursive iteration including dynamic libraries to detect | ||
| 132 | // libc and libc++ linkage. | ||
| 133 | for (compile.getCompileDependencies(true)) |some_compile| { | ||
| 134 | for (some_compile.root_module.getGraph().modules) |mod| { | ||
| 135 | if (mod.link_libc == true) compile.is_linking_libc = true; | ||
| 136 | if (mod.link_libcpp == true) compile.is_linking_libcpp = true; | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | var cli_named_modules = try CliNamedModules.init(arena, compile.root_module); | ||
| 141 | |||
| 142 | // For this loop, don't chase dynamic libraries because their link | ||
| 143 | // objects are already linked. | ||
| 144 | for (compile.getCompileDependencies(false)) |dep_compile| { | ||
| 145 | for (dep_compile.root_module.getGraph().modules) |mod| { | ||
| 146 | // While walking transitive dependencies, if a given link object is | ||
| 147 | // already included in a library, it should not redundantly be | ||
| 148 | // placed on the linker line of the dependee. | ||
| 149 | const my_responsibility = dep_compile == compile; | ||
| 150 | const already_linked = !my_responsibility and dep_compile.isDynamicLibrary(); | ||
| 151 | |||
| 152 | // Inherit dependencies on darwin frameworks. | ||
| 153 | if (!already_linked) { | ||
| 154 | for (mod.frameworks.keys(), mod.frameworks.values()) |name, info| { | ||
| 155 | try frameworks.put(arena, name, info); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | // Inherit dependencies on system libraries and static libraries. | ||
| 160 | for (mod.link_objects.items) |link_object| { | ||
| 161 | switch (link_object) { | ||
| 162 | .static_path => |static_path| { | ||
| 163 | if (my_responsibility) { | ||
| 164 | try zig_args.append(static_path.getPath2(mod.owner, step)); | ||
| 165 | total_linker_objects += 1; | ||
| 166 | } | ||
| 167 | }, | ||
| 168 | .system_lib => |system_lib| { | ||
| 169 | const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name); | ||
| 170 | if (system_lib_gop.found_existing) { | ||
| 171 | try zig_args.appendSlice(system_lib_gop.value_ptr.*); | ||
| 172 | continue; | ||
| 173 | } else { | ||
| 174 | system_lib_gop.value_ptr.* = &.{}; | ||
| 175 | } | ||
| 176 | |||
| 177 | if (already_linked) | ||
| 178 | continue; | ||
| 179 | |||
| 180 | if ((system_lib.search_strategy != prev_search_strategy or | ||
| 181 | system_lib.preferred_link_mode != prev_preferred_link_mode) and | ||
| 182 | compile.linkage != .static) | ||
| 183 | { | ||
| 184 | switch (system_lib.search_strategy) { | ||
| 185 | .no_fallback => switch (system_lib.preferred_link_mode) { | ||
| 186 | .dynamic => try zig_args.append("-search_dylibs_only"), | ||
| 187 | .static => try zig_args.append("-search_static_only"), | ||
| 188 | }, | ||
| 189 | .paths_first => switch (system_lib.preferred_link_mode) { | ||
| 190 | .dynamic => try zig_args.append("-search_paths_first"), | ||
| 191 | .static => try zig_args.append("-search_paths_first_static"), | ||
| 192 | }, | ||
| 193 | .mode_first => switch (system_lib.preferred_link_mode) { | ||
| 194 | .dynamic => try zig_args.append("-search_dylibs_first"), | ||
| 195 | .static => try zig_args.append("-search_static_first"), | ||
| 196 | }, | ||
| 197 | } | ||
| 198 | prev_search_strategy = system_lib.search_strategy; | ||
| 199 | prev_preferred_link_mode = system_lib.preferred_link_mode; | ||
| 200 | } | ||
| 201 | |||
| 202 | const prefix: []const u8 = prefix: { | ||
| 203 | if (system_lib.needed) break :prefix "-needed-l"; | ||
| 204 | if (system_lib.weak) break :prefix "-weak-l"; | ||
| 205 | break :prefix "-l"; | ||
| 206 | }; | ||
| 207 | switch (system_lib.use_pkg_config) { | ||
| 208 | .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })), | ||
| 209 | .yes, .force => { | ||
| 210 | if (compile.runPkgConfig(system_lib.name)) |result| { | ||
| 211 | try zig_args.appendSlice(result.cflags); | ||
| 212 | try zig_args.appendSlice(result.libs); | ||
| 213 | try seen_system_libs.put(arena, system_lib.name, result.cflags); | ||
| 214 | } else |err| switch (err) { | ||
| 215 | error.PkgConfigInvalidOutput, | ||
| 216 | error.PkgConfigCrashed, | ||
| 217 | error.PkgConfigFailed, | ||
| 218 | error.PkgConfigNotInstalled, | ||
| 219 | error.PackageNotFound, | ||
| 220 | => switch (system_lib.use_pkg_config) { | ||
| 221 | .yes => { | ||
| 222 | // pkg-config failed, so fall back to linking the library | ||
| 223 | // by name directly. | ||
| 224 | try zig_args.append(b.fmt("{s}{s}", .{ | ||
| 225 | prefix, | ||
| 226 | system_lib.name, | ||
| 227 | })); | ||
| 228 | }, | ||
| 229 | .force => { | ||
| 230 | panic("pkg-config failed for library {s}", .{system_lib.name}); | ||
| 231 | }, | ||
| 232 | .no => unreachable, | ||
| 233 | }, | ||
| 234 | |||
| 235 | else => |e| return e, | ||
| 236 | } | ||
| 237 | }, | ||
| 238 | } | ||
| 239 | }, | ||
| 240 | .other_step => |other| { | ||
| 241 | switch (other.kind) { | ||
| 242 | .exe => return step.fail("cannot link with an executable build artifact", .{}), | ||
| 243 | .@"test" => return step.fail("cannot link with a test", .{}), | ||
| 244 | .obj, .test_obj => { | ||
| 245 | const included_in_lib_or_obj = !my_responsibility and | ||
| 246 | (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj); | ||
| 247 | if (!already_linked and !included_in_lib_or_obj) { | ||
| 248 | try zig_args.append(other.getEmittedBin().getPath2(b, step)); | ||
| 249 | total_linker_objects += 1; | ||
| 250 | } | ||
| 251 | }, | ||
| 252 | .lib => l: { | ||
| 253 | const other_produces_implib = other.producesImplib(); | ||
| 254 | const other_is_static = other_produces_implib or other.isStaticLibrary(); | ||
| 255 | |||
| 256 | if (compile.isStaticLibrary() and other_is_static) { | ||
| 257 | // Avoid putting a static library inside a static library. | ||
| 258 | break :l; | ||
| 259 | } | ||
| 260 | |||
| 261 | // For DLLs, we must link against the implib. | ||
| 262 | // For everything else, we directly link | ||
| 263 | // against the library file. | ||
| 264 | const full_path_lib = if (other_produces_implib) | ||
| 265 | try other.getGeneratedFilePath("generated_implib", &compile.step) | ||
| 266 | else | ||
| 267 | try other.getGeneratedFilePath("generated_bin", &compile.step); | ||
| 268 | |||
| 269 | try zig_args.append(full_path_lib); | ||
| 270 | total_linker_objects += 1; | ||
| 271 | |||
| 272 | if (other.linkage == .dynamic and | ||
| 273 | compile.rootModuleTarget().os.tag != .windows) | ||
| 274 | { | ||
| 275 | if (fs.path.dirname(full_path_lib)) |dirname| { | ||
| 276 | try zig_args.append("-rpath"); | ||
| 277 | try zig_args.append(dirname); | ||
| 278 | } | ||
| 279 | } | ||
| 280 | }, | ||
| 281 | } | ||
| 282 | }, | ||
| 283 | .assembly_file => |asm_file| l: { | ||
| 284 | if (!my_responsibility) break :l; | ||
| 285 | |||
| 286 | if (prev_has_cflags) { | ||
| 287 | try zig_args.append("-cflags"); | ||
| 288 | try zig_args.append("--"); | ||
| 289 | prev_has_cflags = false; | ||
| 290 | } | ||
| 291 | try zig_args.append(asm_file.getPath2(mod.owner, step)); | ||
| 292 | total_linker_objects += 1; | ||
| 293 | }, | ||
| 294 | |||
| 295 | .c_source_file => |c_source_file| l: { | ||
| 296 | if (!my_responsibility) break :l; | ||
| 297 | |||
| 298 | if (prev_has_cflags or c_source_file.flags.len != 0) { | ||
| 299 | try zig_args.append("-cflags"); | ||
| 300 | for (c_source_file.flags) |arg| { | ||
| 301 | try zig_args.append(arg); | ||
| 302 | } | ||
| 303 | try zig_args.append("--"); | ||
| 304 | } | ||
| 305 | prev_has_cflags = (c_source_file.flags.len != 0); | ||
| 306 | |||
| 307 | if (c_source_file.language) |lang| { | ||
| 308 | try zig_args.append("-x"); | ||
| 309 | try zig_args.append(lang.internalIdentifier()); | ||
| 310 | } | ||
| 311 | |||
| 312 | try zig_args.append(c_source_file.file.getPath2(mod.owner, step)); | ||
| 313 | |||
| 314 | if (c_source_file.language != null) { | ||
| 315 | try zig_args.append("-x"); | ||
| 316 | try zig_args.append("none"); | ||
| 317 | } | ||
| 318 | total_linker_objects += 1; | ||
| 319 | }, | ||
| 320 | |||
| 321 | .c_source_files => |c_source_files| l: { | ||
| 322 | if (!my_responsibility) break :l; | ||
| 323 | |||
| 324 | if (prev_has_cflags or c_source_files.flags.len != 0) { | ||
| 325 | try zig_args.append("-cflags"); | ||
| 326 | for (c_source_files.flags) |arg| { | ||
| 327 | try zig_args.append(arg); | ||
| 328 | } | ||
| 329 | try zig_args.append("--"); | ||
| 330 | } | ||
| 331 | prev_has_cflags = (c_source_files.flags.len != 0); | ||
| 332 | |||
| 333 | if (c_source_files.language) |lang| { | ||
| 334 | try zig_args.append("-x"); | ||
| 335 | try zig_args.append(lang.internalIdentifier()); | ||
| 336 | } | ||
| 337 | |||
| 338 | const root_path = c_source_files.root.getPath2(mod.owner, step); | ||
| 339 | for (c_source_files.files) |file| { | ||
| 340 | try zig_args.append(b.pathJoin(&.{ root_path, file })); | ||
| 341 | } | ||
| 342 | |||
| 343 | if (c_source_files.language != null) { | ||
| 344 | try zig_args.append("-x"); | ||
| 345 | try zig_args.append("none"); | ||
| 346 | } | ||
| 347 | |||
| 348 | total_linker_objects += c_source_files.files.len; | ||
| 349 | }, | ||
| 350 | |||
| 351 | .win32_resource_file => |rc_source_file| l: { | ||
| 352 | if (!my_responsibility) break :l; | ||
| 353 | |||
| 354 | if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) { | ||
| 355 | if (prev_has_rcflags) { | ||
| 356 | try zig_args.append("-rcflags"); | ||
| 357 | try zig_args.append("--"); | ||
| 358 | prev_has_rcflags = false; | ||
| 359 | } | ||
| 360 | } else { | ||
| 361 | try zig_args.append("-rcflags"); | ||
| 362 | for (rc_source_file.flags) |arg| { | ||
| 363 | try zig_args.append(arg); | ||
| 364 | } | ||
| 365 | for (rc_source_file.include_paths) |include_path| { | ||
| 366 | try zig_args.append("/I"); | ||
| 367 | try zig_args.append(include_path.getPath2(mod.owner, step)); | ||
| 368 | } | ||
| 369 | try zig_args.append("--"); | ||
| 370 | prev_has_rcflags = true; | ||
| 371 | } | ||
| 372 | try zig_args.append(rc_source_file.file.getPath2(mod.owner, step)); | ||
| 373 | total_linker_objects += 1; | ||
| 374 | }, | ||
| 375 | } | ||
| 376 | } | ||
| 377 | |||
| 378 | // We need to emit the --mod argument here so that the above link objects | ||
| 379 | // have the correct parent module, but only if the module is part of | ||
| 380 | // this compilation. | ||
| 381 | if (!my_responsibility) continue; | ||
| 382 | if (cli_named_modules.modules.getIndex(mod)) |module_cli_index| { | ||
| 383 | const module_cli_name = cli_named_modules.names.keys()[module_cli_index]; | ||
| 384 | try mod.appendZigProcessFlags(&zig_args, step); | ||
| 385 | |||
| 386 | // --dep arguments | ||
| 387 | try zig_args.ensureUnusedCapacity(mod.import_table.count() * 2); | ||
| 388 | for (mod.import_table.keys(), mod.import_table.values()) |name, import| { | ||
| 389 | const import_index = cli_named_modules.modules.getIndex(import).?; | ||
| 390 | const import_cli_name = cli_named_modules.names.keys()[import_index]; | ||
| 391 | zig_args.appendAssumeCapacity("--dep"); | ||
| 392 | if (std.mem.eql(u8, import_cli_name, name)) { | ||
| 393 | zig_args.appendAssumeCapacity(import_cli_name); | ||
| 394 | } else { | ||
| 395 | zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name })); | ||
| 396 | } | ||
| 397 | } | ||
| 398 | |||
| 399 | // When the CLI sees a -M argument, it determines whether it | ||
| 400 | // implies the existence of a Zig compilation unit based on | ||
| 401 | // whether there is a root source file. If there is no root | ||
| 402 | // source file, then this is not a zig compilation unit - it is | ||
| 403 | // perhaps a set of linker objects, or C source files instead. | ||
| 404 | // Linker objects are added to the CLI globally, while C source | ||
| 405 | // files must have a module parent. | ||
| 406 | if (mod.root_source_file) |lp| { | ||
| 407 | const src = lp.getPath2(mod.owner, step); | ||
| 408 | try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src })); | ||
| 409 | } else if (moduleNeedsCliArg(mod)) { | ||
| 410 | try zig_args.append(b.fmt("-M{s}", .{module_cli_name})); | ||
| 411 | } | ||
| 412 | } | ||
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | if (total_linker_objects == 0) { | ||
| 417 | return step.fail("the linker needs one or more objects to link", .{}); | ||
| 418 | } | ||
| 419 | |||
| 420 | for (frameworks.keys(), frameworks.values()) |name, info| { | ||
| 421 | if (info.needed) { | ||
| 422 | try zig_args.append("-needed_framework"); | ||
| 423 | } else if (info.weak) { | ||
| 424 | try zig_args.append("-weak_framework"); | ||
| 425 | } else { | ||
| 426 | try zig_args.append("-framework"); | ||
| 427 | } | ||
| 428 | try zig_args.append(name); | ||
| 429 | } | ||
| 430 | |||
| 431 | if (compile.is_linking_libcpp) { | ||
| 432 | try zig_args.append("-lc++"); | ||
| 433 | } | ||
| 434 | |||
| 435 | if (compile.is_linking_libc) { | ||
| 436 | try zig_args.append("-lc"); | ||
| 437 | } | ||
| 438 | } | ||
| 439 | |||
| 440 | if (compile.win32_manifest) |manifest_file| { | ||
| 441 | try zig_args.append(manifest_file.getPath2(b, step)); | ||
| 442 | } | ||
| 443 | |||
| 444 | if (compile.win32_module_definition) |module_file| { | ||
| 445 | try zig_args.append(module_file.getPath2(b, step)); | ||
| 446 | } | ||
| 447 | |||
| 448 | if (compile.image_base) |image_base| { | ||
| 449 | try zig_args.append("--image-base"); | ||
| 450 | try zig_args.append(b.fmt("0x{x}", .{image_base})); | ||
| 451 | } | ||
| 452 | |||
| 453 | for (compile.filters) |filter| { | ||
| 454 | try zig_args.append("--test-filter"); | ||
| 455 | try zig_args.append(filter); | ||
| 456 | } | ||
| 457 | |||
| 458 | if (compile.test_runner) |test_runner| { | ||
| 459 | try zig_args.append("--test-runner"); | ||
| 460 | try zig_args.append(test_runner.path.getPath2(b, step)); | ||
| 461 | } | ||
| 462 | |||
| 463 | for (b.debug_log_scopes) |log_scope| { | ||
| 464 | try zig_args.append("--debug-log"); | ||
| 465 | try zig_args.append(log_scope); | ||
| 466 | } | ||
| 467 | |||
| 468 | if (b.debug_compile_errors) { | ||
| 469 | try zig_args.append("--debug-compile-errors"); | ||
| 470 | } | ||
| 471 | |||
| 472 | if (b.debug_incremental) { | ||
| 473 | try zig_args.append("--debug-incremental"); | ||
| 474 | } | ||
| 475 | |||
| 476 | if (b.verbose_air) try zig_args.append("--verbose-air"); | ||
| 477 | if (b.verbose_llvm_ir) |path| try zig_args.append(b.fmt("--verbose-llvm-ir={s}", .{path})); | ||
| 478 | if (b.verbose_llvm_bc) |path| try zig_args.append(b.fmt("--verbose-llvm-bc={s}", .{path})); | ||
| 479 | if (b.verbose_link or compile.verbose_link) try zig_args.append("--verbose-link"); | ||
| 480 | if (b.verbose_cc or compile.verbose_cc) try zig_args.append("--verbose-cc"); | ||
| 481 | if (b.verbose_llvm_cpu_features) try zig_args.append("--verbose-llvm-cpu-features"); | ||
| 482 | if (b.graph.time_report) try zig_args.append("--time-report"); | ||
| 483 | |||
| 484 | if (compile.generated_asm != null) try zig_args.append("-femit-asm"); | ||
| 485 | if (compile.generated_bin == null) try zig_args.append("-fno-emit-bin"); | ||
| 486 | if (compile.generated_docs != null) try zig_args.append("-femit-docs"); | ||
| 487 | if (compile.generated_implib != null) try zig_args.append("-femit-implib"); | ||
| 488 | if (compile.generated_llvm_bc != null) try zig_args.append("-femit-llvm-bc"); | ||
| 489 | if (compile.generated_llvm_ir != null) try zig_args.append("-femit-llvm-ir"); | ||
| 490 | if (compile.generated_h != null) try zig_args.append("-femit-h"); | ||
| 491 | |||
| 492 | try addFlag(&zig_args, "formatted-panics", compile.formatted_panics); | ||
| 493 | |||
| 494 | switch (compile.compress_debug_sections) { | ||
| 495 | .none => {}, | ||
| 496 | .zlib => try zig_args.append("--compress-debug-sections=zlib"), | ||
| 497 | .zstd => try zig_args.append("--compress-debug-sections=zstd"), | ||
| 498 | } | ||
| 499 | |||
| 500 | if (compile.link_eh_frame_hdr) { | ||
| 501 | try zig_args.append("--eh-frame-hdr"); | ||
| 502 | } | ||
| 503 | if (compile.link_emit_relocs) { | ||
| 504 | try zig_args.append("--emit-relocs"); | ||
| 505 | } | ||
| 506 | if (compile.link_function_sections) { | ||
| 507 | try zig_args.append("-ffunction-sections"); | ||
| 508 | } | ||
| 509 | if (compile.link_data_sections) { | ||
| 510 | try zig_args.append("-fdata-sections"); | ||
| 511 | } | ||
| 512 | if (compile.link_gc_sections) |x| { | ||
| 513 | try zig_args.append(if (x) "--gc-sections" else "--no-gc-sections"); | ||
| 514 | } | ||
| 515 | if (!compile.linker_dynamicbase) { | ||
| 516 | try zig_args.append("--no-dynamicbase"); | ||
| 517 | } | ||
| 518 | if (compile.linker_allow_shlib_undefined) |x| { | ||
| 519 | try zig_args.append(if (x) "-fallow-shlib-undefined" else "-fno-allow-shlib-undefined"); | ||
| 520 | } | ||
| 521 | if (compile.link_z_notext) { | ||
| 522 | try zig_args.append("-z"); | ||
| 523 | try zig_args.append("notext"); | ||
| 524 | } | ||
| 525 | if (!compile.link_z_relro) { | ||
| 526 | try zig_args.append("-z"); | ||
| 527 | try zig_args.append("norelro"); | ||
| 528 | } | ||
| 529 | if (compile.link_z_lazy) { | ||
| 530 | try zig_args.append("-z"); | ||
| 531 | try zig_args.append("lazy"); | ||
| 532 | } | ||
| 533 | if (compile.link_z_common_page_size) |size| { | ||
| 534 | try zig_args.append("-z"); | ||
| 535 | try zig_args.append(b.fmt("common-page-size={d}", .{size})); | ||
| 536 | } | ||
| 537 | if (compile.link_z_max_page_size) |size| { | ||
| 538 | try zig_args.append("-z"); | ||
| 539 | try zig_args.append(b.fmt("max-page-size={d}", .{size})); | ||
| 540 | } | ||
| 541 | if (compile.link_z_defs) { | ||
| 542 | try zig_args.append("-z"); | ||
| 543 | try zig_args.append("defs"); | ||
| 544 | } | ||
| 545 | |||
| 546 | if (compile.libc_file) |libc_file| { | ||
| 547 | try zig_args.append("--libc"); | ||
| 548 | try zig_args.append(libc_file.getPath2(b, step)); | ||
| 549 | } else if (b.libc_file) |libc_file| { | ||
| 550 | try zig_args.append("--libc"); | ||
| 551 | try zig_args.append(libc_file); | ||
| 552 | } | ||
| 553 | |||
| 554 | try zig_args.append("--cache-dir"); | ||
| 555 | try zig_args.append(b.cache_root.path orelse "."); | ||
| 556 | |||
| 557 | try zig_args.append("--global-cache-dir"); | ||
| 558 | try zig_args.append(b.graph.global_cache_root.path orelse "."); | ||
| 559 | |||
| 560 | if (b.graph.debug_compiler_runtime_libs) |mode| | ||
| 561 | try zig_args.append(b.fmt("--debug-rt={t}", .{mode})); | ||
| 562 | |||
| 563 | try zig_args.append("--name"); | ||
| 564 | try zig_args.append(compile.name); | ||
| 565 | |||
| 566 | if (compile.linkage) |some| switch (some) { | ||
| 567 | .dynamic => try zig_args.append("-dynamic"), | ||
| 568 | .static => try zig_args.append("-static"), | ||
| 569 | }; | ||
| 570 | if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) { | ||
| 571 | if (compile.version) |version| { | ||
| 572 | try zig_args.append("--version"); | ||
| 573 | try zig_args.append(b.fmt("{f}", .{version})); | ||
| 574 | } | ||
| 575 | |||
| 576 | if (compile.rootModuleTarget().os.tag.isDarwin()) { | ||
| 577 | const install_name = compile.install_name orelse b.fmt("@rpath/{s}{s}{s}", .{ | ||
| 578 | compile.rootModuleTarget().libPrefix(), | ||
| 579 | compile.name, | ||
| 580 | compile.rootModuleTarget().dynamicLibSuffix(), | ||
| 581 | }); | ||
| 582 | try zig_args.append("-install_name"); | ||
| 583 | try zig_args.append(install_name); | ||
| 584 | } | ||
| 585 | } | ||
| 586 | |||
| 587 | if (compile.entitlements) |entitlements| { | ||
| 588 | try zig_args.appendSlice(&[_][]const u8{ "--entitlements", entitlements }); | ||
| 589 | } | ||
| 590 | if (compile.pagezero_size) |pagezero_size| { | ||
| 591 | const size = try std.fmt.allocPrint(arena, "{x}", .{pagezero_size}); | ||
| 592 | try zig_args.appendSlice(&[_][]const u8{ "-pagezero_size", size }); | ||
| 593 | } | ||
| 594 | if (compile.headerpad_size) |headerpad_size| { | ||
| 595 | const size = try std.fmt.allocPrint(arena, "{x}", .{headerpad_size}); | ||
| 596 | try zig_args.appendSlice(&[_][]const u8{ "-headerpad", size }); | ||
| 597 | } | ||
| 598 | if (compile.headerpad_max_install_names) { | ||
| 599 | try zig_args.append("-headerpad_max_install_names"); | ||
| 600 | } | ||
| 601 | if (compile.dead_strip_dylibs) { | ||
| 602 | try zig_args.append("-dead_strip_dylibs"); | ||
| 603 | } | ||
| 604 | if (compile.force_load_objc) { | ||
| 605 | try zig_args.append("-ObjC"); | ||
| 606 | } | ||
| 607 | if (compile.discard_local_symbols) { | ||
| 608 | try zig_args.append("--discard-all"); | ||
| 609 | } | ||
| 610 | |||
| 611 | try addFlag(&zig_args, "compiler-rt", compile.bundle_compiler_rt); | ||
| 612 | try addFlag(&zig_args, "ubsan-rt", compile.bundle_ubsan_rt); | ||
| 613 | try addFlag(&zig_args, "dll-export-fns", compile.dll_export_fns); | ||
| 614 | if (compile.rdynamic) { | ||
| 615 | try zig_args.append("-rdynamic"); | ||
| 616 | } | ||
| 617 | if (compile.import_memory) { | ||
| 618 | try zig_args.append("--import-memory"); | ||
| 619 | } | ||
| 620 | if (compile.export_memory) { | ||
| 621 | try zig_args.append("--export-memory"); | ||
| 622 | } | ||
| 623 | if (compile.import_symbols) { | ||
| 624 | try zig_args.append("--import-symbols"); | ||
| 625 | } | ||
| 626 | if (compile.import_table) { | ||
| 627 | try zig_args.append("--import-table"); | ||
| 628 | } | ||
| 629 | if (compile.export_table) { | ||
| 630 | try zig_args.append("--export-table"); | ||
| 631 | } | ||
| 632 | if (compile.initial_memory) |initial_memory| { | ||
| 633 | try zig_args.append(b.fmt("--initial-memory={d}", .{initial_memory})); | ||
| 634 | } | ||
| 635 | if (compile.max_memory) |max_memory| { | ||
| 636 | try zig_args.append(b.fmt("--max-memory={d}", .{max_memory})); | ||
| 637 | } | ||
| 638 | if (compile.shared_memory) { | ||
| 639 | try zig_args.append("--shared-memory"); | ||
| 640 | } | ||
| 641 | if (compile.global_base) |global_base| { | ||
| 642 | try zig_args.append(b.fmt("--global-base={d}", .{global_base})); | ||
| 643 | } | ||
| 644 | |||
| 645 | if (compile.wasi_exec_model) |model| { | ||
| 646 | try zig_args.append(b.fmt("-mexec-model={s}", .{@tagName(model)})); | ||
| 647 | } | ||
| 648 | if (compile.linker_script) |linker_script| { | ||
| 649 | try zig_args.append("--script"); | ||
| 650 | try zig_args.append(linker_script.getPath2(b, step)); | ||
| 651 | } | ||
| 652 | |||
| 653 | if (compile.version_script) |version_script| { | ||
| 654 | try zig_args.append("--version-script"); | ||
| 655 | try zig_args.append(version_script.getPath2(b, step)); | ||
| 656 | } | ||
| 657 | if (compile.linker_allow_undefined_version) |x| { | ||
| 658 | try zig_args.append(if (x) "--undefined-version" else "--no-undefined-version"); | ||
| 659 | } | ||
| 660 | |||
| 661 | if (compile.linker_enable_new_dtags) |enabled| { | ||
| 662 | try zig_args.append(if (enabled) "--enable-new-dtags" else "--disable-new-dtags"); | ||
| 663 | } | ||
| 664 | |||
| 665 | if (compile.kind == .@"test") { | ||
| 666 | if (compile.exec_cmd_args) |exec_cmd_args| { | ||
| 667 | for (exec_cmd_args) |cmd_arg| { | ||
| 668 | if (cmd_arg) |arg| { | ||
| 669 | try zig_args.append("--test-cmd"); | ||
| 670 | try zig_args.append(arg); | ||
| 671 | } else { | ||
| 672 | try zig_args.append("--test-cmd-bin"); | ||
| 673 | } | ||
| 674 | } | ||
| 675 | } | ||
| 676 | } | ||
| 677 | |||
| 678 | if (b.sysroot) |sysroot| { | ||
| 679 | try zig_args.appendSlice(&[_][]const u8{ "--sysroot", sysroot }); | ||
| 680 | } | ||
| 681 | |||
| 682 | // -I and -L arguments that appear after the last --mod argument apply to all modules. | ||
| 683 | const cwd: Io.Dir = .cwd(); | ||
| 684 | const io = b.graph.io; | ||
| 685 | |||
| 686 | for (b.search_prefixes.items) |search_prefix| { | ||
| 687 | var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| { | ||
| 688 | return step.fail("unable to open prefix directory '{s}': {s}", .{ | ||
| 689 | search_prefix, @errorName(err), | ||
| 690 | }); | ||
| 691 | }; | ||
| 692 | defer prefix_dir.close(io); | ||
| 693 | |||
| 694 | // Avoid passing -L and -I flags for nonexistent directories. | ||
| 695 | // This prevents a warning, that should probably be upgraded to an error in Zig's | ||
| 696 | // CLI parsing code, when the linker sees an -L directory that does not exist. | ||
| 697 | |||
| 698 | if (prefix_dir.access(io, "lib", .{})) |_| { | ||
| 699 | try zig_args.appendSlice(&.{ | ||
| 700 | "-L", b.pathJoin(&.{ search_prefix, "lib" }), | ||
| 701 | }); | ||
| 702 | } else |err| switch (err) { | ||
| 703 | error.FileNotFound => {}, | ||
| 704 | else => |e| return step.fail("unable to access '{s}/lib' directory: {s}", .{ | ||
| 705 | search_prefix, @errorName(e), | ||
| 706 | }), | ||
| 707 | } | ||
| 708 | |||
| 709 | if (prefix_dir.access(io, "include", .{})) |_| { | ||
| 710 | try zig_args.appendSlice(&.{ | ||
| 711 | "-I", b.pathJoin(&.{ search_prefix, "include" }), | ||
| 712 | }); | ||
| 713 | } else |err| switch (err) { | ||
| 714 | error.FileNotFound => {}, | ||
| 715 | else => |e| return step.fail("unable to access '{s}/include' directory: {s}", .{ | ||
| 716 | search_prefix, @errorName(e), | ||
| 717 | }), | ||
| 718 | } | ||
| 719 | } | ||
| 720 | |||
| 721 | if (compile.rc_includes != .any) { | ||
| 722 | try zig_args.append("-rcincludes"); | ||
| 723 | try zig_args.append(@tagName(compile.rc_includes)); | ||
| 724 | } | ||
| 725 | |||
| 726 | try addFlag(&zig_args, "each-lib-rpath", compile.each_lib_rpath); | ||
| 727 | |||
| 728 | if (compile.build_id orelse b.build_id) |build_id| { | ||
| 729 | try zig_args.append(switch (build_id) { | ||
| 730 | .hexstring => |hs| b.fmt("--build-id=0x{x}", .{hs.toSlice()}), | ||
| 731 | .none, .fast, .uuid, .sha1, .md5 => b.fmt("--build-id={s}", .{@tagName(build_id)}), | ||
| 732 | }); | ||
| 733 | } | ||
| 734 | |||
| 735 | const opt_zig_lib_dir = if (compile.zig_lib_dir) |dir| | ||
| 736 | dir.getPath2(b, step) | ||
| 737 | else if (b.graph.zig_lib_directory.path) |_| | ||
| 738 | b.fmt("{f}", .{b.graph.zig_lib_directory}) | ||
| 739 | else | ||
| 740 | null; | ||
| 741 | |||
| 742 | if (opt_zig_lib_dir) |zig_lib_dir| { | ||
| 743 | try zig_args.append("--zig-lib-dir"); | ||
| 744 | try zig_args.append(zig_lib_dir); | ||
| 745 | } | ||
| 746 | |||
| 747 | try addFlag(&zig_args, "PIE", compile.pie); | ||
| 748 | |||
| 749 | if (compile.lto) |lto| { | ||
| 750 | try zig_args.append(switch (lto) { | ||
| 751 | .full => "-flto=full", | ||
| 752 | .thin => "-flto=thin", | ||
| 753 | .none => "-fno-lto", | ||
| 754 | }); | ||
| 755 | } | ||
| 756 | |||
| 757 | try addFlag(&zig_args, "sanitize-coverage-trace-pc-guard", compile.sanitize_coverage_trace_pc_guard); | ||
| 758 | |||
| 759 | if (compile.subsystem) |subsystem| { | ||
| 760 | try zig_args.append("--subsystem"); | ||
| 761 | try zig_args.append(@tagName(subsystem)); | ||
| 762 | } | ||
| 763 | |||
| 764 | if (compile.mingw_unicode_entry_point) { | ||
| 765 | try zig_args.append("-municode"); | ||
| 766 | } | ||
| 767 | |||
| 768 | if (compile.error_limit) |err_limit| try zig_args.appendSlice(&.{ | ||
| 769 | "--error-limit", b.fmt("{d}", .{err_limit}), | ||
| 770 | }); | ||
| 771 | |||
| 772 | try addFlag(&zig_args, "incremental", b.graph.incremental); | ||
| 773 | |||
| 774 | try zig_args.append("--listen=-"); | ||
| 775 | |||
| 776 | // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux | ||
| 777 | // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and | ||
| 778 | // pass that to zig, e.g. via 'zig build-lib @args.rsp' | ||
| 779 | // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html | ||
| 780 | var args_length: usize = 0; | ||
| 781 | for (zig_args.items) |arg| { | ||
| 782 | args_length += arg.len + 1; // +1 to account for null terminator | ||
| 783 | } | ||
| 784 | if (args_length >= 30 * 1024) { | ||
| 785 | try b.cache_root.handle.createDirPath(io, "args"); | ||
| 786 | |||
| 787 | const args_to_escape = zig_args.items[2..]; | ||
| 788 | var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len); | ||
| 789 | arg_blk: for (args_to_escape) |arg| { | ||
| 790 | for (arg, 0..) |c, arg_idx| { | ||
| 791 | if (c == '\\' or c == '"') { | ||
| 792 | // Slow path for arguments that need to be escaped. We'll need to allocate and copy | ||
| 793 | var escaped: std.ArrayList(u8) = .empty; | ||
| 794 | try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1); | ||
| 795 | try escaped.appendSlice(arena, arg[0..arg_idx]); | ||
| 796 | for (arg[arg_idx..]) |to_escape| { | ||
| 797 | if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\'); | ||
| 798 | try escaped.append(arena, to_escape); | ||
| 799 | } | ||
| 800 | escaped_args.appendAssumeCapacity(escaped.items); | ||
| 801 | continue :arg_blk; | ||
| 802 | } | ||
| 803 | } | ||
| 804 | escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument | ||
| 805 | } | ||
| 806 | |||
| 807 | // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with | ||
| 808 | // other zig build commands running in parallel. | ||
| 809 | const partially_quoted = try std.mem.join(arena, "\" \"", escaped_args.items); | ||
| 810 | const args = try std.mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" }); | ||
| 811 | |||
| 812 | var args_hash: [Sha256.digest_length]u8 = undefined; | ||
| 813 | Sha256.hash(args, &args_hash, .{}); | ||
| 814 | var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined; | ||
| 815 | _ = try std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash}); | ||
| 816 | |||
| 817 | const args_file = "args" ++ fs.path.sep_str ++ args_hex_hash; | ||
| 818 | if (b.cache_root.handle.access(io, args_file, .{})) |_| { | ||
| 819 | // The args file is already present from a previous run. | ||
| 820 | } else |err| switch (err) { | ||
| 821 | error.FileNotFound => { | ||
| 822 | var af = b.cache_root.handle.createFileAtomic(io, args_file, .{ | ||
| 823 | .replace = false, | ||
| 824 | .make_path = true, | ||
| 825 | }) catch |e| return step.fail("failed creating tmp args file {f}{s}: {t}", .{ | ||
| 826 | b.cache_root, args_file, e, | ||
| 827 | }); | ||
| 828 | defer af.deinit(io); | ||
| 829 | |||
| 830 | af.file.writeStreamingAll(io, args) catch |e| { | ||
| 831 | return step.fail("failed writing args data to tmp file {f}{s}: {t}", .{ | ||
| 832 | b.cache_root, args_file, e, | ||
| 833 | }); | ||
| 834 | }; | ||
| 835 | // Note we can't clean up this file, not even after build | ||
| 836 | // success, because that might interfere with another build | ||
| 837 | // process that needs the same file. | ||
| 838 | af.link(io) catch |e| switch (e) { | ||
| 839 | error.PathAlreadyExists => { | ||
| 840 | // The args file was created by another concurrent build process. | ||
| 841 | }, | ||
| 842 | else => |other_err| return step.fail("failed linking tmp file {f}{s}: {t}", .{ | ||
| 843 | b.cache_root, args_file, other_err, | ||
| 844 | }), | ||
| 845 | }; | ||
| 846 | }, | ||
| 847 | else => |other_err| return other_err, | ||
| 848 | } | ||
| 849 | |||
| 850 | const resolved_args_file = try mem.concat(arena, u8, &.{ | ||
| 851 | "@", | ||
| 852 | try b.cache_root.join(arena, &.{args_file}), | ||
| 853 | }); | ||
| 854 | |||
| 855 | zig_args.shrinkRetainingCapacity(2); | ||
| 856 | try zig_args.append(resolved_args_file); | ||
| 857 | } | ||
| 858 | |||
| 859 | return try zig_args.toOwnedSlice(); | ||
| 860 | } | ||
| 861 | |||
| 862 | pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progress.Node) !Path { | ||
| 863 | c.step.result_error_msgs.clearRetainingCapacity(); | ||
| 864 | c.step.result_stderr = ""; | ||
| 865 | |||
| 866 | c.step.result_error_bundle.deinit(gpa); | ||
| 867 | c.step.result_error_bundle = std.zig.ErrorBundle.empty; | ||
| 868 | |||
| 869 | if (c.step.result_failed_command) |cmd| { | ||
| 870 | gpa.free(cmd); | ||
| 871 | c.step.result_failed_command = null; | ||
| 872 | } | ||
| 873 | |||
| 874 | const zig_args = try getZigArgs(c, true); | ||
| 875 | const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa); | ||
| 876 | return maybe_output_bin_path.?; | ||
| 877 | } | ||
| 878 | |||
| 879 | pub fn doAtomicSymLinks( | ||
| 880 | step: *Step, | ||
| 881 | output_path: []const u8, | ||
| 882 | filename_major_only: []const u8, | ||
| 883 | filename_name_only: []const u8, | ||
| 884 | ) !void { | ||
| 885 | const b = step.owner; | ||
| 886 | const io = b.graph.io; | ||
| 887 | const out_dir = fs.path.dirname(output_path) orelse "."; | ||
| 888 | const out_basename = fs.path.basename(output_path); | ||
| 889 | // sym link for libfoo.so.1 to libfoo.so.1.2.3 | ||
| 890 | const major_only_path = b.pathJoin(&.{ out_dir, filename_major_only }); | ||
| 891 | const cwd: Io.Dir = .cwd(); | ||
| 892 | cwd.symLinkAtomic(io, out_basename, major_only_path, .{}) catch |err| { | ||
| 893 | return step.fail("unable to symlink {s} -> {s}: {s}", .{ | ||
| 894 | major_only_path, out_basename, @errorName(err), | ||
| 895 | }); | ||
| 896 | }; | ||
| 897 | // sym link for libfoo.so to libfoo.so.1 | ||
| 898 | const name_only_path = b.pathJoin(&.{ out_dir, filename_name_only }); | ||
| 899 | cwd.symLinkAtomic(io, filename_major_only, name_only_path, .{}) catch |err| { | ||
| 900 | return step.fail("Unable to symlink {s} -> {s}: {s}", .{ | ||
| 901 | name_only_path, filename_major_only, @errorName(err), | ||
| 902 | }); | ||
| 903 | }; | ||
| 904 | } | ||
| 905 | |||
| 906 | fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg { | ||
| 907 | const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config"; | ||
| 908 | const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore); | ||
| 909 | var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator); | ||
| 910 | errdefer list.deinit(); | ||
| 911 | var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); | ||
| 912 | while (line_it.next()) |line| { | ||
| 913 | if (mem.trim(u8, line, " \t").len == 0) continue; | ||
| 914 | var tok_it = mem.tokenizeAny(u8, line, " \t"); | ||
| 915 | try list.append(PkgConfigPkg{ | ||
| 916 | .name = tok_it.next() orelse return error.PkgConfigInvalidOutput, | ||
| 917 | .desc = tok_it.rest(), | ||
| 918 | }); | ||
| 919 | } | ||
| 920 | return list.toOwnedSlice(); | ||
| 921 | } | ||
| 922 | |||
| 923 | fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg { | ||
| 924 | if (b.pkg_config_pkg_list) |res| { | ||
| 925 | return res; | ||
| 926 | } | ||
| 927 | var code: u8 = undefined; | ||
| 928 | if (execPkgConfigList(b, &code)) |list| { | ||
| 929 | b.pkg_config_pkg_list = list; | ||
| 930 | return list; | ||
| 931 | } else |err| { | ||
| 932 | const result = switch (err) { | ||
| 933 | error.ProcessTerminated => error.PkgConfigCrashed, | ||
| 934 | error.ExecNotSupported => error.PkgConfigFailed, | ||
| 935 | error.ExitCodeFailure => error.PkgConfigFailed, | ||
| 936 | error.FileNotFound => error.PkgConfigNotInstalled, | ||
| 937 | error.InvalidName => error.PkgConfigNotInstalled, | ||
| 938 | error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput, | ||
| 939 | else => return err, | ||
| 940 | }; | ||
| 941 | b.pkg_config_pkg_list = result; | ||
| 942 | return result; | ||
| 943 | } | ||
| 944 | } | ||
| 945 | |||
| 946 | fn addFlag(args: *std.array_list.Managed([]const u8), comptime name: []const u8, opt: ?bool) !void { | ||
| 947 | const cond = opt orelse return; | ||
| 948 | try args.ensureUnusedCapacity(1); | ||
| 949 | if (cond) { | ||
| 950 | args.appendAssumeCapacity("-f" ++ name); | ||
| 951 | } else { | ||
| 952 | args.appendAssumeCapacity("-fno-" ++ name); | ||
| 953 | } | ||
| 954 | } | ||
| 955 | |||
| 956 | const PkgConfigResult = struct { | ||
| 957 | cflags: []const []const u8, | ||
| 958 | libs: []const []const u8, | ||
| 959 | }; | ||
| 960 | |||
| 961 | /// Run pkg-config for the given library name and parse the output, returning the arguments | ||
| 962 | /// that should be passed to zig to link the given library. | ||
| 963 | fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult { | ||
| 964 | const wl_rpath_prefix = "-Wl,-rpath,"; | ||
| 965 | |||
| 966 | const b = compile.step.owner; | ||
| 967 | const arena = b.allocator; | ||
| 968 | const pkg_name = match: { | ||
| 969 | // First we have to map the library name to pkg config name. Unfortunately, | ||
| 970 | // there are several examples where this is not straightforward: | ||
| 971 | // -lSDL2 -> pkg-config sdl2 | ||
| 972 | // -lgdk-3 -> pkg-config gdk-3.0 | ||
| 973 | // -latk-1.0 -> pkg-config atk | ||
| 974 | // -lpulse -> pkg-config libpulse | ||
| 975 | const pkgs = try getPkgConfigList(b); | ||
| 976 | |||
| 977 | // Exact match means instant winner. | ||
| 978 | for (pkgs) |pkg| { | ||
| 979 | if (mem.eql(u8, pkg.name, lib_name)) { | ||
| 980 | break :match pkg.name; | ||
| 981 | } | ||
| 982 | } | ||
| 983 | |||
| 984 | // Next we'll try ignoring case. | ||
| 985 | for (pkgs) |pkg| { | ||
| 986 | if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) { | ||
| 987 | break :match pkg.name; | ||
| 988 | } | ||
| 989 | } | ||
| 990 | |||
| 991 | // Prefixed "lib" or suffixed ".0". | ||
| 992 | for (pkgs) |pkg| { | ||
| 993 | if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| { | ||
| 994 | const prefix = pkg.name[0..pos]; | ||
| 995 | const suffix = pkg.name[pos + lib_name.len ..]; | ||
| 996 | if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue; | ||
| 997 | if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue; | ||
| 998 | break :match pkg.name; | ||
| 999 | } | ||
| 1000 | } | ||
| 1001 | |||
| 1002 | // Trimming "-1.0". | ||
| 1003 | if (mem.endsWith(u8, lib_name, "-1.0")) { | ||
| 1004 | const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len]; | ||
| 1005 | for (pkgs) |pkg| { | ||
| 1006 | if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) { | ||
| 1007 | break :match pkg.name; | ||
| 1008 | } | ||
| 1009 | } | ||
| 1010 | } | ||
| 1011 | |||
| 1012 | return error.PackageNotFound; | ||
| 1013 | }; | ||
| 1014 | |||
| 1015 | var code: u8 = undefined; | ||
| 1016 | const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config"; | ||
| 1017 | const stdout = if (b.runAllowFail(&[_][]const u8{ | ||
| 1018 | pkg_config_exe, | ||
| 1019 | pkg_name, | ||
| 1020 | "--cflags", | ||
| 1021 | "--libs", | ||
| 1022 | }, &code, .ignore)) |stdout| stdout else |err| switch (err) { | ||
| 1023 | error.ProcessTerminated => return error.PkgConfigCrashed, | ||
| 1024 | error.ExecNotSupported => return error.PkgConfigFailed, | ||
| 1025 | error.ExitCodeFailure => return error.PkgConfigFailed, | ||
| 1026 | error.FileNotFound => return error.PkgConfigNotInstalled, | ||
| 1027 | else => return err, | ||
| 1028 | }; | ||
| 1029 | |||
| 1030 | var zig_cflags: std.ArrayList([]const u8) = .empty; | ||
| 1031 | defer zig_cflags.deinit(arena); | ||
| 1032 | var zig_libs: std.ArrayList([]const u8) = .empty; | ||
| 1033 | defer zig_libs.deinit(arena); | ||
| 1034 | |||
| 1035 | var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t"); | ||
| 1036 | while (arg_it.next()) |arg| { | ||
| 1037 | if (mem.eql(u8, arg, "-I")) { | ||
| 1038 | const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1039 | try zig_cflags.appendSlice(arena, &.{ "-I", dir }); | ||
| 1040 | } else if (mem.startsWith(u8, arg, "-I")) { | ||
| 1041 | try zig_cflags.append(arena, arg); | ||
| 1042 | } else if (mem.eql(u8, arg, "-L")) { | ||
| 1043 | const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1044 | try zig_libs.appendSlice(arena, &.{ "-L", dir }); | ||
| 1045 | } else if (mem.startsWith(u8, arg, "-L")) { | ||
| 1046 | try zig_libs.append(arena, arg); | ||
| 1047 | } else if (mem.eql(u8, arg, "-l")) { | ||
| 1048 | const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1049 | try zig_libs.appendSlice(arena, &.{ "-l", lib }); | ||
| 1050 | } else if (mem.startsWith(u8, arg, "-l")) { | ||
| 1051 | try zig_libs.append(arena, arg); | ||
| 1052 | } else if (mem.eql(u8, arg, "-D")) { | ||
| 1053 | const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1054 | try zig_cflags.appendSlice(arena, &.{ "-D", macro }); | ||
| 1055 | } else if (mem.startsWith(u8, arg, "-D")) { | ||
| 1056 | try zig_cflags.append(arena, arg); | ||
| 1057 | } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) { | ||
| 1058 | try zig_cflags.appendSlice(arena, &.{ "-rpath", arg[wl_rpath_prefix.len..] }); | ||
| 1059 | } else if (b.debug_pkg_config) { | ||
| 1060 | return compile.step.fail("unknown pkg-config flag '{s}'", .{arg}); | ||
| 1061 | } | ||
| 1062 | } | ||
| 1063 | |||
| 1064 | try zig_cflags.shrinkToLen(arena); | ||
| 1065 | try zig_libs.shrinkToLen(arena); | ||
| 1066 | |||
| 1067 | return .{ | ||
| 1068 | .cflags = zig_cflags.toOwnedSliceAssert(), | ||
| 1069 | .libs = zig_libs.toOwnedSliceAssert(), | ||
| 1070 | }; | ||
| 1071 | } | ||
| 1072 | |||
| 1073 | fn checkCompileErrors(compile: *Compile) !void { | ||
| 1074 | // Clear this field so that it does not get printed by the build runner. | ||
| 1075 | const actual_eb = compile.step.result_error_bundle; | ||
| 1076 | compile.step.result_error_bundle = .empty; | ||
| 1077 | |||
| 1078 | const arena = compile.step.owner.allocator; | ||
| 1079 | |||
| 1080 | const actual_errors = ae: { | ||
| 1081 | var aw: std.Io.Writer.Allocating = .init(arena); | ||
| 1082 | defer aw.deinit(); | ||
| 1083 | try actual_eb.renderToWriter(.{ | ||
| 1084 | .include_reference_trace = false, | ||
| 1085 | .include_source_line = false, | ||
| 1086 | }, &aw.writer); | ||
| 1087 | break :ae try aw.toOwnedSlice(); | ||
| 1088 | }; | ||
| 1089 | |||
| 1090 | // Render the expected lines into a string that we can compare verbatim. | ||
| 1091 | var expected_generated: std.ArrayList(u8) = .empty; | ||
| 1092 | const expect_errors = compile.expect_errors.?; | ||
| 1093 | |||
| 1094 | var actual_line_it = mem.splitScalar(u8, actual_errors, '\n'); | ||
| 1095 | |||
| 1096 | // TODO merge this with the testing.expectEqualStrings logic, and also CheckFile | ||
| 1097 | switch (expect_errors) { | ||
| 1098 | .starts_with => |expect_starts_with| { | ||
| 1099 | if (std.mem.startsWith(u8, actual_errors, expect_starts_with)) return; | ||
| 1100 | return compile.step.fail( | ||
| 1101 | \\ | ||
| 1102 | \\========= should start with: ============ | ||
| 1103 | \\{s} | ||
| 1104 | \\========= but not found: ================ | ||
| 1105 | \\{s} | ||
| 1106 | \\========================================= | ||
| 1107 | , .{ expect_starts_with, actual_errors }); | ||
| 1108 | }, | ||
| 1109 | .contains => |expect_line| { | ||
| 1110 | while (actual_line_it.next()) |actual_line| { | ||
| 1111 | if (!matchCompileError(actual_line, expect_line)) continue; | ||
| 1112 | return; | ||
| 1113 | } | ||
| 1114 | |||
| 1115 | return compile.step.fail( | ||
| 1116 | \\ | ||
| 1117 | \\========= should contain: =============== | ||
| 1118 | \\{s} | ||
| 1119 | \\========= but not found: ================ | ||
| 1120 | \\{s} | ||
| 1121 | \\========================================= | ||
| 1122 | , .{ expect_line, actual_errors }); | ||
| 1123 | }, | ||
| 1124 | .stderr_contains => |expect_line| { | ||
| 1125 | const actual_stderr: []const u8 = if (compile.step.result_error_msgs.items.len > 0) | ||
| 1126 | compile.step.result_error_msgs.items[0] | ||
| 1127 | else | ||
| 1128 | &.{}; | ||
| 1129 | compile.step.result_error_msgs.clearRetainingCapacity(); | ||
| 1130 | |||
| 1131 | var stderr_line_it = mem.splitScalar(u8, actual_stderr, '\n'); | ||
| 1132 | |||
| 1133 | while (stderr_line_it.next()) |actual_line| { | ||
| 1134 | if (!matchCompileError(actual_line, expect_line)) continue; | ||
| 1135 | return; | ||
| 1136 | } | ||
| 1137 | |||
| 1138 | return compile.step.fail( | ||
| 1139 | \\ | ||
| 1140 | \\========= should contain: =============== | ||
| 1141 | \\{s} | ||
| 1142 | \\========= but not found: ================ | ||
| 1143 | \\{s} | ||
| 1144 | \\========================================= | ||
| 1145 | , .{ expect_line, actual_stderr }); | ||
| 1146 | }, | ||
| 1147 | .exact => |expect_lines| { | ||
| 1148 | for (expect_lines) |expect_line| { | ||
| 1149 | const actual_line = actual_line_it.next() orelse { | ||
| 1150 | try expected_generated.appendSlice(arena, expect_line); | ||
| 1151 | try expected_generated.append(arena, '\n'); | ||
| 1152 | continue; | ||
| 1153 | }; | ||
| 1154 | if (matchCompileError(actual_line, expect_line)) { | ||
| 1155 | try expected_generated.appendSlice(arena, actual_line); | ||
| 1156 | try expected_generated.append(arena, '\n'); | ||
| 1157 | continue; | ||
| 1158 | } | ||
| 1159 | try expected_generated.appendSlice(arena, expect_line); | ||
| 1160 | try expected_generated.append(arena, '\n'); | ||
| 1161 | } | ||
| 1162 | |||
| 1163 | if (mem.eql(u8, expected_generated.items, actual_errors)) return; | ||
| 1164 | |||
| 1165 | return compile.step.fail( | ||
| 1166 | \\ | ||
| 1167 | \\========= expected: ===================== | ||
| 1168 | \\{s} | ||
| 1169 | \\========= but found: ==================== | ||
| 1170 | \\{s} | ||
| 1171 | \\========================================= | ||
| 1172 | , .{ expected_generated.items, actual_errors }); | ||
| 1173 | }, | ||
| 1174 | } | ||
| 1175 | } | ||
| 1176 | |||
| 1177 | fn matchCompileError(actual: []const u8, expected: []const u8) bool { | ||
| 1178 | if (mem.endsWith(u8, actual, expected)) return true; | ||
| 1179 | if (mem.startsWith(u8, expected, ":?:?: ")) { | ||
| 1180 | if (mem.endsWith(u8, actual, expected[":?:?: ".len..])) return true; | ||
| 1181 | } | ||
| 1182 | // We scan for /?/ in expected line and if there is a match, we match everything | ||
| 1183 | // up to and after /?/. | ||
| 1184 | const expected_trim = mem.trim(u8, expected, " "); | ||
| 1185 | if (mem.find(u8, expected_trim, "/?/")) |index| { | ||
| 1186 | const actual_trim = mem.trim(u8, actual, " "); | ||
| 1187 | const lhs = expected_trim[0..index]; | ||
| 1188 | const rhs = expected_trim[index + "/?/".len ..]; | ||
| 1189 | if (mem.startsWith(u8, actual_trim, lhs) and mem.endsWith(u8, actual_trim, rhs)) return true; | ||
| 1190 | } | ||
| 1191 | return false; | ||
| 1192 | } | ||
| 1193 | |||
| 1194 | fn moduleNeedsCliArg(mod: *const Module) bool { | ||
| 1195 | return for (mod.link_objects.items) |o| switch (o) { | ||
| 1196 | .c_source_file, .c_source_files, .assembly_file, .win32_resource_file => break true, | ||
| 1197 | else => continue, | ||
| 1198 | } else false; | ||
| 1199 | } | ||
| 1200 | |||
lib/compiler/Maker/Step/InstallArtifact.zig created+96| ... | @@ -0,0 +1,96 @@ | ||
| 1 | |||
| 2 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 3 | _ = options; | ||
| 4 | const install_artifact: *InstallArtifact = @fieldParentPtr("step", step); | ||
| 5 | const b = step.owner; | ||
| 6 | const io = b.graph.io; | ||
| 7 | |||
| 8 | var all_cached = true; | ||
| 9 | |||
| 10 | if (install_artifact.dest_dir) |dest_dir| { | ||
| 11 | const full_dest_path = b.getInstallPath(dest_dir, install_artifact.dest_sub_path); | ||
| 12 | const p = try step.installFile(install_artifact.emitted_bin.?, full_dest_path); | ||
| 13 | all_cached = all_cached and p == .fresh; | ||
| 14 | |||
| 15 | if (install_artifact.dylib_symlinks) |dls| { | ||
| 16 | try Step.Compile.doAtomicSymLinks(step, full_dest_path, dls.major_only_filename, dls.name_only_filename); | ||
| 17 | } | ||
| 18 | |||
| 19 | install_artifact.artifact.installed_path = full_dest_path; | ||
| 20 | } | ||
| 21 | |||
| 22 | if (install_artifact.compiler_rt_dyn_lib_dir) |compiler_rt_dir| { | ||
| 23 | const full_compiler_rt_path = b.getInstallPath(compiler_rt_dir, install_artifact.emitted_compiler_rt_dyn_lib.?.basename(b, step)); | ||
| 24 | const p = try step.installFile(install_artifact.emitted_compiler_rt_dyn_lib.?, full_compiler_rt_path); | ||
| 25 | all_cached = all_cached and p == .fresh; | ||
| 26 | } | ||
| 27 | |||
| 28 | if (install_artifact.implib_dir) |implib_dir| { | ||
| 29 | const full_implib_path = b.getInstallPath(implib_dir, install_artifact.emitted_implib.?.basename(b, step)); | ||
| 30 | const p = try step.installFile(install_artifact.emitted_implib.?, full_implib_path); | ||
| 31 | all_cached = all_cached and p == .fresh; | ||
| 32 | } | ||
| 33 | |||
| 34 | if (install_artifact.pdb_dir) |pdb_dir| { | ||
| 35 | const full_pdb_path = b.getInstallPath(pdb_dir, install_artifact.emitted_pdb.?.basename(b, step)); | ||
| 36 | const p = try step.installFile(install_artifact.emitted_pdb.?, full_pdb_path); | ||
| 37 | all_cached = all_cached and p == .fresh; | ||
| 38 | } | ||
| 39 | |||
| 40 | if (install_artifact.h_dir) |h_dir| { | ||
| 41 | if (install_artifact.emitted_h) |emitted_h| { | ||
| 42 | const full_h_path = b.getInstallPath(h_dir, emitted_h.basename(b, step)); | ||
| 43 | const p = try step.installFile(emitted_h, full_h_path); | ||
| 44 | all_cached = all_cached and p == .fresh; | ||
| 45 | } | ||
| 46 | |||
| 47 | for (install_artifact.artifact.installed_headers.items) |installation| switch (installation) { | ||
| 48 | .file => |file| { | ||
| 49 | const full_h_path = b.getInstallPath(h_dir, file.dest_rel_path); | ||
| 50 | const p = try step.installFile(file.source, full_h_path); | ||
| 51 | all_cached = all_cached and p == .fresh; | ||
| 52 | }, | ||
| 53 | .directory => |dir| { | ||
| 54 | const src_dir_path = dir.source.getPath3(b, step); | ||
| 55 | const full_h_prefix = b.getInstallPath(h_dir, dir.dest_rel_path); | ||
| 56 | |||
| 57 | var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { | ||
| 58 | return step.fail("unable to open source directory '{f}': {s}", .{ | ||
| 59 | src_dir_path, @errorName(err), | ||
| 60 | }); | ||
| 61 | }; | ||
| 62 | defer src_dir.close(io); | ||
| 63 | |||
| 64 | var it = try src_dir.walk(b.allocator); | ||
| 65 | next_entry: while (try it.next(io)) |entry| { | ||
| 66 | for (dir.options.exclude_extensions) |ext| { | ||
| 67 | if (std.mem.endsWith(u8, entry.path, ext)) continue :next_entry; | ||
| 68 | } | ||
| 69 | if (dir.options.include_extensions) |incs| { | ||
| 70 | for (incs) |inc| { | ||
| 71 | if (std.mem.endsWith(u8, entry.path, inc)) break; | ||
| 72 | } else { | ||
| 73 | continue :next_entry; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | const full_dest_path = b.pathJoin(&.{ full_h_prefix, entry.path }); | ||
| 78 | switch (entry.kind) { | ||
| 79 | .directory => { | ||
| 80 | try Step.handleVerbose(b, .inherit, &.{ "install", "-d", full_dest_path }); | ||
| 81 | const p = try step.installDir(full_dest_path); | ||
| 82 | all_cached = all_cached and p == .existed; | ||
| 83 | }, | ||
| 84 | .file => { | ||
| 85 | const p = try step.installFile(try dir.source.join(b.allocator, entry.path), full_dest_path); | ||
| 86 | all_cached = all_cached and p == .fresh; | ||
| 87 | }, | ||
| 88 | else => continue, | ||
| 89 | } | ||
| 90 | } | ||
| 91 | }, | ||
| 92 | }; | ||
| 93 | } | ||
| 94 | |||
| 95 | step.result_cached = all_cached; | ||
| 96 | } | ||
lib/compiler/Maker/Step/Run.zig created+2130| ... | @@ -0,0 +1,2130 @@ | ||
| 1 | const Run = @This(); | ||
| 2 | |||
| 3 | const builtin = @import("builtin"); | ||
| 4 | |||
| 5 | const std = @import("std"); | ||
| 6 | const Io = std.Io; | ||
| 7 | const Dir = std.Io.Dir; | ||
| 8 | const mem = std.mem; | ||
| 9 | const process = std.process; | ||
| 10 | const EnvMap = std.process.Environ.Map; | ||
| 11 | const assert = std.debug.assert; | ||
| 12 | const Cache = std.Build.Cache; | ||
| 13 | const Path = std.Build.Cache.Path; | ||
| 14 | |||
| 15 | const Step = @import("../Step.zig"); | ||
| 16 | |||
| 17 | /// If this is a Zig unit test binary, this tracks the names of the unit | ||
| 18 | /// tests that are also fuzz tests. Indexes cannot be used as they may | ||
| 19 | /// change between reruns. | ||
| 20 | fuzz_tests: std.ArrayList([]const u8), | ||
| 21 | cached_test_metadata: ?CachedTestMetadata = null, | ||
| 22 | |||
| 23 | /// Populated during the fuzz phase if this run step corresponds to a unit test | ||
| 24 | /// executable that contains fuzz tests. | ||
| 25 | rebuilt_executable: ?Path, | ||
| 26 | |||
| 27 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 28 | const b = step.owner; | ||
| 29 | const io = b.graph.io; | ||
| 30 | const arena = b.allocator; | ||
| 31 | const run: *Run = @fieldParentPtr("step", step); | ||
| 32 | const has_side_effects = run.hasSideEffects(); | ||
| 33 | |||
| 34 | var argv_list = std.array_list.Managed([]const u8).init(arena); | ||
| 35 | var output_placeholders = std.array_list.Managed(IndexedOutput).init(arena); | ||
| 36 | |||
| 37 | var man = b.graph.cache.obtain(); | ||
| 38 | defer man.deinit(); | ||
| 39 | |||
| 40 | if (run.environ_map) |environ_map| { | ||
| 41 | for (environ_map.keys(), environ_map.values()) |key, value| { | ||
| 42 | man.hash.addBytes(key); | ||
| 43 | man.hash.addBytes(value); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | man.hash.add(run.color); | ||
| 48 | man.hash.add(run.disable_zig_progress); | ||
| 49 | |||
| 50 | for (run.argv.items) |arg| { | ||
| 51 | switch (arg) { | ||
| 52 | .bytes => |bytes| { | ||
| 53 | try argv_list.append(bytes); | ||
| 54 | man.hash.addBytes(bytes); | ||
| 55 | }, | ||
| 56 | .lazy_path => |file| { | ||
| 57 | const file_path = file.lazy_path.getPath3(b, step); | ||
| 58 | try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) })); | ||
| 59 | man.hash.addBytes(file.prefix); | ||
| 60 | _ = try man.addFilePath(file_path, null); | ||
| 61 | }, | ||
| 62 | .decorated_directory => |dd| { | ||
| 63 | const file_path = dd.lazy_path.getPath3(b, step); | ||
| 64 | const resolved_arg = b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }); | ||
| 65 | try argv_list.append(resolved_arg); | ||
| 66 | man.hash.addBytes(resolved_arg); | ||
| 67 | }, | ||
| 68 | .file_content => |file_plp| { | ||
| 69 | const file_path = file_plp.lazy_path.getPath3(b, step); | ||
| 70 | |||
| 71 | var result: std.Io.Writer.Allocating = .init(arena); | ||
| 72 | errdefer result.deinit(); | ||
| 73 | result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory; | ||
| 74 | |||
| 75 | const file = file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}) catch |err| { | ||
| 76 | return step.fail( | ||
| 77 | "unable to open input file '{f}': {t}", | ||
| 78 | .{ file_path, err }, | ||
| 79 | ); | ||
| 80 | }; | ||
| 81 | defer file.close(io); | ||
| 82 | |||
| 83 | var buf: [1024]u8 = undefined; | ||
| 84 | var file_reader = file.reader(io, &buf); | ||
| 85 | _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { | ||
| 86 | error.ReadFailed => return step.fail( | ||
| 87 | "failed to read from '{f}': {t}", | ||
| 88 | .{ file_path, file_reader.err.? }, | ||
| 89 | ), | ||
| 90 | error.WriteFailed => return error.OutOfMemory, | ||
| 91 | }; | ||
| 92 | |||
| 93 | try argv_list.append(result.written()); | ||
| 94 | man.hash.addBytes(file_plp.prefix); | ||
| 95 | _ = try man.addFilePath(file_path, null); | ||
| 96 | }, | ||
| 97 | .artifact => |pa| { | ||
| 98 | const artifact = pa.artifact; | ||
| 99 | |||
| 100 | if (artifact.rootModuleTarget().os.tag == .windows) { | ||
| 101 | // On Windows we don't have rpaths so we have to add .dll search paths to PATH | ||
| 102 | run.addPathForDynLibs(artifact); | ||
| 103 | } | ||
| 104 | const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?; | ||
| 105 | |||
| 106 | try argv_list.append(b.fmt("{s}{s}", .{ | ||
| 107 | pa.prefix, | ||
| 108 | run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }), | ||
| 109 | })); | ||
| 110 | |||
| 111 | _ = try man.addFile(file_path, null); | ||
| 112 | }, | ||
| 113 | .output_file, .output_directory => |output| { | ||
| 114 | man.hash.addBytes(output.prefix); | ||
| 115 | man.hash.addBytes(output.basename); | ||
| 116 | // Add a placeholder into the argument list because we need the | ||
| 117 | // manifest hash to be updated with all arguments before the | ||
| 118 | // object directory is computed. | ||
| 119 | try output_placeholders.append(.{ | ||
| 120 | .index = argv_list.items.len, | ||
| 121 | .tag = arg, | ||
| 122 | .output = output, | ||
| 123 | }); | ||
| 124 | _ = try argv_list.addOne(); | ||
| 125 | }, | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | switch (run.stdin) { | ||
| 130 | .bytes => |bytes| { | ||
| 131 | man.hash.addBytes(bytes); | ||
| 132 | }, | ||
| 133 | .lazy_path => |lazy_path| { | ||
| 134 | const file_path = lazy_path.getPath2(b, step); | ||
| 135 | _ = try man.addFile(file_path, null); | ||
| 136 | }, | ||
| 137 | .none => {}, | ||
| 138 | } | ||
| 139 | |||
| 140 | if (run.captured_stdout) |captured| { | ||
| 141 | man.hash.addBytes(captured.output.basename); | ||
| 142 | man.hash.add(captured.trim_whitespace); | ||
| 143 | } | ||
| 144 | |||
| 145 | if (run.captured_stderr) |captured| { | ||
| 146 | man.hash.addBytes(captured.output.basename); | ||
| 147 | man.hash.add(captured.trim_whitespace); | ||
| 148 | } | ||
| 149 | |||
| 150 | hashStdIo(&man.hash, run.stdio); | ||
| 151 | |||
| 152 | for (run.file_inputs.items) |lazy_path| { | ||
| 153 | _ = try man.addFile(lazy_path.getPath2(b, step), null); | ||
| 154 | } | ||
| 155 | |||
| 156 | if (run.cwd) |cwd| { | ||
| 157 | const cwd_path = cwd.getPath3(b, step); | ||
| 158 | _ = man.hash.addBytes(try cwd_path.toString(arena)); | ||
| 159 | } | ||
| 160 | |||
| 161 | if (!has_side_effects and try step.cacheHitAndWatch(&man)) { | ||
| 162 | // cache hit, skip running command | ||
| 163 | const digest = man.final(); | ||
| 164 | |||
| 165 | try populateGeneratedPaths( | ||
| 166 | arena, | ||
| 167 | output_placeholders.items, | ||
| 168 | run.captured_stdout, | ||
| 169 | run.captured_stderr, | ||
| 170 | b.cache_root, | ||
| 171 | &digest, | ||
| 172 | ); | ||
| 173 | |||
| 174 | step.result_cached = true; | ||
| 175 | return; | ||
| 176 | } | ||
| 177 | |||
| 178 | const dep_output_file = run.dep_output_file orelse { | ||
| 179 | // We already know the final output paths, use them directly. | ||
| 180 | const digest = if (has_side_effects) | ||
| 181 | man.hash.final() | ||
| 182 | else | ||
| 183 | man.final(); | ||
| 184 | |||
| 185 | try populateGeneratedPaths( | ||
| 186 | arena, | ||
| 187 | output_placeholders.items, | ||
| 188 | run.captured_stdout, | ||
| 189 | run.captured_stderr, | ||
| 190 | b.cache_root, | ||
| 191 | &digest, | ||
| 192 | ); | ||
| 193 | |||
| 194 | const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; | ||
| 195 | for (output_placeholders.items) |placeholder| { | ||
| 196 | const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename }); | ||
| 197 | const output_sub_dir_path = switch (placeholder.tag) { | ||
| 198 | .output_file => Dir.path.dirname(output_sub_path).?, | ||
| 199 | .output_directory => output_sub_path, | ||
| 200 | else => unreachable, | ||
| 201 | }; | ||
| 202 | b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| { | ||
| 203 | return step.fail("unable to make path '{f}{s}': {s}", .{ | ||
| 204 | b.cache_root, output_sub_dir_path, @errorName(err), | ||
| 205 | }); | ||
| 206 | }; | ||
| 207 | const arg_output_path = run.convertPathArg(.{ | ||
| 208 | .root_dir = .cwd(), | ||
| 209 | .sub_path = placeholder.output.generated_file.getPath(), | ||
| 210 | }); | ||
| 211 | argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0) | ||
| 212 | arg_output_path | ||
| 213 | else | ||
| 214 | b.fmt("{s}{s}", .{ placeholder.output.prefix, arg_output_path }); | ||
| 215 | } | ||
| 216 | |||
| 217 | try runCommand(run, argv_list.items, has_side_effects, output_dir_path, options, null); | ||
| 218 | if (!has_side_effects) try step.writeManifestAndWatch(&man); | ||
| 219 | return; | ||
| 220 | }; | ||
| 221 | |||
| 222 | // We do not know the final output paths yet, use temp paths to run the command. | ||
| 223 | var rand_int: u64 = undefined; | ||
| 224 | io.random(@ptrCast(&rand_int)); | ||
| 225 | const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); | ||
| 226 | |||
| 227 | for (output_placeholders.items) |placeholder| { | ||
| 228 | const output_components = .{ tmp_dir_path, placeholder.output.basename }; | ||
| 229 | const output_sub_path = b.pathJoin(&output_components); | ||
| 230 | const output_sub_dir_path = switch (placeholder.tag) { | ||
| 231 | .output_file => Dir.path.dirname(output_sub_path).?, | ||
| 232 | .output_directory => output_sub_path, | ||
| 233 | else => unreachable, | ||
| 234 | }; | ||
| 235 | b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| { | ||
| 236 | return step.fail("unable to make path '{f}{s}': {s}", .{ | ||
| 237 | b.cache_root, output_sub_dir_path, @errorName(err), | ||
| 238 | }); | ||
| 239 | }; | ||
| 240 | const raw_output_path: Cache.Path = .{ | ||
| 241 | .root_dir = b.cache_root, | ||
| 242 | .sub_path = b.pathJoin(&output_components), | ||
| 243 | }; | ||
| 244 | placeholder.output.generated_file.path = raw_output_path.toString(b.graph.arena) catch @panic("OOM"); | ||
| 245 | argv_list.items[placeholder.index] = b.fmt("{s}{s}", .{ | ||
| 246 | placeholder.output.prefix, | ||
| 247 | run.convertPathArg(raw_output_path), | ||
| 248 | }); | ||
| 249 | } | ||
| 250 | |||
| 251 | try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, options, null); | ||
| 252 | |||
| 253 | const dep_file_dir = Dir.cwd(); | ||
| 254 | const dep_file_basename = dep_output_file.generated_file.getPath2(b, step); | ||
| 255 | if (has_side_effects) | ||
| 256 | try man.addDepFile(dep_file_dir, dep_file_basename) | ||
| 257 | else | ||
| 258 | try man.addDepFilePost(dep_file_dir, dep_file_basename); | ||
| 259 | |||
| 260 | const digest = if (has_side_effects) | ||
| 261 | man.hash.final() | ||
| 262 | else | ||
| 263 | man.final(); | ||
| 264 | |||
| 265 | const any_output = output_placeholders.items.len > 0 or | ||
| 266 | run.captured_stdout != null or run.captured_stderr != null; | ||
| 267 | |||
| 268 | // Rename into place | ||
| 269 | if (any_output) { | ||
| 270 | const o_sub_path = "o" ++ Dir.path.sep_str ++ &digest; | ||
| 271 | |||
| 272 | b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |err| switch (err) { | ||
| 273 | Dir.RenameError.DirNotEmpty => { | ||
| 274 | b.cache_root.handle.deleteTree(io, o_sub_path) catch |del_err| { | ||
| 275 | return step.fail("unable to remove dir '{f}'{s}: {t}", .{ | ||
| 276 | b.cache_root, tmp_dir_path, del_err, | ||
| 277 | }); | ||
| 278 | }; | ||
| 279 | b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |retry_err| { | ||
| 280 | return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{ | ||
| 281 | b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, retry_err, | ||
| 282 | }); | ||
| 283 | }; | ||
| 284 | }, | ||
| 285 | else => return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{ | ||
| 286 | b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, err, | ||
| 287 | }), | ||
| 288 | }; | ||
| 289 | } | ||
| 290 | |||
| 291 | if (!has_side_effects) try step.writeManifestAndWatch(&man); | ||
| 292 | |||
| 293 | try populateGeneratedPaths( | ||
| 294 | arena, | ||
| 295 | output_placeholders.items, | ||
| 296 | run.captured_stdout, | ||
| 297 | run.captured_stderr, | ||
| 298 | b.cache_root, | ||
| 299 | &digest, | ||
| 300 | ); | ||
| 301 | } | ||
| 302 | |||
| 303 | /// Reads stdout of a Zig test process until a termination condition is reached: | ||
| 304 | /// * A write fails, indicating the child unexpectedly closed stdin | ||
| 305 | /// * A test (or a response from the test runner) times out | ||
| 306 | /// * The wait fails, indicating the child closed stdout and stderr | ||
| 307 | fn waitZigTest( | ||
| 308 | run: *Run, | ||
| 309 | child: *process.Child, | ||
| 310 | options: Step.MakeOptions, | ||
| 311 | multi_reader: *Io.File.MultiReader, | ||
| 312 | opt_metadata: *?TestMetadata, | ||
| 313 | results: *Step.TestResults, | ||
| 314 | ) !union(enum) { | ||
| 315 | write_failed: anyerror, | ||
| 316 | no_poll: struct { | ||
| 317 | active_test_index: ?u32, | ||
| 318 | ns_elapsed: u64, | ||
| 319 | }, | ||
| 320 | timeout: struct { | ||
| 321 | active_test_index: ?u32, | ||
| 322 | ns_elapsed: u64, | ||
| 323 | }, | ||
| 324 | } { | ||
| 325 | const gpa = run.step.owner.allocator; | ||
| 326 | const arena = run.step.owner.allocator; | ||
| 327 | const io = run.step.owner.graph.io; | ||
| 328 | |||
| 329 | var sub_prog_node: ?std.Progress.Node = null; | ||
| 330 | defer if (sub_prog_node) |n| n.end(); | ||
| 331 | |||
| 332 | if (opt_metadata.*) |*md| { | ||
| 333 | // Previous unit test process died or was killed; we're continuing where it left off | ||
| 334 | requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; | ||
| 335 | } else { | ||
| 336 | // Running unit tests normally | ||
| 337 | run.fuzz_tests.clearRetainingCapacity(); | ||
| 338 | sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err }; | ||
| 339 | } | ||
| 340 | |||
| 341 | var active_test_index: ?u32 = null; | ||
| 342 | |||
| 343 | var last_update: Io.Clock.Timestamp = .now(io, .awake); | ||
| 344 | |||
| 345 | // This timeout is used when we're waiting on the test runner itself rather than a user-specified | ||
| 346 | // test. For instance, if the test runner leaves this much time between us requesting a test to | ||
| 347 | // start and it acknowledging the test starting, we terminate the child and raise an error. This | ||
| 348 | // *should* never happen, but could in theory be caused by some very unlucky IB in a test. | ||
| 349 | const response_timeout: Io.Clock.Duration = t: { | ||
| 350 | if (fuzz_context != null) break :t null; // don't timeout fuzz tests | ||
| 351 | const ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s); | ||
| 352 | break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) }; | ||
| 353 | }; | ||
| 354 | const test_timeout: ?Io.Clock.Duration = if (options.unit_test_timeout_ns) |ns| .{ | ||
| 355 | .clock = .awake, | ||
| 356 | .raw = .fromNanoseconds(ns), | ||
| 357 | } else null; | ||
| 358 | |||
| 359 | const stdout = multi_reader.reader(0); | ||
| 360 | const stderr = multi_reader.reader(1); | ||
| 361 | const Header = std.zig.Server.Message.Header; | ||
| 362 | |||
| 363 | while (true) { | ||
| 364 | const timeout: Io.Timeout = t: { | ||
| 365 | const opt_duration = if (active_test_index == null) response_timeout else test_timeout; | ||
| 366 | const duration = opt_duration orelse break :t .none; | ||
| 367 | break :t .{ .deadline = last_update.addDuration(duration) }; | ||
| 368 | }; | ||
| 369 | |||
| 370 | // This block is exited when `stdout` contains enough bytes for a `Header`. | ||
| 371 | header_ready: { | ||
| 372 | if (stdout.buffered().len >= @sizeOf(Header)) { | ||
| 373 | // We already have one, no need to poll! | ||
| 374 | break :header_ready; | ||
| 375 | } | ||
| 376 | |||
| 377 | multi_reader.fill(64, timeout) catch |err| switch (err) { | ||
| 378 | error.Timeout => return .{ .timeout = .{ | ||
| 379 | .active_test_index = active_test_index, | ||
| 380 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 381 | } }, | ||
| 382 | error.EndOfStream => return .{ .no_poll = .{ | ||
| 383 | .active_test_index = active_test_index, | ||
| 384 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 385 | } }, | ||
| 386 | else => |e| return e, | ||
| 387 | }; | ||
| 388 | |||
| 389 | continue; | ||
| 390 | } | ||
| 391 | // There is definitely a header available now -- read it. | ||
| 392 | const header = stdout.takeStruct(Header, .little) catch unreachable; | ||
| 393 | |||
| 394 | while (stdout.buffered().len < header.bytes_len) { | ||
| 395 | multi_reader.fill(64, timeout) catch |err| switch (err) { | ||
| 396 | error.Timeout => return .{ .timeout = .{ | ||
| 397 | .active_test_index = active_test_index, | ||
| 398 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 399 | } }, | ||
| 400 | error.EndOfStream => return .{ .no_poll = .{ | ||
| 401 | .active_test_index = active_test_index, | ||
| 402 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 403 | } }, | ||
| 404 | else => |e| return e, | ||
| 405 | }; | ||
| 406 | } | ||
| 407 | |||
| 408 | const body = stdout.take(header.bytes_len) catch unreachable; | ||
| 409 | var body_r: std.Io.Reader = .fixed(body); | ||
| 410 | switch (header.tag) { | ||
| 411 | .zig_version => { | ||
| 412 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) return run.step.fail( | ||
| 413 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", | ||
| 414 | .{ builtin.zig_version_string, body }, | ||
| 415 | ); | ||
| 416 | }, | ||
| 417 | .test_metadata => { | ||
| 418 | // `metadata` would only be populated if we'd already seen a `test_metadata`, but we | ||
| 419 | // only request it once (and importantly, we don't re-request it if we kill and | ||
| 420 | // restart the test runner). | ||
| 421 | assert(opt_metadata.* == null); | ||
| 422 | |||
| 423 | const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable; | ||
| 424 | results.test_count = tm_hdr.tests_len; | ||
| 425 | |||
| 426 | const names = try arena.alloc(u32, results.test_count); | ||
| 427 | for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; | ||
| 428 | |||
| 429 | const expected_panic_msgs = try arena.alloc(u32, results.test_count); | ||
| 430 | for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; | ||
| 431 | |||
| 432 | const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable; | ||
| 433 | |||
| 434 | options.progress_node.setEstimatedTotalItems(names.len); | ||
| 435 | opt_metadata.* = .{ | ||
| 436 | .string_bytes = try arena.dupe(u8, string_bytes), | ||
| 437 | .ns_per_test = try arena.alloc(u64, results.test_count), | ||
| 438 | .names = names, | ||
| 439 | .expected_panic_msgs = expected_panic_msgs, | ||
| 440 | .next_index = 0, | ||
| 441 | .prog_node = options.progress_node, | ||
| 442 | }; | ||
| 443 | @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64)); | ||
| 444 | |||
| 445 | active_test_index = null; | ||
| 446 | last_update = .now(io, .awake); | ||
| 447 | |||
| 448 | requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err }; | ||
| 449 | }, | ||
| 450 | .test_started => { | ||
| 451 | active_test_index = opt_metadata.*.?.next_index - 1; | ||
| 452 | last_update = .now(io, .awake); | ||
| 453 | }, | ||
| 454 | .test_results => { | ||
| 455 | const md = &opt_metadata.*.?; | ||
| 456 | |||
| 457 | const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable; | ||
| 458 | assert(tr_hdr.index == active_test_index); | ||
| 459 | |||
| 460 | switch (tr_hdr.flags.status) { | ||
| 461 | .pass => {}, | ||
| 462 | .skip => results.skip_count +|= 1, | ||
| 463 | .fail => results.fail_count +|= 1, | ||
| 464 | } | ||
| 465 | const leak_count = tr_hdr.flags.leak_count; | ||
| 466 | const log_err_count = tr_hdr.flags.log_err_count; | ||
| 467 | results.leak_count +|= leak_count; | ||
| 468 | results.log_err_count +|= log_err_count; | ||
| 469 | |||
| 470 | if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index)); | ||
| 471 | |||
| 472 | if (tr_hdr.flags.status == .fail) { | ||
| 473 | const name = md.testName(tr_hdr.index); | ||
| 474 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); | ||
| 475 | stderr.tossBuffered(); | ||
| 476 | if (stderr_bytes.len == 0) { | ||
| 477 | try run.step.addError("'{s}' failed without output", .{name}); | ||
| 478 | } else { | ||
| 479 | try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes }); | ||
| 480 | } | ||
| 481 | } else if (leak_count > 0) { | ||
| 482 | const name = md.testName(tr_hdr.index); | ||
| 483 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); | ||
| 484 | stderr.tossBuffered(); | ||
| 485 | try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes }); | ||
| 486 | } else if (log_err_count > 0) { | ||
| 487 | const name = md.testName(tr_hdr.index); | ||
| 488 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); | ||
| 489 | stderr.tossBuffered(); | ||
| 490 | try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes }); | ||
| 491 | } | ||
| 492 | |||
| 493 | active_test_index = null; | ||
| 494 | |||
| 495 | const now: Io.Clock.Timestamp = .now(io, .awake); | ||
| 496 | md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds); | ||
| 497 | last_update = now; | ||
| 498 | |||
| 499 | requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; | ||
| 500 | }, | ||
| 501 | else => {}, // ignore other messages | ||
| 502 | } | ||
| 503 | } | ||
| 504 | } | ||
| 505 | |||
| 506 | const FuzzTestRunner = struct { | ||
| 507 | run: *Run, | ||
| 508 | ctx: FuzzContext, | ||
| 509 | coverage_id: ?u64, | ||
| 510 | |||
| 511 | instances: []Instance, | ||
| 512 | /// The indexes of this are layed out such that it is effectively an array | ||
| 513 | /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr. | ||
| 514 | batch: Io.Batch, | ||
| 515 | /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter. | ||
| 516 | pending_broadcasts: std.ArrayList(u8), | ||
| 517 | broadcast: std.ArrayList(u8), | ||
| 518 | broadcast_undelivered: u32, | ||
| 519 | |||
| 520 | const Instance = struct { | ||
| 521 | child: process.Child, | ||
| 522 | message: std.ArrayListAligned(u8, .@"4"), | ||
| 523 | broadcast_written: usize, | ||
| 524 | stderr: std.ArrayList(u8), | ||
| 525 | stdin_vec: [1][]u8, | ||
| 526 | stdout_vec: [1][]u8, | ||
| 527 | stderr_vec: [1][]u8, | ||
| 528 | progress_node: std.Progress.Node, | ||
| 529 | |||
| 530 | fn messageHeader(instance: *Instance) InHeader { | ||
| 531 | assert(instance.message.items.len >= @sizeOf(InHeader)); | ||
| 532 | const header_ptr: *InHeader = @ptrCast(instance.message.items); | ||
| 533 | var header = header_ptr.*; | ||
| 534 | if (std.builtin.Endian.native != .little) { | ||
| 535 | std.mem.byteSwapAllFields(InHeader, &header); | ||
| 536 | } | ||
| 537 | return header; | ||
| 538 | } | ||
| 539 | }; | ||
| 540 | |||
| 541 | const PendingBroadcastFooter = struct { | ||
| 542 | from_id: u32, | ||
| 543 | body_len: u32, | ||
| 544 | }; | ||
| 545 | |||
| 546 | const InHeader = std.zig.Server.Message.Header; | ||
| 547 | const OutHeader = std.zig.Client.Message.Header; | ||
| 548 | |||
| 549 | const stdin_i = 0; | ||
| 550 | const stdout_i = 1; | ||
| 551 | const stderr_i = 2; | ||
| 552 | |||
| 553 | fn init( | ||
| 554 | run: *Run, | ||
| 555 | ctx: FuzzContext, | ||
| 556 | progress_node: std.Progress.Node, | ||
| 557 | spawn_options: process.SpawnOptions, | ||
| 558 | ) !FuzzTestRunner { | ||
| 559 | const step_owner = run.step.owner; | ||
| 560 | const gpa = step_owner.allocator; | ||
| 561 | const io = step_owner.graph.io; | ||
| 562 | |||
| 563 | const n_instances = switch (ctx.fuzz.mode) { | ||
| 564 | .forever => step_owner.graph.max_jobs orelse @min( | ||
| 565 | std.Thread.getCpuCount() catch 1, | ||
| 566 | (std.math.maxInt(u32) - 2) / 3, | ||
| 567 | ), | ||
| 568 | .limit => 1, | ||
| 569 | }; | ||
| 570 | const instances = try gpa.alloc(Instance, n_instances); | ||
| 571 | errdefer gpa.free(instances); | ||
| 572 | const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3); | ||
| 573 | errdefer gpa.free(batch_storage); | ||
| 574 | |||
| 575 | @memset(instances, .{ | ||
| 576 | .child = undefined, | ||
| 577 | .message = .empty, | ||
| 578 | .broadcast_written = undefined, | ||
| 579 | .stderr = .empty, | ||
| 580 | .stdin_vec = undefined, | ||
| 581 | .stdout_vec = undefined, | ||
| 582 | .stderr_vec = undefined, | ||
| 583 | .progress_node = undefined, | ||
| 584 | }); | ||
| 585 | for (0.., instances) |id, *instance| { | ||
| 586 | errdefer for (instances[0..id]) |*spawned| { | ||
| 587 | spawned.child.kill(io); | ||
| 588 | spawned.progress_node.end(); | ||
| 589 | }; | ||
| 590 | instance.child = try process.spawn(io, spawn_options); | ||
| 591 | instance.progress_node = progress_node.start("starting fuzzer", 0); | ||
| 592 | } | ||
| 593 | |||
| 594 | return .{ | ||
| 595 | .run = run, | ||
| 596 | .ctx = ctx, | ||
| 597 | .coverage_id = null, | ||
| 598 | |||
| 599 | .instances = instances, | ||
| 600 | .batch = .init(batch_storage), | ||
| 601 | .pending_broadcasts = .empty, | ||
| 602 | .broadcast = .empty, | ||
| 603 | .broadcast_undelivered = 0, | ||
| 604 | }; | ||
| 605 | } | ||
| 606 | |||
| 607 | fn deinit(f: *FuzzTestRunner) void { | ||
| 608 | const step_owner = f.run.step.owner; | ||
| 609 | const gpa = step_owner.allocator; | ||
| 610 | const io = step_owner.graph.io; | ||
| 611 | |||
| 612 | f.batch.cancel(io); | ||
| 613 | gpa.free(f.batch.storage); | ||
| 614 | var total_rss: usize = 0; | ||
| 615 | for (f.instances) |*instance| { | ||
| 616 | instance.child.kill(io); | ||
| 617 | instance.message.deinit(gpa); | ||
| 618 | instance.stderr.deinit(gpa); | ||
| 619 | instance.progress_node.end(); | ||
| 620 | total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 621 | } | ||
| 622 | f.run.step.result_peak_rss = @max(f.run.step.result_peak_rss, total_rss); | ||
| 623 | gpa.free(f.instances); | ||
| 624 | } | ||
| 625 | |||
| 626 | fn startInstances(f: *FuzzTestRunner) !void { | ||
| 627 | const step_owner = f.run.step.owner; | ||
| 628 | const io = step_owner.graph.io; | ||
| 629 | |||
| 630 | for (0.., f.instances) |id, *instance| { | ||
| 631 | const id32: u32 = @intCast(id); | ||
| 632 | (switch (f.ctx.fuzz.mode) { | ||
| 633 | .forever => sendRunFuzzTestMessage( | ||
| 634 | io, | ||
| 635 | instance.child.stdin.?, | ||
| 636 | f.run.fuzz_tests.items, | ||
| 637 | .forever, | ||
| 638 | id32, | ||
| 639 | ), | ||
| 640 | .limit => |limit| sendRunFuzzTestMessage( | ||
| 641 | io, | ||
| 642 | instance.child.stdin.?, | ||
| 643 | f.run.fuzz_tests.items, | ||
| 644 | .iterations, | ||
| 645 | limit.amount, | ||
| 646 | ), | ||
| 647 | }) catch |write_err| { | ||
| 648 | // The runner unexpectedly closed stdin, which means it crashed during initialization. | ||
| 649 | // Clean up everything and wait for the child to exit. | ||
| 650 | instance.child.stdin.?.close(io); | ||
| 651 | instance.child.stdin = null; | ||
| 652 | const term = try instance.child.wait(io); | ||
| 653 | return f.run.step.fail( | ||
| 654 | "unable to write stdin ({t}); test process unexpectedly {f}", | ||
| 655 | .{ write_err, fmtTerm(term) }, | ||
| 656 | ); | ||
| 657 | }; | ||
| 658 | |||
| 659 | try f.addStdoutRead(id32, @sizeOf(InHeader)); | ||
| 660 | try f.addStderrRead(id32); | ||
| 661 | } | ||
| 662 | } | ||
| 663 | |||
| 664 | fn listen(f: *FuzzTestRunner) !void { | ||
| 665 | const step_owner = f.run.step.owner; | ||
| 666 | const io = step_owner.graph.io; | ||
| 667 | |||
| 668 | while (true) { | ||
| 669 | try f.batch.awaitConcurrent(io, .none); | ||
| 670 | while (f.batch.next()) |completion| { | ||
| 671 | const id = completion.index / 3; | ||
| 672 | const result = completion.result; | ||
| 673 | switch (completion.index % 3) { | ||
| 674 | 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) { | ||
| 675 | // Avoid calling `instanceEos` until EndOfStream is seen with stderr so | ||
| 676 | // that all stderr is collected. | ||
| 677 | error.BrokenPipe => continue, | ||
| 678 | else => |write_e| return write_e, | ||
| 679 | }), | ||
| 680 | 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) { | ||
| 681 | // Avoid calling `instanceEos` until EndOfStream is seen with stderr so | ||
| 682 | // that all stderr is collected. | ||
| 683 | error.EndOfStream => continue, | ||
| 684 | else => |read_e| return read_e, | ||
| 685 | }), | ||
| 686 | 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) { | ||
| 687 | error.EndOfStream => return f.instanceEos(id), | ||
| 688 | else => |read_e| return read_e, | ||
| 689 | }), | ||
| 690 | else => unreachable, | ||
| 691 | } | ||
| 692 | } | ||
| 693 | } | ||
| 694 | } | ||
| 695 | |||
| 696 | fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void { | ||
| 697 | const step_owner = f.run.step.owner; | ||
| 698 | const gpa = step_owner.allocator; | ||
| 699 | const io = step_owner.graph.io; | ||
| 700 | const instance = &f.instances[id]; | ||
| 701 | |||
| 702 | instance.message.items.len += n; | ||
| 703 | const total_read = instance.message.items.len; | ||
| 704 | if (total_read < @sizeOf(InHeader)) { | ||
| 705 | try f.addStdoutRead(id, @sizeOf(InHeader)); | ||
| 706 | return; | ||
| 707 | } | ||
| 708 | |||
| 709 | const header = instance.messageHeader(); | ||
| 710 | const body = instance.message.items[@sizeOf(InHeader)..]; | ||
| 711 | if (body.len != header.bytes_len) { | ||
| 712 | try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len); | ||
| 713 | return; | ||
| 714 | } | ||
| 715 | |||
| 716 | switch (header.tag) { | ||
| 717 | .zig_version => { | ||
| 718 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) return f.run.step.fail( | ||
| 719 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", | ||
| 720 | .{ builtin.zig_version_string, body }, | ||
| 721 | ); | ||
| 722 | }, | ||
| 723 | .coverage_id => { | ||
| 724 | var body_r: Io.Reader = .fixed(body); | ||
| 725 | f.coverage_id = body_r.takeInt(u64, .little) catch unreachable; | ||
| 726 | const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable; | ||
| 727 | const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable; | ||
| 728 | const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable; | ||
| 729 | |||
| 730 | const fuzz = f.ctx.fuzz; | ||
| 731 | fuzz.queue_mutex.lockUncancelable(io); | ||
| 732 | defer fuzz.queue_mutex.unlock(io); | ||
| 733 | try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{ | ||
| 734 | .id = f.coverage_id.?, | ||
| 735 | .cumulative = .{ | ||
| 736 | .runs = cumulative_runs, | ||
| 737 | .unique = cumulative_unique, | ||
| 738 | .coverage = cumulative_coverage, | ||
| 739 | }, | ||
| 740 | .run = f.run, | ||
| 741 | } }); | ||
| 742 | fuzz.queue_cond.signal(io); | ||
| 743 | }, | ||
| 744 | .fuzz_start_addr => { | ||
| 745 | var body_r: Io.Reader = .fixed(body); | ||
| 746 | const fuzz = f.ctx.fuzz; | ||
| 747 | const addr = body_r.takeInt(u64, .little) catch unreachable; | ||
| 748 | |||
| 749 | fuzz.queue_mutex.lockUncancelable(io); | ||
| 750 | defer fuzz.queue_mutex.unlock(io); | ||
| 751 | try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{ | ||
| 752 | .addr = addr, | ||
| 753 | .coverage_id = f.coverage_id.?, | ||
| 754 | } }); | ||
| 755 | fuzz.queue_cond.signal(io); | ||
| 756 | }, | ||
| 757 | .fuzz_test_change => { | ||
| 758 | const test_i = std.mem.readInt(u32, body[0..4], .little); | ||
| 759 | instance.progress_node.setName(f.run.fuzz_tests.items[test_i]); | ||
| 760 | }, | ||
| 761 | .broadcast_fuzz_input => { | ||
| 762 | if (f.instances.len == 1) { | ||
| 763 | // No other processes to broadcast to. | ||
| 764 | } else if (f.broadcast_undelivered == 0) { | ||
| 765 | try f.instanceBroadcast(id, body); | ||
| 766 | } else { | ||
| 767 | const footer: PendingBroadcastFooter = .{ | ||
| 768 | .from_id = id, | ||
| 769 | .body_len = @intCast(body.len), | ||
| 770 | }; | ||
| 771 | // There is another broadcast in progress so add this one to the queue. | ||
| 772 | const size = @sizeOf(PendingBroadcastFooter) + body.len; | ||
| 773 | try f.pending_broadcasts.ensureUnusedCapacity(gpa, size); | ||
| 774 | f.pending_broadcasts.appendSliceAssumeCapacity(body); | ||
| 775 | f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer)); | ||
| 776 | } | ||
| 777 | }, | ||
| 778 | else => {}, // ignore other messages | ||
| 779 | } | ||
| 780 | |||
| 781 | instance.message.clearRetainingCapacity(); | ||
| 782 | try f.addStdoutRead(id, @sizeOf(InHeader)); | ||
| 783 | } | ||
| 784 | |||
| 785 | fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void { | ||
| 786 | const instance = &f.instances[id]; | ||
| 787 | instance.stderr.items.len += n; | ||
| 788 | try f.addStderrRead(id); | ||
| 789 | } | ||
| 790 | |||
| 791 | fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void { | ||
| 792 | const instance = &f.instances[id]; | ||
| 793 | |||
| 794 | instance.broadcast_written += n; | ||
| 795 | if (instance.broadcast_written == f.broadcast.items.len) { | ||
| 796 | f.broadcast_undelivered -= 1; | ||
| 797 | if (f.broadcast_undelivered == 0) { | ||
| 798 | try f.broadcastComplete(); | ||
| 799 | } | ||
| 800 | } else { | ||
| 801 | f.addStdinWrite(id); | ||
| 802 | } | ||
| 803 | } | ||
| 804 | |||
| 805 | fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void { | ||
| 806 | const step_owner = f.run.step.owner; | ||
| 807 | const gpa = step_owner.allocator; | ||
| 808 | const instance = &f.instances[id]; | ||
| 809 | |||
| 810 | try instance.message.ensureTotalCapacity(gpa, end); | ||
| 811 | const start = instance.message.items.len; | ||
| 812 | instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]}; | ||
| 813 | f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{ | ||
| 814 | .file = instance.child.stdout.?, | ||
| 815 | .data = &instance.stdout_vec, | ||
| 816 | } }); | ||
| 817 | } | ||
| 818 | |||
| 819 | fn addStderrRead(f: *FuzzTestRunner, id: u32) !void { | ||
| 820 | const step_owner = f.run.step.owner; | ||
| 821 | const gpa = step_owner.allocator; | ||
| 822 | const instance = &f.instances[id]; | ||
| 823 | |||
| 824 | try instance.stderr.ensureUnusedCapacity(gpa, 1); | ||
| 825 | instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()}; | ||
| 826 | f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{ | ||
| 827 | .file = instance.child.stderr.?, | ||
| 828 | .data = &instance.stderr_vec, | ||
| 829 | } }); | ||
| 830 | } | ||
| 831 | |||
| 832 | fn addStdinWrite(f: *FuzzTestRunner, id: u32) void { | ||
| 833 | const instance = &f.instances[id]; | ||
| 834 | |||
| 835 | assert(f.broadcast.items.len != instance.broadcast_written); | ||
| 836 | instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]}; | ||
| 837 | f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{ | ||
| 838 | .file = instance.child.stdin.?, | ||
| 839 | .data = &instance.stdin_vec, | ||
| 840 | } }); | ||
| 841 | } | ||
| 842 | |||
| 843 | fn instanceEos(f: *FuzzTestRunner, id: u32) !void { | ||
| 844 | const step_owner = f.run.step.owner; | ||
| 845 | const io = step_owner.graph.io; | ||
| 846 | const instance = &f.instances[id]; | ||
| 847 | |||
| 848 | instance.child.stdin.?.close(io); | ||
| 849 | instance.child.stdin = null; | ||
| 850 | const term = try instance.child.wait(io); | ||
| 851 | if (!termMatches(.{ .exited = 0 }, term)) { | ||
| 852 | f.run.step.result_stderr = try f.mergedStderr(); | ||
| 853 | try f.saveCrash(id, term); | ||
| 854 | return f.run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); | ||
| 855 | } | ||
| 856 | } | ||
| 857 | |||
| 858 | fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void { | ||
| 859 | const step = &f.run.step; | ||
| 860 | const b = step.owner; | ||
| 861 | const io = b.graph.io; | ||
| 862 | |||
| 863 | if (f.coverage_id == null) return; | ||
| 864 | |||
| 865 | // Search for the input file corresponding to the instance | ||
| 866 | const InputHeader = Build.abi.fuzz.MmapInputHeader; | ||
| 867 | var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined; | ||
| 868 | var in_r: Io.File.Reader = undefined; | ||
| 869 | var in_f: Io.File = undefined; | ||
| 870 | var in_name_buf: [12]u8 = undefined; | ||
| 871 | var in_name: []const u8 = undefined; | ||
| 872 | var i: u32 = 0; | ||
| 873 | const header: InputHeader = while (true) : ({ | ||
| 874 | if (i == std.math.maxInt(u32)) return; | ||
| 875 | i += 1; | ||
| 876 | }) { | ||
| 877 | const name_prefix = "f" ++ Io.Dir.path.sep_str ++ "in"; | ||
| 878 | in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable; | ||
| 879 | in_f = b.cache_root.handle.openFile(io, in_name, .{ | ||
| 880 | .lock = .exclusive, | ||
| 881 | .lock_nonblocking = true, | ||
| 882 | }) catch |e| switch (e) { | ||
| 883 | error.FileNotFound => return, | ||
| 884 | error.WouldBlock => continue, // Can not be from | ||
| 885 | // the crashed instance since it is still locked. | ||
| 886 | else => return step.fail("failed to open file '{f}{s}': {t}", .{ | ||
| 887 | b.cache_root, in_name, e, | ||
| 888 | }), | ||
| 889 | }; | ||
| 890 | |||
| 891 | in_r = in_f.readerStreaming(io, &in_r_buf); | ||
| 892 | const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| { | ||
| 893 | in_f.close(io); | ||
| 894 | switch (e) { | ||
| 895 | error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{ | ||
| 896 | b.cache_root, in_name, in_r.err.?, | ||
| 897 | }), | ||
| 898 | error.EndOfStream => continue, | ||
| 899 | } | ||
| 900 | }; | ||
| 901 | |||
| 902 | if (header.pc_digest == f.coverage_id.? and | ||
| 903 | header.instance_id == id and | ||
| 904 | header.test_i < f.run.fuzz_tests.items.len) | ||
| 905 | { | ||
| 906 | break header; | ||
| 907 | } | ||
| 908 | |||
| 909 | in_f.close(io); | ||
| 910 | }; | ||
| 911 | defer in_f.close(io); | ||
| 912 | |||
| 913 | // Save it to a seperate file | ||
| 914 | const crash_name = "f" ++ Io.Dir.path.sep_str ++ "crash"; | ||
| 915 | const out = b.cache_root.handle.createFile(io, crash_name, .{ | ||
| 916 | .lock = .exclusive, // Multiple run steps could have found a crash at the same time | ||
| 917 | }) catch |e| return step.fail("failed to create file '{f}{s}': {t}", .{ | ||
| 918 | b.cache_root, crash_name, e, | ||
| 919 | }); | ||
| 920 | defer out.close(io); | ||
| 921 | |||
| 922 | var out_w_buf: [512]u8 = undefined; | ||
| 923 | var out_w = out.writerStreaming(io, &out_w_buf); | ||
| 924 | _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) { | ||
| 925 | error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{ | ||
| 926 | b.cache_root, in_name, in_r.err.?, | ||
| 927 | }), | ||
| 928 | error.WriteFailed => return step.fail("failed to write file '{f}{s}': {t}", .{ | ||
| 929 | b.cache_root, crash_name, out_w.err.?, | ||
| 930 | }), | ||
| 931 | }; | ||
| 932 | |||
| 933 | return f.run.step.fail("test '{s}' {f}; input saved to '{f}{s}'", .{ | ||
| 934 | f.run.fuzz_tests.items[header.test_i], | ||
| 935 | fmtTerm(term), | ||
| 936 | b.cache_root, | ||
| 937 | crash_name, | ||
| 938 | }); | ||
| 939 | } | ||
| 940 | |||
| 941 | fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void { | ||
| 942 | assert(f.instances.len > 1); | ||
| 943 | assert(f.broadcast_undelivered == 0); // no other broadcast is progress | ||
| 944 | assert(f.broadcast.items.len == 0); | ||
| 945 | assert(from_id < f.instances.len); | ||
| 946 | |||
| 947 | const step_owner = f.run.step.owner; | ||
| 948 | const gpa = step_owner.allocator; | ||
| 949 | |||
| 950 | var out_header: OutHeader = .{ | ||
| 951 | .tag = .new_fuzz_input, | ||
| 952 | .bytes_len = @intCast(bytes.len), | ||
| 953 | }; | ||
| 954 | if (std.builtin.Endian.native != .little) { | ||
| 955 | std.mem.byteSwapAllFields(OutHeader, &out_header); | ||
| 956 | } | ||
| 957 | try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len); | ||
| 958 | f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header)); | ||
| 959 | f.broadcast.appendSliceAssumeCapacity(bytes); | ||
| 960 | |||
| 961 | f.broadcast_undelivered = @intCast(f.instances.len - 1); | ||
| 962 | for (0.., f.instances) |to_id, *instance| { | ||
| 963 | if (to_id == from_id) continue; | ||
| 964 | instance.broadcast_written = 0; | ||
| 965 | f.addStdinWrite(@intCast(to_id)); | ||
| 966 | } | ||
| 967 | } | ||
| 968 | |||
| 969 | fn broadcastComplete(f: *FuzzTestRunner) !void { | ||
| 970 | assert(f.instances.len > 1); | ||
| 971 | assert(f.broadcast_undelivered == 0); | ||
| 972 | f.broadcast.clearRetainingCapacity(); | ||
| 973 | |||
| 974 | const pending = &f.pending_broadcasts; | ||
| 975 | if (pending.items.len != 0) { | ||
| 976 | // Another broadcast is pending; copy it over to `broadcast` | ||
| 977 | |||
| 978 | const footer_len = @sizeOf(PendingBroadcastFooter); | ||
| 979 | const footer_bytes = pending.items[pending.items.len - footer_len ..]; | ||
| 980 | const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes); | ||
| 981 | pending.items.len -= footer_len; | ||
| 982 | |||
| 983 | const body = pending.items[pending.items.len - footer.body_len ..]; | ||
| 984 | try f.instanceBroadcast(footer.from_id, body); | ||
| 985 | pending.items.len -= body.len; | ||
| 986 | } | ||
| 987 | } | ||
| 988 | |||
| 989 | fn mergedStderr(f: *FuzzTestRunner) std.mem.Allocator.Error![]const u8 { | ||
| 990 | const step_owner = f.run.step.owner; | ||
| 991 | const arena = step_owner.allocator; | ||
| 992 | |||
| 993 | // Collect any available stderr | ||
| 994 | while (f.batch.next()) |completion| { | ||
| 995 | if (completion.index % 3 != 2) continue; | ||
| 996 | const len = completion.result.file_read_streaming catch continue; | ||
| 997 | f.instances[completion.index / 3].stderr.items.len += len; | ||
| 998 | } | ||
| 999 | |||
| 1000 | var stderr_len: usize = 0; | ||
| 1001 | for (f.instances) |*instance| stderr_len += instance.stderr.items.len; | ||
| 1002 | const stderr = try arena.alloc(u8, stderr_len); | ||
| 1003 | |||
| 1004 | stderr_len = 0; | ||
| 1005 | for (f.instances) |*instance| { | ||
| 1006 | @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items); | ||
| 1007 | stderr_len += instance.stderr.items.len; | ||
| 1008 | } | ||
| 1009 | return stderr; | ||
| 1010 | } | ||
| 1011 | }; | ||
| 1012 | |||
| 1013 | fn evalFuzzTest( | ||
| 1014 | run: *Run, | ||
| 1015 | spawn_options: process.SpawnOptions, | ||
| 1016 | options: Step.MakeOptions, | ||
| 1017 | fuzz_context: FuzzContext, | ||
| 1018 | ) !void { | ||
| 1019 | var f: FuzzTestRunner = try .init(run, fuzz_context, options.progress_node, spawn_options); | ||
| 1020 | defer f.deinit(); | ||
| 1021 | try f.startInstances(); | ||
| 1022 | try f.listen(); | ||
| 1023 | } | ||
| 1024 | |||
| 1025 | const StdioPollEnum = enum { stdout, stderr }; | ||
| 1026 | |||
| 1027 | fn evalZigTest( | ||
| 1028 | run: *Run, | ||
| 1029 | spawn_options: process.SpawnOptions, | ||
| 1030 | options: Step.MakeOptions, | ||
| 1031 | fuzz_context: ?FuzzContext, | ||
| 1032 | ) !void { | ||
| 1033 | if (fuzz_context != null) { | ||
| 1034 | try evalFuzzTest(run, spawn_options, options, fuzz_context.?); | ||
| 1035 | return; | ||
| 1036 | } | ||
| 1037 | |||
| 1038 | const step_owner = run.step.owner; | ||
| 1039 | const gpa = step_owner.allocator; | ||
| 1040 | const arena = step_owner.allocator; | ||
| 1041 | const io = step_owner.graph.io; | ||
| 1042 | |||
| 1043 | // We will update this every time a child runs. | ||
| 1044 | run.step.result_peak_rss = 0; | ||
| 1045 | |||
| 1046 | var test_results: Step.TestResults = .{ | ||
| 1047 | .test_count = 0, | ||
| 1048 | .skip_count = 0, | ||
| 1049 | .fail_count = 0, | ||
| 1050 | .crash_count = 0, | ||
| 1051 | .timeout_count = 0, | ||
| 1052 | .leak_count = 0, | ||
| 1053 | .log_err_count = 0, | ||
| 1054 | }; | ||
| 1055 | var test_metadata: ?TestMetadata = null; | ||
| 1056 | |||
| 1057 | while (true) { | ||
| 1058 | var child = try process.spawn(io, spawn_options); | ||
| 1059 | var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; | ||
| 1060 | var multi_reader: Io.File.MultiReader = undefined; | ||
| 1061 | multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? }); | ||
| 1062 | var child_killed = false; | ||
| 1063 | defer if (!child_killed) { | ||
| 1064 | child.kill(io); | ||
| 1065 | multi_reader.deinit(); | ||
| 1066 | run.step.result_peak_rss = @max( | ||
| 1067 | run.step.result_peak_rss, | ||
| 1068 | child.resource_usage_statistics.getMaxRss() orelse 0, | ||
| 1069 | ); | ||
| 1070 | }; | ||
| 1071 | |||
| 1072 | switch (try waitZigTest( | ||
| 1073 | run, | ||
| 1074 | &child, | ||
| 1075 | options, | ||
| 1076 | &multi_reader, | ||
| 1077 | &test_metadata, | ||
| 1078 | &test_results, | ||
| 1079 | )) { | ||
| 1080 | .write_failed => |err| { | ||
| 1081 | // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured | ||
| 1082 | // all available stderr to make our error output as useful as possible. | ||
| 1083 | const stderr_fr = multi_reader.fileReader(1); | ||
| 1084 | while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) { | ||
| 1085 | error.ReadFailed => return stderr_fr.err.?, | ||
| 1086 | error.EndOfStream => {}, | ||
| 1087 | } | ||
| 1088 | run.step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered()); | ||
| 1089 | |||
| 1090 | // Clean up everything and wait for the child to exit. | ||
| 1091 | child.stdin.?.close(io); | ||
| 1092 | child.stdin = null; | ||
| 1093 | multi_reader.deinit(); | ||
| 1094 | child_killed = true; | ||
| 1095 | const term = try child.wait(io); | ||
| 1096 | run.step.result_peak_rss = @max( | ||
| 1097 | run.step.result_peak_rss, | ||
| 1098 | child.resource_usage_statistics.getMaxRss() orelse 0, | ||
| 1099 | ); | ||
| 1100 | |||
| 1101 | // The individual unit test results are irrelevant: the test runner itself broke! | ||
| 1102 | // Fail immediately without populating `s.test_results`. | ||
| 1103 | return run.step.fail("unable to write stdin ({t}); test process unexpectedly {f}", .{ err, fmtTerm(term) }); | ||
| 1104 | }, | ||
| 1105 | .no_poll => |no_poll| { | ||
| 1106 | // This might be a success (we requested exit and the child dutifully closed stdout) or | ||
| 1107 | // a crash of some kind. Either way, the child will terminate by itself -- wait for it. | ||
| 1108 | const stderr_reader = multi_reader.reader(1); | ||
| 1109 | const stderr_owned = try arena.dupe(u8, stderr_reader.buffered()); | ||
| 1110 | |||
| 1111 | // Clean up everything and wait for the child to exit. | ||
| 1112 | child.stdin.?.close(io); | ||
| 1113 | child.stdin = null; | ||
| 1114 | multi_reader.deinit(); | ||
| 1115 | child_killed = true; | ||
| 1116 | const term = try child.wait(io); | ||
| 1117 | run.step.result_peak_rss = @max( | ||
| 1118 | run.step.result_peak_rss, | ||
| 1119 | child.resource_usage_statistics.getMaxRss() orelse 0, | ||
| 1120 | ); | ||
| 1121 | |||
| 1122 | if (no_poll.active_test_index) |test_index| { | ||
| 1123 | // A test was running, so this is definitely a crash. Report it against that | ||
| 1124 | // test, and continue to the next test. | ||
| 1125 | test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed; | ||
| 1126 | test_results.crash_count += 1; | ||
| 1127 | try run.step.addError("'{s}' {f}{s}{s}", .{ | ||
| 1128 | test_metadata.?.testName(test_index), | ||
| 1129 | fmtTerm(term), | ||
| 1130 | if (stderr_owned.len != 0) " with stderr:\n" else "", | ||
| 1131 | std.mem.trim(u8, stderr_owned, "\n"), | ||
| 1132 | }); | ||
| 1133 | continue; | ||
| 1134 | } | ||
| 1135 | |||
| 1136 | // Report an error if the child terminated uncleanly or if we were still trying to run more tests. | ||
| 1137 | run.step.result_stderr = stderr_owned; | ||
| 1138 | const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); | ||
| 1139 | if (!tests_done or !termMatches(.{ .exited = 0 }, term)) { | ||
| 1140 | // The individual unit test results are irrelevant: the test runner itself broke! | ||
| 1141 | // Fail immediately without populating `s.test_results`. | ||
| 1142 | return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); | ||
| 1143 | } | ||
| 1144 | |||
| 1145 | // We're done with all of the tests! Commit the test results and return. | ||
| 1146 | run.step.test_results = test_results; | ||
| 1147 | if (test_metadata) |tm| { | ||
| 1148 | run.cached_test_metadata = tm.toCachedTestMetadata(); | ||
| 1149 | if (options.web_server) |ws| { | ||
| 1150 | if (run.step.owner.graph.time_report) { | ||
| 1151 | ws.updateTimeReportRunTest( | ||
| 1152 | run, | ||
| 1153 | &run.cached_test_metadata.?, | ||
| 1154 | tm.ns_per_test, | ||
| 1155 | ); | ||
| 1156 | } | ||
| 1157 | } | ||
| 1158 | } | ||
| 1159 | return; | ||
| 1160 | }, | ||
| 1161 | .timeout => |timeout| { | ||
| 1162 | const stderr_reader = multi_reader.reader(1); | ||
| 1163 | const stderr = stderr_reader.buffered(); | ||
| 1164 | stderr_reader.tossBuffered(); | ||
| 1165 | if (timeout.active_test_index) |test_index| { | ||
| 1166 | // A test was running. Report the timeout against that test, and continue on to | ||
| 1167 | // the next test. | ||
| 1168 | test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed; | ||
| 1169 | test_results.timeout_count += 1; | ||
| 1170 | try run.step.addError("'{s}' timed out after {f}{s}{s}", .{ | ||
| 1171 | test_metadata.?.testName(test_index), | ||
| 1172 | Io.Duration{ .nanoseconds = timeout.ns_elapsed }, | ||
| 1173 | if (stderr.len != 0) " with stderr:\n" else "", | ||
| 1174 | std.mem.trim(u8, stderr, "\n"), | ||
| 1175 | }); | ||
| 1176 | continue; | ||
| 1177 | } | ||
| 1178 | // Just log an error and let the child be killed. | ||
| 1179 | run.step.result_stderr = try arena.dupe(u8, stderr); | ||
| 1180 | // The individual unit test results in `results` are irrelevant: the test runner | ||
| 1181 | // is broken! Fail immediately without populating `s.test_results`. | ||
| 1182 | return run.step.fail("test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }}); | ||
| 1183 | }, | ||
| 1184 | } | ||
| 1185 | comptime unreachable; | ||
| 1186 | } | ||
| 1187 | } | ||
| 1188 | |||
| 1189 | const TestMetadata = struct { | ||
| 1190 | names: []const u32, | ||
| 1191 | ns_per_test: []u64, | ||
| 1192 | expected_panic_msgs: []const u32, | ||
| 1193 | string_bytes: []const u8, | ||
| 1194 | next_index: u32, | ||
| 1195 | prog_node: std.Progress.Node, | ||
| 1196 | |||
| 1197 | fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { | ||
| 1198 | return .{ | ||
| 1199 | .names = tm.names, | ||
| 1200 | .string_bytes = tm.string_bytes, | ||
| 1201 | }; | ||
| 1202 | } | ||
| 1203 | |||
| 1204 | fn testName(tm: TestMetadata, index: u32) []const u8 { | ||
| 1205 | return tm.toCachedTestMetadata().testName(index); | ||
| 1206 | } | ||
| 1207 | }; | ||
| 1208 | |||
| 1209 | pub const CachedTestMetadata = struct { | ||
| 1210 | names: []const u32, | ||
| 1211 | string_bytes: []const u8, | ||
| 1212 | |||
| 1213 | pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 { | ||
| 1214 | return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0); | ||
| 1215 | } | ||
| 1216 | }; | ||
| 1217 | |||
| 1218 | fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { | ||
| 1219 | while (metadata.next_index < metadata.names.len) { | ||
| 1220 | const i = metadata.next_index; | ||
| 1221 | metadata.next_index += 1; | ||
| 1222 | |||
| 1223 | if (metadata.expected_panic_msgs[i] != 0) continue; | ||
| 1224 | |||
| 1225 | const name = metadata.testName(i); | ||
| 1226 | if (sub_prog_node.*) |n| n.end(); | ||
| 1227 | sub_prog_node.* = metadata.prog_node.start(name, 0); | ||
| 1228 | |||
| 1229 | try sendRunTestMessage(io, in, .run_test, i); | ||
| 1230 | return; | ||
| 1231 | } else { | ||
| 1232 | metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done | ||
| 1233 | try sendMessage(io, in, .exit); | ||
| 1234 | } | ||
| 1235 | } | ||
| 1236 | |||
| 1237 | fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { | ||
| 1238 | const header: std.zig.Client.Message.Header = .{ | ||
| 1239 | .tag = tag, | ||
| 1240 | .bytes_len = 0, | ||
| 1241 | }; | ||
| 1242 | var w = file.writerStreaming(io, &.{}); | ||
| 1243 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 1244 | error.WriteFailed => return w.err.?, | ||
| 1245 | }; | ||
| 1246 | } | ||
| 1247 | |||
| 1248 | fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void { | ||
| 1249 | const header: std.zig.Client.Message.Header = .{ | ||
| 1250 | .tag = tag, | ||
| 1251 | .bytes_len = 4, | ||
| 1252 | }; | ||
| 1253 | var w = file.writerStreaming(io, &.{}); | ||
| 1254 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 1255 | error.WriteFailed => return w.err.?, | ||
| 1256 | }; | ||
| 1257 | w.interface.writeInt(u32, index, .little) catch |err| switch (err) { | ||
| 1258 | error.WriteFailed => return w.err.?, | ||
| 1259 | }; | ||
| 1260 | } | ||
| 1261 | |||
| 1262 | fn sendRunFuzzTestMessage( | ||
| 1263 | io: Io, | ||
| 1264 | file: Io.File, | ||
| 1265 | test_names: []const []const u8, | ||
| 1266 | kind: std.Build.abi.fuzz.LimitKind, | ||
| 1267 | amount_or_instance: u64, | ||
| 1268 | ) !void { | ||
| 1269 | const header: std.zig.Client.Message.Header = .{ | ||
| 1270 | .tag = .start_fuzzing, | ||
| 1271 | .bytes_len = 1 + 8 + 4 + count: { | ||
| 1272 | var c: u32 = @intCast(test_names.len * 4); | ||
| 1273 | for (test_names) |name| { | ||
| 1274 | c += @intCast(name.len); | ||
| 1275 | } | ||
| 1276 | break :count c; | ||
| 1277 | }, | ||
| 1278 | }; | ||
| 1279 | var w = file.writerStreaming(io, &.{}); | ||
| 1280 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 1281 | error.WriteFailed => return w.err.?, | ||
| 1282 | }; | ||
| 1283 | w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) { | ||
| 1284 | error.WriteFailed => return w.err.?, | ||
| 1285 | }; | ||
| 1286 | w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) { | ||
| 1287 | error.WriteFailed => return w.err.?, | ||
| 1288 | }; | ||
| 1289 | w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) { | ||
| 1290 | error.WriteFailed => return w.err.?, | ||
| 1291 | }; | ||
| 1292 | for (test_names) |test_name| { | ||
| 1293 | w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) { | ||
| 1294 | error.WriteFailed => return w.err.?, | ||
| 1295 | }; | ||
| 1296 | w.interface.writeAll(test_name) catch |err| switch (err) { | ||
| 1297 | error.WriteFailed => return w.err.?, | ||
| 1298 | }; | ||
| 1299 | } | ||
| 1300 | } | ||
| 1301 | |||
| 1302 | fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult { | ||
| 1303 | const b = run.step.owner; | ||
| 1304 | const io = b.graph.io; | ||
| 1305 | const arena = b.allocator; | ||
| 1306 | const gpa = b.allocator; | ||
| 1307 | |||
| 1308 | var child = try process.spawn(io, spawn_options); | ||
| 1309 | defer child.kill(io); | ||
| 1310 | |||
| 1311 | switch (run.stdin) { | ||
| 1312 | .bytes => |bytes| { | ||
| 1313 | child.stdin.?.writeStreamingAll(io, bytes) catch |err| { | ||
| 1314 | return run.step.fail("unable to write stdin: {t}", .{err}); | ||
| 1315 | }; | ||
| 1316 | child.stdin.?.close(io); | ||
| 1317 | child.stdin = null; | ||
| 1318 | }, | ||
| 1319 | .lazy_path => |lazy_path| { | ||
| 1320 | const path = lazy_path.getPath3(b, &run.step); | ||
| 1321 | const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| { | ||
| 1322 | return run.step.fail("unable to open stdin file: {t}", .{err}); | ||
| 1323 | }; | ||
| 1324 | defer file.close(io); | ||
| 1325 | // TODO https://github.com/ziglang/zig/issues/23955 | ||
| 1326 | var read_buffer: [1024]u8 = undefined; | ||
| 1327 | var file_reader = file.reader(io, &read_buffer); | ||
| 1328 | var write_buffer: [1024]u8 = undefined; | ||
| 1329 | var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer); | ||
| 1330 | _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { | ||
| 1331 | error.ReadFailed => return run.step.fail("failed to read from {f}: {t}", .{ | ||
| 1332 | path, file_reader.err.?, | ||
| 1333 | }), | ||
| 1334 | error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{ | ||
| 1335 | stdin_writer.err.?, | ||
| 1336 | }), | ||
| 1337 | }; | ||
| 1338 | stdin_writer.interface.flush() catch |err| switch (err) { | ||
| 1339 | error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{ | ||
| 1340 | stdin_writer.err.?, | ||
| 1341 | }), | ||
| 1342 | }; | ||
| 1343 | child.stdin.?.close(io); | ||
| 1344 | child.stdin = null; | ||
| 1345 | }, | ||
| 1346 | .none => {}, | ||
| 1347 | } | ||
| 1348 | |||
| 1349 | var stdout_bytes: ?[]const u8 = null; | ||
| 1350 | var stderr_bytes: ?[]const u8 = null; | ||
| 1351 | |||
| 1352 | if (child.stdout) |stdout| { | ||
| 1353 | if (child.stderr) |stderr| { | ||
| 1354 | var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; | ||
| 1355 | var multi_reader: Io.File.MultiReader = undefined; | ||
| 1356 | multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr }); | ||
| 1357 | defer multi_reader.deinit(); | ||
| 1358 | |||
| 1359 | const stdout_reader = multi_reader.reader(0); | ||
| 1360 | const stderr_reader = multi_reader.reader(1); | ||
| 1361 | |||
| 1362 | while (multi_reader.fill(64, .none)) |_| { | ||
| 1363 | if (run.stdio_limit.toInt()) |limit| { | ||
| 1364 | if (stdout_reader.buffered().len > limit) | ||
| 1365 | return error.StdoutStreamTooLong; | ||
| 1366 | if (stderr_reader.buffered().len > limit) | ||
| 1367 | return error.StderrStreamTooLong; | ||
| 1368 | } | ||
| 1369 | } else |err| switch (err) { | ||
| 1370 | error.Timeout => unreachable, | ||
| 1371 | error.EndOfStream => {}, | ||
| 1372 | else => |e| return e, | ||
| 1373 | } | ||
| 1374 | |||
| 1375 | try multi_reader.checkAnyError(); | ||
| 1376 | |||
| 1377 | // TODO: this string can leak since alloc below can return error. | ||
| 1378 | stdout_bytes = try multi_reader.toOwnedSlice(0); | ||
| 1379 | // TODO: this string can leak since its allocated using gpa and `try child.wait(io)` below can fail. | ||
| 1380 | stderr_bytes = try multi_reader.toOwnedSlice(1); | ||
| 1381 | } else { | ||
| 1382 | var stdout_reader = stdout.readerStreaming(io, &.{}); | ||
| 1383 | stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { | ||
| 1384 | error.OutOfMemory => |e| return e, | ||
| 1385 | error.ReadFailed => return stdout_reader.err.?, | ||
| 1386 | error.StreamTooLong => return error.StdoutStreamTooLong, | ||
| 1387 | }; | ||
| 1388 | } | ||
| 1389 | } else if (child.stderr) |stderr| { | ||
| 1390 | var stderr_reader = stderr.readerStreaming(io, &.{}); | ||
| 1391 | stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { | ||
| 1392 | error.OutOfMemory => |e| return e, | ||
| 1393 | error.ReadFailed => return stderr_reader.err.?, | ||
| 1394 | error.StreamTooLong => return error.StderrStreamTooLong, | ||
| 1395 | }; | ||
| 1396 | } | ||
| 1397 | |||
| 1398 | if (stderr_bytes) |bytes| if (bytes.len > 0) { | ||
| 1399 | // Treat stderr as an error message. | ||
| 1400 | const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) { | ||
| 1401 | .check => |checks| !checksContainStderr(checks.items), | ||
| 1402 | else => true, | ||
| 1403 | }; | ||
| 1404 | if (stderr_is_diagnostic) { | ||
| 1405 | run.step.result_stderr = bytes; | ||
| 1406 | } | ||
| 1407 | }; | ||
| 1408 | |||
| 1409 | run.step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 1410 | |||
| 1411 | return .{ | ||
| 1412 | .term = try child.wait(io), | ||
| 1413 | .stdout = stdout_bytes, | ||
| 1414 | .stderr = stderr_bytes, | ||
| 1415 | }; | ||
| 1416 | } | ||
| 1417 | |||
| 1418 | const IndexedOutput = struct { | ||
| 1419 | index: usize, | ||
| 1420 | tag: @typeInfo(Arg).@"union".tag_type.?, | ||
| 1421 | output: *Output, | ||
| 1422 | }; | ||
| 1423 | |||
| 1424 | pub fn rerunInFuzzMode( | ||
| 1425 | run: *Run, | ||
| 1426 | fuzz: *std.Build.Fuzz, | ||
| 1427 | prog_node: std.Progress.Node, | ||
| 1428 | ) !void { | ||
| 1429 | const step = &run.step; | ||
| 1430 | const b = step.owner; | ||
| 1431 | const io = b.graph.io; | ||
| 1432 | const arena = b.allocator; | ||
| 1433 | var argv_list: std.ArrayList([]const u8) = .empty; | ||
| 1434 | for (run.argv.items) |arg| { | ||
| 1435 | switch (arg) { | ||
| 1436 | .bytes => |bytes| { | ||
| 1437 | try argv_list.append(arena, bytes); | ||
| 1438 | }, | ||
| 1439 | .lazy_path => |file| { | ||
| 1440 | const file_path = file.lazy_path.getPath3(b, step); | ||
| 1441 | try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) })); | ||
| 1442 | }, | ||
| 1443 | .decorated_directory => |dd| { | ||
| 1444 | const file_path = dd.lazy_path.getPath3(b, step); | ||
| 1445 | try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix })); | ||
| 1446 | }, | ||
| 1447 | .file_content => |file_plp| { | ||
| 1448 | const file_path = file_plp.lazy_path.getPath3(b, step); | ||
| 1449 | |||
| 1450 | var result: std.Io.Writer.Allocating = .init(arena); | ||
| 1451 | errdefer result.deinit(); | ||
| 1452 | result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory; | ||
| 1453 | |||
| 1454 | const file = try file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}); | ||
| 1455 | defer file.close(io); | ||
| 1456 | |||
| 1457 | var buf: [1024]u8 = undefined; | ||
| 1458 | var file_reader = file.reader(io, &buf); | ||
| 1459 | _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { | ||
| 1460 | error.ReadFailed => return file_reader.err.?, | ||
| 1461 | error.WriteFailed => return error.OutOfMemory, | ||
| 1462 | }; | ||
| 1463 | |||
| 1464 | try argv_list.append(arena, result.written()); | ||
| 1465 | }, | ||
| 1466 | .artifact => |pa| { | ||
| 1467 | const artifact = pa.artifact; | ||
| 1468 | const file_path: []const u8 = p: { | ||
| 1469 | if (artifact == run.producer.?) break :p b.fmt("{f}", .{run.rebuilt_executable.?}); | ||
| 1470 | break :p artifact.installed_path orelse artifact.generated_bin.?.path.?; | ||
| 1471 | }; | ||
| 1472 | try argv_list.append(arena, b.fmt("{s}{s}", .{ | ||
| 1473 | pa.prefix, | ||
| 1474 | run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }), | ||
| 1475 | })); | ||
| 1476 | }, | ||
| 1477 | .output_file, .output_directory => unreachable, | ||
| 1478 | } | ||
| 1479 | } | ||
| 1480 | |||
| 1481 | if (run.step.result_failed_command) |cmd| { | ||
| 1482 | fuzz.gpa.free(cmd); | ||
| 1483 | run.step.result_failed_command = null; | ||
| 1484 | } | ||
| 1485 | |||
| 1486 | const has_side_effects = false; | ||
| 1487 | var rand_int: u64 = undefined; | ||
| 1488 | io.random(@ptrCast(&rand_int)); | ||
| 1489 | const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); | ||
| 1490 | try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{ | ||
| 1491 | .progress_node = prog_node, | ||
| 1492 | .watch = undefined, // not used by `runCommand` | ||
| 1493 | .web_server = null, // only needed for time reports | ||
| 1494 | .unit_test_timeout_ns = null, // don't time out fuzz tests for now | ||
| 1495 | .gpa = fuzz.gpa, | ||
| 1496 | }, .{ | ||
| 1497 | .fuzz = fuzz, | ||
| 1498 | }); | ||
| 1499 | } | ||
| 1500 | |||
| 1501 | fn populateGeneratedPaths( | ||
| 1502 | arena: std.mem.Allocator, | ||
| 1503 | output_placeholders: []const IndexedOutput, | ||
| 1504 | captured_stdout: ?*CapturedStdIo, | ||
| 1505 | captured_stderr: ?*CapturedStdIo, | ||
| 1506 | cache_root: Cache.Directory, | ||
| 1507 | digest: *const Cache.HexDigest, | ||
| 1508 | ) !void { | ||
| 1509 | for (output_placeholders) |placeholder| { | ||
| 1510 | placeholder.output.generated_file.path = try cache_root.join(arena, &.{ | ||
| 1511 | "o", digest, placeholder.output.basename, | ||
| 1512 | }); | ||
| 1513 | } | ||
| 1514 | |||
| 1515 | if (captured_stdout) |captured| { | ||
| 1516 | captured.output.generated_file.path = try cache_root.join(arena, &.{ | ||
| 1517 | "o", digest, captured.output.basename, | ||
| 1518 | }); | ||
| 1519 | } | ||
| 1520 | |||
| 1521 | if (captured_stderr) |captured| { | ||
| 1522 | captured.output.generated_file.path = try cache_root.join(arena, &.{ | ||
| 1523 | "o", digest, captured.output.basename, | ||
| 1524 | }); | ||
| 1525 | } | ||
| 1526 | } | ||
| 1527 | |||
| 1528 | fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { | ||
| 1529 | if (term) |t| switch (t) { | ||
| 1530 | .exited => |code| try w.print("exited with code {d}", .{code}), | ||
| 1531 | .signal => |sig| try w.print("terminated with signal {t}", .{sig}), | ||
| 1532 | .stopped => |sig| try w.print("stopped with signal {t}", .{sig}), | ||
| 1533 | .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), | ||
| 1534 | } else { | ||
| 1535 | try w.writeAll("exited with any code"); | ||
| 1536 | } | ||
| 1537 | } | ||
| 1538 | fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) { | ||
| 1539 | return .{ .data = term }; | ||
| 1540 | } | ||
| 1541 | |||
| 1542 | const FuzzContext = struct { | ||
| 1543 | fuzz: *std.Build.Fuzz, | ||
| 1544 | }; | ||
| 1545 | |||
| 1546 | fn runCommand( | ||
| 1547 | run: *Run, | ||
| 1548 | argv: []const []const u8, | ||
| 1549 | has_side_effects: bool, | ||
| 1550 | output_dir_path: []const u8, | ||
| 1551 | options: Step.MakeOptions, | ||
| 1552 | fuzz_context: ?FuzzContext, | ||
| 1553 | ) !void { | ||
| 1554 | const step = &run.step; | ||
| 1555 | const b = step.owner; | ||
| 1556 | const arena = b.allocator; | ||
| 1557 | const gpa = options.gpa; | ||
| 1558 | const io = b.graph.io; | ||
| 1559 | |||
| 1560 | const cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, step) } else .inherit; | ||
| 1561 | |||
| 1562 | try step.handleChildProcUnsupported(); | ||
| 1563 | try Step.handleVerbose2(step.owner, cwd, run.environ_map, argv); | ||
| 1564 | |||
| 1565 | const allow_skip = switch (run.stdio) { | ||
| 1566 | .check, .zig_test => run.skip_foreign_checks, | ||
| 1567 | else => false, | ||
| 1568 | }; | ||
| 1569 | |||
| 1570 | var interp_argv = std.array_list.Managed([]const u8).init(b.allocator); | ||
| 1571 | defer interp_argv.deinit(); | ||
| 1572 | |||
| 1573 | var environ_map: EnvMap = env: { | ||
| 1574 | const orig = run.environ_map orelse &b.graph.environ_map; | ||
| 1575 | break :env try orig.clone(gpa); | ||
| 1576 | }; | ||
| 1577 | defer environ_map.deinit(); | ||
| 1578 | |||
| 1579 | const opt_generic_result = spawnChildAndCollect(run, argv, &environ_map, has_side_effects, options, fuzz_context) catch |err| term: { | ||
| 1580 | // InvalidExe: cpu arch mismatch | ||
| 1581 | // FileNotFound: can happen with a wrong dynamic linker path | ||
| 1582 | if (err == error.InvalidExe or err == error.FileNotFound) interpret: { | ||
| 1583 | // TODO: learn the target from the binary directly rather than from | ||
| 1584 | // relying on it being a Compile step. This will make this logic | ||
| 1585 | // work even for the edge case that the binary was produced by a | ||
| 1586 | // third party. | ||
| 1587 | const exe = switch (run.argv.items[0]) { | ||
| 1588 | .artifact => |exe| exe.artifact, | ||
| 1589 | else => break :interpret, | ||
| 1590 | }; | ||
| 1591 | switch (exe.kind) { | ||
| 1592 | .exe, .@"test" => {}, | ||
| 1593 | else => break :interpret, | ||
| 1594 | } | ||
| 1595 | |||
| 1596 | const root_target = exe.rootModuleTarget(); | ||
| 1597 | const need_cross_libc = exe.is_linking_libc and | ||
| 1598 | (root_target.isGnuLibC() or (root_target.isMuslLibC() and exe.linkage == .dynamic)); | ||
| 1599 | const other_target = exe.root_module.resolved_target.?.result; | ||
| 1600 | switch (std.zig.system.getExternalExecutor(io, &b.graph.host.result, &other_target, .{ | ||
| 1601 | .qemu_fixes_dl = need_cross_libc and b.libc_runtimes_dir != null, | ||
| 1602 | .link_libc = exe.is_linking_libc, | ||
| 1603 | })) { | ||
| 1604 | .native, .rosetta => { | ||
| 1605 | if (allow_skip) return error.MakeSkipped; | ||
| 1606 | break :interpret; | ||
| 1607 | }, | ||
| 1608 | .wine => |bin_name| { | ||
| 1609 | if (b.enable_wine) { | ||
| 1610 | try interp_argv.append(bin_name); | ||
| 1611 | try interp_argv.appendSlice(argv); | ||
| 1612 | |||
| 1613 | // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but | ||
| 1614 | // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired. | ||
| 1615 | if (environ_map.get("WINEDEBUG") == null) { | ||
| 1616 | try environ_map.put("WINEDEBUG", "-all"); | ||
| 1617 | } | ||
| 1618 | } else { | ||
| 1619 | return failForeign(run, "-fwine", argv[0], exe); | ||
| 1620 | } | ||
| 1621 | }, | ||
| 1622 | .qemu => |bin_name| { | ||
| 1623 | if (b.enable_qemu) { | ||
| 1624 | try interp_argv.append(bin_name); | ||
| 1625 | |||
| 1626 | if (need_cross_libc) { | ||
| 1627 | if (b.libc_runtimes_dir) |dir| { | ||
| 1628 | try interp_argv.append("-L"); | ||
| 1629 | try interp_argv.append(b.pathJoin(&.{ | ||
| 1630 | dir, | ||
| 1631 | try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple( | ||
| 1632 | b.allocator, | ||
| 1633 | root_target.cpu.arch, | ||
| 1634 | root_target.os.tag, | ||
| 1635 | root_target.abi, | ||
| 1636 | ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple( | ||
| 1637 | b.allocator, | ||
| 1638 | root_target.cpu.arch, | ||
| 1639 | root_target.abi, | ||
| 1640 | ) else unreachable, | ||
| 1641 | })); | ||
| 1642 | } else return failForeign(run, "--libc-runtimes", argv[0], exe); | ||
| 1643 | } | ||
| 1644 | |||
| 1645 | try interp_argv.appendSlice(argv); | ||
| 1646 | } else return failForeign(run, "-fqemu", argv[0], exe); | ||
| 1647 | }, | ||
| 1648 | .darling => |bin_name| { | ||
| 1649 | if (b.enable_darling) { | ||
| 1650 | try interp_argv.append(bin_name); | ||
| 1651 | try interp_argv.appendSlice(argv); | ||
| 1652 | } else { | ||
| 1653 | return failForeign(run, "-fdarling", argv[0], exe); | ||
| 1654 | } | ||
| 1655 | }, | ||
| 1656 | .wasmtime => |bin_name| { | ||
| 1657 | if (b.enable_wasmtime) { | ||
| 1658 | try interp_argv.append(bin_name); | ||
| 1659 | try interp_argv.append("--dir=."); | ||
| 1660 | // Wasmtime doeesn't inherit environment variables from the parent process | ||
| 1661 | // by default. '-S inherit-env' was added in Wasmtime version 20. | ||
| 1662 | try interp_argv.append("-Sinherit-env"); | ||
| 1663 | try interp_argv.append(argv[0]); | ||
| 1664 | try interp_argv.appendSlice(argv[1..]); | ||
| 1665 | } else { | ||
| 1666 | return failForeign(run, "-fwasmtime", argv[0], exe); | ||
| 1667 | } | ||
| 1668 | }, | ||
| 1669 | .bad_dl => |foreign_dl| { | ||
| 1670 | if (allow_skip) return error.MakeSkipped; | ||
| 1671 | |||
| 1672 | const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)"; | ||
| 1673 | |||
| 1674 | return step.fail( | ||
| 1675 | \\the host system is unable to execute binaries from the target | ||
| 1676 | \\ because the host dynamic linker is '{s}', | ||
| 1677 | \\ while the target dynamic linker is '{s}'. | ||
| 1678 | \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step | ||
| 1679 | , .{ host_dl, foreign_dl }); | ||
| 1680 | }, | ||
| 1681 | .bad_os_or_cpu => { | ||
| 1682 | if (allow_skip) return error.MakeSkipped; | ||
| 1683 | |||
| 1684 | const host_name = try b.graph.host.result.zigTriple(b.allocator); | ||
| 1685 | const foreign_name = try root_target.zigTriple(b.allocator); | ||
| 1686 | |||
| 1687 | return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{ | ||
| 1688 | host_name, foreign_name, | ||
| 1689 | }); | ||
| 1690 | }, | ||
| 1691 | } | ||
| 1692 | |||
| 1693 | if (root_target.os.tag == .windows) { | ||
| 1694 | // On Windows we don't have rpaths so we have to add .dll search paths to PATH | ||
| 1695 | run.addPathForDynLibs(exe); | ||
| 1696 | } | ||
| 1697 | |||
| 1698 | gpa.free(step.result_failed_command.?); | ||
| 1699 | step.result_failed_command = null; | ||
| 1700 | try Step.handleVerbose2(step.owner, cwd, run.environ_map, interp_argv.items); | ||
| 1701 | |||
| 1702 | break :term spawnChildAndCollect(run, interp_argv.items, &environ_map, has_side_effects, options, fuzz_context) catch |e| { | ||
| 1703 | if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped; | ||
| 1704 | if (e == error.MakeFailed) return error.MakeFailed; // error already reported | ||
| 1705 | return step.fail("unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e }); | ||
| 1706 | }; | ||
| 1707 | } | ||
| 1708 | if (err == error.MakeFailed) return error.MakeFailed; // error already reported | ||
| 1709 | |||
| 1710 | return step.fail("failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err }); | ||
| 1711 | }; | ||
| 1712 | |||
| 1713 | const generic_result = opt_generic_result orelse { | ||
| 1714 | assert(run.stdio == .zig_test); | ||
| 1715 | // Specific errors have already been reported, and test results are populated. All we need | ||
| 1716 | // to do is report step failure if any test failed. | ||
| 1717 | if (!step.test_results.isSuccess()) return error.MakeFailed; | ||
| 1718 | return; | ||
| 1719 | }; | ||
| 1720 | |||
| 1721 | assert(fuzz_context == null); | ||
| 1722 | assert(run.stdio != .zig_test); | ||
| 1723 | |||
| 1724 | // Capture stdout and stderr to GeneratedFile objects. | ||
| 1725 | const Stream = struct { | ||
| 1726 | captured: ?*CapturedStdIo, | ||
| 1727 | bytes: ?[]const u8, | ||
| 1728 | }; | ||
| 1729 | for ([_]Stream{ | ||
| 1730 | .{ | ||
| 1731 | .captured = run.captured_stdout, | ||
| 1732 | .bytes = generic_result.stdout, | ||
| 1733 | }, | ||
| 1734 | .{ | ||
| 1735 | .captured = run.captured_stderr, | ||
| 1736 | .bytes = generic_result.stderr, | ||
| 1737 | }, | ||
| 1738 | }) |stream| { | ||
| 1739 | if (stream.captured) |captured| { | ||
| 1740 | const output_components = .{ output_dir_path, captured.output.basename }; | ||
| 1741 | const output_path = try b.cache_root.join(arena, &output_components); | ||
| 1742 | captured.output.generated_file.path = output_path; | ||
| 1743 | |||
| 1744 | const sub_path = b.pathJoin(&output_components); | ||
| 1745 | const sub_path_dirname = Dir.path.dirname(sub_path).?; | ||
| 1746 | b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| { | ||
| 1747 | return step.fail("unable to make path '{f}{s}': {s}", .{ | ||
| 1748 | b.cache_root, sub_path_dirname, @errorName(err), | ||
| 1749 | }); | ||
| 1750 | }; | ||
| 1751 | const data = switch (captured.trim_whitespace) { | ||
| 1752 | .none => stream.bytes.?, | ||
| 1753 | .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace), | ||
| 1754 | .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace), | ||
| 1755 | .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace), | ||
| 1756 | }; | ||
| 1757 | b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = data }) catch |err| { | ||
| 1758 | return step.fail("unable to write file '{f}{s}': {s}", .{ | ||
| 1759 | b.cache_root, sub_path, @errorName(err), | ||
| 1760 | }); | ||
| 1761 | }; | ||
| 1762 | } | ||
| 1763 | } | ||
| 1764 | |||
| 1765 | switch (run.stdio) { | ||
| 1766 | .zig_test => unreachable, | ||
| 1767 | .check => |checks| for (checks.items) |check| switch (check) { | ||
| 1768 | .expect_stderr_exact => |expected_bytes| { | ||
| 1769 | if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) { | ||
| 1770 | return step.fail( | ||
| 1771 | \\========= expected this stderr: ========= | ||
| 1772 | \\{s} | ||
| 1773 | \\========= but found: ==================== | ||
| 1774 | \\{s} | ||
| 1775 | , .{ | ||
| 1776 | expected_bytes, | ||
| 1777 | generic_result.stderr.?, | ||
| 1778 | }); | ||
| 1779 | } | ||
| 1780 | }, | ||
| 1781 | .expect_stderr_match => |match| { | ||
| 1782 | if (mem.find(u8, generic_result.stderr.?, match) == null) { | ||
| 1783 | return step.fail( | ||
| 1784 | \\========= expected to find in stderr: ========= | ||
| 1785 | \\{s} | ||
| 1786 | \\========= but stderr does not contain it: ===== | ||
| 1787 | \\{s} | ||
| 1788 | , .{ | ||
| 1789 | match, | ||
| 1790 | generic_result.stderr.?, | ||
| 1791 | }); | ||
| 1792 | } | ||
| 1793 | }, | ||
| 1794 | .expect_stdout_exact => |expected_bytes| { | ||
| 1795 | if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) { | ||
| 1796 | return step.fail( | ||
| 1797 | \\========= expected this stdout: ========= | ||
| 1798 | \\{s} | ||
| 1799 | \\========= but found: ==================== | ||
| 1800 | \\{s} | ||
| 1801 | , .{ | ||
| 1802 | expected_bytes, | ||
| 1803 | generic_result.stdout.?, | ||
| 1804 | }); | ||
| 1805 | } | ||
| 1806 | }, | ||
| 1807 | .expect_stdout_match => |match| { | ||
| 1808 | if (mem.find(u8, generic_result.stdout.?, match) == null) { | ||
| 1809 | return step.fail( | ||
| 1810 | \\========= expected to find in stdout: ========= | ||
| 1811 | \\{s} | ||
| 1812 | \\========= but stdout does not contain it: ===== | ||
| 1813 | \\{s} | ||
| 1814 | , .{ | ||
| 1815 | match, | ||
| 1816 | generic_result.stdout.?, | ||
| 1817 | }); | ||
| 1818 | } | ||
| 1819 | }, | ||
| 1820 | .expect_term => |expected_term| { | ||
| 1821 | if (!termMatches(expected_term, generic_result.term)) { | ||
| 1822 | return step.fail("process {f} (expected {f})", .{ | ||
| 1823 | fmtTerm(generic_result.term), | ||
| 1824 | fmtTerm(expected_term), | ||
| 1825 | }); | ||
| 1826 | } | ||
| 1827 | }, | ||
| 1828 | }, | ||
| 1829 | else => { | ||
| 1830 | // On failure, report captured stderr like normal standard error output. | ||
| 1831 | const bad_exit = switch (generic_result.term) { | ||
| 1832 | .exited => |code| code != 0, | ||
| 1833 | .signal, .stopped, .unknown => true, | ||
| 1834 | }; | ||
| 1835 | if (bad_exit) { | ||
| 1836 | if (generic_result.stderr) |bytes| { | ||
| 1837 | run.step.result_stderr = bytes; | ||
| 1838 | } | ||
| 1839 | } | ||
| 1840 | |||
| 1841 | try step.handleChildProcessTerm(generic_result.term); | ||
| 1842 | }, | ||
| 1843 | } | ||
| 1844 | } | ||
| 1845 | |||
| 1846 | const EvalGenericResult = struct { | ||
| 1847 | term: process.Child.Term, | ||
| 1848 | stdout: ?[]const u8, | ||
| 1849 | stderr: ?[]const u8, | ||
| 1850 | }; | ||
| 1851 | |||
| 1852 | fn spawnChildAndCollect( | ||
| 1853 | run: *Run, | ||
| 1854 | argv: []const []const u8, | ||
| 1855 | environ_map: *EnvMap, | ||
| 1856 | has_side_effects: bool, | ||
| 1857 | options: Step.MakeOptions, | ||
| 1858 | fuzz_context: ?FuzzContext, | ||
| 1859 | ) !?EvalGenericResult { | ||
| 1860 | const b = run.step.owner; | ||
| 1861 | const graph = b.graph; | ||
| 1862 | const io = graph.io; | ||
| 1863 | |||
| 1864 | if (fuzz_context != null) { | ||
| 1865 | assert(!has_side_effects); | ||
| 1866 | assert(run.stdio == .zig_test); | ||
| 1867 | } | ||
| 1868 | |||
| 1869 | const child_cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, &run.step) } else .inherit; | ||
| 1870 | |||
| 1871 | // If an error occurs, it's caused by this command: | ||
| 1872 | assert(run.step.result_failed_command == null); | ||
| 1873 | run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{ | ||
| 1874 | .child = environ_map, | ||
| 1875 | .parent = &graph.environ_map, | ||
| 1876 | }, argv); | ||
| 1877 | |||
| 1878 | var spawn_options: process.SpawnOptions = .{ | ||
| 1879 | .argv = argv, | ||
| 1880 | .cwd = child_cwd, | ||
| 1881 | .environ_map = environ_map, | ||
| 1882 | .request_resource_usage_statistics = true, | ||
| 1883 | .stdin = if (run.stdin != .none) s: { | ||
| 1884 | assert(run.stdio != .inherit); | ||
| 1885 | break :s .pipe; | ||
| 1886 | } else switch (run.stdio) { | ||
| 1887 | .infer_from_args => if (has_side_effects) .inherit else .ignore, | ||
| 1888 | .inherit => .inherit, | ||
| 1889 | .check => .ignore, | ||
| 1890 | .zig_test => .pipe, | ||
| 1891 | }, | ||
| 1892 | .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) { | ||
| 1893 | .infer_from_args => if (has_side_effects) .inherit else .ignore, | ||
| 1894 | .inherit => .inherit, | ||
| 1895 | .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore, | ||
| 1896 | .zig_test => .pipe, | ||
| 1897 | }, | ||
| 1898 | .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) { | ||
| 1899 | .infer_from_args => if (has_side_effects) .inherit else .pipe, | ||
| 1900 | .inherit => .inherit, | ||
| 1901 | .check => .pipe, | ||
| 1902 | .zig_test => .pipe, | ||
| 1903 | }, | ||
| 1904 | }; | ||
| 1905 | |||
| 1906 | if (run.stdio == .zig_test) { | ||
| 1907 | const started: Io.Clock.Timestamp = .now(io, .awake); | ||
| 1908 | const result = evalZigTest(run, spawn_options, options, fuzz_context) catch |err| switch (err) { | ||
| 1909 | error.Canceled => |e| return e, | ||
| 1910 | else => |e| e, | ||
| 1911 | }; | ||
| 1912 | run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); | ||
| 1913 | try result; | ||
| 1914 | return null; | ||
| 1915 | } else { | ||
| 1916 | const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; | ||
| 1917 | if (!run.disable_zig_progress and !inherit) { | ||
| 1918 | spawn_options.progress_node = options.progress_node; | ||
| 1919 | } | ||
| 1920 | const terminal_mode: Io.Terminal.Mode = if (inherit) m: { | ||
| 1921 | const stderr = try io.lockStderr(&.{}, graph.stderr_mode); | ||
| 1922 | break :m stderr.terminal_mode; | ||
| 1923 | } else .no_color; | ||
| 1924 | defer if (inherit) io.unlockStderr(); | ||
| 1925 | try setColorEnvironmentVariables(run, environ_map, terminal_mode); | ||
| 1926 | |||
| 1927 | const started: Io.Clock.Timestamp = .now(io, .awake); | ||
| 1928 | const result = evalGeneric(run, spawn_options) catch |err| switch (err) { | ||
| 1929 | error.Canceled => |e| return e, | ||
| 1930 | else => |e| e, | ||
| 1931 | }; | ||
| 1932 | run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); | ||
| 1933 | return try result; | ||
| 1934 | } | ||
| 1935 | } | ||
| 1936 | |||
| 1937 | fn hashStdIo(hh: *Cache.HashHelper, stdio: StdIo) void { | ||
| 1938 | switch (stdio) { | ||
| 1939 | .infer_from_args, .inherit, .zig_test => {}, | ||
| 1940 | .check => |checks| for (checks.items) |check| { | ||
| 1941 | hh.add(@as(std.meta.Tag(StdIo.Check), check)); | ||
| 1942 | switch (check) { | ||
| 1943 | .expect_stderr_exact, | ||
| 1944 | .expect_stderr_match, | ||
| 1945 | .expect_stdout_exact, | ||
| 1946 | .expect_stdout_match, | ||
| 1947 | => |s| hh.addBytes(s), | ||
| 1948 | |||
| 1949 | .expect_term => |term| { | ||
| 1950 | hh.add(@as(std.meta.Tag(process.Child.Term), term)); | ||
| 1951 | switch (term) { | ||
| 1952 | inline .exited, .signal, .stopped => |x| hh.add(x), | ||
| 1953 | .unknown => |x| hh.add(x), | ||
| 1954 | } | ||
| 1955 | }, | ||
| 1956 | } | ||
| 1957 | }, | ||
| 1958 | } | ||
| 1959 | } | ||
| 1960 | fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool { | ||
| 1961 | return if (expected) |e| switch (e) { | ||
| 1962 | .exited => |expected_code| switch (actual) { | ||
| 1963 | .exited => |actual_code| expected_code == actual_code, | ||
| 1964 | else => false, | ||
| 1965 | }, | ||
| 1966 | .signal => |expected_sig| switch (actual) { | ||
| 1967 | .signal => |actual_sig| expected_sig == actual_sig, | ||
| 1968 | else => false, | ||
| 1969 | }, | ||
| 1970 | .stopped => |expected_sig| switch (actual) { | ||
| 1971 | .stopped => |actual_sig| expected_sig == actual_sig, | ||
| 1972 | else => false, | ||
| 1973 | }, | ||
| 1974 | .unknown => |expected_code| switch (actual) { | ||
| 1975 | .unknown => |actual_code| expected_code == actual_code, | ||
| 1976 | else => false, | ||
| 1977 | }, | ||
| 1978 | } else switch (actual) { | ||
| 1979 | .exited => true, | ||
| 1980 | else => false, | ||
| 1981 | }; | ||
| 1982 | } | ||
| 1983 | |||
| 1984 | fn setColorEnvironmentVariables(run: *Run, environ_map: *EnvMap, terminal_mode: Io.Terminal.Mode) !void { | ||
| 1985 | color: switch (run.color) { | ||
| 1986 | .manual => {}, | ||
| 1987 | .enable => { | ||
| 1988 | try environ_map.put("CLICOLOR_FORCE", "1"); | ||
| 1989 | _ = environ_map.swapRemove("NO_COLOR"); | ||
| 1990 | }, | ||
| 1991 | .disable => { | ||
| 1992 | try environ_map.put("NO_COLOR", "1"); | ||
| 1993 | _ = environ_map.swapRemove("CLICOLOR_FORCE"); | ||
| 1994 | }, | ||
| 1995 | .inherit => switch (terminal_mode) { | ||
| 1996 | .no_color, .windows_api => continue :color .disable, | ||
| 1997 | .escape_codes => continue :color .enable, | ||
| 1998 | }, | ||
| 1999 | .auto => { | ||
| 2000 | const capture_stderr = run.captured_stderr != null or switch (run.stdio) { | ||
| 2001 | .check => |checks| checksContainStderr(checks.items), | ||
| 2002 | .infer_from_args, .inherit, .zig_test => false, | ||
| 2003 | }; | ||
| 2004 | if (capture_stderr) { | ||
| 2005 | continue :color .disable; | ||
| 2006 | } else { | ||
| 2007 | continue :color .inherit; | ||
| 2008 | } | ||
| 2009 | }, | ||
| 2010 | } | ||
| 2011 | } | ||
| 2012 | |||
| 2013 | fn checksContainStdout(checks: []const StdIo.Check) bool { | ||
| 2014 | for (checks) |check| switch (check) { | ||
| 2015 | .expect_stderr_exact, | ||
| 2016 | .expect_stderr_match, | ||
| 2017 | .expect_term, | ||
| 2018 | => continue, | ||
| 2019 | |||
| 2020 | .expect_stdout_exact, | ||
| 2021 | .expect_stdout_match, | ||
| 2022 | => return true, | ||
| 2023 | }; | ||
| 2024 | return false; | ||
| 2025 | } | ||
| 2026 | |||
| 2027 | fn checksContainStderr(checks: []const StdIo.Check) bool { | ||
| 2028 | for (checks) |check| switch (check) { | ||
| 2029 | .expect_stdout_exact, | ||
| 2030 | .expect_stdout_match, | ||
| 2031 | .expect_term, | ||
| 2032 | => continue, | ||
| 2033 | |||
| 2034 | .expect_stderr_exact, | ||
| 2035 | .expect_stderr_match, | ||
| 2036 | => return true, | ||
| 2037 | }; | ||
| 2038 | return false; | ||
| 2039 | } | ||
| 2040 | |||
| 2041 | /// Returns whether the Run step has side effects *other than* updating the output arguments. | ||
| 2042 | fn hasSideEffects(run: Run) bool { | ||
| 2043 | if (run.has_side_effects) return true; | ||
| 2044 | return switch (run.stdio) { | ||
| 2045 | .infer_from_args => !run.hasAnyOutputArgs(), | ||
| 2046 | .inherit => true, | ||
| 2047 | .check => false, | ||
| 2048 | .zig_test => false, | ||
| 2049 | }; | ||
| 2050 | } | ||
| 2051 | |||
| 2052 | fn hasAnyOutputArgs(run: Run) bool { | ||
| 2053 | if (run.captured_stdout != null) return true; | ||
| 2054 | if (run.captured_stderr != null) return true; | ||
| 2055 | for (run.argv.items) |arg| switch (arg) { | ||
| 2056 | .output_file, .output_directory => return true, | ||
| 2057 | else => continue, | ||
| 2058 | }; | ||
| 2059 | return false; | ||
| 2060 | } | ||
| 2061 | |||
| 2062 | /// If `path` is cwd-relative, make it relative to the cwd of the child instead. | ||
| 2063 | /// | ||
| 2064 | /// Whenever a path is included in the argv of a child, it should be put through this function first | ||
| 2065 | /// to make sure the child doesn't see paths relative to a cwd other than its own. | ||
| 2066 | fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 { | ||
| 2067 | const b = run.step.owner; | ||
| 2068 | const graph = b.graph; | ||
| 2069 | const arena = graph.arena; | ||
| 2070 | |||
| 2071 | const path_str = path.toString(arena) catch @panic("OOM"); | ||
| 2072 | if (Dir.path.isAbsolute(path_str)) { | ||
| 2073 | // Absolute paths don't need changing. | ||
| 2074 | return path_str; | ||
| 2075 | } | ||
| 2076 | const child_cwd_rel: []const u8 = rel: { | ||
| 2077 | const child_lazy_cwd = run.cwd orelse break :rel path_str; | ||
| 2078 | const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(arena) catch @panic("OOM"); | ||
| 2079 | // Convert it from relative to *our* cwd, to relative to the *child's* cwd. | ||
| 2080 | break :rel Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str) catch @panic("OOM"); | ||
| 2081 | }; | ||
| 2082 | // Not every path can be made relative, e.g. if the path and the child cwd are on different | ||
| 2083 | // disk designators on Windows. In that case, `relative` will return an absolute path which we can | ||
| 2084 | // just return. | ||
| 2085 | if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel; | ||
| 2086 | |||
| 2087 | // We're not done yet. In some cases this path must be prefixed with './': | ||
| 2088 | // * On POSIX, the executable name cannot be a single component like 'foo' | ||
| 2089 | // * Some executables might treat a leading '-' like a flag, which we must avoid | ||
| 2090 | // There's no harm in it, so just *always* apply this prefix. | ||
| 2091 | return Dir.path.join(arena, &.{ ".", child_cwd_rel }) catch @panic("OOM"); | ||
| 2092 | } | ||
| 2093 | |||
| 2094 | fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void { | ||
| 2095 | const b = run.step.owner; | ||
| 2096 | const compiles = artifact.getCompileDependencies(true); | ||
| 2097 | for (compiles) |compile| { | ||
| 2098 | if (compile.root_module.resolved_target.?.result.os.tag == .windows and | ||
| 2099 | compile.isDynamicLibrary()) | ||
| 2100 | { | ||
| 2101 | addPathDir(run, Dir.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?); | ||
| 2102 | } | ||
| 2103 | } | ||
| 2104 | } | ||
| 2105 | |||
| 2106 | fn failForeign( | ||
| 2107 | run: *Run, | ||
| 2108 | suggested_flag: []const u8, | ||
| 2109 | argv0: []const u8, | ||
| 2110 | exe: *Step.Compile, | ||
| 2111 | ) error{ MakeFailed, MakeSkipped, OutOfMemory } { | ||
| 2112 | switch (run.stdio) { | ||
| 2113 | .check, .zig_test => { | ||
| 2114 | if (run.skip_foreign_checks) | ||
| 2115 | return error.MakeSkipped; | ||
| 2116 | |||
| 2117 | const b = run.step.owner; | ||
| 2118 | const host_name = try b.graph.host.result.zigTriple(b.allocator); | ||
| 2119 | const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator); | ||
| 2120 | |||
| 2121 | return run.step.fail( | ||
| 2122 | \\unable to spawn foreign binary '{s}' ({s}) on host system ({s}) | ||
| 2123 | \\ consider using {s} or enabling skip_foreign_checks in the Run step | ||
| 2124 | , .{ argv0, foreign_name, host_name, suggested_flag }); | ||
| 2125 | }, | ||
| 2126 | else => { | ||
| 2127 | return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0}); | ||
| 2128 | }, | ||
| 2129 | } | ||
| 2130 | } | ||
lib/compiler/Maker/Step/WriteFile.zig created+206| ... | @@ -0,0 +1,206 @@ | ||
| 1 | |||
| 2 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 3 | _ = options; | ||
| 4 | const b = step.owner; | ||
| 5 | const graph = b.graph; | ||
| 6 | const io = graph.io; | ||
| 7 | const arena = b.allocator; | ||
| 8 | const gpa = graph.cache.gpa; | ||
| 9 | const write_file: *WriteFile = @fieldParentPtr("step", step); | ||
| 10 | |||
| 11 | const open_dir_cache = try arena.alloc(Io.Dir, write_file.directories.items.len); | ||
| 12 | var open_dirs_count: usize = 0; | ||
| 13 | defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]); | ||
| 14 | |||
| 15 | switch (write_file.mode) { | ||
| 16 | .whole_cached => { | ||
| 17 | step.clearWatchInputs(); | ||
| 18 | |||
| 19 | // The cache is used here not really as a way to speed things up - because writing | ||
| 20 | // the data to a file would probably be very fast - but as a way to find a canonical | ||
| 21 | // location to put build artifacts. | ||
| 22 | |||
| 23 | // If, for example, a hard-coded path was used as the location to put WriteFile | ||
| 24 | // files, then two WriteFiles executing in parallel might clobber each other. | ||
| 25 | |||
| 26 | var man = b.graph.cache.obtain(); | ||
| 27 | defer man.deinit(); | ||
| 28 | |||
| 29 | for (write_file.files.items) |file| { | ||
| 30 | man.hash.addBytes(file.sub_path); | ||
| 31 | |||
| 32 | switch (file.contents) { | ||
| 33 | .bytes => |bytes| { | ||
| 34 | man.hash.addBytes(bytes); | ||
| 35 | }, | ||
| 36 | .copy => |lazy_path| { | ||
| 37 | const path = lazy_path.getPath3(b, step); | ||
| 38 | _ = try man.addFilePath(path, null); | ||
| 39 | try step.addWatchInput(lazy_path); | ||
| 40 | }, | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| { | ||
| 45 | man.hash.addBytes(dir.sub_path); | ||
| 46 | for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext); | ||
| 47 | if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc); | ||
| 48 | |||
| 49 | const need_derived_inputs = try step.addDirectoryWatchInput(dir.source); | ||
| 50 | const src_dir_path = dir.source.getPath3(b, step); | ||
| 51 | |||
| 52 | var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { | ||
| 53 | return step.fail("unable to open source directory '{f}': {s}", .{ | ||
| 54 | src_dir_path, @errorName(err), | ||
| 55 | }); | ||
| 56 | }; | ||
| 57 | open_dir_cache_elem.* = src_dir; | ||
| 58 | open_dirs_count += 1; | ||
| 59 | |||
| 60 | var it = try src_dir.walk(gpa); | ||
| 61 | defer it.deinit(); | ||
| 62 | while (try it.next(io)) |entry| { | ||
| 63 | if (!dir.options.pathIncluded(entry.path)) continue; | ||
| 64 | |||
| 65 | switch (entry.kind) { | ||
| 66 | .directory => { | ||
| 67 | if (need_derived_inputs) { | ||
| 68 | const entry_path = try src_dir_path.join(arena, entry.path); | ||
| 69 | try step.addDirectoryWatchInputFromPath(entry_path); | ||
| 70 | } | ||
| 71 | }, | ||
| 72 | .file => { | ||
| 73 | const entry_path = try src_dir_path.join(arena, entry.path); | ||
| 74 | _ = try man.addFilePath(entry_path, null); | ||
| 75 | }, | ||
| 76 | else => continue, | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | if (try step.cacheHit(&man)) { | ||
| 82 | const digest = man.final(); | ||
| 83 | write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest }); | ||
| 84 | assert(step.result_cached); | ||
| 85 | return; | ||
| 86 | } | ||
| 87 | |||
| 88 | const digest = man.final(); | ||
| 89 | const cache_path = "o" ++ Dir.path.sep_str ++ digest; | ||
| 90 | |||
| 91 | write_file.generated_directory.path = try b.cache_root.join(arena, &.{cache_path}); | ||
| 92 | |||
| 93 | try operate(write_file, open_dir_cache, .{ | ||
| 94 | .root_dir = b.cache_root, | ||
| 95 | .sub_path = cache_path, | ||
| 96 | }); | ||
| 97 | |||
| 98 | try step.writeManifest(&man); | ||
| 99 | }, | ||
| 100 | .tmp => { | ||
| 101 | step.result_cached = false; | ||
| 102 | |||
| 103 | var rand_int: u64 = undefined; | ||
| 104 | io.random(@ptrCast(&rand_int)); | ||
| 105 | const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); | ||
| 106 | |||
| 107 | write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path}); | ||
| 108 | |||
| 109 | try operate(write_file, open_dir_cache, .{ | ||
| 110 | .root_dir = b.cache_root, | ||
| 111 | .sub_path = tmp_dir_sub_path, | ||
| 112 | }); | ||
| 113 | }, | ||
| 114 | .mutate => |lp| { | ||
| 115 | step.result_cached = false; | ||
| 116 | const root_path = try lp.getPath4(b, step); | ||
| 117 | write_file.generated_directory.path = try root_path.toString(arena); | ||
| 118 | try operate(write_file, open_dir_cache, root_path); | ||
| 119 | }, | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | fn operate(write_file: *WriteFile, open_dir_cache: []const Io.Dir, root_path: std.Build.Cache.Path) !void { | ||
| 124 | const step = &write_file.step; | ||
| 125 | const b = step.owner; | ||
| 126 | const io = b.graph.io; | ||
| 127 | const gpa = b.graph.cache.gpa; | ||
| 128 | const arena = b.allocator; | ||
| 129 | |||
| 130 | var cache_dir = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err| | ||
| 131 | return step.fail("unable to make path {f}: {t}", .{ root_path, err }); | ||
| 132 | defer cache_dir.close(io); | ||
| 133 | |||
| 134 | for (write_file.files.items) |file| { | ||
| 135 | if (Dir.path.dirname(file.sub_path)) |dirname| { | ||
| 136 | cache_dir.createDirPath(io, dirname) catch |err| { | ||
| 137 | return step.fail("unable to make path '{f}{c}{s}': {t}", .{ | ||
| 138 | root_path, Dir.path.sep, dirname, err, | ||
| 139 | }); | ||
| 140 | }; | ||
| 141 | } | ||
| 142 | switch (file.contents) { | ||
| 143 | .bytes => |bytes| { | ||
| 144 | cache_dir.writeFile(io, .{ .sub_path = file.sub_path, .data = bytes }) catch |err| { | ||
| 145 | return step.fail("unable to write file '{f}{c}{s}': {t}", .{ | ||
| 146 | root_path, Dir.path.sep, file.sub_path, err, | ||
| 147 | }); | ||
| 148 | }; | ||
| 149 | }, | ||
| 150 | .copy => |file_source| { | ||
| 151 | const source_path = file_source.getPath2(b, step); | ||
| 152 | const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir, file.sub_path, .{}) catch |err| { | ||
| 153 | return step.fail("unable to update file from '{s}' to '{f}{c}{s}': {t}", .{ | ||
| 154 | source_path, root_path, Dir.path.sep, file.sub_path, err, | ||
| 155 | }); | ||
| 156 | }; | ||
| 157 | // At this point we already will mark the step as a cache miss. | ||
| 158 | // But this is kind of a partial cache hit since individual | ||
| 159 | // file copies may be avoided. Oh well, this information is | ||
| 160 | // discarded. | ||
| 161 | _ = prev_status; | ||
| 162 | }, | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| { | ||
| 167 | const src_dir_path = dir.source.getPath3(b, step); | ||
| 168 | const dest_dirname = dir.sub_path; | ||
| 169 | |||
| 170 | if (dest_dirname.len != 0) { | ||
| 171 | cache_dir.createDirPath(io, dest_dirname) catch |err| { | ||
| 172 | return step.fail("unable to make path '{f}{c}{s}': {t}", .{ | ||
| 173 | root_path, Dir.path.sep, dest_dirname, err, | ||
| 174 | }); | ||
| 175 | }; | ||
| 176 | } | ||
| 177 | |||
| 178 | var it = try already_open_dir.walk(gpa); | ||
| 179 | defer it.deinit(); | ||
| 180 | while (try it.next(io)) |entry| { | ||
| 181 | if (!dir.options.pathIncluded(entry.path)) continue; | ||
| 182 | |||
| 183 | const src_entry_path = try src_dir_path.join(arena, entry.path); | ||
| 184 | const dest_path = b.pathJoin(&.{ dest_dirname, entry.path }); | ||
| 185 | switch (entry.kind) { | ||
| 186 | .directory => try cache_dir.createDirPath(io, dest_path), | ||
| 187 | .file => { | ||
| 188 | const prev_status = Io.Dir.updateFile( | ||
| 189 | src_entry_path.root_dir.handle, | ||
| 190 | io, | ||
| 191 | src_entry_path.sub_path, | ||
| 192 | cache_dir, | ||
| 193 | dest_path, | ||
| 194 | .{}, | ||
| 195 | ) catch |err| { | ||
| 196 | return step.fail("unable to update file from '{f}' to '{f}{c}{s}': {t}", .{ | ||
| 197 | src_entry_path, root_path, Dir.path.sep, dest_path, err, | ||
| 198 | }); | ||
| 199 | }; | ||
| 200 | _ = prev_status; | ||
| 201 | }, | ||
| 202 | else => continue, | ||
| 203 | } | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
lib/compiler/Maker/Watch.zig created+976| ... | @@ -0,0 +1,976 @@ | ||
| 1 | const Watch = @This(); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | |||
| 4 | const std = @import("std"); | ||
| 5 | const Io = std.Io; | ||
| 6 | const Allocator = std.mem.Allocator; | ||
| 7 | const assert = std.debug.assert; | ||
| 8 | const fatal = std.process.fatal; | ||
| 9 | const Configuration = std.Build.Configuration; | ||
| 10 | |||
| 11 | const FsEvents = @import("Watch/FsEvents.zig"); | ||
| 12 | const Step = @import("Step.zig"); | ||
| 13 | |||
| 14 | os: Os, | ||
| 15 | /// The number to show as the number of directories being watched. | ||
| 16 | dir_count: usize, | ||
| 17 | // These fields are common to most implementations so are kept here for simplicity. | ||
| 18 | // They are `undefined` on implementations which do not utilize then. | ||
| 19 | dir_table: DirTable, | ||
| 20 | generation: Generation, | ||
| 21 | configuration: *const Configuration, | ||
| 22 | make_steps: []Step, | ||
| 23 | |||
| 24 | pub const have_impl = Os != void; | ||
| 25 | |||
| 26 | /// Key is the directory to watch which contains one or more files we are | ||
| 27 | /// interested in noticing changes to. | ||
| 28 | /// | ||
| 29 | /// Value is generation. | ||
| 30 | const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false); | ||
| 31 | |||
| 32 | /// Special key of "." means any changes in this directory trigger the steps. | ||
| 33 | const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet); | ||
| 34 | const StepSet = std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, Generation); | ||
| 35 | |||
| 36 | const Generation = u8; | ||
| 37 | |||
| 38 | const Hash = std.hash.Wyhash; | ||
| 39 | const Cache = std.Build.Cache; | ||
| 40 | |||
| 41 | const Os = switch (builtin.os.tag) { | ||
| 42 | .linux => struct { | ||
| 43 | const posix = std.posix; | ||
| 44 | |||
| 45 | /// Keyed differently but indexes correspond 1:1 with `dir_table`. | ||
| 46 | handle_table: HandleTable, | ||
| 47 | /// fanotify file descriptors are keyed by mount id since marks | ||
| 48 | /// are limited to a single filesystem. | ||
| 49 | poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd), | ||
| 50 | |||
| 51 | const MountId = i32; | ||
| 52 | const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false); | ||
| 53 | |||
| 54 | const fan_mask: std.os.linux.fanotify.MarkMask = .{ | ||
| 55 | .CLOSE_WRITE = true, | ||
| 56 | .CREATE = true, | ||
| 57 | .DELETE = true, | ||
| 58 | .DELETE_SELF = true, | ||
| 59 | .EVENT_ON_CHILD = true, | ||
| 60 | .MOVED_FROM = true, | ||
| 61 | .MOVED_TO = true, | ||
| 62 | .MOVE_SELF = true, | ||
| 63 | .ONDIR = true, | ||
| 64 | }; | ||
| 65 | |||
| 66 | const FileHandle = struct { | ||
| 67 | handle: *align(1) std.os.linux.file_handle, | ||
| 68 | |||
| 69 | fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle { | ||
| 70 | const bytes = lfh.slice(); | ||
| 71 | const new_ptr = try gpa.alignedAlloc( | ||
| 72 | u8, | ||
| 73 | .of(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: FileHandle = .{ .handle = new_header }; | ||
| 79 | @memcpy(new.slice(), lfh.slice()); | ||
| 80 | return new; | ||
| 81 | } | ||
| 82 | |||
| 83 | fn destroy(lfh: FileHandle, 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 | fn slice(lfh: FileHandle) []u8 { | ||
| 90 | const ptr: [*]u8 = &lfh.handle.f_handle; | ||
| 91 | return ptr[0..lfh.handle.handle_bytes]; | ||
| 92 | } | ||
| 93 | |||
| 94 | const Adapter = struct { | ||
| 95 | pub fn hash(self: Adapter, a: FileHandle) 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: FileHandle, b: FileHandle, 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 | fn init(cwd_path: []const u8, configuration: *const Configuration, make_steps: []Step) !Watch { | ||
| 109 | _ = cwd_path; | ||
| 110 | return .{ | ||
| 111 | .dir_table = .{}, | ||
| 112 | .dir_count = 0, | ||
| 113 | .os = switch (builtin.os.tag) { | ||
| 114 | .linux => .{ | ||
| 115 | .handle_table = .{}, | ||
| 116 | .poll_fds = .{}, | ||
| 117 | }, | ||
| 118 | else => {}, | ||
| 119 | }, | ||
| 120 | .generation = 0, | ||
| 121 | .make_steps = make_steps, | ||
| 122 | .configuration = configuration, | ||
| 123 | }; | ||
| 124 | } | ||
| 125 | |||
| 126 | fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle { | ||
| 127 | var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; | ||
| 128 | var buf: [std.fs.max_path_bytes]u8 = undefined; | ||
| 129 | const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ | ||
| 130 | path.sub_path, | ||
| 131 | }) catch return error.NameTooLong; | ||
| 132 | const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); | ||
| 133 | stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); | ||
| 134 | try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID); | ||
| 135 | const stack_lfh: FileHandle = .{ .handle = stack_ptr }; | ||
| 136 | return stack_lfh.clone(gpa); | ||
| 137 | } | ||
| 138 | |||
| 139 | fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool { | ||
| 140 | const fanotify = std.os.linux.fanotify; | ||
| 141 | const M = fanotify.event_metadata; | ||
| 142 | var events_buf: [256 + 4096]u8 = undefined; | ||
| 143 | var any_dirty = false; | ||
| 144 | while (true) { | ||
| 145 | var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) { | ||
| 146 | error.WouldBlock => return any_dirty, | ||
| 147 | else => |e| return e, | ||
| 148 | }; | ||
| 149 | var meta: [*]align(1) M = @ptrCast(&events_buf); | ||
| 150 | while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ | ||
| 151 | len -= meta[0].event_len; | ||
| 152 | meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); | ||
| 153 | }) { | ||
| 154 | assert(meta[0].vers == M.VERSION); | ||
| 155 | if (meta[0].mask.Q_OVERFLOW) { | ||
| 156 | any_dirty = true; | ||
| 157 | std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); | ||
| 158 | markAllFilesDirty(w, gpa); | ||
| 159 | return true; | ||
| 160 | } | ||
| 161 | const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); | ||
| 162 | switch (fid.hdr.info_type) { | ||
| 163 | .DFID_NAME => { | ||
| 164 | const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); | ||
| 165 | const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); | ||
| 166 | const file_name = std.mem.span(file_name_z); | ||
| 167 | const lfh: FileHandle = .{ .handle = file_handle }; | ||
| 168 | if (w.os.handle_table.getPtr(lfh)) |value| { | ||
| 169 | if (value.reaction_set.getPtr(".")) |glob_set| | ||
| 170 | any_dirty = markStepSetDirty(gpa, w.make_steps, glob_set, any_dirty); | ||
| 171 | if (value.reaction_set.getPtr(file_name)) |step_set| | ||
| 172 | any_dirty = markStepSetDirty(gpa, w.make_steps, step_set, any_dirty); | ||
| 173 | } | ||
| 174 | }, | ||
| 175 | else => |t| std.log.warn("unexpected fanotify event '{t}'", .{t}), | ||
| 176 | } | ||
| 177 | } | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 182 | // Add missing marks and note persisted ones. | ||
| 183 | for (steps) |step_index| { | ||
| 184 | const step = &w.make_steps[@intFromEnum(step_index)]; | ||
| 185 | for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { | ||
| 186 | const reaction_set = rs: { | ||
| 187 | const gop = try w.dir_table.getOrPut(gpa, path); | ||
| 188 | if (!gop.found_existing) { | ||
| 189 | var mount_id: MountId = undefined; | ||
| 190 | const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) { | ||
| 191 | error.FileNotFound => { | ||
| 192 | std.debug.assert(w.dir_table.swapRemove(path)); | ||
| 193 | continue; | ||
| 194 | }, | ||
| 195 | else => return err, | ||
| 196 | }; | ||
| 197 | const fan_fd = blk: { | ||
| 198 | const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id); | ||
| 199 | if (!fd_gop.found_existing) { | ||
| 200 | const fan_fd = std.posix.fanotify_init(.{ | ||
| 201 | .CLASS = .NOTIF, | ||
| 202 | .CLOEXEC = true, | ||
| 203 | .NONBLOCK = true, | ||
| 204 | .REPORT_NAME = true, | ||
| 205 | .REPORT_DIR_FID = true, | ||
| 206 | .REPORT_FID = true, | ||
| 207 | .REPORT_TARGET_FID = true, | ||
| 208 | }, 0) catch |err| switch (err) { | ||
| 209 | error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}), | ||
| 210 | else => |e| return e, | ||
| 211 | }; | ||
| 212 | fd_gop.value_ptr.* = .{ | ||
| 213 | .fd = fan_fd, | ||
| 214 | .events = std.posix.POLL.IN, | ||
| 215 | .revents = undefined, | ||
| 216 | }; | ||
| 217 | } | ||
| 218 | break :blk fd_gop.value_ptr.*.fd; | ||
| 219 | }; | ||
| 220 | // `dir_handle` may already be present in the table in | ||
| 221 | // the case that we have multiple Cache.Path instances | ||
| 222 | // that compare inequal but ultimately point to the same | ||
| 223 | // directory on the file system. | ||
| 224 | // In such case, we must revert adding this directory, but keep | ||
| 225 | // the additions to the step set. | ||
| 226 | const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle); | ||
| 227 | if (dh_gop.found_existing) { | ||
| 228 | _ = w.dir_table.pop(); | ||
| 229 | } else { | ||
| 230 | assert(dh_gop.index == gop.index); | ||
| 231 | dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} }; | ||
| 232 | posix.fanotify_mark(fan_fd, .{ | ||
| 233 | .ADD = true, | ||
| 234 | .ONLYDIR = true, | ||
| 235 | }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| { | ||
| 236 | fatal("unable to watch {f}: {s}", .{ path, @errorName(err) }); | ||
| 237 | }; | ||
| 238 | } | ||
| 239 | break :rs &dh_gop.value_ptr.reaction_set; | ||
| 240 | } | ||
| 241 | break :rs &w.os.handle_table.values()[gop.index].reaction_set; | ||
| 242 | }; | ||
| 243 | for (files.items) |basename| { | ||
| 244 | const gop = try reaction_set.getOrPut(gpa, basename); | ||
| 245 | if (!gop.found_existing) gop.value_ptr.* = .{}; | ||
| 246 | try gop.value_ptr.put(gpa, step_index, w.generation); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | { | ||
| 252 | // Remove marks for files that are no longer inputs. | ||
| 253 | var i: usize = 0; | ||
| 254 | while (i < w.os.handle_table.entries.len) { | ||
| 255 | { | ||
| 256 | const reaction_set = &w.os.handle_table.values()[i].reaction_set; | ||
| 257 | var step_set_i: usize = 0; | ||
| 258 | while (step_set_i < reaction_set.entries.len) { | ||
| 259 | const step_set = &reaction_set.values()[step_set_i]; | ||
| 260 | var dirent_i: usize = 0; | ||
| 261 | while (dirent_i < step_set.entries.len) { | ||
| 262 | const generations = step_set.values(); | ||
| 263 | if (generations[dirent_i] == w.generation) { | ||
| 264 | dirent_i += 1; | ||
| 265 | continue; | ||
| 266 | } | ||
| 267 | step_set.swapRemoveAt(dirent_i); | ||
| 268 | } | ||
| 269 | if (step_set.entries.len > 0) { | ||
| 270 | step_set_i += 1; | ||
| 271 | continue; | ||
| 272 | } | ||
| 273 | reaction_set.swapRemoveAt(step_set_i); | ||
| 274 | } | ||
| 275 | if (reaction_set.entries.len > 0) { | ||
| 276 | i += 1; | ||
| 277 | continue; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | const path = w.dir_table.keys()[i]; | ||
| 282 | |||
| 283 | const mount_id = w.os.handle_table.values()[i].mount_id; | ||
| 284 | const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd; | ||
| 285 | posix.fanotify_mark(fan_fd, .{ | ||
| 286 | .REMOVE = true, | ||
| 287 | .ONLYDIR = true, | ||
| 288 | }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) { | ||
| 289 | error.FileNotFound => {}, // Expected, harmless. | ||
| 290 | else => |e| std.log.warn("unable to unwatch '{f}': {s}", .{ path, @errorName(e) }), | ||
| 291 | }; | ||
| 292 | |||
| 293 | w.dir_table.swapRemoveAt(i); | ||
| 294 | w.os.handle_table.swapRemoveAt(i); | ||
| 295 | } | ||
| 296 | w.generation +%= 1; | ||
| 297 | } | ||
| 298 | w.dir_count = w.dir_table.count(); | ||
| 299 | } | ||
| 300 | |||
| 301 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 302 | _ = io; | ||
| 303 | const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms()); | ||
| 304 | if (events_len == 0) | ||
| 305 | return .timeout; | ||
| 306 | 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, gpa, poll_fd.fd)) | ||
| 308 | return .dirty; | ||
| 309 | } | ||
| 310 | return .clean; | ||
| 311 | } | ||
| 312 | }, | ||
| 313 | .windows => struct { | ||
| 314 | const windows = std.os.windows; | ||
| 315 | |||
| 316 | /// Keyed differently but indexes correspond 1:1 with `dir_table`. | ||
| 317 | handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false), | ||
| 318 | ready_dirs: std.DoublyLinkedList, | ||
| 319 | |||
| 320 | const FileId = struct { | ||
| 321 | volumeSerialNumber: windows.ULONG, | ||
| 322 | indexNumber: windows.LARGE_INTEGER, | ||
| 323 | }; | ||
| 324 | |||
| 325 | const Directory = struct { | ||
| 326 | reaction_set: ReactionSet, | ||
| 327 | id: FileId, | ||
| 328 | file: Io.File, | ||
| 329 | state: enum { idle, listening, ready }, | ||
| 330 | iosb: windows.IO_STATUS_BLOCK, | ||
| 331 | // 64 KB is the packet size limit when monitoring over a network. | ||
| 332 | // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks | ||
| 333 | buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)), | ||
| 334 | ready_node: std.DoublyLinkedList.Node, | ||
| 335 | |||
| 336 | /// Start listening for events, buffer field will be overwritten eventually. | ||
| 337 | fn startListening(dir: *Directory, w: *Watch) !void { | ||
| 338 | assert(dir.file.flags.nonblocking); | ||
| 339 | assert(dir.state == .idle); | ||
| 340 | switch (windows.ntdll.NtNotifyChangeDirectoryFileEx( | ||
| 341 | dir.file.handle, | ||
| 342 | null, | ||
| 343 | &notifyApc, | ||
| 344 | w, | ||
| 345 | &dir.iosb, | ||
| 346 | &dir.buffer, | ||
| 347 | dir.buffer.len, | ||
| 348 | .{ | ||
| 349 | .FILE_NAME = true, | ||
| 350 | .DIR_NAME = true, | ||
| 351 | .SIZE = true, | ||
| 352 | .LAST_WRITE = true, | ||
| 353 | .CREATION = true, | ||
| 354 | }, | ||
| 355 | .FALSE, | ||
| 356 | .Notify, | ||
| 357 | )) { | ||
| 358 | .SUCCESS, .PENDING => dir.state = .listening, | ||
| 359 | .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported, | ||
| 360 | else => |status| return windows.unexpectedStatus(status), | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void { | ||
| 365 | const w: *Watch = @ptrCast(@alignCast(apc_context)); | ||
| 366 | const dir: *Directory = @fieldParentPtr("iosb", iosb); | ||
| 367 | assert(iosb.u.Status != .PENDING); | ||
| 368 | assert(dir.state == .listening); | ||
| 369 | w.os.ready_dirs.append(&dir.ready_node); | ||
| 370 | dir.state = .ready; | ||
| 371 | } | ||
| 372 | |||
| 373 | fn init(gpa: Allocator, path: Cache.Path) !*Directory { | ||
| 374 | // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW) | ||
| 375 | // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW. | ||
| 376 | var dir_handle: windows.HANDLE = undefined; | ||
| 377 | const root_fd = path.root_dir.handle.handle; | ||
| 378 | const sub_path = path.subPathOrDot(); | ||
| 379 | const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call | ||
| 380 | var iosb: windows.IO_STATUS_BLOCK = undefined; | ||
| 381 | switch (windows.ntdll.NtCreateFile( | ||
| 382 | &dir_handle, | ||
| 383 | .{ | ||
| 384 | .SPECIFIC = .{ .FILE_DIRECTORY = .{ | ||
| 385 | .LIST = true, | ||
| 386 | } }, | ||
| 387 | .STANDARD = .{ .SYNCHRONIZE = true }, | ||
| 388 | .GENERIC = .{ .READ = true }, | ||
| 389 | }, | ||
| 390 | &.{ | ||
| 391 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd, | ||
| 392 | .ObjectName = @constCast(&sub_path_w.string()), | ||
| 393 | }, | ||
| 394 | &iosb, | ||
| 395 | null, | ||
| 396 | .{}, | ||
| 397 | .VALID_FLAGS, | ||
| 398 | .OPEN, | ||
| 399 | .{ | ||
| 400 | .DIRECTORY_FILE = true, | ||
| 401 | .IO = .ASYNCHRONOUS, | ||
| 402 | .OPEN_FOR_BACKUP_INTENT = true, | ||
| 403 | }, | ||
| 404 | null, | ||
| 405 | 0, | ||
| 406 | )) { | ||
| 407 | .SUCCESS => {}, | ||
| 408 | .OBJECT_NAME_INVALID => return error.BadPathName, | ||
| 409 | .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, | ||
| 410 | .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, | ||
| 411 | .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, | ||
| 412 | .NOT_A_DIRECTORY => return error.NotDir, | ||
| 413 | // This can happen if the directory has 'List folder contents' permission set to 'Deny' | ||
| 414 | .ACCESS_DENIED => return error.AccessDenied, | ||
| 415 | .INVALID_PARAMETER => unreachable, | ||
| 416 | else => |rc| return windows.unexpectedStatus(rc), | ||
| 417 | } | ||
| 418 | assert(dir_handle != windows.INVALID_HANDLE_VALUE); | ||
| 419 | errdefer windows.CloseHandle(dir_handle); | ||
| 420 | |||
| 421 | const dir_id = try getFileId(dir_handle); | ||
| 422 | |||
| 423 | const dir = try gpa.create(Directory); | ||
| 424 | dir.* = .{ | ||
| 425 | .reaction_set = .empty, | ||
| 426 | .id = dir_id, | ||
| 427 | .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } }, | ||
| 428 | .state = .idle, | ||
| 429 | .iosb = undefined, | ||
| 430 | .buffer = undefined, | ||
| 431 | .ready_node = undefined, | ||
| 432 | }; | ||
| 433 | return dir; | ||
| 434 | } | ||
| 435 | |||
| 436 | fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void { | ||
| 437 | state: switch (dir.state) { | ||
| 438 | .idle => {}, | ||
| 439 | .listening => { | ||
| 440 | var cancel_iosb: windows.IO_STATUS_BLOCK = undefined; | ||
| 441 | _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb); | ||
| 442 | while (switch (dir.state) { | ||
| 443 | .idle => unreachable, | ||
| 444 | .listening => true, | ||
| 445 | .ready => false, | ||
| 446 | }) Io.Threaded.waitForApcOrAlert(); | ||
| 447 | continue :state .ready; | ||
| 448 | }, | ||
| 449 | .ready => w.os.ready_dirs.remove(&dir.ready_node), | ||
| 450 | } | ||
| 451 | windows.CloseHandle(dir.file.handle); | ||
| 452 | gpa.destroy(dir); | ||
| 453 | } | ||
| 454 | |||
| 455 | /// Useful to make `*Directory` a key in `std.ArrayHashMap`. | ||
| 456 | const TableAdapter = struct { | ||
| 457 | pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 { | ||
| 458 | return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber))); | ||
| 459 | } | ||
| 460 | pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool { | ||
| 461 | _ = rhs_index; | ||
| 462 | return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and | ||
| 463 | lhs_dir.id.indexNumber == rhs_dir.id.indexNumber; | ||
| 464 | } | ||
| 465 | }; | ||
| 466 | }; | ||
| 467 | |||
| 468 | fn init(cwd_path: []const u8) !Watch { | ||
| 469 | _ = cwd_path; | ||
| 470 | return .{ | ||
| 471 | .dir_table = .{}, | ||
| 472 | .dir_count = 0, | ||
| 473 | .os = switch (builtin.os.tag) { | ||
| 474 | .windows => .{ | ||
| 475 | .handle_table = .empty, | ||
| 476 | .ready_dirs = .{}, | ||
| 477 | }, | ||
| 478 | else => {}, | ||
| 479 | }, | ||
| 480 | .generation = 0, | ||
| 481 | }; | ||
| 482 | } | ||
| 483 | |||
| 484 | fn getFileId(handle: windows.HANDLE) !FileId { | ||
| 485 | var file_id: FileId = undefined; | ||
| 486 | var io_status: windows.IO_STATUS_BLOCK = undefined; | ||
| 487 | var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined; | ||
| 488 | switch (windows.ntdll.NtQueryVolumeInformationFile( | ||
| 489 | handle, | ||
| 490 | &io_status, | ||
| 491 | &volume_info, | ||
| 492 | @sizeOf(windows.FILE.FS_VOLUME_INFORMATION), | ||
| 493 | .Volume, | ||
| 494 | )) { | ||
| 495 | .SUCCESS => {}, | ||
| 496 | // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer | ||
| 497 | // size provided. This is treated as success because the type of variable-length information that this would be relevant for | ||
| 498 | // (name, volume name, etc) we don't care about. | ||
| 499 | .BUFFER_OVERFLOW => {}, | ||
| 500 | else => |rc| return windows.unexpectedStatus(rc), | ||
| 501 | } | ||
| 502 | file_id.volumeSerialNumber = volume_info.VolumeSerialNumber; | ||
| 503 | var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined; | ||
| 504 | switch (windows.ntdll.NtQueryInformationFile( | ||
| 505 | handle, | ||
| 506 | &io_status, | ||
| 507 | &internal_info, | ||
| 508 | @sizeOf(windows.FILE.INTERNAL_INFORMATION), | ||
| 509 | .Internal, | ||
| 510 | )) { | ||
| 511 | .SUCCESS => {}, | ||
| 512 | else => |rc| return windows.unexpectedStatus(rc), | ||
| 513 | } | ||
| 514 | file_id.indexNumber = internal_info.IndexNumber; | ||
| 515 | return file_id; | ||
| 516 | } | ||
| 517 | |||
| 518 | fn markDirtySteps(w: *Watch, gpa: Allocator, dir: *Directory) !bool { | ||
| 519 | var any_dirty = false; | ||
| 520 | const bytes_returned = dir.iosb.Information; | ||
| 521 | if (bytes_returned == 0) { | ||
| 522 | std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); | ||
| 523 | markAllFilesDirty(w, gpa); | ||
| 524 | try dir.startListening(w); | ||
| 525 | return true; | ||
| 526 | } | ||
| 527 | var file_name_buf: [std.fs.max_path_bytes]u8 = undefined; | ||
| 528 | var offset: usize = 0; | ||
| 529 | while (true) { | ||
| 530 | const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset])); | ||
| 531 | const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())]; | ||
| 532 | if (dir.reaction_set.getPtr(".")) |glob_set| | ||
| 533 | any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); | ||
| 534 | if (dir.reaction_set.getPtr(file_name)) |step_set| | ||
| 535 | any_dirty = markStepSetDirty(gpa, step_set, any_dirty); | ||
| 536 | if (notify.NextEntryOffset == 0) | ||
| 537 | break; | ||
| 538 | |||
| 539 | offset += notify.NextEntryOffset; | ||
| 540 | } | ||
| 541 | |||
| 542 | // We call this now since at this point we have finished reading dir.buffer. | ||
| 543 | try dir.startListening(w); | ||
| 544 | return any_dirty; | ||
| 545 | } | ||
| 546 | |||
| 547 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 548 | // Add missing marks and note persisted ones. | ||
| 549 | for (steps) |step| { | ||
| 550 | for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { | ||
| 551 | const dir = dir: { | ||
| 552 | const gop = try w.dir_table.getOrPut(gpa, path); | ||
| 553 | if (!gop.found_existing) { | ||
| 554 | const dir: *Directory = try .init(gpa, path); | ||
| 555 | errdefer dir.deinit(gpa, w); | ||
| 556 | // `dir.id` may already be present in the table in | ||
| 557 | // the case that we have multiple Cache.Path instances | ||
| 558 | // that compare inequal but ultimately point to the same | ||
| 559 | // directory on the file system. | ||
| 560 | // In such case, we must revert adding this directory, but keep | ||
| 561 | // the additions to the step set. | ||
| 562 | const dh_gop = try w.os.handle_table.getOrPut(gpa, dir); | ||
| 563 | if (dh_gop.found_existing) { | ||
| 564 | dir.deinit(gpa, w); | ||
| 565 | _ = w.dir_table.pop(); | ||
| 566 | break :dir w.os.handle_table.keys()[dh_gop.index]; | ||
| 567 | } else { | ||
| 568 | assert(dh_gop.index == gop.index); | ||
| 569 | try dir.startListening(w); | ||
| 570 | break :dir dir; | ||
| 571 | } | ||
| 572 | } | ||
| 573 | break :dir w.os.handle_table.keys()[gop.index]; | ||
| 574 | }; | ||
| 575 | for (files.items) |basename| { | ||
| 576 | const gop = try dir.reaction_set.getOrPut(gpa, basename); | ||
| 577 | if (!gop.found_existing) gop.value_ptr.* = .{}; | ||
| 578 | try gop.value_ptr.put(gpa, step, w.generation); | ||
| 579 | } | ||
| 580 | } | ||
| 581 | } | ||
| 582 | |||
| 583 | { | ||
| 584 | // Remove marks for files that are no longer inputs. | ||
| 585 | var i: usize = 0; | ||
| 586 | while (i < w.os.handle_table.entries.len) { | ||
| 587 | const dir = w.os.handle_table.keys()[i]; | ||
| 588 | { | ||
| 589 | var step_set_i: usize = 0; | ||
| 590 | while (step_set_i < dir.reaction_set.entries.len) { | ||
| 591 | const step_set = &dir.reaction_set.values()[step_set_i]; | ||
| 592 | var dirent_i: usize = 0; | ||
| 593 | while (dirent_i < step_set.entries.len) { | ||
| 594 | const generations = step_set.values(); | ||
| 595 | if (generations[dirent_i] == w.generation) { | ||
| 596 | dirent_i += 1; | ||
| 597 | continue; | ||
| 598 | } | ||
| 599 | step_set.swapRemoveAt(dirent_i); | ||
| 600 | } | ||
| 601 | if (step_set.entries.len > 0) { | ||
| 602 | step_set_i += 1; | ||
| 603 | continue; | ||
| 604 | } | ||
| 605 | dir.reaction_set.swapRemoveAt(step_set_i); | ||
| 606 | } | ||
| 607 | if (dir.reaction_set.entries.len > 0) { | ||
| 608 | i += 1; | ||
| 609 | continue; | ||
| 610 | } | ||
| 611 | } | ||
| 612 | |||
| 613 | w.dir_table.swapRemoveAt(i); | ||
| 614 | w.os.handle_table.swapRemoveAt(i); | ||
| 615 | dir.deinit(gpa, w); | ||
| 616 | } | ||
| 617 | w.generation +%= 1; | ||
| 618 | } | ||
| 619 | w.dir_count = w.dir_table.count(); | ||
| 620 | } | ||
| 621 | |||
| 622 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 623 | for (0..2) |attempt| { | ||
| 624 | while (w.os.ready_dirs.popFirst()) |ready_node| { | ||
| 625 | const dir: *Directory = @fieldParentPtr("ready_node", ready_node); | ||
| 626 | assert(dir.state == .ready); | ||
| 627 | dir.state = .idle; | ||
| 628 | switch (dir.iosb.u.Status) { | ||
| 629 | .SUCCESS => return if (try markDirtySteps(w, gpa, dir)) .dirty else .clean, | ||
| 630 | .PENDING => unreachable, | ||
| 631 | .CANCELLED => {}, | ||
| 632 | else => |status| return windows.unexpectedStatus(status), | ||
| 633 | } | ||
| 634 | try dir.startListening(w); | ||
| 635 | } | ||
| 636 | try io.checkCancel(); | ||
| 637 | if (attempt == 1) return .timeout; | ||
| 638 | const delay_interval: windows.LARGE_INTEGER = switch (timeout) { | ||
| 639 | .none => std.math.minInt(windows.LARGE_INTEGER), | ||
| 640 | .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100), | ||
| 641 | }; | ||
| 642 | _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval); | ||
| 643 | } else unreachable; | ||
| 644 | } | ||
| 645 | }, | ||
| 646 | .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct { | ||
| 647 | const posix = std.posix; | ||
| 648 | |||
| 649 | kq_fd: i32, | ||
| 650 | /// Indexes correspond 1:1 with `dir_table`. | ||
| 651 | handles: std.MultiArrayList(struct { | ||
| 652 | rs: ReactionSet, | ||
| 653 | /// If the corresponding dir_table Path has sub_path == "", then it | ||
| 654 | /// suffices as the open directory handle, and this value will be | ||
| 655 | /// -1. Otherwise, it needs to be opened in update(), and will be | ||
| 656 | /// stored here. | ||
| 657 | dir_fd: i32, | ||
| 658 | }), | ||
| 659 | |||
| 660 | const dir_open_flags: posix.O = f: { | ||
| 661 | var f: posix.O = .{ | ||
| 662 | .ACCMODE = .RDONLY, | ||
| 663 | .NOFOLLOW = false, | ||
| 664 | .DIRECTORY = true, | ||
| 665 | .CLOEXEC = true, | ||
| 666 | }; | ||
| 667 | if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true; | ||
| 668 | if (@hasField(posix.O, "PATH")) f.PATH = true; | ||
| 669 | break :f f; | ||
| 670 | }; | ||
| 671 | |||
| 672 | const EV = std.c.EV; | ||
| 673 | const NOTE = std.c.NOTE; | ||
| 674 | |||
| 675 | fn init(cwd_path: []const u8) !Watch { | ||
| 676 | _ = cwd_path; | ||
| 677 | return .{ | ||
| 678 | .dir_table = .{}, | ||
| 679 | .dir_count = 0, | ||
| 680 | .os = .{ | ||
| 681 | .kq_fd = try Io.Kqueue.createFileDescriptor(), | ||
| 682 | .handles = .empty, | ||
| 683 | }, | ||
| 684 | .generation = 0, | ||
| 685 | }; | ||
| 686 | } | ||
| 687 | |||
| 688 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 689 | const handles = &w.os.handles; | ||
| 690 | for (steps) |step| { | ||
| 691 | for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { | ||
| 692 | const reaction_set = rs: { | ||
| 693 | const gop = try w.dir_table.getOrPut(gpa, path); | ||
| 694 | if (!gop.found_existing) { | ||
| 695 | const skip_open_dir = path.sub_path.len == 0; | ||
| 696 | const dir_fd = if (skip_open_dir) | ||
| 697 | path.root_dir.handle.handle | ||
| 698 | else | ||
| 699 | posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| { | ||
| 700 | fatal("failed to open directory {f}: {t}", .{ path, err }); | ||
| 701 | }; | ||
| 702 | // Empirically the dir has to stay open or else no events are triggered. | ||
| 703 | errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd); | ||
| 704 | const changes = [1]posix.Kevent{.{ | ||
| 705 | .ident = @bitCast(@as(isize, dir_fd)), | ||
| 706 | .filter = std.c.EVFILT.VNODE, | ||
| 707 | .flags = EV.ADD | EV.ENABLE | EV.CLEAR, | ||
| 708 | .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, | ||
| 709 | .data = 0, | ||
| 710 | .udata = gop.index, | ||
| 711 | }}; | ||
| 712 | _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null); | ||
| 713 | assert(handles.len == gop.index); | ||
| 714 | try handles.append(gpa, .{ | ||
| 715 | .rs = .{}, | ||
| 716 | .dir_fd = if (skip_open_dir) -1 else dir_fd, | ||
| 717 | }); | ||
| 718 | } | ||
| 719 | |||
| 720 | break :rs &handles.items(.rs)[gop.index]; | ||
| 721 | }; | ||
| 722 | for (files.items) |basename| { | ||
| 723 | const gop = try reaction_set.getOrPut(gpa, basename); | ||
| 724 | if (!gop.found_existing) gop.value_ptr.* = .{}; | ||
| 725 | try gop.value_ptr.put(gpa, step, w.generation); | ||
| 726 | } | ||
| 727 | } | ||
| 728 | } | ||
| 729 | |||
| 730 | { | ||
| 731 | // Remove marks for files that are no longer inputs. | ||
| 732 | var i: usize = 0; | ||
| 733 | while (i < handles.len) { | ||
| 734 | { | ||
| 735 | const reaction_set = &handles.items(.rs)[i]; | ||
| 736 | var step_set_i: usize = 0; | ||
| 737 | while (step_set_i < reaction_set.entries.len) { | ||
| 738 | const step_set = &reaction_set.values()[step_set_i]; | ||
| 739 | var dirent_i: usize = 0; | ||
| 740 | while (dirent_i < step_set.entries.len) { | ||
| 741 | const generations = step_set.values(); | ||
| 742 | if (generations[dirent_i] == w.generation) { | ||
| 743 | dirent_i += 1; | ||
| 744 | continue; | ||
| 745 | } | ||
| 746 | step_set.swapRemoveAt(dirent_i); | ||
| 747 | } | ||
| 748 | if (step_set.entries.len > 0) { | ||
| 749 | step_set_i += 1; | ||
| 750 | continue; | ||
| 751 | } | ||
| 752 | reaction_set.swapRemoveAt(step_set_i); | ||
| 753 | } | ||
| 754 | if (reaction_set.entries.len > 0) { | ||
| 755 | i += 1; | ||
| 756 | continue; | ||
| 757 | } | ||
| 758 | } | ||
| 759 | |||
| 760 | // If the sub_path == "" then this patch has already the | ||
| 761 | // dir fd that we need to use as the ident to remove the | ||
| 762 | // event. If it was opened above with openat() then we need | ||
| 763 | // to access that data via the dir_fd field. | ||
| 764 | const path = w.dir_table.keys()[i]; | ||
| 765 | const dir_fd = if (path.sub_path.len == 0) | ||
| 766 | path.root_dir.handle.handle | ||
| 767 | else | ||
| 768 | handles.items(.dir_fd)[i]; | ||
| 769 | assert(dir_fd != -1); | ||
| 770 | |||
| 771 | // The changelist also needs to update the udata field of the last | ||
| 772 | // event, since we are doing a swap remove, and we store the dir_table | ||
| 773 | // index in the udata field. | ||
| 774 | const last_dir_fd = fd: { | ||
| 775 | const last_path = w.dir_table.keys()[handles.len - 1]; | ||
| 776 | const last_dir_fd = if (last_path.sub_path.len == 0) | ||
| 777 | last_path.root_dir.handle.handle | ||
| 778 | else | ||
| 779 | handles.items(.dir_fd)[handles.len - 1]; | ||
| 780 | assert(last_dir_fd != -1); | ||
| 781 | break :fd last_dir_fd; | ||
| 782 | }; | ||
| 783 | const changes = [_]posix.Kevent{ | ||
| 784 | .{ | ||
| 785 | .ident = @bitCast(@as(isize, dir_fd)), | ||
| 786 | .filter = std.c.EVFILT.VNODE, | ||
| 787 | .flags = EV.DELETE, | ||
| 788 | .fflags = 0, | ||
| 789 | .data = 0, | ||
| 790 | .udata = i, | ||
| 791 | }, | ||
| 792 | .{ | ||
| 793 | .ident = @bitCast(@as(isize, last_dir_fd)), | ||
| 794 | .filter = std.c.EVFILT.VNODE, | ||
| 795 | .flags = EV.ADD, | ||
| 796 | .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, | ||
| 797 | .data = 0, | ||
| 798 | .udata = i, | ||
| 799 | }, | ||
| 800 | }; | ||
| 801 | const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes; | ||
| 802 | _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null); | ||
| 803 | if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd); | ||
| 804 | |||
| 805 | w.dir_table.swapRemoveAt(i); | ||
| 806 | handles.swapRemove(i); | ||
| 807 | } | ||
| 808 | w.generation +%= 1; | ||
| 809 | } | ||
| 810 | w.dir_count = w.dir_table.count(); | ||
| 811 | } | ||
| 812 | |||
| 813 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 814 | _ = io; | ||
| 815 | var timespec_buffer: posix.timespec = undefined; | ||
| 816 | var event_buffer: [100]posix.Kevent = undefined; | ||
| 817 | var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer)); | ||
| 818 | if (n == 0) return .timeout; | ||
| 819 | const reaction_sets = w.os.handles.items(.rs); | ||
| 820 | var any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], false); | ||
| 821 | timespec_buffer = .{ .sec = 0, .nsec = 0 }; | ||
| 822 | while (n == event_buffer.len) { | ||
| 823 | n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer); | ||
| 824 | if (n == 0) break; | ||
| 825 | any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], any_dirty); | ||
| 826 | } | ||
| 827 | return if (any_dirty) .dirty else .clean; | ||
| 828 | } | ||
| 829 | |||
| 830 | fn markDirtySteps( | ||
| 831 | gpa: Allocator, | ||
| 832 | reaction_sets: []ReactionSet, | ||
| 833 | events: []const std.c.Kevent, | ||
| 834 | start_any_dirty: bool, | ||
| 835 | ) bool { | ||
| 836 | var any_dirty = start_any_dirty; | ||
| 837 | for (events) |event| { | ||
| 838 | const index: usize = @intCast(event.udata); | ||
| 839 | const reaction_set = &reaction_sets[index]; | ||
| 840 | // If we knew the basename of the changed file, here we would | ||
| 841 | // mark only the step set dirty, and possibly the glob set: | ||
| 842 | //if (reaction_set.getPtr(".")) |glob_set| | ||
| 843 | // any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); | ||
| 844 | //if (reaction_set.getPtr(file_name)) |step_set| | ||
| 845 | // any_dirty = markStepSetDirty(gpa, step_set, any_dirty); | ||
| 846 | // However we don't know the file name so just mark all the | ||
| 847 | // sets dirty for this directory. | ||
| 848 | for (reaction_set.values()) |*step_set| { | ||
| 849 | any_dirty = markStepSetDirty(gpa, step_set, any_dirty); | ||
| 850 | } | ||
| 851 | } | ||
| 852 | return any_dirty; | ||
| 853 | } | ||
| 854 | }, | ||
| 855 | .macos => struct { | ||
| 856 | fse: FsEvents, | ||
| 857 | |||
| 858 | fn init(cwd_path: []const u8) !Watch { | ||
| 859 | return .{ | ||
| 860 | .os = .{ .fse = try .init(cwd_path) }, | ||
| 861 | .dir_count = 0, | ||
| 862 | .dir_table = undefined, | ||
| 863 | .generation = undefined, | ||
| 864 | }; | ||
| 865 | } | ||
| 866 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 867 | try w.os.fse.setPaths(gpa, steps); | ||
| 868 | w.dir_count = w.os.fse.watch_roots.len; | ||
| 869 | } | ||
| 870 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 871 | _ = io; | ||
| 872 | return w.os.fse.wait(gpa, switch (timeout) { | ||
| 873 | .none => null, | ||
| 874 | .ms => |ms| @as(u64, ms) * std.time.ns_per_ms, | ||
| 875 | }); | ||
| 876 | } | ||
| 877 | }, | ||
| 878 | else => void, | ||
| 879 | }; | ||
| 880 | |||
| 881 | pub fn init(cwd_path: []const u8, configuration: *const Configuration, make_steps: []Step) !Watch { | ||
| 882 | return Os.init(cwd_path, configuration, make_steps); | ||
| 883 | } | ||
| 884 | |||
| 885 | pub const Match = struct { | ||
| 886 | /// Relative to the watched directory, the file path that triggers this | ||
| 887 | /// match. | ||
| 888 | basename: []const u8, | ||
| 889 | /// The step to re-run when file corresponding to `basename` is changed. | ||
| 890 | step_index: Configuration.Step.Index, | ||
| 891 | |||
| 892 | pub const Context = struct { | ||
| 893 | pub fn hash(self: Context, a: Match) u32 { | ||
| 894 | _ = self; | ||
| 895 | var hasher = Hash.init(@intFromEnum(a.step_index)); | ||
| 896 | hasher.update(a.basename); | ||
| 897 | return @truncate(hasher.final()); | ||
| 898 | } | ||
| 899 | pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool { | ||
| 900 | _ = self; | ||
| 901 | _ = b_index; | ||
| 902 | return a.step_index == b.step_index and std.mem.eql(u8, a.basename, b.basename); | ||
| 903 | } | ||
| 904 | }; | ||
| 905 | }; | ||
| 906 | |||
| 907 | fn markAllFilesDirty(w: *Watch, gpa: Allocator) void { | ||
| 908 | for (switch (builtin.os.tag) { | ||
| 909 | .windows => w.os.handle_table.keys(), | ||
| 910 | else => w.os.handle_table.values(), | ||
| 911 | }) |item| { | ||
| 912 | const reaction_set = switch (builtin.os.tag) { | ||
| 913 | .linux, .windows => item.reaction_set, | ||
| 914 | else => item, | ||
| 915 | }; | ||
| 916 | for (reaction_set.values()) |step_set| { | ||
| 917 | for (step_set.keys()) |step_index| { | ||
| 918 | const step = &w.make_steps[@intFromEnum(step_index)]; | ||
| 919 | _ = step.invalidateResult(gpa); | ||
| 920 | } | ||
| 921 | } | ||
| 922 | } | ||
| 923 | } | ||
| 924 | |||
| 925 | fn markStepSetDirty(gpa: Allocator, make_steps: []Step, step_set: *StepSet, any_dirty: bool) bool { | ||
| 926 | var this_any_dirty = false; | ||
| 927 | for (step_set.keys()) |step_index| { | ||
| 928 | const step = &make_steps[@intFromEnum(step_index)]; | ||
| 929 | if (step.invalidateResult(gpa)) this_any_dirty = true; | ||
| 930 | } | ||
| 931 | return any_dirty or this_any_dirty; | ||
| 932 | } | ||
| 933 | |||
| 934 | pub fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 935 | return Os.update(w, gpa, steps); | ||
| 936 | } | ||
| 937 | |||
| 938 | pub const Timeout = union(enum) { | ||
| 939 | none, | ||
| 940 | ms: u16, | ||
| 941 | |||
| 942 | pub fn to_i32_ms(t: Timeout) i32 { | ||
| 943 | return switch (t) { | ||
| 944 | .none => -1, | ||
| 945 | .ms => |ms| ms, | ||
| 946 | }; | ||
| 947 | } | ||
| 948 | |||
| 949 | pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec { | ||
| 950 | return switch (t) { | ||
| 951 | .none => null, | ||
| 952 | .ms => |ms_u16| { | ||
| 953 | const ms: isize = ms_u16; | ||
| 954 | buf.* = .{ | ||
| 955 | .sec = @divTrunc(ms, std.time.ms_per_s), | ||
| 956 | .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms, | ||
| 957 | }; | ||
| 958 | return buf; | ||
| 959 | }, | ||
| 960 | }; | ||
| 961 | } | ||
| 962 | }; | ||
| 963 | |||
| 964 | pub const WaitResult = enum { | ||
| 965 | timeout, | ||
| 966 | /// File system watching triggered on files that were marked as inputs to at least one Step. | ||
| 967 | /// Relevant steps have been marked dirty. | ||
| 968 | dirty, | ||
| 969 | /// File system watching triggered but none of the events were relevant to | ||
| 970 | /// what we are listening to. There is nothing to do. | ||
| 971 | clean, | ||
| 972 | }; | ||
| 973 | |||
| 974 | pub fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 975 | return Os.wait(w, gpa, io, timeout); | ||
| 976 | } | ||
lib/compiler/Maker/Watch/FsEvents.zig created+479| ... | @@ -0,0 +1,479 @@ | ||
| 1 | //! An implementation of file-system watching based on the `FSEventStream` API in macOS. | ||
| 2 | //! While macOS supports kqueue, it does not allow detecting changes to files without | ||
| 3 | //! placing watches on each individual file, meaning FD limits are reached incredibly | ||
| 4 | //! quickly. The File System Events API works differently: it implements *recursive* | ||
| 5 | //! directory watches, managed by a system service. Rather than being in libc, the API is | ||
| 6 | //! exposed by the CoreServices framework. To avoid a compile dependency on the framework | ||
| 7 | //! bundle, we dynamically load CoreServices with `std.DynLib`. | ||
| 8 | //! | ||
| 9 | //! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been | ||
| 10 | //! made to keep that specialization to a minimum. Other use cases could be served with | ||
| 11 | //! relatively minimal modifications to the `watch_paths` field and its usages (in | ||
| 12 | //! particular the `setPaths` function). We avoid using the global GCD dispatch queue in | ||
| 13 | //! favour of creating our own and synchronizing with an explicit semaphore, meaning this | ||
| 14 | //! logic is thread-safe and does not affect process-global state. | ||
| 15 | //! | ||
| 16 | //! In theory, this API is quite good at avoiding filesystem race conditions. In practice, | ||
| 17 | //! the logic that would avoid them is currently disabled, because the build system kind | ||
| 18 | //! of relies on them at the time of writing to avoid redundant work -- see the comment at | ||
| 19 | //! the top of `wait` for details. | ||
| 20 | |||
| 21 | const enable_debug_logs = false; | ||
| 22 | |||
| 23 | core_services: std.DynLib, | ||
| 24 | resolved_symbols: ResolvedSymbols, | ||
| 25 | |||
| 26 | paths_arena: std.heap.ArenaAllocator.State, | ||
| 27 | /// The roots of the recursive watches. FSEvents has relatively small limits on the number | ||
| 28 | /// of watched paths, so this slice must not be too long. The paths themselves are allocated | ||
| 29 | /// into `paths_arena`, but this slice is allocated into the GPA. | ||
| 30 | watch_roots: [][:0]const u8, | ||
| 31 | /// All of the paths being watched. Value is the set of steps which depend on the file/directory. | ||
| 32 | /// Keys and values are in `paths_arena`, but this map is allocated into the GPA. | ||
| 33 | watch_paths: std.StringArrayHashMapUnmanaged([]const *std.Build.Step), | ||
| 34 | |||
| 35 | /// The semaphore we use to block the thread calling `wait` until the callback determines a relevant | ||
| 36 | /// event has occurred. This is retained across `wait` calls for simplicity and efficiency. | ||
| 37 | waiting_semaphore: dispatch.semaphore_t, | ||
| 38 | /// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the | ||
| 39 | /// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained | ||
| 40 | /// across `wait` calls for simplicity and efficiency. | ||
| 41 | dispatch_queue: dispatch.queue_t, | ||
| 42 | /// In theory, this field avoids race conditions. In practice, it is essentially unused at the time | ||
| 43 | /// of writing. See the comment at the start of `wait` for details. | ||
| 44 | since_event: FSEventStreamEventId, | ||
| 45 | |||
| 46 | cwd_path: []const u8, | ||
| 47 | |||
| 48 | /// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols | ||
| 49 | /// is not present, `init` will close the framework and return an error. | ||
| 50 | const ResolvedSymbols = struct { | ||
| 51 | FSEventStreamCreate: *const fn ( | ||
| 52 | allocator: CFAllocatorRef, | ||
| 53 | callback: FSEventStreamCallback, | ||
| 54 | ctx: ?*const FSEventStreamContext, | ||
| 55 | paths_to_watch: CFArrayRef, | ||
| 56 | since_when: FSEventStreamEventId, | ||
| 57 | latency: CFTimeInterval, | ||
| 58 | flags: FSEventStreamCreateFlags, | ||
| 59 | ) callconv(.c) FSEventStreamRef, | ||
| 60 | FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void, | ||
| 61 | FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool, | ||
| 62 | FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void, | ||
| 63 | FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void, | ||
| 64 | FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void, | ||
| 65 | FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId, | ||
| 66 | FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId, | ||
| 67 | CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void, | ||
| 68 | CFArrayCreate: *const fn ( | ||
| 69 | allocator: CFAllocatorRef, | ||
| 70 | values: [*]const usize, | ||
| 71 | num_values: CFIndex, | ||
| 72 | call_backs: ?*const CFArrayCallBacks, | ||
| 73 | ) callconv(.c) CFArrayRef, | ||
| 74 | CFStringCreateWithCString: *const fn ( | ||
| 75 | alloc: CFAllocatorRef, | ||
| 76 | c_str: [*:0]const u8, | ||
| 77 | encoding: CFStringEncoding, | ||
| 78 | ) callconv(.c) CFStringRef, | ||
| 79 | CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef, | ||
| 80 | kCFAllocatorUseContext: *const CFAllocatorRef, | ||
| 81 | }; | ||
| 82 | |||
| 83 | pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents { | ||
| 84 | var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch | ||
| 85 | return error.OpenFrameworkFailed; | ||
| 86 | errdefer core_services.close(); | ||
| 87 | |||
| 88 | var resolved_symbols: ResolvedSymbols = undefined; | ||
| 89 | inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| { | ||
| 90 | @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol; | ||
| 91 | } | ||
| 92 | |||
| 93 | return .{ | ||
| 94 | .core_services = core_services, | ||
| 95 | .resolved_symbols = resolved_symbols, | ||
| 96 | .paths_arena = .{}, | ||
| 97 | .watch_roots = &.{}, | ||
| 98 | .watch_paths = .empty, | ||
| 99 | .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources, | ||
| 100 | .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources, | ||
| 101 | // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order | ||
| 102 | // to notice any changes which happened during said work. | ||
| 103 | .since_event = resolved_symbols.FSEventsGetCurrentEventId(), | ||
| 104 | .cwd_path = cwd_path, | ||
| 105 | }; | ||
| 106 | } | ||
| 107 | |||
| 108 | pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void { | ||
| 109 | fse.waiting_semaphore.as_object().release(); | ||
| 110 | fse.dispatch_queue.as_object().release(); | ||
| 111 | fse.core_services.close(io); | ||
| 112 | |||
| 113 | gpa.free(fse.watch_roots); | ||
| 114 | fse.watch_paths.deinit(gpa); | ||
| 115 | { | ||
| 116 | var paths_arena = fse.paths_arena.promote(gpa); | ||
| 117 | paths_arena.deinit(); | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) !void { | ||
| 122 | var paths_arena_instance = fse.paths_arena.promote(gpa); | ||
| 123 | defer fse.paths_arena = paths_arena_instance.state; | ||
| 124 | const paths_arena = paths_arena_instance.allocator(); | ||
| 125 | |||
| 126 | var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty; | ||
| 127 | defer need_dirs.deinit(gpa); | ||
| 128 | |||
| 129 | fse.watch_paths.clearRetainingCapacity(); | ||
| 130 | |||
| 131 | // We take `step` by pointer for a slight memory optimization in a moment. | ||
| 132 | for (steps) |*step| { | ||
| 133 | for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| { | ||
| 134 | const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{ | ||
| 135 | fse.cwd_path, path.root_dir.path orelse ".", path.sub_path, | ||
| 136 | }); | ||
| 137 | try need_dirs.put(gpa, resolved_dir, {}); | ||
| 138 | for (files.items) |file_name| { | ||
| 139 | const watch_path = if (std.mem.eql(u8, file_name, ".")) | ||
| 140 | resolved_dir | ||
| 141 | else | ||
| 142 | try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name }); | ||
| 143 | const gop = try fse.watch_paths.getOrPut(gpa, watch_path); | ||
| 144 | if (gop.found_existing) { | ||
| 145 | const old_steps = gop.value_ptr.*; | ||
| 146 | const new_steps = try paths_arena.alloc(*std.Build.Step, old_steps.len + 1); | ||
| 147 | @memcpy(new_steps[0..old_steps.len], old_steps); | ||
| 148 | new_steps[old_steps.len] = step.*; | ||
| 149 | gop.value_ptr.* = new_steps; | ||
| 150 | } else { | ||
| 151 | // This is why we captured `step` by pointer! We can avoid allocating a slice of one | ||
| 152 | // step in the arena in the common case where a file is referenced by only one step. | ||
| 153 | gop.value_ptr.* = step[0..1]; | ||
| 154 | } | ||
| 155 | } | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | { | ||
| 160 | // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar"). | ||
| 161 | // To eliminate these, we'll re-add directories in order of path length with a redundancy check. | ||
| 162 | const old_dirs = try gpa.dupe([]const u8, need_dirs.keys()); | ||
| 163 | defer gpa.free(old_dirs); | ||
| 164 | std.mem.sort([]const u8, old_dirs, {}, struct { | ||
| 165 | fn lessThan(ctx: void, a: []const u8, b: []const u8) bool { | ||
| 166 | ctx; | ||
| 167 | return std.mem.lessThan(u8, a, b); | ||
| 168 | } | ||
| 169 | }.lessThan); | ||
| 170 | need_dirs.clearRetainingCapacity(); | ||
| 171 | for (old_dirs) |dir_path| { | ||
| 172 | var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path); | ||
| 173 | while (it.next()) |component| { | ||
| 174 | if (need_dirs.contains(component.path)) { | ||
| 175 | // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added | ||
| 176 | break; | ||
| 177 | } | ||
| 178 | } else { | ||
| 179 | need_dirs.putAssumeCapacityNoClobber(dir_path, {}); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very | ||
| 185 | // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/` | ||
| 186 | // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit | ||
| 187 | // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be | ||
| 188 | // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below* | ||
| 189 | // that known limit. | ||
| 190 | if (need_dirs.count() > 2048) { | ||
| 191 | // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P | ||
| 192 | if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{}); | ||
| 193 | fse.watch_roots = try gpa.realloc(fse.watch_roots, 1); | ||
| 194 | fse.watch_roots[0] = "/"; | ||
| 195 | } else { | ||
| 196 | fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count()); | ||
| 197 | for (fse.watch_roots, need_dirs.keys()) |*out, in| { | ||
| 198 | out.* = try paths_arena.dupeSentinel(u8, in, 0); | ||
| 199 | } | ||
| 200 | } | ||
| 201 | if (enable_debug_logs) { | ||
| 202 | watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len }); | ||
| 203 | for (fse.watch_roots) |dir_path| { | ||
| 204 | watch_log.debug("- '{s}'", .{dir_path}); | ||
| 205 | } | ||
| 206 | } | ||
| 207 | } | ||
| 208 | |||
| 209 | pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!std.Build.Watch.WaitResult { | ||
| 210 | if (fse.watch_roots.len == 0) @panic("nothing to watch"); | ||
| 211 | |||
| 212 | const rs = fse.resolved_symbols; | ||
| 213 | |||
| 214 | // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds | ||
| 215 | // to occur, because one step modifies a file which is an input to another step. The solution | ||
| 216 | // to this problem will probably be either: | ||
| 217 | // | ||
| 218 | // a) Don't include the output of one step as a watch input of another; only mark external | ||
| 219 | // files as watch inputs. Or... | ||
| 220 | // | ||
| 221 | // b) Note the current event ID when a step begins, and disregard events preceding that ID | ||
| 222 | // when considering whether to dirty that step in `eventCallback`. | ||
| 223 | // | ||
| 224 | // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does | ||
| 225 | // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those | ||
| 226 | // too at the time of writing, so this is kind of expected. | ||
| 227 | fse.since_event = .since_now; | ||
| 228 | |||
| 229 | const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{ | ||
| 230 | .version = 0, | ||
| 231 | .info = @constCast(&gpa), | ||
| 232 | .retain = null, | ||
| 233 | .release = null, | ||
| 234 | .copy_description = null, | ||
| 235 | .allocate = &cf_alloc_callbacks.allocate, | ||
| 236 | .reallocate = &cf_alloc_callbacks.reallocate, | ||
| 237 | .deallocate = &cf_alloc_callbacks.deallocate, | ||
| 238 | .preferred_size = null, | ||
| 239 | }) orelse return error.OutOfMemory; | ||
| 240 | defer rs.CFRelease(cf_allocator); | ||
| 241 | |||
| 242 | const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len); | ||
| 243 | @memset(cf_paths, null); | ||
| 244 | defer { | ||
| 245 | for (cf_paths) |o| if (o) |p| rs.CFRelease(p); | ||
| 246 | gpa.free(cf_paths); | ||
| 247 | } | ||
| 248 | for (fse.watch_roots, cf_paths) |raw_path, *cf_path| { | ||
| 249 | cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8); | ||
| 250 | } | ||
| 251 | const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null); | ||
| 252 | defer rs.CFRelease(cf_paths_array); | ||
| 253 | |||
| 254 | const callback_ctx: EventCallbackCtx = .{ | ||
| 255 | .fse = fse, | ||
| 256 | .gpa = gpa, | ||
| 257 | }; | ||
| 258 | const event_stream = rs.FSEventStreamCreate( | ||
| 259 | null, | ||
| 260 | &eventCallback, | ||
| 261 | &.{ | ||
| 262 | .version = 0, | ||
| 263 | .info = @constCast(&callback_ctx), | ||
| 264 | .retain = null, | ||
| 265 | .release = null, | ||
| 266 | .copy_description = null, | ||
| 267 | }, | ||
| 268 | cf_paths_array, | ||
| 269 | fse.since_event, | ||
| 270 | 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events | ||
| 271 | .{ .watch_root = true, .file_events = true }, | ||
| 272 | ); | ||
| 273 | defer rs.FSEventStreamRelease(event_stream); | ||
| 274 | rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue); | ||
| 275 | defer rs.FSEventStreamInvalidate(event_stream); | ||
| 276 | if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed; | ||
| 277 | defer rs.FSEventStreamStop(event_stream); | ||
| 278 | const result = fse.waiting_semaphore.wait(timeout: { | ||
| 279 | const ns = timeout_ns orelse break :timeout .FOREVER; | ||
| 280 | break :timeout .time(.NOW, @intCast(ns)); | ||
| 281 | }); | ||
| 282 | return switch (result) { | ||
| 283 | 0 => .dirty, | ||
| 284 | else => .timeout, | ||
| 285 | }; | ||
| 286 | } | ||
| 287 | |||
| 288 | const cf_alloc_callbacks = struct { | ||
| 289 | const log = std.log.scoped(.cf_alloc); | ||
| 290 | fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { | ||
| 291 | if (enable_debug_logs) log.debug("allocate {d}", .{size}); | ||
| 292 | _ = hint; | ||
| 293 | const gpa: *const Allocator = @ptrCast(@alignCast(info)); | ||
| 294 | const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null; | ||
| 295 | const metadata: *usize = @ptrCast(mem); | ||
| 296 | metadata.* = @intCast(size); | ||
| 297 | return mem[@sizeOf(usize)..].ptr; | ||
| 298 | } | ||
| 299 | fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { | ||
| 300 | if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size }); | ||
| 301 | _ = hint; | ||
| 302 | if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL | ||
| 303 | const gpa: *const Allocator = @ptrCast(@alignCast(info)); | ||
| 304 | const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); | ||
| 305 | const old_size = @as(*const usize, @ptrCast(old_base)).*; | ||
| 306 | const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; | ||
| 307 | const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null; | ||
| 308 | const metadata: *usize = @ptrCast(new_mem); | ||
| 309 | metadata.* = @intCast(new_size); | ||
| 310 | return new_mem[@sizeOf(usize)..].ptr; | ||
| 311 | } | ||
| 312 | fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void { | ||
| 313 | if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr}); | ||
| 314 | const gpa: *const Allocator = @ptrCast(@alignCast(info)); | ||
| 315 | const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); | ||
| 316 | const old_size = @as(*const usize, @ptrCast(old_base)).*; | ||
| 317 | const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; | ||
| 318 | gpa.free(old_mem); | ||
| 319 | } | ||
| 320 | }; | ||
| 321 | |||
| 322 | const EventCallbackCtx = struct { | ||
| 323 | fse: *FsEvents, | ||
| 324 | gpa: Allocator, | ||
| 325 | }; | ||
| 326 | |||
| 327 | fn eventCallback( | ||
| 328 | stream: ConstFSEventStreamRef, | ||
| 329 | client_callback_info: ?*anyopaque, | ||
| 330 | num_events: usize, | ||
| 331 | events_paths_ptr: *anyopaque, | ||
| 332 | events_flags_ptr: [*]const FSEventStreamEventFlags, | ||
| 333 | events_ids_ptr: [*]const FSEventStreamEventId, | ||
| 334 | ) callconv(.c) void { | ||
| 335 | const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info)); | ||
| 336 | const fse = ctx.fse; | ||
| 337 | const gpa = ctx.gpa; | ||
| 338 | const rs = fse.resolved_symbols; | ||
| 339 | const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr)); | ||
| 340 | const events_paths = events_paths_ptr_casted[0..num_events]; | ||
| 341 | const events_ids = events_ids_ptr[0..num_events]; | ||
| 342 | const events_flags = events_flags_ptr[0..num_events]; | ||
| 343 | var any_dirty = false; | ||
| 344 | for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| { | ||
| 345 | _ = event_id; | ||
| 346 | if (event_flags.history_done) continue; // sentinel | ||
| 347 | const event_path = std.mem.span(event_path_nts); | ||
| 348 | switch (event_flags.must_scan_sub_dirs) { | ||
| 349 | false => { | ||
| 350 | if (fse.watch_paths.get(event_path)) |steps| { | ||
| 351 | assert(steps.len > 0); | ||
| 352 | for (steps) |s| { | ||
| 353 | if (s.invalidateResult(gpa)) any_dirty = true; | ||
| 354 | } | ||
| 355 | } | ||
| 356 | if (std.fs.path.dirname(event_path)) |event_dirname| { | ||
| 357 | // Modifying '/foo/bar' triggers the watch on '/foo'. | ||
| 358 | if (fse.watch_paths.get(event_dirname)) |steps| { | ||
| 359 | assert(steps.len > 0); | ||
| 360 | for (steps) |s| { | ||
| 361 | if (s.invalidateResult(gpa)) any_dirty = true; | ||
| 362 | } | ||
| 363 | } | ||
| 364 | } | ||
| 365 | }, | ||
| 366 | true => { | ||
| 367 | // This is unlikely, but can occasionally happen when bottlenecked: events have been | ||
| 368 | // coalesced into one. We want to see if any of these events are actually relevant | ||
| 369 | // to us. The only way we can reasonably do that in this rare edge case is iterate | ||
| 370 | // the watch paths and see if any is under this directory. That's acceptable because | ||
| 371 | // we would otherwise kick off a rebuild which would be clearing those paths anyway. | ||
| 372 | const changed_path = std.fs.path.dirname(event_path) orelse event_path; | ||
| 373 | for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| { | ||
| 374 | if (dirStartsWith(watching_path, changed_path)) { | ||
| 375 | for (steps) |s| { | ||
| 376 | if (s.invalidateResult(gpa)) any_dirty = true; | ||
| 377 | } | ||
| 378 | } | ||
| 379 | } | ||
| 380 | }, | ||
| 381 | } | ||
| 382 | } | ||
| 383 | if (any_dirty) { | ||
| 384 | fse.since_event = rs.FSEventStreamGetLatestEventId(stream); | ||
| 385 | _ = fse.waiting_semaphore.signal(); | ||
| 386 | } | ||
| 387 | } | ||
| 388 | fn dirStartsWith(path: []const u8, prefix: []const u8) bool { | ||
| 389 | if (std.mem.eql(u8, path, prefix)) return true; | ||
| 390 | if (!std.mem.startsWith(u8, path, prefix)) return false; | ||
| 391 | if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar` | ||
| 392 | return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar` | ||
| 393 | } | ||
| 394 | |||
| 395 | const CFAllocatorRef = ?*const opaque {}; | ||
| 396 | const CFArrayRef = *const opaque {}; | ||
| 397 | const CFStringRef = *const opaque {}; | ||
| 398 | const CFTimeInterval = f64; | ||
| 399 | const CFIndex = i32; | ||
| 400 | const CFOptionFlags = enum(u32) { _ }; | ||
| 401 | const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque; | ||
| 402 | const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void; | ||
| 403 | const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef; | ||
| 404 | const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; | ||
| 405 | const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; | ||
| 406 | const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void; | ||
| 407 | const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex; | ||
| 408 | const CFAllocatorContext = extern struct { | ||
| 409 | version: CFIndex, | ||
| 410 | info: ?*anyopaque, | ||
| 411 | retain: ?CFAllocatorRetainCallBack, | ||
| 412 | release: ?CFAllocatorReleaseCallBack, | ||
| 413 | copy_description: ?CFAllocatorCopyDescriptionCallBack, | ||
| 414 | allocate: CFAllocatorAllocateCallBack, | ||
| 415 | reallocate: ?CFAllocatorReallocateCallBack, | ||
| 416 | deallocate: ?CFAllocatorDeallocateCallBack, | ||
| 417 | preferred_size: ?CFAllocatorPreferredSizeCallBack, | ||
| 418 | }; | ||
| 419 | const CFArrayCallBacks = opaque {}; | ||
| 420 | const CFStringEncoding = enum(u32) { | ||
| 421 | invalid_id = std.math.maxInt(u32), | ||
| 422 | mac_roman = 0, | ||
| 423 | windows_latin_1 = 0x500, | ||
| 424 | iso_latin_1 = 0x201, | ||
| 425 | next_step_latin = 0xB01, | ||
| 426 | ascii = 0x600, | ||
| 427 | unicode = 0x100, | ||
| 428 | utf8 = 0x8000100, | ||
| 429 | non_lossy_ascii = 0xBFF, | ||
| 430 | }; | ||
| 431 | |||
| 432 | const FSEventStreamRef = *opaque {}; | ||
| 433 | const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child; | ||
| 434 | const FSEventStreamCallback = *const fn ( | ||
| 435 | stream: ConstFSEventStreamRef, | ||
| 436 | client_callback_info: ?*anyopaque, | ||
| 437 | num_events: usize, | ||
| 438 | event_paths: *anyopaque, | ||
| 439 | event_flags: [*]const FSEventStreamEventFlags, | ||
| 440 | event_ids: [*]const FSEventStreamEventId, | ||
| 441 | ) callconv(.c) void; | ||
| 442 | const FSEventStreamContext = extern struct { | ||
| 443 | version: CFIndex, | ||
| 444 | info: ?*anyopaque, | ||
| 445 | retain: ?CFAllocatorRetainCallBack, | ||
| 446 | release: ?CFAllocatorReleaseCallBack, | ||
| 447 | copy_description: ?CFAllocatorCopyDescriptionCallBack, | ||
| 448 | }; | ||
| 449 | const FSEventStreamEventId = enum(u64) { | ||
| 450 | since_now = std.math.maxInt(u64), | ||
| 451 | _, | ||
| 452 | }; | ||
| 453 | const FSEventStreamCreateFlags = packed struct(u32) { | ||
| 454 | use_cf_types: bool = false, | ||
| 455 | no_defer: bool = false, | ||
| 456 | watch_root: bool = false, | ||
| 457 | ignore_self: bool = false, | ||
| 458 | file_events: bool = false, | ||
| 459 | _: u27 = 0, | ||
| 460 | }; | ||
| 461 | const FSEventStreamEventFlags = packed struct(u32) { | ||
| 462 | must_scan_sub_dirs: bool, | ||
| 463 | user_dropped: bool, | ||
| 464 | kernel_dropped: bool, | ||
| 465 | event_ids_wrapped: bool, | ||
| 466 | history_done: bool, | ||
| 467 | root_changed: bool, | ||
| 468 | mount: bool, | ||
| 469 | unmount: bool, | ||
| 470 | _: u24 = 0, | ||
| 471 | }; | ||
| 472 | |||
| 473 | const dispatch = std.c.dispatch; | ||
| 474 | const std = @import("std"); | ||
| 475 | const Io = std.Io; | ||
| 476 | const assert = std.debug.assert; | ||
| 477 | const Allocator = std.mem.Allocator; | ||
| 478 | const watch_log = std.log.scoped(.watch); | ||
| 479 | const FsEvents = @This(); | ||
lib/compiler/Maker/WebServer.zig created+940| ... | @@ -0,0 +1,940 @@ | ||
| 1 | const WebServer = @This(); | ||
| 2 | |||
| 3 | const builtin = @import("builtin"); | ||
| 4 | |||
| 5 | const std = @import("std"); | ||
| 6 | const Allocator = std.mem.Allocator; | ||
| 7 | const Cache = std.Build.Cache; | ||
| 8 | const Configuration = std.Build.Configuration; | ||
| 9 | const Io = std.Io; | ||
| 10 | const abi = std.Build.abi; | ||
| 11 | const assert = std.debug.assert; | ||
| 12 | const http = std.http; | ||
| 13 | const log = std.log.scoped(.web_server); | ||
| 14 | const mem = std.mem; | ||
| 15 | const net = std.Io.net; | ||
| 16 | |||
| 17 | const Fuzz = @import("Fuzz.zig"); | ||
| 18 | const Graph = @import("Graph.zig"); | ||
| 19 | const Step = @import("Step.zig"); | ||
| 20 | |||
| 21 | gpa: Allocator, | ||
| 22 | graph: *const Graph, | ||
| 23 | all_steps: []const Configuration.Step.Index, | ||
| 24 | listen_address: net.IpAddress, | ||
| 25 | root_prog_node: std.Progress.Node, | ||
| 26 | watch: bool, | ||
| 27 | |||
| 28 | tcp_server: ?net.Server, | ||
| 29 | serve_task: ?Io.Future(Io.Cancelable!void), | ||
| 30 | |||
| 31 | /// Uses `Io.Clock.awake`. | ||
| 32 | base_timestamp: Io.Timestamp, | ||
| 33 | /// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`. | ||
| 34 | step_names_trailing: []u8, | ||
| 35 | |||
| 36 | /// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps. | ||
| 37 | /// Accessed atomically. | ||
| 38 | step_status_bits: []u8, | ||
| 39 | |||
| 40 | fuzz: ?Fuzz, | ||
| 41 | time_report_mutex: Io.Mutex, | ||
| 42 | time_report_msgs: [][]u8, | ||
| 43 | time_report_update_times: []i64, | ||
| 44 | |||
| 45 | build_status: std.atomic.Value(abi.BuildStatus), | ||
| 46 | /// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate` | ||
| 47 | /// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so | ||
| 48 | /// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it | ||
| 49 | /// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For | ||
| 50 | /// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes, | ||
| 51 | /// because this value changes quickly so this would result in constantly spamming all clients with | ||
| 52 | /// an unreasonable number of packets. | ||
| 53 | update_id: std.atomic.Value(u32), | ||
| 54 | |||
| 55 | runner_request_mutex: Io.Mutex, | ||
| 56 | runner_request_ready_cond: Io.Condition, | ||
| 57 | runner_request_empty_cond: Io.Condition, | ||
| 58 | runner_request: ?RunnerRequest, | ||
| 59 | |||
| 60 | /// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates | ||
| 61 | /// on a fixed interval of this many milliseconds. | ||
| 62 | const default_update_interval_ms = 500; | ||
| 63 | |||
| 64 | pub const base_clock: Io.Clock = .awake; | ||
| 65 | |||
| 66 | /// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`. | ||
| 67 | pub fn notifyUpdate(ws: *WebServer) void { | ||
| 68 | _ = ws.update_id.rmw(.Add, 1, .release); | ||
| 69 | ws.graph.io.futexWake(u32, &ws.update_id.raw, 16); | ||
| 70 | } | ||
| 71 | |||
| 72 | pub const Options = struct { | ||
| 73 | gpa: Allocator, | ||
| 74 | graph: *const Graph, | ||
| 75 | all_steps: []const Configuration.Step.Index, | ||
| 76 | root_prog_node: std.Progress.Node, | ||
| 77 | watch: bool, | ||
| 78 | listen_address: net.IpAddress, | ||
| 79 | base_timestamp: Io.Clock.Timestamp, | ||
| 80 | configuration: *const Configuration, | ||
| 81 | }; | ||
| 82 | pub fn init(opts: Options) WebServer { | ||
| 83 | // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent` | ||
| 84 | // instead of threads, so that the web server can function in single-threaded builds. | ||
| 85 | comptime assert(!builtin.single_threaded); | ||
| 86 | assert(opts.base_timestamp.clock == base_clock); | ||
| 87 | |||
| 88 | const all_steps = opts.all_steps; | ||
| 89 | const c = opts.configuration; | ||
| 90 | |||
| 91 | const step_names_trailing = opts.gpa.alloc(u8, len: { | ||
| 92 | var name_bytes: usize = 0; | ||
| 93 | for (all_steps) |step_index| name_bytes += step_index.ptr(c).name.slice(c).len; | ||
| 94 | break :len name_bytes + all_steps.len * 4; | ||
| 95 | }) catch @panic("out of memory"); | ||
| 96 | { | ||
| 97 | const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]); | ||
| 98 | var idx: usize = all_steps.len * 4; | ||
| 99 | for (all_steps, step_name_lens) |step_index, *name_len| { | ||
| 100 | const step_name = step_index.ptr(c).name.slice(c); | ||
| 101 | name_len.* = @intCast(step_name.len); | ||
| 102 | @memcpy(step_names_trailing[idx..][0..step_name.len], step_name); | ||
| 103 | idx += step_name.len; | ||
| 104 | } | ||
| 105 | assert(idx == step_names_trailing.len); | ||
| 106 | } | ||
| 107 | |||
| 108 | const step_status_bits = opts.gpa.alloc( | ||
| 109 | u8, | ||
| 110 | std.math.divCeil(usize, all_steps.len, 4) catch unreachable, | ||
| 111 | ) catch @panic("out of memory"); | ||
| 112 | @memset(step_status_bits, 0); | ||
| 113 | |||
| 114 | const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0; | ||
| 115 | const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory"); | ||
| 116 | const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory"); | ||
| 117 | @memset(time_report_msgs, &.{}); | ||
| 118 | @memset(time_report_update_times, std.math.minInt(i64)); | ||
| 119 | |||
| 120 | return .{ | ||
| 121 | .gpa = opts.gpa, | ||
| 122 | .graph = opts.graph, | ||
| 123 | .all_steps = all_steps, | ||
| 124 | .listen_address = opts.listen_address, | ||
| 125 | .root_prog_node = opts.root_prog_node, | ||
| 126 | .watch = opts.watch, | ||
| 127 | |||
| 128 | .tcp_server = null, | ||
| 129 | .serve_task = null, | ||
| 130 | |||
| 131 | .base_timestamp = opts.base_timestamp.raw, | ||
| 132 | .step_names_trailing = step_names_trailing, | ||
| 133 | |||
| 134 | .step_status_bits = step_status_bits, | ||
| 135 | |||
| 136 | .fuzz = null, | ||
| 137 | .time_report_mutex = .init, | ||
| 138 | .time_report_msgs = time_report_msgs, | ||
| 139 | .time_report_update_times = time_report_update_times, | ||
| 140 | |||
| 141 | .build_status = .init(.idle), | ||
| 142 | .update_id = .init(0), | ||
| 143 | |||
| 144 | .runner_request_mutex = .init, | ||
| 145 | .runner_request_ready_cond = .init, | ||
| 146 | .runner_request_empty_cond = .init, | ||
| 147 | .runner_request = null, | ||
| 148 | }; | ||
| 149 | } | ||
| 150 | pub fn deinit(ws: *WebServer) void { | ||
| 151 | const gpa = ws.gpa; | ||
| 152 | const io = ws.graph.io; | ||
| 153 | |||
| 154 | gpa.free(ws.step_names_trailing); | ||
| 155 | gpa.free(ws.step_status_bits); | ||
| 156 | |||
| 157 | if (ws.fuzz) |*f| f.deinit(); | ||
| 158 | for (ws.time_report_msgs) |msg| gpa.free(msg); | ||
| 159 | gpa.free(ws.time_report_msgs); | ||
| 160 | gpa.free(ws.time_report_update_times); | ||
| 161 | |||
| 162 | if (ws.serve_task) |t| { | ||
| 163 | if (ws.tcp_server) |*s| s.stream.close(io); | ||
| 164 | t.await(); | ||
| 165 | } | ||
| 166 | if (ws.tcp_server) |*s| s.deinit(); | ||
| 167 | |||
| 168 | gpa.free(ws.step_names_trailing); | ||
| 169 | } | ||
| 170 | pub fn start(ws: *WebServer) error{AlreadyReported}!void { | ||
| 171 | assert(ws.tcp_server == null); | ||
| 172 | assert(ws.serve_task == null); | ||
| 173 | const io = ws.graph.io; | ||
| 174 | |||
| 175 | ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| { | ||
| 176 | log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err }); | ||
| 177 | return error.AlreadyReported; | ||
| 178 | }; | ||
| 179 | ws.serve_task = io.concurrent(serve, .{ws}) catch |err| { | ||
| 180 | log.err("unable to spawn web server thread: {t}", .{err}); | ||
| 181 | ws.tcp_server.?.deinit(io); | ||
| 182 | ws.tcp_server = null; | ||
| 183 | return error.AlreadyReported; | ||
| 184 | }; | ||
| 185 | |||
| 186 | log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address}); | ||
| 187 | if (ws.listen_address.getPort() == 0) { | ||
| 188 | log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address}); | ||
| 189 | } | ||
| 190 | } | ||
| 191 | fn serve(ws: *WebServer) Io.Cancelable!void { | ||
| 192 | const io = ws.graph.io; | ||
| 193 | var group: Io.Group = .init; | ||
| 194 | defer group.cancel(io); | ||
| 195 | while (true) { | ||
| 196 | var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) { | ||
| 197 | error.Canceled => |e| return e, | ||
| 198 | else => |e| { | ||
| 199 | log.err("failed to accept connection: {t}", .{e}); | ||
| 200 | return; | ||
| 201 | }, | ||
| 202 | }; | ||
| 203 | group.concurrent(io, accept, .{ ws, stream }) catch |err| { | ||
| 204 | log.err("unable to spawn connection thread: {t}", .{err}); | ||
| 205 | stream.close(io); | ||
| 206 | continue; | ||
| 207 | }; | ||
| 208 | } | ||
| 209 | } | ||
| 210 | |||
| 211 | pub fn startBuild(ws: *WebServer) void { | ||
| 212 | if (ws.fuzz) |*fuzz| { | ||
| 213 | fuzz.deinit(); | ||
| 214 | ws.fuzz = null; | ||
| 215 | } | ||
| 216 | for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic); | ||
| 217 | ws.build_status.store(.running, .monotonic); | ||
| 218 | ws.notifyUpdate(); | ||
| 219 | } | ||
| 220 | |||
| 221 | pub fn updateStepStatus( | ||
| 222 | ws: *WebServer, | ||
| 223 | step_index: Configuration.Step.Index, | ||
| 224 | new_status: abi.StepUpdate.Status, | ||
| 225 | ) void { | ||
| 226 | // TODO don't do linear search, especially in a hot loop like this | ||
| 227 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 228 | if (s == step_index) break @intCast(i); | ||
| 229 | } else unreachable; | ||
| 230 | const ptr = &ws.step_status_bits[step_idx / 4]; | ||
| 231 | const bit_offset: u3 = @intCast((step_idx % 4) * 2); | ||
| 232 | const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset); | ||
| 233 | const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset; | ||
| 234 | _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic); | ||
| 235 | ws.notifyUpdate(); | ||
| 236 | } | ||
| 237 | |||
| 238 | pub fn finishBuild(ws: *WebServer, opts: struct { | ||
| 239 | fuzz: bool, | ||
| 240 | }) void { | ||
| 241 | if (opts.fuzz) { | ||
| 242 | switch (builtin.os.tag) { | ||
| 243 | // Current implementation depends on two things that need to be ported to Windows: | ||
| 244 | // * Memory-mapping to share data between the fuzzer and build runner. | ||
| 245 | // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving | ||
| 246 | // many addresses to source locations). | ||
| 247 | .windows => std.process.fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}), | ||
| 248 | else => {}, | ||
| 249 | } | ||
| 250 | if (@bitSizeOf(usize) != 64) { | ||
| 251 | // Current implementation depends on posix.mmap()'s second | ||
| 252 | // parameter, `length: usize`, being compatible with file system's | ||
| 253 | // u64 return value. This is not the case on 32-bit platforms. | ||
| 254 | // Affects or affected by issues #5185, #22523, and #22464. | ||
| 255 | std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); | ||
| 256 | } | ||
| 257 | |||
| 258 | assert(ws.fuzz == null); | ||
| 259 | |||
| 260 | ws.build_status.store(.fuzz_init, .monotonic); | ||
| 261 | ws.notifyUpdate(); | ||
| 262 | |||
| 263 | ws.fuzz = Fuzz.init( | ||
| 264 | ws.gpa, | ||
| 265 | ws.graph.io, | ||
| 266 | ws.all_steps, | ||
| 267 | ws.root_prog_node, | ||
| 268 | .{ .forever = .{ .ws = ws } }, | ||
| 269 | ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)}); | ||
| 270 | ws.fuzz.?.start(); | ||
| 271 | } | ||
| 272 | |||
| 273 | ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic); | ||
| 274 | ws.notifyUpdate(); | ||
| 275 | } | ||
| 276 | |||
| 277 | pub fn now(s: *const WebServer) i64 { | ||
| 278 | const io = s.graph.io; | ||
| 279 | const ts = base_clock.now(io); | ||
| 280 | return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds()); | ||
| 281 | } | ||
| 282 | |||
| 283 | fn accept(ws: *WebServer, stream: net.Stream) void { | ||
| 284 | const io = ws.graph.io; | ||
| 285 | defer { | ||
| 286 | // `net.Stream.close` wants to helpfully overwrite `stream` with | ||
| 287 | // `undefined`, but it cannot do so since it is an immutable parameter. | ||
| 288 | var copy = stream; | ||
| 289 | copy.close(io); | ||
| 290 | } | ||
| 291 | var send_buffer: [4096]u8 = undefined; | ||
| 292 | var recv_buffer: [4096]u8 = undefined; | ||
| 293 | var connection_reader = stream.reader(io, &recv_buffer); | ||
| 294 | var connection_writer = stream.writer(io, &send_buffer); | ||
| 295 | var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); | ||
| 296 | |||
| 297 | while (true) { | ||
| 298 | var request = server.receiveHead() catch |err| switch (err) { | ||
| 299 | error.HttpConnectionClosing => return, | ||
| 300 | else => return log.err("failed to receive http request: {t}", .{err}), | ||
| 301 | }; | ||
| 302 | switch (request.upgradeRequested()) { | ||
| 303 | .websocket => |opt_key| { | ||
| 304 | const key = opt_key orelse return log.err("missing websocket key", .{}); | ||
| 305 | var web_socket = request.respondWebSocket(.{ .key = key }) catch { | ||
| 306 | return log.err("failed to respond web socket: {t}", .{connection_writer.err.?}); | ||
| 307 | }; | ||
| 308 | ws.serveWebSocket(&web_socket) catch |err| { | ||
| 309 | log.err("failed to serve websocket: {t}", .{err}); | ||
| 310 | return; | ||
| 311 | }; | ||
| 312 | comptime unreachable; | ||
| 313 | }, | ||
| 314 | .other => |name| return log.err("unknown upgrade request: {s}", .{name}), | ||
| 315 | .none => { | ||
| 316 | ws.serveRequest(&request) catch |err| switch (err) { | ||
| 317 | error.AlreadyReported => return, | ||
| 318 | else => { | ||
| 319 | log.err("failed to serve '{s}': {t}", .{ request.head.target, err }); | ||
| 320 | return; | ||
| 321 | }, | ||
| 322 | }; | ||
| 323 | }, | ||
| 324 | } | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn { | ||
| 329 | const io = ws.graph.io; | ||
| 330 | |||
| 331 | var prev_build_status = ws.build_status.load(.monotonic); | ||
| 332 | |||
| 333 | const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len); | ||
| 334 | defer ws.gpa.free(prev_step_status_bits); | ||
| 335 | for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| { | ||
| 336 | copy.* = @atomicLoad(u8, shared, .monotonic); | ||
| 337 | } | ||
| 338 | |||
| 339 | var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock }); | ||
| 340 | defer recv_thread.cancel(io); | ||
| 341 | |||
| 342 | { | ||
| 343 | const hello_header: abi.Hello = .{ | ||
| 344 | .status = prev_build_status, | ||
| 345 | .flags = .{ | ||
| 346 | .time_report = ws.graph.time_report, | ||
| 347 | }, | ||
| 348 | .timestamp = ws.now(), | ||
| 349 | .steps_len = @intCast(ws.all_steps.len), | ||
| 350 | }; | ||
| 351 | var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits }; | ||
| 352 | try sock.writeMessageVec(&bufs, .binary); | ||
| 353 | } | ||
| 354 | |||
| 355 | var prev_fuzz: Fuzz.Previous = .init; | ||
| 356 | var prev_time: i64 = std.math.minInt(i64); | ||
| 357 | while (true) { | ||
| 358 | const start_time = ws.now(); | ||
| 359 | const start_update_id = ws.update_id.load(.acquire); | ||
| 360 | |||
| 361 | if (ws.fuzz) |*fuzz| { | ||
| 362 | try fuzz.sendUpdate(sock, &prev_fuzz); | ||
| 363 | } | ||
| 364 | |||
| 365 | { | ||
| 366 | try ws.time_report_mutex.lock(io); | ||
| 367 | defer ws.time_report_mutex.unlock(io); | ||
| 368 | for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| { | ||
| 369 | if (update_time <= prev_time) continue; | ||
| 370 | // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so | ||
| 371 | // that we don't hold up the build system on the client accepting this packet. | ||
| 372 | const owned_msg = try ws.gpa.dupe(u8, msg); | ||
| 373 | defer ws.gpa.free(owned_msg); | ||
| 374 | // Temporarily unlock, then re-lock after the message is sent. | ||
| 375 | ws.time_report_mutex.unlock(io); | ||
| 376 | defer ws.time_report_mutex.lockUncancelable(io); | ||
| 377 | try sock.writeMessage(owned_msg, .binary); | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | { | ||
| 382 | const build_status = ws.build_status.load(.monotonic); | ||
| 383 | if (build_status != prev_build_status) { | ||
| 384 | prev_build_status = build_status; | ||
| 385 | const msg: abi.StatusUpdate = .{ .new = build_status }; | ||
| 386 | try sock.writeMessage(@ptrCast(&msg), .binary); | ||
| 387 | } | ||
| 388 | } | ||
| 389 | |||
| 390 | for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| { | ||
| 391 | const cur_byte = @atomicLoad(u8, shared, .monotonic); | ||
| 392 | if (prev_byte.* == cur_byte) continue; | ||
| 393 | const cur: [4]abi.StepUpdate.Status = .{ | ||
| 394 | @enumFromInt(@as(u2, @truncate(cur_byte >> 0))), | ||
| 395 | @enumFromInt(@as(u2, @truncate(cur_byte >> 2))), | ||
| 396 | @enumFromInt(@as(u2, @truncate(cur_byte >> 4))), | ||
| 397 | @enumFromInt(@as(u2, @truncate(cur_byte >> 6))), | ||
| 398 | }; | ||
| 399 | const prev: [4]abi.StepUpdate.Status = .{ | ||
| 400 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))), | ||
| 401 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))), | ||
| 402 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))), | ||
| 403 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))), | ||
| 404 | }; | ||
| 405 | for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| { | ||
| 406 | const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } }; | ||
| 407 | if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary); | ||
| 408 | } | ||
| 409 | prev_byte.* = cur_byte; | ||
| 410 | } | ||
| 411 | |||
| 412 | prev_time = start_time; | ||
| 413 | |||
| 414 | const old_cp = io.swapCancelProtection(.blocked); | ||
| 415 | defer _ = io.swapCancelProtection(old_cp); | ||
| 416 | io.futexWaitTimeout( | ||
| 417 | u32, | ||
| 418 | &ws.update_id.raw, | ||
| 419 | start_update_id, | ||
| 420 | .{ .duration = .{ | ||
| 421 | .clock = .awake, | ||
| 422 | .raw = .fromMilliseconds(default_update_interval_ms), | ||
| 423 | } }, | ||
| 424 | ) catch |err| switch (err) { | ||
| 425 | error.Canceled => unreachable, | ||
| 426 | }; | ||
| 427 | } | ||
| 428 | } | ||
| 429 | fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void { | ||
| 430 | const io = ws.graph.io; | ||
| 431 | |||
| 432 | while (true) { | ||
| 433 | const msg = sock.readSmallMessage() catch return; | ||
| 434 | if (msg.opcode != .binary) continue; | ||
| 435 | if (msg.data.len == 0) continue; | ||
| 436 | const tag: abi.ToServerTag = @enumFromInt(msg.data[0]); | ||
| 437 | switch (tag) { | ||
| 438 | _ => continue, | ||
| 439 | .rebuild => while (true) { | ||
| 440 | ws.runner_request_mutex.lock(io) catch |err| switch (err) { | ||
| 441 | error.Canceled => return, | ||
| 442 | }; | ||
| 443 | defer ws.runner_request_mutex.unlock(io); | ||
| 444 | if (ws.runner_request == null) { | ||
| 445 | ws.runner_request = .rebuild; | ||
| 446 | ws.runner_request_ready_cond.signal(io); | ||
| 447 | break; | ||
| 448 | } | ||
| 449 | ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return; | ||
| 450 | }, | ||
| 451 | } | ||
| 452 | } | ||
| 453 | } | ||
| 454 | |||
| 455 | fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void { | ||
| 456 | // Strip an optional leading '/debug' component from the request. | ||
| 457 | const target: []const u8, const debug: bool = target: { | ||
| 458 | if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true }; | ||
| 459 | if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true }; | ||
| 460 | if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true }; | ||
| 461 | break :target .{ req.head.target, false }; | ||
| 462 | }; | ||
| 463 | |||
| 464 | if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html"); | ||
| 465 | if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript"); | ||
| 466 | if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css"); | ||
| 467 | if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css"); | ||
| 468 | if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast); | ||
| 469 | |||
| 470 | if (ws.fuzz) |*fuzz| { | ||
| 471 | if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req); | ||
| 472 | } | ||
| 473 | |||
| 474 | try req.respond("not found", .{ | ||
| 475 | .status = .not_found, | ||
| 476 | .extra_headers = &.{ | ||
| 477 | .{ .name = "Content-Type", .value = "text/plain" }, | ||
| 478 | }, | ||
| 479 | }); | ||
| 480 | } | ||
| 481 | |||
| 482 | fn serveLibFile( | ||
| 483 | ws: *WebServer, | ||
| 484 | request: *http.Server.Request, | ||
| 485 | sub_path: []const u8, | ||
| 486 | content_type: []const u8, | ||
| 487 | ) !void { | ||
| 488 | return serveFile(ws, request, .{ | ||
| 489 | .root_dir = ws.graph.zig_lib_directory, | ||
| 490 | .sub_path = sub_path, | ||
| 491 | }, content_type); | ||
| 492 | } | ||
| 493 | fn serveClientWasm( | ||
| 494 | ws: *WebServer, | ||
| 495 | req: *http.Server.Request, | ||
| 496 | optimize_mode: std.builtin.OptimizeMode, | ||
| 497 | ) !void { | ||
| 498 | var arena_state: std.heap.ArenaAllocator = .init(ws.gpa); | ||
| 499 | defer arena_state.deinit(); | ||
| 500 | const arena = arena_state.allocator(); | ||
| 501 | |||
| 502 | // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page. | ||
| 503 | const bin_path = try buildClientWasm(ws, arena, optimize_mode); | ||
| 504 | return serveFile(ws, req, bin_path, "application/wasm"); | ||
| 505 | } | ||
| 506 | |||
| 507 | pub fn serveFile( | ||
| 508 | ws: *WebServer, | ||
| 509 | request: *http.Server.Request, | ||
| 510 | path: Cache.Path, | ||
| 511 | content_type: []const u8, | ||
| 512 | ) !void { | ||
| 513 | const gpa = ws.gpa; | ||
| 514 | const io = ws.graph.io; | ||
| 515 | // The desired API is actually sendfile, which will require enhancing http.Server. | ||
| 516 | // We load the file with every request so that the user can make changes to the file | ||
| 517 | // and refresh the HTML page without restarting this server. | ||
| 518 | const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| { | ||
| 519 | log.err("failed to read '{f}': {t}", .{ path, err }); | ||
| 520 | return error.AlreadyReported; | ||
| 521 | }; | ||
| 522 | defer gpa.free(file_contents); | ||
| 523 | try request.respond(file_contents, .{ | ||
| 524 | .extra_headers = &.{ | ||
| 525 | .{ .name = "Content-Type", .value = content_type }, | ||
| 526 | cache_control_header, | ||
| 527 | }, | ||
| 528 | }); | ||
| 529 | } | ||
| 530 | pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void { | ||
| 531 | const graph = ws.graph; | ||
| 532 | const io = graph.io; | ||
| 533 | |||
| 534 | var send_buffer: [0x4000]u8 = undefined; | ||
| 535 | var response = try request.respondStreaming(&send_buffer, .{ | ||
| 536 | .respond_options = .{ | ||
| 537 | .extra_headers = &.{ | ||
| 538 | .{ .name = "Content-Type", .value = "application/x-tar" }, | ||
| 539 | cache_control_header, | ||
| 540 | }, | ||
| 541 | }, | ||
| 542 | }); | ||
| 543 | |||
| 544 | var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer }; | ||
| 545 | |||
| 546 | for (paths) |path| { | ||
| 547 | var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| { | ||
| 548 | log.err("failed to open '{f}': {s}", .{ path, @errorName(err) }); | ||
| 549 | continue; | ||
| 550 | }; | ||
| 551 | defer file.close(io); | ||
| 552 | const stat = try file.stat(io); | ||
| 553 | var read_buffer: [1024]u8 = undefined; | ||
| 554 | var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size); | ||
| 555 | |||
| 556 | // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can | ||
| 557 | // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI: | ||
| 558 | // it turns out the WASM treats the first path component as the module name, typically | ||
| 559 | // resulting in modules named "" and "src". The compiler needs to tell the build system | ||
| 560 | // about the module graph so that the build system can correctly encode this information in | ||
| 561 | // the tar file. | ||
| 562 | // | ||
| 563 | // Additionally, this needs to ensure that all path separators for both prefix and | ||
| 564 | // sub_path are using the POSIX-style `/` on platforms that don't use it as their native | ||
| 565 | // path separator. | ||
| 566 | archiver.prefix = path.root_dir.path orelse graph.cache.cwd; | ||
| 567 | try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); | ||
| 568 | } | ||
| 569 | |||
| 570 | // intentionally not calling `archiver.finishPedantically` | ||
| 571 | try response.end(); | ||
| 572 | } | ||
| 573 | |||
| 574 | fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path { | ||
| 575 | const root_name = "build-web"; | ||
| 576 | const arch_os_abi = "wasm32-freestanding"; | ||
| 577 | const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; | ||
| 578 | |||
| 579 | const gpa = ws.gpa; | ||
| 580 | const graph = ws.graph; | ||
| 581 | const io = graph.io; | ||
| 582 | |||
| 583 | const main_src_path: Cache.Path = .{ | ||
| 584 | .root_dir = graph.zig_lib_directory, | ||
| 585 | .sub_path = "build-web/main.zig", | ||
| 586 | }; | ||
| 587 | const walk_src_path: Cache.Path = .{ | ||
| 588 | .root_dir = graph.zig_lib_directory, | ||
| 589 | .sub_path = "docs/wasm/Walk.zig", | ||
| 590 | }; | ||
| 591 | const html_render_src_path: Cache.Path = .{ | ||
| 592 | .root_dir = graph.zig_lib_directory, | ||
| 593 | .sub_path = "docs/wasm/html_render.zig", | ||
| 594 | }; | ||
| 595 | |||
| 596 | var argv: std.ArrayList([]const u8) = .empty; | ||
| 597 | |||
| 598 | try argv.appendSlice(arena, &.{ | ||
| 599 | graph.zig_exe, "build-exe", // | ||
| 600 | "-fno-entry", // | ||
| 601 | "-O", @tagName(optimize), // | ||
| 602 | "-target", arch_os_abi, // | ||
| 603 | "-mcpu", cpu_features, // | ||
| 604 | "--cache-dir", graph.global_cache_root.path orelse ".", // | ||
| 605 | "--global-cache-dir", graph.global_cache_root.path orelse ".", // | ||
| 606 | "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", // | ||
| 607 | "--name", root_name, // | ||
| 608 | "-rdynamic", // | ||
| 609 | "-fsingle-threaded", // | ||
| 610 | "--dep", "Walk", // | ||
| 611 | "--dep", "html_render", // | ||
| 612 | try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), // | ||
| 613 | try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), // | ||
| 614 | "--dep", "Walk", // | ||
| 615 | try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), // | ||
| 616 | "--listen=-", | ||
| 617 | }); | ||
| 618 | |||
| 619 | var child = try std.process.spawn(io, .{ | ||
| 620 | .argv = argv.items, | ||
| 621 | .environ_map = &graph.environ_map, | ||
| 622 | .stdin = .pipe, | ||
| 623 | .stdout = .pipe, | ||
| 624 | .stderr = .pipe, | ||
| 625 | }); | ||
| 626 | defer child.kill(io); | ||
| 627 | |||
| 628 | var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited }); | ||
| 629 | defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {}; | ||
| 630 | |||
| 631 | var stdout_buffer: [512]u8 = undefined; | ||
| 632 | var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer); | ||
| 633 | const stdout = &stdout_reader.interface; | ||
| 634 | |||
| 635 | { | ||
| 636 | var w = child.stdin.?.writer(io, &.{}); | ||
| 637 | w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) { | ||
| 638 | error.WriteFailed => return w.err.?, | ||
| 639 | }; | ||
| 640 | w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) { | ||
| 641 | error.WriteFailed => return w.err.?, | ||
| 642 | }; | ||
| 643 | } | ||
| 644 | |||
| 645 | const Header = std.zig.Server.Message.Header; | ||
| 646 | |||
| 647 | var result: ?Cache.Path = null; | ||
| 648 | var result_error_bundle = std.zig.ErrorBundle.empty; | ||
| 649 | var body_buffer: std.ArrayList(u8) = .empty; | ||
| 650 | defer body_buffer.deinit(gpa); | ||
| 651 | |||
| 652 | while (true) { | ||
| 653 | const header = stdout.takeStruct(Header, .little) catch |err| switch (err) { | ||
| 654 | error.ReadFailed => |e| return e, | ||
| 655 | error.EndOfStream => break, | ||
| 656 | }; | ||
| 657 | body_buffer.clearRetainingCapacity(); | ||
| 658 | try stdout.appendExact(gpa, &body_buffer, header.bytes_len); | ||
| 659 | const body = body_buffer.items; | ||
| 660 | |||
| 661 | switch (header.tag) { | ||
| 662 | .zig_version => { | ||
| 663 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) { | ||
| 664 | return error.ZigProtocolVersionMismatch; | ||
| 665 | } | ||
| 666 | }, | ||
| 667 | .error_bundle => { | ||
| 668 | result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body); | ||
| 669 | }, | ||
| 670 | .emit_digest => { | ||
| 671 | const EmitDigest = std.zig.Server.Message.EmitDigest; | ||
| 672 | const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body); | ||
| 673 | if (!ebp_hdr.flags.cache_hit) { | ||
| 674 | log.info("source changes detected; rebuilt wasm component", .{}); | ||
| 675 | } | ||
| 676 | const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; | ||
| 677 | result = .{ | ||
| 678 | .root_dir = graph.global_cache_root, | ||
| 679 | .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)), | ||
| 680 | }; | ||
| 681 | }, | ||
| 682 | else => {}, // ignore other messages | ||
| 683 | } | ||
| 684 | } | ||
| 685 | |||
| 686 | const stderr_contents = try stderr_task.await(io); | ||
| 687 | if (stderr_contents.len > 0) { | ||
| 688 | std.debug.print("{s}", .{stderr_contents}); | ||
| 689 | } | ||
| 690 | |||
| 691 | // Send EOF to stdin. | ||
| 692 | child.stdin.?.close(io); | ||
| 693 | child.stdin = null; | ||
| 694 | |||
| 695 | switch (try child.wait(io)) { | ||
| 696 | .exited => |code| { | ||
| 697 | if (code != 0) { | ||
| 698 | log.err( | ||
| 699 | "the following command exited with error code {d}:\n{s}", | ||
| 700 | .{ code, try Step.allocPrintCmd(arena, .inherit, null, argv.items) }, | ||
| 701 | ); | ||
| 702 | return error.WasmCompilationFailed; | ||
| 703 | } | ||
| 704 | }, | ||
| 705 | .signal => |sig| { | ||
| 706 | log.err( | ||
| 707 | "the following command terminated with signal {t}:\n{s}", | ||
| 708 | .{ sig, try Step.allocPrintCmd(arena, .inherit, null, argv.items) }, | ||
| 709 | ); | ||
| 710 | return error.WasmCompilationFailed; | ||
| 711 | }, | ||
| 712 | .stopped => |sig| { | ||
| 713 | log.err( | ||
| 714 | "the following command stopped unexpectedly with signal {t}:\n{s}", | ||
| 715 | .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, | ||
| 716 | ); | ||
| 717 | return error.WasmCompilationFailed; | ||
| 718 | }, | ||
| 719 | .unknown => { | ||
| 720 | log.err( | ||
| 721 | "the following command terminated unexpectedly:\n{s}", | ||
| 722 | .{try Step.allocPrintCmd(arena, .inherit, null, argv.items)}, | ||
| 723 | ); | ||
| 724 | return error.WasmCompilationFailed; | ||
| 725 | }, | ||
| 726 | } | ||
| 727 | |||
| 728 | if (result_error_bundle.errorMessageCount() > 0) { | ||
| 729 | try result_error_bundle.renderToStderr(io, .{}, .auto); | ||
| 730 | log.err("the following command failed with {d} compilation errors:\n{s}", .{ | ||
| 731 | result_error_bundle.errorMessageCount(), | ||
| 732 | try Step.allocPrintCmd(arena, .inherit, null, argv.items), | ||
| 733 | }); | ||
| 734 | return error.WasmCompilationFailed; | ||
| 735 | } | ||
| 736 | |||
| 737 | const base_path = result orelse { | ||
| 738 | log.err("child process failed to report result\n{s}", .{ | ||
| 739 | try Step.allocPrintCmd(arena, .inherit, null, argv.items), | ||
| 740 | }); | ||
| 741 | return error.WasmCompilationFailed; | ||
| 742 | }; | ||
| 743 | const bin_name = try std.zig.binNameAlloc(arena, .{ | ||
| 744 | .root_name = root_name, | ||
| 745 | .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ | ||
| 746 | .arch_os_abi = arch_os_abi, | ||
| 747 | .cpu_features = cpu_features, | ||
| 748 | }) catch unreachable) catch unreachable), | ||
| 749 | .output_mode = .Exe, | ||
| 750 | }); | ||
| 751 | return base_path.join(arena, bin_name); | ||
| 752 | } | ||
| 753 | |||
| 754 | fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 { | ||
| 755 | var file_reader: Io.File.Reader = .initStreaming(file, io, &.{}); | ||
| 756 | return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) { | ||
| 757 | error.ReadFailed => return file_reader.err.?, | ||
| 758 | else => |e| return e, | ||
| 759 | }; | ||
| 760 | } | ||
| 761 | |||
| 762 | pub fn updateTimeReportCompile(ws: *WebServer, opts: struct { | ||
| 763 | compile_step: Configuration.Step.Index, | ||
| 764 | |||
| 765 | use_llvm: bool, | ||
| 766 | stats: abi.time_report.CompileResult.Stats, | ||
| 767 | ns_total: u64, | ||
| 768 | |||
| 769 | llvm_pass_timings_len: u32, | ||
| 770 | files_len: u32, | ||
| 771 | decls_len: u32, | ||
| 772 | |||
| 773 | /// The trailing data of `abi.time_report.CompileResult`, except the step name. | ||
| 774 | trailing: []const u8, | ||
| 775 | }) void { | ||
| 776 | const gpa = ws.gpa; | ||
| 777 | const io = ws.graph.io; | ||
| 778 | |||
| 779 | // TODO don't do linear search | ||
| 780 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 781 | if (s == opts.compile_step) break @intCast(i); | ||
| 782 | } else unreachable; | ||
| 783 | |||
| 784 | const old_buf = old: { | ||
| 785 | ws.time_report_mutex.lock(io) catch return; | ||
| 786 | defer ws.time_report_mutex.unlock(io); | ||
| 787 | const old = ws.time_report_msgs[step_idx]; | ||
| 788 | ws.time_report_msgs[step_idx] = &.{}; | ||
| 789 | break :old old; | ||
| 790 | }; | ||
| 791 | const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory"); | ||
| 792 | |||
| 793 | const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]); | ||
| 794 | out_header.* = .{ | ||
| 795 | .step_idx = step_idx, | ||
| 796 | .flags = .{ | ||
| 797 | .use_llvm = opts.use_llvm, | ||
| 798 | }, | ||
| 799 | .stats = opts.stats, | ||
| 800 | .ns_total = opts.ns_total, | ||
| 801 | .llvm_pass_timings_len = opts.llvm_pass_timings_len, | ||
| 802 | .files_len = opts.files_len, | ||
| 803 | .decls_len = opts.decls_len, | ||
| 804 | }; | ||
| 805 | @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing); | ||
| 806 | |||
| 807 | { | ||
| 808 | ws.time_report_mutex.lock(io) catch return; | ||
| 809 | defer ws.time_report_mutex.unlock(io); | ||
| 810 | assert(ws.time_report_msgs[step_idx].len == 0); | ||
| 811 | ws.time_report_msgs[step_idx] = buf; | ||
| 812 | ws.time_report_update_times[step_idx] = ws.now(); | ||
| 813 | } | ||
| 814 | ws.notifyUpdate(); | ||
| 815 | } | ||
| 816 | |||
| 817 | pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.Index, duration: Io.Duration) void { | ||
| 818 | const gpa = ws.gpa; | ||
| 819 | const io = ws.graph.io; | ||
| 820 | |||
| 821 | // TODO don't do linear search | ||
| 822 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 823 | if (s == step_index) break @intCast(i); | ||
| 824 | } else unreachable; | ||
| 825 | |||
| 826 | const old_buf = old: { | ||
| 827 | ws.time_report_mutex.lock(io) catch return; | ||
| 828 | defer ws.time_report_mutex.unlock(io); | ||
| 829 | const old = ws.time_report_msgs[step_idx]; | ||
| 830 | ws.time_report_msgs[step_idx] = &.{}; | ||
| 831 | break :old old; | ||
| 832 | }; | ||
| 833 | const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory"); | ||
| 834 | const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf); | ||
| 835 | out.* = .{ | ||
| 836 | .step_idx = step_idx, | ||
| 837 | .ns_total = @intCast(duration.toNanoseconds()), | ||
| 838 | }; | ||
| 839 | { | ||
| 840 | ws.time_report_mutex.lock(io) catch return; | ||
| 841 | defer ws.time_report_mutex.unlock(io); | ||
| 842 | assert(ws.time_report_msgs[step_idx].len == 0); | ||
| 843 | ws.time_report_msgs[step_idx] = buf; | ||
| 844 | ws.time_report_update_times[step_idx] = ws.now(); | ||
| 845 | } | ||
| 846 | ws.notifyUpdate(); | ||
| 847 | } | ||
| 848 | |||
| 849 | pub fn updateTimeReportRunTest( | ||
| 850 | ws: *WebServer, | ||
| 851 | run_step_index: Configuration.Step.Index, | ||
| 852 | tests: *const Step.Run.CachedTestMetadata, | ||
| 853 | ns_per_test: []const u64, | ||
| 854 | ) void { | ||
| 855 | const gpa = ws.gpa; | ||
| 856 | const io = ws.graph.io; | ||
| 857 | |||
| 858 | // TODO don't do linear search | ||
| 859 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 860 | if (s == run_step_index) break @intCast(i); | ||
| 861 | } else unreachable; | ||
| 862 | |||
| 863 | assert(tests.names.len == ns_per_test.len); | ||
| 864 | const tests_len: u32 = @intCast(tests.names.len); | ||
| 865 | |||
| 866 | const new_len: u64 = len: { | ||
| 867 | var names_len: u64 = 0; | ||
| 868 | for (0..tests_len) |i| { | ||
| 869 | names_len += tests.testName(@intCast(i)).len + 1; | ||
| 870 | } | ||
| 871 | break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len; | ||
| 872 | }; | ||
| 873 | const old_buf = old: { | ||
| 874 | ws.time_report_mutex.lock(io) catch return; | ||
| 875 | defer ws.time_report_mutex.unlock(io); | ||
| 876 | const old = ws.time_report_msgs[step_idx]; | ||
| 877 | ws.time_report_msgs[step_idx] = &.{}; | ||
| 878 | break :old old; | ||
| 879 | }; | ||
| 880 | const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory"); | ||
| 881 | |||
| 882 | const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]); | ||
| 883 | out_header.* = .{ | ||
| 884 | .step_idx = step_idx, | ||
| 885 | .tests_len = tests_len, | ||
| 886 | }; | ||
| 887 | var offset: usize = @sizeOf(abi.time_report.RunTestResult); | ||
| 888 | const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]); | ||
| 889 | @memcpy(ns_per_test_out, ns_per_test); | ||
| 890 | offset += tests_len * 8; | ||
| 891 | for (0..tests_len) |i| { | ||
| 892 | const name = tests.testName(@intCast(i)); | ||
| 893 | @memcpy(buf[offset..][0..name.len], name); | ||
| 894 | buf[offset..][name.len] = 0; | ||
| 895 | offset += name.len + 1; | ||
| 896 | } | ||
| 897 | assert(offset == buf.len); | ||
| 898 | |||
| 899 | { | ||
| 900 | ws.time_report_mutex.lock(io) catch return; | ||
| 901 | defer ws.time_report_mutex.unlock(io); | ||
| 902 | assert(ws.time_report_msgs[step_idx].len == 0); | ||
| 903 | ws.time_report_msgs[step_idx] = buf; | ||
| 904 | ws.time_report_update_times[step_idx] = ws.now(); | ||
| 905 | } | ||
| 906 | ws.notifyUpdate(); | ||
| 907 | } | ||
| 908 | |||
| 909 | const RunnerRequest = union(enum) { | ||
| 910 | rebuild, | ||
| 911 | }; | ||
| 912 | pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest { | ||
| 913 | const io = ws.graph.io; | ||
| 914 | ws.runner_request_mutex.lock(io) catch return; | ||
| 915 | defer ws.runner_request_mutex.unlock(io); | ||
| 916 | if (ws.runner_request) |req| { | ||
| 917 | ws.runner_request = null; | ||
| 918 | ws.runner_request_empty_cond.signal(); | ||
| 919 | return req; | ||
| 920 | } | ||
| 921 | return null; | ||
| 922 | } | ||
| 923 | pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest { | ||
| 924 | const io = ws.graph.io; | ||
| 925 | try ws.runner_request_mutex.lock(io); | ||
| 926 | defer ws.runner_request_mutex.unlock(io); | ||
| 927 | while (true) { | ||
| 928 | if (ws.runner_request) |req| { | ||
| 929 | ws.runner_request = null; | ||
| 930 | ws.runner_request_empty_cond.signal(io); | ||
| 931 | return req; | ||
| 932 | } | ||
| 933 | try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex); | ||
| 934 | } | ||
| 935 | } | ||
| 936 | |||
| 937 | const cache_control_header: http.Header = .{ | ||
| 938 | .name = "Cache-Control", | ||
| 939 | .value = "max-age=0, must-revalidate", | ||
| 940 | }; | ||
lib/compiler/maker.zig deleted-1848| ... | @@ -1,1848 +0,0 @@ | ||
| 1 | const Maker = @This(); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | |||
| 4 | const std = @import("std"); | ||
| 5 | const Allocator = std.mem.Allocator; | ||
| 6 | const Cache = std.Build.Cache; | ||
| 7 | const Configuration = std.Build.Configuration; | ||
| 8 | const File = std.Io.File; | ||
| 9 | const Io = std.Io; | ||
| 10 | const Path = std.Build.Cache.Path; | ||
| 11 | const Writer = std.Io.Writer; | ||
| 12 | const assert = std.debug.assert; | ||
| 13 | const fatal = std.process.fatal; | ||
| 14 | const fmt = std.fmt; | ||
| 15 | const log = std.log; | ||
| 16 | const mem = std.mem; | ||
| 17 | const process = std.process; | ||
| 18 | |||
| 19 | const Fuzz = @import("maker/Fuzz.zig"); | ||
| 20 | const Graph = @import("maker/Graph.zig"); | ||
| 21 | const Step = @import("maker/Step.zig"); | ||
| 22 | const Watch = @import("maker/Watch.zig"); | ||
| 23 | const WebServer = @import("maker/WebServer.zig"); | ||
| 24 | |||
| 25 | pub const std_options: std.Options = .{ | ||
| 26 | .side_channels_mitigations = .none, | ||
| 27 | .http_disable_tls = true, | ||
| 28 | }; | ||
| 29 | |||
| 30 | gpa: Allocator, | ||
| 31 | graph: *Graph, | ||
| 32 | install_paths: InstallPaths, | ||
| 33 | scanned_config: *const ScannedConfig, | ||
| 34 | steps: []Step, | ||
| 35 | |||
| 36 | available_rss: usize, | ||
| 37 | max_rss_is_default: bool, | ||
| 38 | max_rss_mutex: Io.Mutex, | ||
| 39 | skip_oom_steps: bool, | ||
| 40 | unit_test_timeout_ns: ?u64, | ||
| 41 | watch: bool, | ||
| 42 | web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn, | ||
| 43 | /// Allocated into `gpa`. | ||
| 44 | memory_blocked_steps: std.ArrayList(Configuration.Step.Index), | ||
| 45 | /// Allocated into `gpa`. | ||
| 46 | step_stack: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void), | ||
| 47 | |||
| 48 | error_style: ErrorStyle, | ||
| 49 | multiline_errors: MultilineErrors, | ||
| 50 | summary: Summary, | ||
| 51 | |||
| 52 | pub fn main(init: process.Init.Minimal) !void { | ||
| 53 | // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not | ||
| 54 | // always the case. So, we do need a true gpa for some things. | ||
| 55 | var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{}); | ||
| 56 | defer _ = safe_gpa_state.deinit(); | ||
| 57 | const gpa = safe_gpa_state.allocator(); | ||
| 58 | |||
| 59 | var threaded: std.Io.Threaded = .init(gpa, .{ | ||
| 60 | .environ = init.environ, | ||
| 61 | .argv0 = .init(init.args), | ||
| 62 | }); | ||
| 63 | defer threaded.deinit(); | ||
| 64 | const io = threaded.io(); | ||
| 65 | |||
| 66 | // ...but we'll back our arena by `std.heap.page_allocator` for efficiency. | ||
| 67 | var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); | ||
| 68 | defer arena_instance.deinit(); | ||
| 69 | const arena = arena_instance.allocator(); | ||
| 70 | |||
| 71 | const args = try init.args.toSlice(arena); | ||
| 72 | |||
| 73 | // skip my own exe name | ||
| 74 | var arg_idx: usize = 1; | ||
| 75 | |||
| 76 | const zig_exe = expectArgOrFatal(args, &arg_idx, "--zig"); | ||
| 77 | const zig_lib_dir = expectArgOrFatal(args, &arg_idx, "--zig-lib-dir"); | ||
| 78 | const build_root = expectArgOrFatal(args, &arg_idx, "--build-root"); | ||
| 79 | const local_cache_root = expectArgOrFatal(args, &arg_idx, "--local-cache"); | ||
| 80 | const global_cache_root = expectArgOrFatal(args, &arg_idx, "--global-cache"); | ||
| 81 | const configure_path = expectArgOrFatal(args, &arg_idx, "--configuration"); | ||
| 82 | |||
| 83 | const cwd: Io.Dir = .cwd(); | ||
| 84 | |||
| 85 | const zig_lib_directory: Cache.Directory = .{ | ||
| 86 | .path = zig_lib_dir, | ||
| 87 | .handle = try cwd.openDir(io, zig_lib_dir, .{}), | ||
| 88 | }; | ||
| 89 | |||
| 90 | const build_root_directory: Cache.Directory = .{ | ||
| 91 | .path = build_root, | ||
| 92 | .handle = try cwd.openDir(io, build_root, .{}), | ||
| 93 | }; | ||
| 94 | |||
| 95 | const local_cache_directory: Cache.Directory = .{ | ||
| 96 | .path = local_cache_root, | ||
| 97 | .handle = try cwd.createDirPathOpen(io, local_cache_root, .{}), | ||
| 98 | }; | ||
| 99 | |||
| 100 | const global_cache_directory: Cache.Directory = .{ | ||
| 101 | .path = global_cache_root, | ||
| 102 | .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}), | ||
| 103 | }; | ||
| 104 | |||
| 105 | var graph: Graph = .{ | ||
| 106 | .io = io, | ||
| 107 | .arena = arena, | ||
| 108 | .cache = .{ | ||
| 109 | .io = io, | ||
| 110 | .gpa = gpa, | ||
| 111 | .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}), | ||
| 112 | .cwd = try process.currentPathAlloc(io, arena), | ||
| 113 | }, | ||
| 114 | .zig_exe = zig_exe, | ||
| 115 | .environ_map = try init.environ.createMap(arena), | ||
| 116 | .global_cache_root = global_cache_directory, | ||
| 117 | .zig_lib_directory = zig_lib_directory, | ||
| 118 | }; | ||
| 119 | |||
| 120 | graph.cache.addPrefix(.{ .path = null, .handle = cwd }); | ||
| 121 | graph.cache.addPrefix(build_root_directory); | ||
| 122 | graph.cache.addPrefix(local_cache_directory); | ||
| 123 | graph.cache.addPrefix(global_cache_directory); | ||
| 124 | graph.cache.hash.addBytes(builtin.zig_version_string); | ||
| 125 | |||
| 126 | var step_names: std.ArrayList([]const u8) = .empty; | ||
| 127 | var debug_log_scopes: std.ArrayList([]const u8) = .empty; | ||
| 128 | var help_menu = false; | ||
| 129 | var steps_menu = false; | ||
| 130 | var print_configuration = false; | ||
| 131 | var override_install_prefix: ?[]const u8 = null; | ||
| 132 | var override_lib_dir: ?[]const u8 = null; | ||
| 133 | var override_bin_dir: ?[]const u8 = null; | ||
| 134 | var override_include_dir: ?[]const u8 = null; | ||
| 135 | var error_style: ErrorStyle = .verbose; | ||
| 136 | var multiline_errors: MultilineErrors = .indent; | ||
| 137 | var summary: ?Summary = null; | ||
| 138 | var max_rss: u64 = 0; | ||
| 139 | var skip_oom_steps = false; | ||
| 140 | var test_timeout_ns: ?u64 = null; | ||
| 141 | var color: Color = .auto; | ||
| 142 | var watch = false; | ||
| 143 | var fuzz: ?Fuzz.Mode = null; | ||
| 144 | var debounce_interval_ms: u16 = 50; | ||
| 145 | var webui_listen: ?Io.net.IpAddress = null; | ||
| 146 | var verbose = false; | ||
| 147 | var sysroot: ?[]const u8 = null; | ||
| 148 | var search_prefixes: std.ArrayList([]const u8) = .empty; | ||
| 149 | var libc_file: ?[]const u8 = null; | ||
| 150 | var debug_pkg_config: bool = false; | ||
| 151 | // After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md, | ||
| 152 | // this will be the directory $glibc-build-dir/install/glibcs | ||
| 153 | // Given the example of the aarch64 target, this is the directory | ||
| 154 | // that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`. | ||
| 155 | // Also works for dynamic musl. | ||
| 156 | var libc_runtimes_dir: ?[]const u8 = null; | ||
| 157 | var enable_wine = false; | ||
| 158 | var enable_qemu = false; | ||
| 159 | var enable_wasmtime = false; | ||
| 160 | var enable_darling = false; | ||
| 161 | var enable_rosetta = false; | ||
| 162 | var reference_trace: ?u32 = null; | ||
| 163 | var run_args: ?[]const []const u8 = null; | ||
| 164 | |||
| 165 | if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| { | ||
| 166 | if (std.meta.stringToEnum(ErrorStyle, str)) |style| { | ||
| 167 | error_style = style; | ||
| 168 | } | ||
| 169 | } | ||
| 170 | |||
| 171 | if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| { | ||
| 172 | if (std.meta.stringToEnum(MultilineErrors, str)) |style| { | ||
| 173 | multiline_errors = style; | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | while (nextArg(args, &arg_idx)) |arg| { | ||
| 178 | if (mem.startsWith(u8, arg, "-")) { | ||
| 179 | if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { | ||
| 180 | help_menu = true; | ||
| 181 | } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) { | ||
| 182 | steps_menu = true; | ||
| 183 | } else if (mem.eql(u8, arg, "--print-configuration")) { | ||
| 184 | print_configuration = true; | ||
| 185 | } else if (mem.eql(u8, arg, "--verbose")) { | ||
| 186 | verbose = true; | ||
| 187 | } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) { | ||
| 188 | override_install_prefix = nextArgOrFatal(args, &arg_idx); | ||
| 189 | } else if (mem.eql(u8, arg, "--prefix-lib-dir")) { | ||
| 190 | override_lib_dir = nextArgOrFatal(args, &arg_idx); | ||
| 191 | } else if (mem.eql(u8, arg, "--prefix-exe-dir")) { | ||
| 192 | override_bin_dir = nextArgOrFatal(args, &arg_idx); | ||
| 193 | } else if (mem.eql(u8, arg, "--prefix-include-dir")) { | ||
| 194 | override_include_dir = nextArgOrFatal(args, &arg_idx); | ||
| 195 | } else if (mem.eql(u8, arg, "--sysroot")) { | ||
| 196 | sysroot = nextArgOrFatal(args, &arg_idx); | ||
| 197 | } else if (mem.eql(u8, arg, "--maxrss")) { | ||
| 198 | const max_rss_text = nextArgOrFatal(args, &arg_idx); | ||
| 199 | max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| | ||
| 200 | fatal("invalid byte size: '{s}': {t}", .{ max_rss_text, err }); | ||
| 201 | } else if (mem.eql(u8, arg, "--skip-oom-steps")) { | ||
| 202 | skip_oom_steps = true; | ||
| 203 | } else if (mem.eql(u8, arg, "--test-timeout")) { | ||
| 204 | const units: []const struct { []const u8, u64 } = &.{ | ||
| 205 | .{ "ns", 1 }, | ||
| 206 | .{ "nanosecond", 1 }, | ||
| 207 | .{ "us", std.time.ns_per_us }, | ||
| 208 | .{ "microsecond", std.time.ns_per_us }, | ||
| 209 | .{ "ms", std.time.ns_per_ms }, | ||
| 210 | .{ "millisecond", std.time.ns_per_ms }, | ||
| 211 | .{ "s", std.time.ns_per_s }, | ||
| 212 | .{ "second", std.time.ns_per_s }, | ||
| 213 | .{ "m", std.time.ns_per_min }, | ||
| 214 | .{ "minute", std.time.ns_per_min }, | ||
| 215 | .{ "h", std.time.ns_per_hour }, | ||
| 216 | .{ "hour", std.time.ns_per_hour }, | ||
| 217 | }; | ||
| 218 | const timeout_str = nextArgOrFatal(args, &arg_idx); | ||
| 219 | const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal( | ||
| 220 | "invalid timeout '{s}': expected unit (ns, us, ms, s, m, h)", | ||
| 221 | .{timeout_str}, | ||
| 222 | ); | ||
| 223 | const num_str = timeout_str[0 .. num_end_idx + 1]; | ||
| 224 | const unit_str = timeout_str[num_end_idx + 1 ..]; | ||
| 225 | const unit_factor: f64 = for (units) |unit_and_factor| { | ||
| 226 | if (std.mem.eql(u8, unit_str, unit_and_factor[0])) { | ||
| 227 | break @floatFromInt(unit_and_factor[1]); | ||
| 228 | } | ||
| 229 | } else fatal( | ||
| 230 | "invalid timeout '{s}': invalid unit '{s}' (expected ns, us, ms, s, m, h)", | ||
| 231 | .{ timeout_str, unit_str }, | ||
| 232 | ); | ||
| 233 | const num_parsed = std.fmt.parseFloat(f64, num_str) catch |err| fatal( | ||
| 234 | "invalid timeout '{s}': invalid number '{s}' ({t})", | ||
| 235 | .{ timeout_str, num_str, err }, | ||
| 236 | ); | ||
| 237 | test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed); | ||
| 238 | } else if (mem.eql(u8, arg, "--search-prefix")) { | ||
| 239 | try search_prefixes.append(arena, nextArgOrFatal(args, &arg_idx)); | ||
| 240 | } else if (mem.eql(u8, arg, "--libc")) { | ||
| 241 | libc_file = nextArgOrFatal(args, &arg_idx); | ||
| 242 | } else if (mem.eql(u8, arg, "--color")) { | ||
| 243 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 244 | fatalWithHint("expected [auto|on|off] after '{s}'", .{arg}); | ||
| 245 | color = std.meta.stringToEnum(Color, next_arg) orelse { | ||
| 246 | fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{ | ||
| 247 | arg, next_arg, | ||
| 248 | }); | ||
| 249 | }; | ||
| 250 | } else if (mem.eql(u8, arg, "--error-style")) { | ||
| 251 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 252 | fatalWithHint("expected style after '{s}'", .{arg}); | ||
| 253 | error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse { | ||
| 254 | fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg }); | ||
| 255 | }; | ||
| 256 | } else if (mem.eql(u8, arg, "--multiline-errors")) { | ||
| 257 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 258 | fatalWithHint("expected style after '{s}'", .{arg}); | ||
| 259 | multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse { | ||
| 260 | fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg }); | ||
| 261 | }; | ||
| 262 | } else if (mem.eql(u8, arg, "--summary")) { | ||
| 263 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 264 | fatalWithHint("expected [all|new|failures|line|none] after '{s}'", .{arg}); | ||
| 265 | summary = std.meta.stringToEnum(Summary, next_arg) orelse { | ||
| 266 | fatalWithHint("expected [all|new|failures|line|none] after '{s}', found '{s}'", .{ | ||
| 267 | arg, next_arg, | ||
| 268 | }); | ||
| 269 | }; | ||
| 270 | } else if (mem.eql(u8, arg, "--seed")) { | ||
| 271 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 272 | fatalWithHint("expected u32 after '{s}'", .{arg}); | ||
| 273 | graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| { | ||
| 274 | fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {t}", .{ next_arg, err }); | ||
| 275 | }; | ||
| 276 | } else if (mem.eql(u8, arg, "--debounce")) { | ||
| 277 | const next_arg = nextArg(args, &arg_idx) orelse | ||
| 278 | fatalWithHint("expected u16 after '{s}'", .{arg}); | ||
| 279 | debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| { | ||
| 280 | fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {t}\n", .{ | ||
| 281 | next_arg, err, | ||
| 282 | }); | ||
| 283 | }; | ||
| 284 | } else if (mem.eql(u8, arg, "--webui")) { | ||
| 285 | if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; | ||
| 286 | } else if (mem.startsWith(u8, arg, "--webui=")) { | ||
| 287 | const addr_str = arg["--webui=".len..]; | ||
| 288 | if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{}); | ||
| 289 | webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| { | ||
| 290 | fatal("invalid web UI address '{s}': {t}", .{ addr_str, err }); | ||
| 291 | }; | ||
| 292 | } else if (mem.eql(u8, arg, "--debug-log")) { | ||
| 293 | const next_arg = nextArgOrFatal(args, &arg_idx); | ||
| 294 | try debug_log_scopes.append(arena, next_arg); | ||
| 295 | } else if (mem.eql(u8, arg, "--debug-pkg-config")) { | ||
| 296 | debug_pkg_config = true; | ||
| 297 | } else if (mem.eql(u8, arg, "--debug-rt")) { | ||
| 298 | graph.debug_compiler_runtime_libs = .Debug; | ||
| 299 | } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| { | ||
| 300 | graph.debug_compiler_runtime_libs = std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse | ||
| 301 | fatal("unrecognized optimization mode: {s}", .{rest}); | ||
| 302 | } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) { | ||
| 303 | // --glibc-runtimes was the old name of the flag; kept for compatibility for now. | ||
| 304 | libc_runtimes_dir = nextArgOrFatal(args, &arg_idx); | ||
| 305 | } else if (mem.eql(u8, arg, "--watch")) { | ||
| 306 | watch = true; | ||
| 307 | } else if (mem.eql(u8, arg, "--time-report")) { | ||
| 308 | graph.time_report = true; | ||
| 309 | if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; | ||
| 310 | } else if (mem.eql(u8, arg, "--fuzz")) { | ||
| 311 | fuzz = .{ .forever = undefined }; | ||
| 312 | if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) }; | ||
| 313 | } else if (mem.startsWith(u8, arg, "--fuzz=")) { | ||
| 314 | const value = arg["--fuzz=".len..]; | ||
| 315 | if (value.len == 0) fatal("missing argument to --fuzz", .{}); | ||
| 316 | |||
| 317 | const unit: u8 = value[value.len - 1]; | ||
| 318 | const digits = switch (unit) { | ||
| 319 | '0'...'9' => value, | ||
| 320 | 'K', 'M', 'G' => value[0 .. value.len - 1], | ||
| 321 | else => fatal( | ||
| 322 | "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]", | ||
| 323 | .{}, | ||
| 324 | ), | ||
| 325 | }; | ||
| 326 | |||
| 327 | const amount = std.fmt.parseInt(u64, digits, 10) catch { | ||
| 328 | fatal( | ||
| 329 | "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]", | ||
| 330 | .{}, | ||
| 331 | ); | ||
| 332 | }; | ||
| 333 | |||
| 334 | const normalized_amount = std.math.mul(u64, amount, switch (unit) { | ||
| 335 | else => unreachable, | ||
| 336 | '0'...'9' => 1, | ||
| 337 | 'K' => 1000, | ||
| 338 | 'M' => 1_000_000, | ||
| 339 | 'G' => 1_000_000_000, | ||
| 340 | }) catch fatal("fuzzing limit amount overflows u64", .{}); | ||
| 341 | |||
| 342 | fuzz = .{ | ||
| 343 | .limit = .{ | ||
| 344 | .amount = normalized_amount, | ||
| 345 | }, | ||
| 346 | }; | ||
| 347 | } else if (mem.eql(u8, arg, "-fincremental")) { | ||
| 348 | graph.incremental = true; | ||
| 349 | } else if (mem.eql(u8, arg, "-fno-incremental")) { | ||
| 350 | graph.incremental = false; | ||
| 351 | } else if (mem.eql(u8, arg, "-fwine")) { | ||
| 352 | enable_wine = true; | ||
| 353 | } else if (mem.eql(u8, arg, "-fno-wine")) { | ||
| 354 | enable_wine = false; | ||
| 355 | } else if (mem.eql(u8, arg, "-fqemu")) { | ||
| 356 | enable_qemu = true; | ||
| 357 | } else if (mem.eql(u8, arg, "-fno-qemu")) { | ||
| 358 | enable_qemu = false; | ||
| 359 | } else if (mem.eql(u8, arg, "-fwasmtime")) { | ||
| 360 | enable_wasmtime = true; | ||
| 361 | } else if (mem.eql(u8, arg, "-fno-wasmtime")) { | ||
| 362 | enable_wasmtime = false; | ||
| 363 | } else if (mem.eql(u8, arg, "-frosetta")) { | ||
| 364 | enable_rosetta = true; | ||
| 365 | } else if (mem.eql(u8, arg, "-fno-rosetta")) { | ||
| 366 | enable_rosetta = false; | ||
| 367 | } else if (mem.eql(u8, arg, "-fdarling")) { | ||
| 368 | enable_darling = true; | ||
| 369 | } else if (mem.eql(u8, arg, "-fno-darling")) { | ||
| 370 | enable_darling = false; | ||
| 371 | } else if (mem.eql(u8, arg, "-fallow-so-scripts")) { | ||
| 372 | graph.allow_so_scripts = true; | ||
| 373 | } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) { | ||
| 374 | graph.allow_so_scripts = false; | ||
| 375 | } else if (mem.eql(u8, arg, "-freference-trace")) { | ||
| 376 | reference_trace = 256; | ||
| 377 | } else if (mem.startsWith(u8, arg, "-freference-trace=")) { | ||
| 378 | const num = arg["-freference-trace=".len..]; | ||
| 379 | reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| { | ||
| 380 | std.debug.print("unable to parse reference_trace count '{s}': {t}", .{ num, err }); | ||
| 381 | process.exit(1); | ||
| 382 | }; | ||
| 383 | } else if (mem.eql(u8, arg, "-fno-reference-trace")) { | ||
| 384 | reference_trace = null; | ||
| 385 | } else if (mem.cutPrefix(u8, arg, "-j")) |text| { | ||
| 386 | const n = std.fmt.parseUnsigned(u32, text, 10) catch |err| | ||
| 387 | fatal("unable to parse jobs count '{s}': {t}", .{ text, err }); | ||
| 388 | if (n < 1) fatal("number of jobs must be at least 1", .{}); | ||
| 389 | threaded.setAsyncLimit(.limited(n)); | ||
| 390 | graph.max_jobs = n; | ||
| 391 | } else if (mem.eql(u8, arg, "--")) { | ||
| 392 | run_args = argsRest(args, arg_idx); | ||
| 393 | break; | ||
| 394 | } else { | ||
| 395 | fatalWithHint("unrecognized argument: '{s}'", .{arg}); | ||
| 396 | } | ||
| 397 | } else { | ||
| 398 | try step_names.append(arena, arg); | ||
| 399 | } | ||
| 400 | } | ||
| 401 | |||
| 402 | const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map); | ||
| 403 | const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map); | ||
| 404 | |||
| 405 | graph.stderr_mode = switch (color) { | ||
| 406 | .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE), | ||
| 407 | .on => .escape_codes, | ||
| 408 | .off => .no_color, | ||
| 409 | }; | ||
| 410 | |||
| 411 | const scanned_config: ScannedConfig = sc: { | ||
| 412 | const configuration = c: { | ||
| 413 | var file = cwd.openFile(io, configure_path, .{}) catch |err| | ||
| 414 | fatal("failed to open configuration file {s}: {t}", .{ configure_path, err }); | ||
| 415 | defer file.close(io); | ||
| 416 | break :c Configuration.loadFile(arena, io, file) catch |err| | ||
| 417 | fatal("failed to load configuration file {s}: {t}", .{ configure_path, err }); | ||
| 418 | }; | ||
| 419 | var top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index) = .empty; | ||
| 420 | for (configuration.steps, 0..) |*conf_step, step_index_usize| { | ||
| 421 | const step_index: Configuration.Step.Index = @enumFromInt(step_index_usize); | ||
| 422 | const flags: Configuration.Step.Flags = @bitCast(configuration.extra[conf_step.extra_index]); | ||
| 423 | if (flags.tag == .top_level) { | ||
| 424 | const name = step_index.ptr(&configuration).name.slice(&configuration); | ||
| 425 | try top_level_steps.put(arena, name, step_index); | ||
| 426 | } | ||
| 427 | } | ||
| 428 | break :sc .{ | ||
| 429 | .configuration = configuration, | ||
| 430 | .top_level_steps = top_level_steps, | ||
| 431 | }; | ||
| 432 | }; | ||
| 433 | |||
| 434 | if (help_menu) { | ||
| 435 | var w = initStdoutWriter(io); | ||
| 436 | scanned_config.printUsage(&graph, w) catch |err| switch (err) { | ||
| 437 | error.WriteFailed => return stdout_writer_allocation.err.?, | ||
| 438 | else => |e| return e, | ||
| 439 | }; | ||
| 440 | w.flush() catch return stdout_writer_allocation.err.?; | ||
| 441 | return; | ||
| 442 | } else if (steps_menu) { | ||
| 443 | var w = initStdoutWriter(io); | ||
| 444 | scanned_config.printSteps(&graph, w) catch |err| switch (err) { | ||
| 445 | error.WriteFailed => return stdout_writer_allocation.err.?, | ||
| 446 | else => |e| return e, | ||
| 447 | }; | ||
| 448 | w.flush() catch return stdout_writer_allocation.err.?; | ||
| 449 | return; | ||
| 450 | } else if (print_configuration) { | ||
| 451 | var w = initStdoutWriter(io); | ||
| 452 | scanned_config.print(w) catch return stdout_writer_allocation.err.?; | ||
| 453 | w.flush() catch return stdout_writer_allocation.err.?; | ||
| 454 | return; | ||
| 455 | } | ||
| 456 | |||
| 457 | if (webui_listen != null) { | ||
| 458 | if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{}); | ||
| 459 | if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{}); | ||
| 460 | } | ||
| 461 | |||
| 462 | const main_progress_node = std.Progress.start(io, .{ | ||
| 463 | .disable_printing = (color == .off), | ||
| 464 | }); | ||
| 465 | defer main_progress_node.end(); | ||
| 466 | |||
| 467 | const install_prefix_path: Path = if (graph.environ_map.get("DESTDIR")) |dest_dir| .{ | ||
| 468 | .root_dir = .cwd(), | ||
| 469 | .sub_path = try Io.Dir.path.join(arena, &.{ dest_dir, override_install_prefix orelse "/usr" }), | ||
| 470 | } else if (override_install_prefix) |cwd_relative| .{ | ||
| 471 | .root_dir = .cwd(), | ||
| 472 | .sub_path = cwd_relative, | ||
| 473 | } else .{ | ||
| 474 | .root_dir = build_root_directory, | ||
| 475 | .sub_path = "zig-out", | ||
| 476 | }; | ||
| 477 | |||
| 478 | const install_lib_path: Path = if (override_lib_dir) |cwd_relative| .{ | ||
| 479 | .root_dir = .cwd(), | ||
| 480 | .sub_path = cwd_relative, | ||
| 481 | } else try install_prefix_path.join(arena, "lib"); | ||
| 482 | |||
| 483 | const install_bin_path: Path = if (override_bin_dir) |cwd_relative| .{ | ||
| 484 | .root_dir = .cwd(), | ||
| 485 | .sub_path = cwd_relative, | ||
| 486 | } else try install_prefix_path.join(arena, "bin"); | ||
| 487 | |||
| 488 | const install_include_path: Path = if (override_include_dir) |cwd_relative| .{ | ||
| 489 | .root_dir = .cwd(), | ||
| 490 | .sub_path = cwd_relative, | ||
| 491 | } else try install_prefix_path.join(arena, "include"); | ||
| 492 | |||
| 493 | var maker: Maker = .{ | ||
| 494 | .gpa = gpa, | ||
| 495 | .graph = &graph, | ||
| 496 | .scanned_config = &scanned_config, | ||
| 497 | .install_paths = .{ | ||
| 498 | .prefix = install_prefix_path, | ||
| 499 | .lib = install_lib_path, | ||
| 500 | .bin = install_bin_path, | ||
| 501 | .include = install_include_path, | ||
| 502 | }, | ||
| 503 | .steps = try arena.alloc(Step, scanned_config.configuration.steps.len), | ||
| 504 | |||
| 505 | .available_rss = max_rss, | ||
| 506 | .max_rss_is_default = false, | ||
| 507 | .max_rss_mutex = .init, | ||
| 508 | .skip_oom_steps = skip_oom_steps, | ||
| 509 | .unit_test_timeout_ns = test_timeout_ns, | ||
| 510 | |||
| 511 | .watch = watch, | ||
| 512 | .web_server = undefined, // set after `prepare` | ||
| 513 | .memory_blocked_steps = .empty, | ||
| 514 | .step_stack = .empty, | ||
| 515 | |||
| 516 | .error_style = error_style, | ||
| 517 | .multiline_errors = multiline_errors, | ||
| 518 | .summary = summary orelse if (watch or webui_listen != null) .line else .failures, | ||
| 519 | }; | ||
| 520 | defer { | ||
| 521 | maker.memory_blocked_steps.deinit(gpa); | ||
| 522 | maker.step_stack.deinit(gpa); | ||
| 523 | } | ||
| 524 | |||
| 525 | if (maker.available_rss == 0) { | ||
| 526 | maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64); | ||
| 527 | maker.max_rss_is_default = true; | ||
| 528 | } | ||
| 529 | |||
| 530 | maker.prepare(step_names.items) catch |err| switch (err) { | ||
| 531 | error.DependencyLoopDetected, error.InsufficientMemory => { | ||
| 532 | _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; | ||
| 533 | process.exit(1); | ||
| 534 | }, | ||
| 535 | else => |e| return e, | ||
| 536 | }; | ||
| 537 | |||
| 538 | var w: Watch = w: { | ||
| 539 | if (!watch) break :w undefined; | ||
| 540 | if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag}); | ||
| 541 | break :w try .init(graph.cache.cwd, &scanned_config.configuration, maker.steps); | ||
| 542 | }; | ||
| 543 | |||
| 544 | const now = Io.Clock.Timestamp.now(io, .awake); | ||
| 545 | |||
| 546 | maker.web_server = if (webui_listen) |listen_address| ws: { | ||
| 547 | if (builtin.single_threaded) unreachable; // `fatal` above | ||
| 548 | break :ws .init(.{ | ||
| 549 | .gpa = gpa, | ||
| 550 | .graph = &graph, | ||
| 551 | .all_steps = maker.step_stack.keys(), | ||
| 552 | .root_prog_node = main_progress_node, | ||
| 553 | .watch = watch, | ||
| 554 | .listen_address = listen_address, | ||
| 555 | .base_timestamp = now, | ||
| 556 | .configuration = &scanned_config.configuration, | ||
| 557 | }); | ||
| 558 | } else null; | ||
| 559 | |||
| 560 | if (maker.web_server) |*ws| { | ||
| 561 | ws.start() catch |err| fatal("failed to start web server: {t}", .{err}); | ||
| 562 | } | ||
| 563 | |||
| 564 | rebuild: while (true) : (if (maker.error_style.clearOnUpdate()) { | ||
| 565 | const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); | ||
| 566 | defer io.unlockStderr(); | ||
| 567 | try stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H"); | ||
| 568 | }) { | ||
| 569 | if (maker.web_server) |*ws| ws.startBuild(); | ||
| 570 | |||
| 571 | try maker.makeStepNames(step_names.items, main_progress_node, fuzz); | ||
| 572 | |||
| 573 | if (maker.web_server) |*web_server| { | ||
| 574 | if (fuzz) |mode| if (mode != .forever) fatal( | ||
| 575 | "error: limited fuzzing is not implemented yet for --webui", | ||
| 576 | .{}, | ||
| 577 | ); | ||
| 578 | |||
| 579 | web_server.finishBuild(.{ .fuzz = fuzz != null }); | ||
| 580 | } | ||
| 581 | |||
| 582 | if (maker.web_server) |*ws| { | ||
| 583 | const c = &scanned_config.configuration; | ||
| 584 | assert(!watch); // fatal error after CLI parsing | ||
| 585 | while (true) switch (try ws.wait()) { | ||
| 586 | .rebuild => { | ||
| 587 | for (maker.step_stack.keys()) |step_index| { | ||
| 588 | const step = maker.stepByIndex(step_index); | ||
| 589 | step.state = .precheck_done; | ||
| 590 | const deps = step_index.ptr(c).deps.slice(c); | ||
| 591 | step.pending_deps = @intCast(deps.len); | ||
| 592 | step.reset(gpa); | ||
| 593 | } | ||
| 594 | continue :rebuild; | ||
| 595 | }, | ||
| 596 | }; | ||
| 597 | } | ||
| 598 | |||
| 599 | // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`. | ||
| 600 | if (!Watch.have_impl) unreachable; | ||
| 601 | |||
| 602 | try w.update(gpa, maker.step_stack.keys()); | ||
| 603 | |||
| 604 | // Wait until a file system notification arrives. Read all such events | ||
| 605 | // until the buffer is empty. Then wait for a debounce interval, resetting | ||
| 606 | // if any more events come in. After the debounce interval has passed, | ||
| 607 | // trigger a rebuild on all steps with modified inputs, as well as their | ||
| 608 | // recursive dependants. | ||
| 609 | var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined; | ||
| 610 | const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{ | ||
| 611 | w.dir_count, countSubProcesses(maker.steps, maker.step_stack.keys()), | ||
| 612 | }) catch &caption_buf; | ||
| 613 | var debouncing_node = main_progress_node.start(caption, 0); | ||
| 614 | var in_debounce = false; | ||
| 615 | while (true) switch (try w.wait(gpa, io, if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) { | ||
| 616 | .timeout => { | ||
| 617 | assert(in_debounce); | ||
| 618 | debouncing_node.end(); | ||
| 619 | markFailedStepsDirty(gpa, maker.steps, maker.step_stack.keys()); | ||
| 620 | continue :rebuild; | ||
| 621 | }, | ||
| 622 | .dirty => if (!in_debounce) { | ||
| 623 | in_debounce = true; | ||
| 624 | debouncing_node.end(); | ||
| 625 | debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0); | ||
| 626 | }, | ||
| 627 | .clean => {}, | ||
| 628 | }; | ||
| 629 | } | ||
| 630 | } | ||
| 631 | |||
| 632 | fn markFailedStepsDirty(gpa: Allocator, make_steps: []Step, all_steps: []const Configuration.Step.Index) void { | ||
| 633 | for (all_steps) |step_index| { | ||
| 634 | const step = &make_steps[@intFromEnum(step_index)]; | ||
| 635 | switch (step.state) { | ||
| 636 | .dependency_failure, .failure, .skipped => _ = step.invalidateResult(gpa), | ||
| 637 | else => continue, | ||
| 638 | } | ||
| 639 | } | ||
| 640 | // Now that all dirty steps have been found, the remaining steps that | ||
| 641 | // succeeded from last run shall be marked "cached". | ||
| 642 | for (all_steps) |step_index| { | ||
| 643 | const step = &make_steps[@intFromEnum(step_index)]; | ||
| 644 | switch (step.state) { | ||
| 645 | .success => step.result_cached = true, | ||
| 646 | else => continue, | ||
| 647 | } | ||
| 648 | } | ||
| 649 | } | ||
| 650 | |||
| 651 | fn countSubProcesses(make_steps: []Step, all_steps: []const Configuration.Step.Index) usize { | ||
| 652 | var count: usize = 0; | ||
| 653 | for (all_steps) |step_index| { | ||
| 654 | const s = &make_steps[@intFromEnum(step_index)]; | ||
| 655 | count += @intFromBool(s.getZigProcess() != null); | ||
| 656 | } | ||
| 657 | return count; | ||
| 658 | } | ||
| 659 | |||
| 660 | const InstallPaths = struct { | ||
| 661 | prefix: Path, | ||
| 662 | lib: Path, | ||
| 663 | bin: Path, | ||
| 664 | include: Path, | ||
| 665 | }; | ||
| 666 | |||
| 667 | fn stepByIndex(maker: *const Maker, i: Configuration.Step.Index) *Step { | ||
| 668 | return &maker.steps[@intFromEnum(i)]; | ||
| 669 | } | ||
| 670 | |||
| 671 | fn prepare(maker: *Maker, step_names: []const []const u8) !void { | ||
| 672 | const gpa = maker.gpa; | ||
| 673 | const graph = maker.graph; | ||
| 674 | const arena = graph.arena; | ||
| 675 | const seed: u32 = graph.random_seed; | ||
| 676 | const step_stack = &maker.step_stack; | ||
| 677 | const c = &maker.scanned_config.configuration; | ||
| 678 | |||
| 679 | @memset(maker.steps, .{}); | ||
| 680 | |||
| 681 | if (step_names.len == 0) { | ||
| 682 | try step_stack.put(gpa, c.default_step, {}); | ||
| 683 | } else { | ||
| 684 | try step_stack.ensureUnusedCapacity(gpa, step_names.len); | ||
| 685 | for (0..step_names.len) |i| { | ||
| 686 | const step_name = step_names[step_names.len - i - 1]; | ||
| 687 | const s = maker.scanned_config.top_level_steps.get(step_name) orelse { | ||
| 688 | log.info("to list available steps: zig build -l", .{}); | ||
| 689 | fatal("no such step: {s}", .{step_name}); | ||
| 690 | }; | ||
| 691 | step_stack.putAssumeCapacity(s, {}); | ||
| 692 | } | ||
| 693 | } | ||
| 694 | |||
| 695 | const starting_steps = try arena.dupe(Configuration.Step.Index, step_stack.keys()); | ||
| 696 | |||
| 697 | var rng = std.Random.DefaultPrng.init(seed); | ||
| 698 | const rand = rng.random(); | ||
| 699 | rand.shuffle(Configuration.Step.Index, starting_steps); | ||
| 700 | |||
| 701 | for (starting_steps) |s| { | ||
| 702 | try constructGraphAndCheckForDependencyLoop(gpa, c, maker.steps, s, &maker.step_stack, rand); | ||
| 703 | } | ||
| 704 | |||
| 705 | { | ||
| 706 | // Check that we have enough memory to complete the build. | ||
| 707 | var any_problems = false; | ||
| 708 | var max_needed: usize = 0; | ||
| 709 | for (step_stack.keys()) |step_index| { | ||
| 710 | const make_step = maker.stepByIndex(step_index); | ||
| 711 | const conf_step = step_index.ptr(c); | ||
| 712 | const max_rss = conf_step.max_rss.toBytes(); | ||
| 713 | if (max_rss == 0) continue; | ||
| 714 | max_needed = @max(max_needed, max_rss); | ||
| 715 | if (max_rss > maker.available_rss) { | ||
| 716 | if (maker.skip_oom_steps) { | ||
| 717 | make_step.state = .skipped_oom; | ||
| 718 | for (make_step.dependants.items) |dependant| { | ||
| 719 | maker.stepByIndex(dependant).pending_deps -= 1; | ||
| 720 | } | ||
| 721 | } else { | ||
| 722 | log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{ | ||
| 723 | conf_step.owner.depPrefixSlice(c), | ||
| 724 | conf_step.name.slice(c), | ||
| 725 | max_rss, | ||
| 726 | maker.available_rss, | ||
| 727 | }); | ||
| 728 | any_problems = true; | ||
| 729 | } | ||
| 730 | } | ||
| 731 | } | ||
| 732 | if (any_problems) { | ||
| 733 | if (maker.max_rss_is_default) { | ||
| 734 | std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{ | ||
| 735 | max_needed, | ||
| 736 | }); | ||
| 737 | } | ||
| 738 | return error.InsufficientMemory; | ||
| 739 | } | ||
| 740 | } | ||
| 741 | } | ||
| 742 | |||
| 743 | fn makeStepNames( | ||
| 744 | maker: *Maker, | ||
| 745 | step_names: []const []const u8, | ||
| 746 | parent_prog_node: std.Progress.Node, | ||
| 747 | fuzz: ?Fuzz.Mode, | ||
| 748 | ) !void { | ||
| 749 | const graph = maker.graph; | ||
| 750 | const gpa = maker.gpa; | ||
| 751 | const io = graph.io; | ||
| 752 | const step_stack = &maker.step_stack; | ||
| 753 | const top_level_steps = &maker.scanned_config.top_level_steps; | ||
| 754 | const c = &maker.scanned_config.configuration; | ||
| 755 | |||
| 756 | { | ||
| 757 | // Collect the initial set of tasks (those with no outstanding dependencies) into a buffer, | ||
| 758 | // then spawn them. The buffer is so that we don't race with `makeStep` and end up thinking | ||
| 759 | // a step is initial when it actually became ready due to an earlier initial step. | ||
| 760 | var initial_set: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 761 | defer initial_set.deinit(gpa); | ||
| 762 | try initial_set.ensureUnusedCapacity(gpa, step_stack.count()); | ||
| 763 | for (step_stack.keys()) |step_index| { | ||
| 764 | const s = maker.stepByIndex(step_index); | ||
| 765 | if (s.state == .precheck_done and s.pending_deps == 0) { | ||
| 766 | initial_set.appendAssumeCapacity(step_index); | ||
| 767 | } | ||
| 768 | } | ||
| 769 | |||
| 770 | const step_prog = parent_prog_node.start("steps", step_stack.count()); | ||
| 771 | defer step_prog.end(); | ||
| 772 | |||
| 773 | var group: Io.Group = .init; | ||
| 774 | defer group.cancel(io); | ||
| 775 | // Start working on all of the initial steps... | ||
| 776 | for (initial_set.items) |step_index| try stepReady(maker, &group, step_index, step_prog); | ||
| 777 | // ...and `makeStep` will trigger every other step when their last dependency finishes. | ||
| 778 | try group.await(io); | ||
| 779 | } | ||
| 780 | |||
| 781 | assert(maker.memory_blocked_steps.items.len == 0); | ||
| 782 | |||
| 783 | var test_pass_count: usize = 0; | ||
| 784 | var test_skip_count: usize = 0; | ||
| 785 | var test_fail_count: usize = 0; | ||
| 786 | var test_crash_count: usize = 0; | ||
| 787 | var test_timeout_count: usize = 0; | ||
| 788 | |||
| 789 | var test_count: usize = 0; | ||
| 790 | |||
| 791 | var success_count: usize = 0; | ||
| 792 | var skipped_count: usize = 0; | ||
| 793 | var failure_count: usize = 0; | ||
| 794 | var pending_count: usize = 0; | ||
| 795 | var total_compile_errors: usize = 0; | ||
| 796 | |||
| 797 | var cleanup_task = io.async(cleanTmpFiles, .{ io, step_stack.keys() }); | ||
| 798 | defer cleanup_task.await(io); | ||
| 799 | |||
| 800 | for (step_stack.keys()) |step_index| { | ||
| 801 | const make_step = maker.stepByIndex(step_index); | ||
| 802 | test_pass_count += make_step.test_results.passCount(); | ||
| 803 | test_skip_count += make_step.test_results.skip_count; | ||
| 804 | test_fail_count += make_step.test_results.fail_count; | ||
| 805 | test_crash_count += make_step.test_results.crash_count; | ||
| 806 | test_timeout_count += make_step.test_results.timeout_count; | ||
| 807 | |||
| 808 | test_count += make_step.test_results.test_count; | ||
| 809 | |||
| 810 | switch (make_step.state) { | ||
| 811 | .precheck_unstarted => unreachable, | ||
| 812 | .precheck_started => unreachable, | ||
| 813 | .precheck_done => unreachable, | ||
| 814 | .dependency_failure => pending_count += 1, | ||
| 815 | .success => success_count += 1, | ||
| 816 | .skipped, .skipped_oom => skipped_count += 1, | ||
| 817 | .failure => { | ||
| 818 | failure_count += 1; | ||
| 819 | const compile_errors_len = make_step.result_error_bundle.errorMessageCount(); | ||
| 820 | if (compile_errors_len > 0) { | ||
| 821 | total_compile_errors += compile_errors_len; | ||
| 822 | } | ||
| 823 | }, | ||
| 824 | } | ||
| 825 | } | ||
| 826 | |||
| 827 | if (fuzz) |mode| blk: { | ||
| 828 | switch (builtin.os.tag) { | ||
| 829 | // Current implementation depends on two things that need to be ported to Windows: | ||
| 830 | // * Memory-mapping to share data between the fuzzer and build runner. | ||
| 831 | // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving | ||
| 832 | // many addresses to source locations). | ||
| 833 | .windows => fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}), | ||
| 834 | else => {}, | ||
| 835 | } | ||
| 836 | if (@bitSizeOf(usize) != 64) { | ||
| 837 | // Current implementation depends on posix.mmap()'s second parameter, `length: usize`, | ||
| 838 | // being compatible with file system's u64 return value. This is not the case | ||
| 839 | // on 32-bit platforms. | ||
| 840 | // Affects or affected by issues #5185, #22523, and #22464. | ||
| 841 | fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); | ||
| 842 | } | ||
| 843 | |||
| 844 | switch (mode) { | ||
| 845 | .forever => break :blk, | ||
| 846 | .limit => {}, | ||
| 847 | } | ||
| 848 | |||
| 849 | assert(mode == .limit); | ||
| 850 | var f = Fuzz.init( | ||
| 851 | gpa, | ||
| 852 | io, | ||
| 853 | step_stack.keys(), | ||
| 854 | parent_prog_node, | ||
| 855 | mode, | ||
| 856 | ) catch |err| fatal("failed to start fuzzer: {t}", .{err}); | ||
| 857 | defer f.deinit(); | ||
| 858 | |||
| 859 | f.start(); | ||
| 860 | try f.waitAndPrintReport(); | ||
| 861 | } | ||
| 862 | |||
| 863 | // Every test has a state | ||
| 864 | assert(test_pass_count + test_skip_count + test_fail_count + test_crash_count + test_timeout_count == test_count); | ||
| 865 | |||
| 866 | if (failure_count == 0) { | ||
| 867 | std.Progress.setStatus(.success); | ||
| 868 | } else { | ||
| 869 | std.Progress.setStatus(.failure); | ||
| 870 | } | ||
| 871 | |||
| 872 | summary: { | ||
| 873 | switch (maker.summary) { | ||
| 874 | .all, .new, .line => {}, | ||
| 875 | .failures => if (failure_count == 0) break :summary, | ||
| 876 | .none => break :summary, | ||
| 877 | } | ||
| 878 | |||
| 879 | const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); | ||
| 880 | defer io.unlockStderr(); | ||
| 881 | const t = stderr.terminal(); | ||
| 882 | const w = &stderr.file_writer.interface; | ||
| 883 | |||
| 884 | const total_count = success_count + failure_count + pending_count + skipped_count; | ||
| 885 | t.setColor(.cyan) catch {}; | ||
| 886 | t.setColor(.bold) catch {}; | ||
| 887 | w.writeAll("Build Summary: ") catch {}; | ||
| 888 | t.setColor(.reset) catch {}; | ||
| 889 | w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {}; | ||
| 890 | { | ||
| 891 | t.setColor(.dim) catch {}; | ||
| 892 | var first = true; | ||
| 893 | if (skipped_count > 0) { | ||
| 894 | w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {}; | ||
| 895 | first = false; | ||
| 896 | } | ||
| 897 | if (failure_count > 0) { | ||
| 898 | w.print("{s}{d} failed", .{ if (first) " (" else ", ", failure_count }) catch {}; | ||
| 899 | first = false; | ||
| 900 | } | ||
| 901 | if (!first) w.writeByte(')') catch {}; | ||
| 902 | t.setColor(.reset) catch {}; | ||
| 903 | } | ||
| 904 | |||
| 905 | if (test_count > 0) { | ||
| 906 | w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {}; | ||
| 907 | t.setColor(.dim) catch {}; | ||
| 908 | var first = true; | ||
| 909 | if (test_skip_count > 0) { | ||
| 910 | w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {}; | ||
| 911 | first = false; | ||
| 912 | } | ||
| 913 | if (test_fail_count > 0) { | ||
| 914 | w.print("{s}{d} failed", .{ if (first) " (" else ", ", test_fail_count }) catch {}; | ||
| 915 | first = false; | ||
| 916 | } | ||
| 917 | if (test_crash_count > 0) { | ||
| 918 | w.print("{s}{d} crashed", .{ if (first) " (" else ", ", test_crash_count }) catch {}; | ||
| 919 | first = false; | ||
| 920 | } | ||
| 921 | if (test_timeout_count > 0) { | ||
| 922 | w.print("{s}{d} timed out", .{ if (first) " (" else ", ", test_timeout_count }) catch {}; | ||
| 923 | first = false; | ||
| 924 | } | ||
| 925 | if (!first) w.writeByte(')') catch {}; | ||
| 926 | t.setColor(.reset) catch {}; | ||
| 927 | } | ||
| 928 | |||
| 929 | w.writeAll("\n") catch {}; | ||
| 930 | |||
| 931 | if (maker.summary == .line) break :summary; | ||
| 932 | |||
| 933 | // Print a fancy tree with build results. | ||
| 934 | var step_stack_copy = try step_stack.clone(gpa); | ||
| 935 | defer step_stack_copy.deinit(gpa); | ||
| 936 | |||
| 937 | var print_node: PrintNode = .{ .parent = null }; | ||
| 938 | if (step_names.len == 0) { | ||
| 939 | print_node.last = true; | ||
| 940 | printTreeStep(maker, c.default_step, t, &print_node, &step_stack_copy) catch |err| switch (err) { | ||
| 941 | error.Canceled => |e| return e, | ||
| 942 | else => {}, | ||
| 943 | }; | ||
| 944 | } else { | ||
| 945 | const last_index = if (maker.summary == .all) top_level_steps.count() else blk: { | ||
| 946 | var i: usize = step_names.len; | ||
| 947 | while (i > 0) { | ||
| 948 | i -= 1; | ||
| 949 | const step_index = top_level_steps.get(step_names[i]).?; | ||
| 950 | const step = maker.stepByIndex(step_index); | ||
| 951 | const found = switch (maker.summary) { | ||
| 952 | .all, .line, .none => unreachable, | ||
| 953 | .failures => step.state != .success, | ||
| 954 | .new => !step.result_cached, | ||
| 955 | }; | ||
| 956 | if (found) break :blk i; | ||
| 957 | } | ||
| 958 | break :blk top_level_steps.count(); | ||
| 959 | }; | ||
| 960 | for (step_names, 0..) |step_name, i| { | ||
| 961 | const step_index = top_level_steps.get(step_name).?; | ||
| 962 | print_node.last = i + 1 == last_index; | ||
| 963 | printTreeStep(maker, step_index, t, &print_node, &step_stack_copy) catch |err| switch (err) { | ||
| 964 | error.Canceled => |e| return e, | ||
| 965 | else => {}, | ||
| 966 | }; | ||
| 967 | } | ||
| 968 | } | ||
| 969 | w.writeByte('\n') catch {}; | ||
| 970 | } | ||
| 971 | |||
| 972 | if (maker.watch or maker.web_server != null) return; | ||
| 973 | |||
| 974 | // Perhaps in the future there could be an Advanced Options flag such as | ||
| 975 | // --debug-build-runner-leaks which would make this code return instead of | ||
| 976 | // calling exit. | ||
| 977 | |||
| 978 | const code: u8 = code: { | ||
| 979 | if (failure_count == 0) break :code 0; // success | ||
| 980 | if (maker.error_style.verboseContext()) break :code 1; // failure; print build command | ||
| 981 | break :code 2; // failure; do not print build command | ||
| 982 | }; | ||
| 983 | _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; | ||
| 984 | process.exit(code); | ||
| 985 | } | ||
| 986 | |||
| 987 | fn stepReady( | ||
| 988 | maker: *Maker, | ||
| 989 | group: *Io.Group, | ||
| 990 | step_index: Configuration.Step.Index, | ||
| 991 | root_prog_node: std.Progress.Node, | ||
| 992 | ) Io.Cancelable!void { | ||
| 993 | const graph = maker.graph; | ||
| 994 | const io = graph.io; | ||
| 995 | const c = &maker.scanned_config.configuration; | ||
| 996 | const max_rss = step_index.ptr(c).max_rss.toBytes(); | ||
| 997 | if (max_rss != 0) { | ||
| 998 | try maker.max_rss_mutex.lock(io); | ||
| 999 | defer maker.max_rss_mutex.unlock(io); | ||
| 1000 | if (maker.available_rss < max_rss) { | ||
| 1001 | // Running this step right now could possibly exceed the allotted RSS. | ||
| 1002 | maker.memory_blocked_steps.append(maker.gpa, step_index) catch | ||
| 1003 | @panic("TODO eliminate memory allocation here"); | ||
| 1004 | return; | ||
| 1005 | } | ||
| 1006 | maker.available_rss -= max_rss; | ||
| 1007 | } | ||
| 1008 | group.async(io, makeStep, .{ maker, group, step_index, root_prog_node }); | ||
| 1009 | } | ||
| 1010 | |||
| 1011 | /// Runs the "make" function of the single step `s`, updates its state, and then spawns newly-ready | ||
| 1012 | /// dependant steps in `group`. If `s` makes an RSS claim (i.e. `s.max_rss != 0`), the caller must | ||
| 1013 | /// have already subtracted this value from `maker.available_rss`. This function will release the RSS | ||
| 1014 | /// claim (i.e. add `s.max_rss` back into `maker.available_rss`) and queue any viable memory-blocked | ||
| 1015 | /// steps after "make" completes for `s`. | ||
| 1016 | fn makeStep( | ||
| 1017 | maker: *Maker, | ||
| 1018 | group: *Io.Group, | ||
| 1019 | step_index: Configuration.Step.Index, | ||
| 1020 | root_prog_node: std.Progress.Node, | ||
| 1021 | ) Io.Cancelable!void { | ||
| 1022 | const graph = maker.graph; | ||
| 1023 | const io = graph.io; | ||
| 1024 | const gpa = maker.gpa; | ||
| 1025 | const c = &maker.scanned_config.configuration; | ||
| 1026 | const conf_step = step_index.ptr(c); | ||
| 1027 | const step_name = conf_step.name.slice(c); | ||
| 1028 | const deps = conf_step.deps.slice(c); | ||
| 1029 | const make_step = maker.stepByIndex(step_index); | ||
| 1030 | |||
| 1031 | { | ||
| 1032 | const step_prog_node = root_prog_node.start(step_name, 0); | ||
| 1033 | defer step_prog_node.end(); | ||
| 1034 | |||
| 1035 | if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .wip); | ||
| 1036 | |||
| 1037 | const new_state: Step.State = for (deps) |dep_index| { | ||
| 1038 | const dep_make_step = maker.stepByIndex(dep_index); | ||
| 1039 | switch (@atomicLoad(Step.State, &dep_make_step.state, .monotonic)) { | ||
| 1040 | .precheck_unstarted => unreachable, | ||
| 1041 | .precheck_started => unreachable, | ||
| 1042 | .precheck_done => unreachable, | ||
| 1043 | |||
| 1044 | .failure, | ||
| 1045 | .dependency_failure, | ||
| 1046 | .skipped_oom, | ||
| 1047 | => break .dependency_failure, | ||
| 1048 | |||
| 1049 | .success, .skipped => {}, | ||
| 1050 | } | ||
| 1051 | } else if (make_step.make(.{ | ||
| 1052 | .progress_node = step_prog_node, | ||
| 1053 | .watch = maker.watch, | ||
| 1054 | .web_server = if (maker.web_server) |*ws| ws else null, | ||
| 1055 | .unit_test_timeout_ns = maker.unit_test_timeout_ns, | ||
| 1056 | .gpa = gpa, | ||
| 1057 | })) state: { | ||
| 1058 | break :state .success; | ||
| 1059 | } else |err| switch (err) { | ||
| 1060 | error.MakeFailed => .failure, | ||
| 1061 | error.MakeSkipped => .skipped, | ||
| 1062 | }; | ||
| 1063 | |||
| 1064 | @atomicStore(Step.State, &make_step.state, new_state, .monotonic); | ||
| 1065 | |||
| 1066 | switch (new_state) { | ||
| 1067 | .precheck_unstarted => unreachable, | ||
| 1068 | .precheck_started => unreachable, | ||
| 1069 | .precheck_done => unreachable, | ||
| 1070 | |||
| 1071 | .failure, | ||
| 1072 | .dependency_failure, | ||
| 1073 | .skipped_oom, | ||
| 1074 | => { | ||
| 1075 | if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .failure); | ||
| 1076 | std.Progress.setStatus(.failure_working); | ||
| 1077 | }, | ||
| 1078 | |||
| 1079 | .success, | ||
| 1080 | .skipped, | ||
| 1081 | => { | ||
| 1082 | if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .success); | ||
| 1083 | }, | ||
| 1084 | } | ||
| 1085 | } | ||
| 1086 | |||
| 1087 | // No matter the result, we want to display error/warning messages. | ||
| 1088 | if (make_step.result_error_bundle.errorMessageCount() > 0 or | ||
| 1089 | make_step.result_error_msgs.items.len > 0 or | ||
| 1090 | make_step.result_stderr.len > 0) | ||
| 1091 | { | ||
| 1092 | const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); | ||
| 1093 | defer io.unlockStderr(); | ||
| 1094 | printErrorMessages(gpa, c, maker.steps, step_index, .{}, stderr.terminal(), maker.error_style, maker.multiline_errors) catch |err| switch (err) { | ||
| 1095 | error.Canceled => |e| return e, | ||
| 1096 | error.WriteFailed => switch (stderr.file_writer.err.?) { | ||
| 1097 | error.Canceled => |e| return e, | ||
| 1098 | else => {}, | ||
| 1099 | }, | ||
| 1100 | else => {}, | ||
| 1101 | }; | ||
| 1102 | } | ||
| 1103 | |||
| 1104 | const max_rss = conf_step.max_rss.toBytes(); | ||
| 1105 | if (max_rss != 0) { | ||
| 1106 | var dispatch_set: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 1107 | defer dispatch_set.deinit(gpa); | ||
| 1108 | |||
| 1109 | // Release our RSS claim and kick off some blocked steps if possible. We use `dispatch_set` | ||
| 1110 | // as a staging buffer to avoid recursing into `makeStep` while `maker.max_rss_mutex` is held. | ||
| 1111 | { | ||
| 1112 | try maker.max_rss_mutex.lock(io); | ||
| 1113 | defer maker.max_rss_mutex.unlock(io); | ||
| 1114 | maker.available_rss += max_rss; | ||
| 1115 | dispatch_set.ensureUnusedCapacity(gpa, maker.memory_blocked_steps.items.len) catch | ||
| 1116 | @panic("TODO eliminate memory allocation here"); | ||
| 1117 | while (maker.memory_blocked_steps.getLast()) |candidate_index| { | ||
| 1118 | const candidate_max_rss = candidate_index.ptr(c).max_rss.toBytes(); | ||
| 1119 | if (maker.available_rss < candidate_max_rss) break; | ||
| 1120 | assert(maker.memory_blocked_steps.pop() == candidate_index); | ||
| 1121 | dispatch_set.appendAssumeCapacity(candidate_index); | ||
| 1122 | } | ||
| 1123 | } | ||
| 1124 | for (dispatch_set.items) |candidate| { | ||
| 1125 | group.async(io, makeStep, .{ maker, group, candidate, root_prog_node }); | ||
| 1126 | } | ||
| 1127 | } | ||
| 1128 | |||
| 1129 | for (make_step.dependants.items) |dependant_index| { | ||
| 1130 | const dependant = maker.stepByIndex(dependant_index); | ||
| 1131 | // `.acq_rel` synchronizes with itself to ensure all dependencies' final states are visible when this hits 0. | ||
| 1132 | if (@atomicRmw(u32, &dependant.pending_deps, .Sub, 1, .acq_rel) == 1) { | ||
| 1133 | try stepReady(maker, group, dependant_index, root_prog_node); | ||
| 1134 | } | ||
| 1135 | } | ||
| 1136 | } | ||
| 1137 | |||
| 1138 | fn printTreeStep( | ||
| 1139 | maker: *const Maker, | ||
| 1140 | step_index: Configuration.Step.Index, | ||
| 1141 | stderr: Io.Terminal, | ||
| 1142 | parent_node: *PrintNode, | ||
| 1143 | step_stack: *std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void), | ||
| 1144 | ) !void { | ||
| 1145 | const writer = stderr.writer; | ||
| 1146 | const first = step_stack.swapRemove(step_index); | ||
| 1147 | const summary = maker.summary; | ||
| 1148 | const c = &maker.scanned_config.configuration; | ||
| 1149 | const conf_step = step_index.ptr(c); | ||
| 1150 | const make_step = maker.stepByIndex(step_index); | ||
| 1151 | const skip = switch (summary) { | ||
| 1152 | .none, .line => unreachable, | ||
| 1153 | .all => false, | ||
| 1154 | .new => make_step.result_cached, | ||
| 1155 | .failures => make_step.state == .success, | ||
| 1156 | }; | ||
| 1157 | if (skip) return; | ||
| 1158 | try printPrefix(parent_node, stderr); | ||
| 1159 | |||
| 1160 | if (parent_node.parent != null) { | ||
| 1161 | if (parent_node.last) { | ||
| 1162 | try printChildNodePrefix(stderr); | ||
| 1163 | } else { | ||
| 1164 | try writer.writeAll(switch (stderr.mode) { | ||
| 1165 | .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ | ||
| 1166 | else => "+- ", | ||
| 1167 | }); | ||
| 1168 | } | ||
| 1169 | } | ||
| 1170 | |||
| 1171 | if (!first) try stderr.setColor(.dim); | ||
| 1172 | |||
| 1173 | // dep_prefix omitted here because it is redundant with the tree. | ||
| 1174 | try writer.writeAll(conf_step.name.slice(c)); | ||
| 1175 | |||
| 1176 | const deps = conf_step.deps.slice(c); | ||
| 1177 | |||
| 1178 | if (first) { | ||
| 1179 | try printStepStatus(maker, step_index, stderr); | ||
| 1180 | |||
| 1181 | const last_index = if (summary == .all) deps.len -| 1 else blk: { | ||
| 1182 | var i: usize = deps.len; | ||
| 1183 | while (i > 0) { | ||
| 1184 | i -= 1; | ||
| 1185 | |||
| 1186 | const dep_index = deps[i]; | ||
| 1187 | const dep = maker.stepByIndex(dep_index); | ||
| 1188 | const found = switch (summary) { | ||
| 1189 | .all, .line, .none => unreachable, | ||
| 1190 | .failures => dep.state != .success, | ||
| 1191 | .new => !dep.result_cached, | ||
| 1192 | }; | ||
| 1193 | if (found) break :blk i; | ||
| 1194 | } | ||
| 1195 | break :blk deps.len -| 1; | ||
| 1196 | }; | ||
| 1197 | for (deps, 0..) |dep, i| { | ||
| 1198 | var print_node: PrintNode = .{ | ||
| 1199 | .parent = parent_node, | ||
| 1200 | .last = i == last_index, | ||
| 1201 | }; | ||
| 1202 | try printTreeStep(maker, dep, stderr, &print_node, step_stack); | ||
| 1203 | } | ||
| 1204 | } else { | ||
| 1205 | if (deps.len == 0) { | ||
| 1206 | try writer.writeAll(" (reused)\n"); | ||
| 1207 | } else { | ||
| 1208 | try writer.print(" (+{d} more reused dependencies)\n", .{deps.len}); | ||
| 1209 | } | ||
| 1210 | try stderr.setColor(.reset); | ||
| 1211 | } | ||
| 1212 | } | ||
| 1213 | |||
| 1214 | fn printStepStatus(maker: *const Maker, step_index: Configuration.Step.Index, stderr: Io.Terminal) !void { | ||
| 1215 | const s = maker.stepByIndex(step_index); | ||
| 1216 | const writer = stderr.writer; | ||
| 1217 | switch (s.state) { | ||
| 1218 | .precheck_unstarted => unreachable, | ||
| 1219 | .precheck_started => unreachable, | ||
| 1220 | .precheck_done => unreachable, | ||
| 1221 | |||
| 1222 | .dependency_failure => { | ||
| 1223 | try stderr.setColor(.dim); | ||
| 1224 | try writer.writeAll(" transitive failure\n"); | ||
| 1225 | try stderr.setColor(.reset); | ||
| 1226 | }, | ||
| 1227 | |||
| 1228 | .success => { | ||
| 1229 | try stderr.setColor(.green); | ||
| 1230 | if (s.result_cached) { | ||
| 1231 | try writer.writeAll(" cached"); | ||
| 1232 | } else if (s.test_results.test_count > 0) { | ||
| 1233 | const pass_count = s.test_results.passCount(); | ||
| 1234 | assert(s.test_results.test_count == pass_count + s.test_results.skip_count); | ||
| 1235 | try writer.print(" {d} pass", .{pass_count}); | ||
| 1236 | if (s.test_results.skip_count > 0) { | ||
| 1237 | try stderr.setColor(.reset); | ||
| 1238 | try writer.writeAll(", "); | ||
| 1239 | try stderr.setColor(.yellow); | ||
| 1240 | try writer.print("{d} skip", .{s.test_results.skip_count}); | ||
| 1241 | } | ||
| 1242 | try stderr.setColor(.reset); | ||
| 1243 | try writer.print(" ({d} total)", .{s.test_results.test_count}); | ||
| 1244 | } else { | ||
| 1245 | try writer.writeAll(" success"); | ||
| 1246 | } | ||
| 1247 | try stderr.setColor(.reset); | ||
| 1248 | if (s.result_duration_ns) |ns| { | ||
| 1249 | try stderr.setColor(.dim); | ||
| 1250 | if (ns >= std.time.ns_per_min) { | ||
| 1251 | try writer.print(" {d}m", .{ns / std.time.ns_per_min}); | ||
| 1252 | } else if (ns >= std.time.ns_per_s) { | ||
| 1253 | try writer.print(" {d}s", .{ns / std.time.ns_per_s}); | ||
| 1254 | } else if (ns >= std.time.ns_per_ms) { | ||
| 1255 | try writer.print(" {d}ms", .{ns / std.time.ns_per_ms}); | ||
| 1256 | } else if (ns >= std.time.ns_per_us) { | ||
| 1257 | try writer.print(" {d}us", .{ns / std.time.ns_per_us}); | ||
| 1258 | } else { | ||
| 1259 | try writer.print(" {d}ns", .{ns}); | ||
| 1260 | } | ||
| 1261 | try stderr.setColor(.reset); | ||
| 1262 | } | ||
| 1263 | if (s.result_peak_rss != 0) { | ||
| 1264 | const rss = s.result_peak_rss; | ||
| 1265 | try stderr.setColor(.dim); | ||
| 1266 | if (rss >= 1000_000_000) { | ||
| 1267 | try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000}); | ||
| 1268 | } else if (rss >= 1000_000) { | ||
| 1269 | try writer.print(" MaxRSS:{d}M", .{rss / 1000_000}); | ||
| 1270 | } else if (rss >= 1000) { | ||
| 1271 | try writer.print(" MaxRSS:{d}K", .{rss / 1000}); | ||
| 1272 | } else { | ||
| 1273 | try writer.print(" MaxRSS:{d}B", .{rss}); | ||
| 1274 | } | ||
| 1275 | try stderr.setColor(.reset); | ||
| 1276 | } | ||
| 1277 | try writer.writeAll("\n"); | ||
| 1278 | }, | ||
| 1279 | .skipped => { | ||
| 1280 | try stderr.setColor(.yellow); | ||
| 1281 | try writer.writeAll(" skipped\n"); | ||
| 1282 | try stderr.setColor(.reset); | ||
| 1283 | }, | ||
| 1284 | .skipped_oom => { | ||
| 1285 | const c = &maker.scanned_config.configuration; | ||
| 1286 | const max_rss = step_index.ptr(c).max_rss.toBytes(); | ||
| 1287 | try stderr.setColor(.yellow); | ||
| 1288 | try writer.writeAll(" skipped (not enough memory)"); | ||
| 1289 | try stderr.setColor(.dim); | ||
| 1290 | try writer.print(" upper bound of {d} exceeded runner limit ({d})\n", .{ | ||
| 1291 | max_rss, maker.available_rss, | ||
| 1292 | }); | ||
| 1293 | try stderr.setColor(.reset); | ||
| 1294 | }, | ||
| 1295 | .failure => { | ||
| 1296 | try printStepFailure(maker.steps, step_index, stderr, false); | ||
| 1297 | try stderr.setColor(.reset); | ||
| 1298 | }, | ||
| 1299 | } | ||
| 1300 | } | ||
| 1301 | |||
| 1302 | fn printStepFailure( | ||
| 1303 | make_steps: []Step, | ||
| 1304 | step_index: Configuration.Step.Index, | ||
| 1305 | stderr: Io.Terminal, | ||
| 1306 | dim: bool, | ||
| 1307 | ) !void { | ||
| 1308 | const w = stderr.writer; | ||
| 1309 | const s = &make_steps[@intFromEnum(step_index)]; | ||
| 1310 | if (s.result_error_bundle.errorMessageCount() > 0) { | ||
| 1311 | try stderr.setColor(.red); | ||
| 1312 | try w.print(" {d} errors\n", .{ | ||
| 1313 | s.result_error_bundle.errorMessageCount(), | ||
| 1314 | }); | ||
| 1315 | } else if (!s.test_results.isSuccess()) { | ||
| 1316 | // These first values include all of the test "statuses". Every test is either passsed, | ||
| 1317 | // skipped, failed, crashed, or timed out. | ||
| 1318 | try stderr.setColor(.green); | ||
| 1319 | try w.print(" {d} pass", .{s.test_results.passCount()}); | ||
| 1320 | try stderr.setColor(.reset); | ||
| 1321 | if (dim) try stderr.setColor(.dim); | ||
| 1322 | if (s.test_results.skip_count > 0) { | ||
| 1323 | try w.writeAll(", "); | ||
| 1324 | try stderr.setColor(.yellow); | ||
| 1325 | try w.print("{d} skip", .{s.test_results.skip_count}); | ||
| 1326 | try stderr.setColor(.reset); | ||
| 1327 | if (dim) try stderr.setColor(.dim); | ||
| 1328 | } | ||
| 1329 | if (s.test_results.fail_count > 0) { | ||
| 1330 | try w.writeAll(", "); | ||
| 1331 | try stderr.setColor(.red); | ||
| 1332 | try w.print("{d} fail", .{s.test_results.fail_count}); | ||
| 1333 | try stderr.setColor(.reset); | ||
| 1334 | if (dim) try stderr.setColor(.dim); | ||
| 1335 | } | ||
| 1336 | if (s.test_results.crash_count > 0) { | ||
| 1337 | try w.writeAll(", "); | ||
| 1338 | try stderr.setColor(.red); | ||
| 1339 | try w.print("{d} crash", .{s.test_results.crash_count}); | ||
| 1340 | try stderr.setColor(.reset); | ||
| 1341 | if (dim) try stderr.setColor(.dim); | ||
| 1342 | } | ||
| 1343 | if (s.test_results.timeout_count > 0) { | ||
| 1344 | try w.writeAll(", "); | ||
| 1345 | try stderr.setColor(.red); | ||
| 1346 | try w.print("{d} timeout", .{s.test_results.timeout_count}); | ||
| 1347 | try stderr.setColor(.reset); | ||
| 1348 | if (dim) try stderr.setColor(.dim); | ||
| 1349 | } | ||
| 1350 | try w.print(" ({d} total)", .{s.test_results.test_count}); | ||
| 1351 | |||
| 1352 | // Memory leaks are intentionally written after the total, because is isn't a test *status*, | ||
| 1353 | // but just a flag that any tests -- even passed ones -- can have. We also use a different | ||
| 1354 | // separator, so it looks like: | ||
| 1355 | // 2 pass, 1 skip, 2 fail (5 total); 2 leaks | ||
| 1356 | if (s.test_results.leak_count > 0) { | ||
| 1357 | try w.writeAll("; "); | ||
| 1358 | try stderr.setColor(.red); | ||
| 1359 | try w.print("{d} leaks", .{s.test_results.leak_count}); | ||
| 1360 | try stderr.setColor(.reset); | ||
| 1361 | if (dim) try stderr.setColor(.dim); | ||
| 1362 | } | ||
| 1363 | |||
| 1364 | // It's usually not helpful to know how many error logs there were because they tend to | ||
| 1365 | // just come with other errors (e.g. crashes and leaks print stack traces, and clean | ||
| 1366 | // failures print error traces). So only mention them if they're the only thing causing | ||
| 1367 | // the failure. | ||
| 1368 | const show_err_logs: bool = show: { | ||
| 1369 | var alt_results = s.test_results; | ||
| 1370 | alt_results.log_err_count = 0; | ||
| 1371 | break :show alt_results.isSuccess(); | ||
| 1372 | }; | ||
| 1373 | if (show_err_logs) { | ||
| 1374 | try w.writeAll("; "); | ||
| 1375 | try stderr.setColor(.red); | ||
| 1376 | try w.print("{d} error logs", .{s.test_results.log_err_count}); | ||
| 1377 | try stderr.setColor(.reset); | ||
| 1378 | if (dim) try stderr.setColor(.dim); | ||
| 1379 | } | ||
| 1380 | |||
| 1381 | try w.writeAll("\n"); | ||
| 1382 | } else if (s.result_error_msgs.items.len > 0) { | ||
| 1383 | try stderr.setColor(.red); | ||
| 1384 | try w.writeAll(" failure\n"); | ||
| 1385 | } else { | ||
| 1386 | assert(s.result_stderr.len > 0); | ||
| 1387 | try stderr.setColor(.red); | ||
| 1388 | try w.writeAll(" w\n"); | ||
| 1389 | } | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | const PrintNode = struct { | ||
| 1393 | parent: ?*PrintNode, | ||
| 1394 | last: bool = false, | ||
| 1395 | }; | ||
| 1396 | |||
| 1397 | fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void { | ||
| 1398 | const parent = node.parent orelse return; | ||
| 1399 | const writer = stderr.writer; | ||
| 1400 | if (parent.parent == null) return; | ||
| 1401 | try printPrefix(parent, stderr); | ||
| 1402 | if (parent.last) { | ||
| 1403 | try writer.writeAll(" "); | ||
| 1404 | } else { | ||
| 1405 | try writer.writeAll(switch (stderr.mode) { | ||
| 1406 | .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ | ||
| 1407 | else => "| ", | ||
| 1408 | }); | ||
| 1409 | } | ||
| 1410 | } | ||
| 1411 | |||
| 1412 | fn printChildNodePrefix(stderr: Io.Terminal) !void { | ||
| 1413 | try stderr.writer.writeAll(switch (stderr.mode) { | ||
| 1414 | .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─ | ||
| 1415 | else => "+- ", | ||
| 1416 | }); | ||
| 1417 | } | ||
| 1418 | |||
| 1419 | /// Traverse the dependency graph depth-first and make it undirected by having | ||
| 1420 | /// steps know their dependants (they only know dependencies at start). | ||
| 1421 | /// Along the way, check that there is no dependency loop, and record the steps | ||
| 1422 | /// in traversal order in `step_stack`. | ||
| 1423 | /// Each step has its dependencies traversed in random order, this accomplishes | ||
| 1424 | /// two things: | ||
| 1425 | /// - `step_stack` will be in randomized-depth-first order, so the build runner | ||
| 1426 | /// spawns initial steps in a random order | ||
| 1427 | /// - each step's `dependants` list is also filled in a random order, so that | ||
| 1428 | /// when it finishes executing in `makeStep`, it spawns next steps to run in | ||
| 1429 | /// random order | ||
| 1430 | fn constructGraphAndCheckForDependencyLoop( | ||
| 1431 | gpa: Allocator, | ||
| 1432 | c: *const Configuration, | ||
| 1433 | steps: []Step, | ||
| 1434 | step_index: Configuration.Step.Index, | ||
| 1435 | step_stack: *std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void), | ||
| 1436 | rand: std.Random, | ||
| 1437 | ) error{ DependencyLoopDetected, OutOfMemory }!void { | ||
| 1438 | const make_step: *Step = &steps[@intFromEnum(step_index)]; | ||
| 1439 | switch (make_step.state) { | ||
| 1440 | .precheck_started => { | ||
| 1441 | log.err("dependency loop detected: {s}", .{step_index.ptr(c).name.slice(c)}); | ||
| 1442 | return error.DependencyLoopDetected; | ||
| 1443 | }, | ||
| 1444 | .precheck_unstarted => { | ||
| 1445 | make_step.state = .precheck_started; | ||
| 1446 | |||
| 1447 | const step = step_index.ptr(c); | ||
| 1448 | const dependencies = step.deps.slice(c); | ||
| 1449 | try step_stack.ensureUnusedCapacity(gpa, dependencies.len); | ||
| 1450 | |||
| 1451 | // We dupe to avoid shuffling the steps in the summary, it depends | ||
| 1452 | // on dependencies' order. | ||
| 1453 | const deps = try gpa.dupe(Configuration.Step.Index, dependencies); | ||
| 1454 | defer gpa.free(deps); | ||
| 1455 | |||
| 1456 | rand.shuffle(Configuration.Step.Index, deps); | ||
| 1457 | |||
| 1458 | for (deps) |dep| { | ||
| 1459 | const dep_step: *Step = &steps[@intFromEnum(dep)]; | ||
| 1460 | try step_stack.put(gpa, dep, {}); | ||
| 1461 | try dep_step.dependants.append(gpa, step_index); | ||
| 1462 | constructGraphAndCheckForDependencyLoop(gpa, c, steps, dep, step_stack, rand) catch |err| switch (err) { | ||
| 1463 | error.DependencyLoopDetected => { | ||
| 1464 | log.info("needed by: {s}", .{step_index.ptr(c).name.slice(c)}); | ||
| 1465 | return err; | ||
| 1466 | }, | ||
| 1467 | else => return err, | ||
| 1468 | }; | ||
| 1469 | } | ||
| 1470 | |||
| 1471 | make_step.state = .precheck_done; | ||
| 1472 | make_step.pending_deps = @intCast(dependencies.len); | ||
| 1473 | }, | ||
| 1474 | .precheck_done => {}, | ||
| 1475 | |||
| 1476 | // These don't happen until we actually run the step graph. | ||
| 1477 | .dependency_failure => unreachable, | ||
| 1478 | .success => unreachable, | ||
| 1479 | .failure => unreachable, | ||
| 1480 | .skipped => unreachable, | ||
| 1481 | .skipped_oom => unreachable, | ||
| 1482 | } | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | pub fn printErrorMessages( | ||
| 1486 | gpa: Allocator, | ||
| 1487 | c: *const Configuration, | ||
| 1488 | make_steps: []Step, | ||
| 1489 | failing_step_index: Configuration.Step.Index, | ||
| 1490 | options: std.zig.ErrorBundle.RenderOptions, | ||
| 1491 | stderr: Io.Terminal, | ||
| 1492 | error_style: ErrorStyle, | ||
| 1493 | multiline_errors: MultilineErrors, | ||
| 1494 | ) !void { | ||
| 1495 | const writer = stderr.writer; | ||
| 1496 | if (error_style.verboseContext()) { | ||
| 1497 | // Provide context for where these error messages are coming from by | ||
| 1498 | // printing the corresponding Step subtree. | ||
| 1499 | var step_stack: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 1500 | defer step_stack.deinit(gpa); | ||
| 1501 | try step_stack.append(gpa, failing_step_index); | ||
| 1502 | while (true) { | ||
| 1503 | const last_step = &make_steps[@intFromEnum(step_stack.items[step_stack.items.len - 1])]; | ||
| 1504 | if (last_step.dependants.items.len == 0) break; | ||
| 1505 | try step_stack.append(gpa, last_step.dependants.items[0]); | ||
| 1506 | } | ||
| 1507 | |||
| 1508 | // Now, `step_stack` has the subtree that we want to print, in reverse order. | ||
| 1509 | try stderr.setColor(.dim); | ||
| 1510 | var indent: usize = 0; | ||
| 1511 | while (step_stack.pop()) |step_index| : (indent += 1) { | ||
| 1512 | if (indent > 0) { | ||
| 1513 | try writer.splatByteAll(' ', (indent - 1) * 3); | ||
| 1514 | try printChildNodePrefix(stderr); | ||
| 1515 | } | ||
| 1516 | |||
| 1517 | try writer.writeAll(step_index.ptr(c).name.slice(c)); | ||
| 1518 | |||
| 1519 | if (step_index == failing_step_index) { | ||
| 1520 | try printStepFailure(make_steps, step_index, stderr, true); | ||
| 1521 | } else { | ||
| 1522 | try writer.writeAll("\n"); | ||
| 1523 | } | ||
| 1524 | } | ||
| 1525 | try stderr.setColor(.reset); | ||
| 1526 | } else { | ||
| 1527 | // Just print the failing step itself. | ||
| 1528 | try stderr.setColor(.dim); | ||
| 1529 | try writer.writeAll(failing_step_index.ptr(c).name.slice(c)); | ||
| 1530 | try printStepFailure(make_steps, failing_step_index, stderr, true); | ||
| 1531 | try stderr.setColor(.reset); | ||
| 1532 | } | ||
| 1533 | |||
| 1534 | const failing_step = &make_steps[@intFromEnum(failing_step_index)]; | ||
| 1535 | |||
| 1536 | if (failing_step.result_stderr.len > 0) { | ||
| 1537 | try writer.writeAll(failing_step.result_stderr); | ||
| 1538 | if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) { | ||
| 1539 | try writer.writeAll("\n"); | ||
| 1540 | } | ||
| 1541 | } | ||
| 1542 | |||
| 1543 | try failing_step.result_error_bundle.renderToTerminal(options, stderr); | ||
| 1544 | |||
| 1545 | for (failing_step.result_error_msgs.items) |msg| { | ||
| 1546 | try stderr.setColor(.red); | ||
| 1547 | try writer.writeAll("error:"); | ||
| 1548 | try stderr.setColor(.reset); | ||
| 1549 | if (std.mem.indexOfScalar(u8, msg, '\n') == null) { | ||
| 1550 | try writer.print(" {s}\n", .{msg}); | ||
| 1551 | } else switch (multiline_errors) { | ||
| 1552 | .indent => { | ||
| 1553 | var it = std.mem.splitScalar(u8, msg, '\n'); | ||
| 1554 | try writer.print(" {s}\n", .{it.first()}); | ||
| 1555 | while (it.next()) |line| { | ||
| 1556 | try writer.print(" {s}\n", .{line}); | ||
| 1557 | } | ||
| 1558 | }, | ||
| 1559 | .newline => try writer.print("\n{s}\n", .{msg}), | ||
| 1560 | .none => try writer.print(" {s}\n", .{msg}), | ||
| 1561 | } | ||
| 1562 | } | ||
| 1563 | |||
| 1564 | if (error_style.verboseContext()) { | ||
| 1565 | if (failing_step.result_failed_command) |cmd_str| { | ||
| 1566 | try stderr.setColor(.red); | ||
| 1567 | try writer.writeAll("failed command: "); | ||
| 1568 | try stderr.setColor(.reset); | ||
| 1569 | try writer.writeAll(cmd_str); | ||
| 1570 | try writer.writeByte('\n'); | ||
| 1571 | } | ||
| 1572 | } | ||
| 1573 | |||
| 1574 | try writer.writeByte('\n'); | ||
| 1575 | } | ||
| 1576 | |||
| 1577 | fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 { | ||
| 1578 | if (idx.* >= args.len) return null; | ||
| 1579 | defer idx.* += 1; | ||
| 1580 | return args[idx.*]; | ||
| 1581 | } | ||
| 1582 | |||
| 1583 | fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 { | ||
| 1584 | return nextArg(args, idx) orelse { | ||
| 1585 | fatalWithHint("expected argument after {q}", .{args[idx.* - 1]}); | ||
| 1586 | }; | ||
| 1587 | } | ||
| 1588 | |||
| 1589 | fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 { | ||
| 1590 | const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first}); | ||
| 1591 | if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg }); | ||
| 1592 | const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first}); | ||
| 1593 | return arg; | ||
| 1594 | } | ||
| 1595 | |||
| 1596 | fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 { | ||
| 1597 | if (idx >= args.len) return null; | ||
| 1598 | return args[idx..]; | ||
| 1599 | } | ||
| 1600 | |||
| 1601 | const Color = std.zig.Color; | ||
| 1602 | const ErrorStyle = enum { | ||
| 1603 | verbose, | ||
| 1604 | minimal, | ||
| 1605 | verbose_clear, | ||
| 1606 | minimal_clear, | ||
| 1607 | fn verboseContext(s: ErrorStyle) bool { | ||
| 1608 | return switch (s) { | ||
| 1609 | .verbose, .verbose_clear => true, | ||
| 1610 | .minimal, .minimal_clear => false, | ||
| 1611 | }; | ||
| 1612 | } | ||
| 1613 | fn clearOnUpdate(s: ErrorStyle) bool { | ||
| 1614 | return switch (s) { | ||
| 1615 | .verbose, .minimal => false, | ||
| 1616 | .verbose_clear, .minimal_clear => true, | ||
| 1617 | }; | ||
| 1618 | } | ||
| 1619 | }; | ||
| 1620 | const MultilineErrors = enum { indent, newline, none }; | ||
| 1621 | const Summary = enum { all, new, failures, line, none }; | ||
| 1622 | |||
| 1623 | fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn { | ||
| 1624 | log.info("to access the help menu: zig build -h", .{}); | ||
| 1625 | fatal(f, args); | ||
| 1626 | } | ||
| 1627 | |||
| 1628 | fn cleanTmpFiles(io: Io, steps: []const Configuration.Step.Index) void { | ||
| 1629 | for (steps) |step_index| { | ||
| 1630 | if (true) @panic("TODO"); | ||
| 1631 | const wf = step_index.cast(std.Build.Step.WriteFile) orelse continue; | ||
| 1632 | if (wf.mode != .tmp) continue; | ||
| 1633 | const path = wf.generated_directory.path orelse continue; | ||
| 1634 | Io.Dir.cwd().deleteTree(io, path) catch |err| { | ||
| 1635 | log.warn("failed to delete {s}: {t}", .{ path, err }); | ||
| 1636 | }; | ||
| 1637 | } | ||
| 1638 | } | ||
| 1639 | |||
| 1640 | var stdio_buffer_allocation: [256]u8 = undefined; | ||
| 1641 | var stdout_writer_allocation: Io.File.Writer = undefined; | ||
| 1642 | |||
| 1643 | fn initStdoutWriter(io: Io) *Writer { | ||
| 1644 | stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation); | ||
| 1645 | return &stdout_writer_allocation.interface; | ||
| 1646 | } | ||
| 1647 | |||
| 1648 | const ScannedConfig = struct { | ||
| 1649 | configuration: Configuration, | ||
| 1650 | top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index), | ||
| 1651 | |||
| 1652 | fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void { | ||
| 1653 | const c = &sc.configuration; | ||
| 1654 | var serializer: std.zon.Serializer = .{ .writer = w }; | ||
| 1655 | var s = try serializer.beginStruct(.{}); | ||
| 1656 | |||
| 1657 | try s.field("default_step", @intFromEnum(c.default_step), .{}); | ||
| 1658 | { | ||
| 1659 | var ss = try s.beginStructField("top_level_steps", .{}); | ||
| 1660 | for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step| { | ||
| 1661 | try ss.field(name, @intFromEnum(step), .{}); | ||
| 1662 | } | ||
| 1663 | try ss.end(); | ||
| 1664 | } | ||
| 1665 | |||
| 1666 | try s.end(); | ||
| 1667 | } | ||
| 1668 | |||
| 1669 | fn printSteps(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { | ||
| 1670 | const arena = graph.arena; | ||
| 1671 | const c = &sc.configuration; | ||
| 1672 | for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step_index| { | ||
| 1673 | const step = step_index.ptr(c); | ||
| 1674 | const decorated_name = if (step_index == c.default_step) | ||
| 1675 | try fmt.allocPrint(arena, "{s} (default)", .{name}) | ||
| 1676 | else | ||
| 1677 | name; | ||
| 1678 | const top_level = c.extraData(Configuration.Step.TopLevel, step.extra_index); | ||
| 1679 | const description = top_level.description.slice(c); | ||
| 1680 | try w.print(" {s:<28} {s}\n", .{ decorated_name, description }); | ||
| 1681 | } | ||
| 1682 | } | ||
| 1683 | |||
| 1684 | fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { | ||
| 1685 | const arena = graph.arena; | ||
| 1686 | |||
| 1687 | try w.print( | ||
| 1688 | \\Usage: {s} build [steps] [options] | ||
| 1689 | \\ | ||
| 1690 | \\Steps: | ||
| 1691 | \\ | ||
| 1692 | , .{graph.zig_exe}); | ||
| 1693 | try printSteps(sc, graph, w); | ||
| 1694 | try w.writeAll( | ||
| 1695 | \\ | ||
| 1696 | \\Project-Specific Options: | ||
| 1697 | \\ | ||
| 1698 | ); | ||
| 1699 | |||
| 1700 | const available_options = sc.configuration.available_options; | ||
| 1701 | if (available_options.len == 0) { | ||
| 1702 | try w.print(" (none)\n", .{}); | ||
| 1703 | } else { | ||
| 1704 | for (available_options) |option| { | ||
| 1705 | const name = option.name.slice(&sc.configuration); | ||
| 1706 | const description = option.description.slice(&sc.configuration); | ||
| 1707 | const help = try fmt.allocPrint(arena, " -D{s}=[{t}]", .{ name, option.type }); | ||
| 1708 | try w.print("{s:<30} {s}\n", .{ help, description }); | ||
| 1709 | if (option.enum_options.slice(&sc.configuration)) |enum_options| { | ||
| 1710 | const padding: [33]u8 = @splat(' '); | ||
| 1711 | try w.writeAll(padding ++ "Supported Values:\n"); | ||
| 1712 | for (enum_options) |enum_option_index| { | ||
| 1713 | const enum_option = enum_option_index.slice(&sc.configuration); | ||
| 1714 | try w.print(padding ++ " {s}\n", .{enum_option}); | ||
| 1715 | } | ||
| 1716 | } | ||
| 1717 | } | ||
| 1718 | } | ||
| 1719 | |||
| 1720 | try w.writeAll( | ||
| 1721 | \\ | ||
| 1722 | \\System Integration Options: | ||
| 1723 | \\ --search-prefix [path] Add a path to look for binaries, libraries, headers | ||
| 1724 | \\ --sysroot [path] Set the system root directory (usually /) | ||
| 1725 | \\ --libc [file] Provide a file which specifies libc paths | ||
| 1726 | \\ | ||
| 1727 | \\ --system [pkgdir] Disable package fetching; enable all integrations | ||
| 1728 | \\ -fsys=[name] Enable a system integration | ||
| 1729 | \\ -fno-sys=[name] Disable a system integration | ||
| 1730 | \\ | ||
| 1731 | \\ -fdarling, -fno-darling Integration with system-installed Darling to | ||
| 1732 | \\ execute macOS programs on Linux hosts | ||
| 1733 | \\ (default: no) | ||
| 1734 | \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute | ||
| 1735 | \\ foreign-architecture programs on Linux hosts | ||
| 1736 | \\ (default: no) | ||
| 1737 | \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc | ||
| 1738 | \\ (e.g. glibc or musl) built for multiple foreign | ||
| 1739 | \\ architectures, allowing execution of non-native | ||
| 1740 | \\ programs that link with libc. | ||
| 1741 | \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on | ||
| 1742 | \\ ARM64 macOS hosts. (default: no) | ||
| 1743 | \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to | ||
| 1744 | \\ execute WASI binaries. (default: no) | ||
| 1745 | \\ -fwine, -fno-wine Integration with system-installed Wine to execute | ||
| 1746 | \\ Windows programs on Linux hosts. (default: no) | ||
| 1747 | \\ | ||
| 1748 | \\ Available System Integrations: Enabled: | ||
| 1749 | \\ | ||
| 1750 | ); | ||
| 1751 | if (sc.configuration.system_integrations.len == 0) { | ||
| 1752 | try w.writeAll(" (none) -\n"); | ||
| 1753 | } else { | ||
| 1754 | for (sc.configuration.system_integrations) |system_integration| { | ||
| 1755 | const name = system_integration.name.slice(&sc.configuration); | ||
| 1756 | const status = switch (system_integration.status) { | ||
| 1757 | .disabled => "no", | ||
| 1758 | .enabled => "yes", | ||
| 1759 | }; | ||
| 1760 | try w.print(" {s:<43} {s}\n", .{ name, status }); | ||
| 1761 | } | ||
| 1762 | } | ||
| 1763 | |||
| 1764 | try w.writeAll( | ||
| 1765 | \\ | ||
| 1766 | \\General Options: | ||
| 1767 | \\ -h, --help Print this help to stdout and exit | ||
| 1768 | \\ -l, --list-steps Print available steps to stdout and exit | ||
| 1769 | \\ | ||
| 1770 | \\ -p, --prefix [path] Where to install files (default: zig-out) | ||
| 1771 | \\ --prefix-lib-dir [path] Where to install libraries | ||
| 1772 | \\ --prefix-exe-dir [path] Where to install executables | ||
| 1773 | \\ --prefix-include-dir [path] Where to install C header files | ||
| 1774 | \\ --release[=mode] Request release mode, optionally specifying a | ||
| 1775 | \\ preferred optimization mode: fast, safe, small | ||
| 1776 | \\ | ||
| 1777 | \\ --verbose Print commands before executing them | ||
| 1778 | \\ --color [auto|off|on] Enable or disable colored error messages | ||
| 1779 | \\ --error-style [style] Control how build errors are printed | ||
| 1780 | \\ verbose (Default) Report errors with full context | ||
| 1781 | \\ minimal Report errors after summary, excluding context like command lines | ||
| 1782 | \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update | ||
| 1783 | \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update | ||
| 1784 | \\ --multiline-errors [style] Control how multi-line error messages are printed | ||
| 1785 | \\ indent (Default) Indent non-initial lines to align with initial line | ||
| 1786 | \\ newline Include a leading newline so that the error message is on its own lines | ||
| 1787 | \\ none Print as usual so the first line is misaligned | ||
| 1788 | \\ --summary [mode] Control the printing of the build summary | ||
| 1789 | \\ all Print the build summary in its entirety | ||
| 1790 | \\ new Omit cached steps | ||
| 1791 | \\ failures (Default if short-lived) Only print failed steps | ||
| 1792 | \\ line (Default if long-lived) Only print the single-line summary | ||
| 1793 | \\ none Do not print the build summary | ||
| 1794 | \\ -j<N> Limit concurrent jobs (default is to use all CPU cores) | ||
| 1795 | \\ --maxrss <bytes> Limit memory usage (default is to use available memory) | ||
| 1796 | \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss | ||
| 1797 | \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded. | ||
| 1798 | \\ The timeout must include a unit: ns, us, ms, s, m, h | ||
| 1799 | \\ --watch Continuously rebuild when source files are modified | ||
| 1800 | \\ --debounce <ms> Delay before rebuilding after changed file detected | ||
| 1801 | \\ --webui[=ip] Enable the web interface on the given IP address | ||
| 1802 | \\ --fuzz[=limit] Continuously search for unit test failures with an optional | ||
| 1803 | \\ limit to the max number of iterations. The argument supports | ||
| 1804 | \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies | ||
| 1805 | \\ '--webui' when no limit is specified. | ||
| 1806 | \\ --time-report Force full rebuild and provide detailed information on | ||
| 1807 | \\ compilation time of Zig source code (implies '--webui') | ||
| 1808 | \\ -fincremental Enable incremental compilation | ||
| 1809 | \\ -fno-incremental Disable incremental compilation | ||
| 1810 | \\ | ||
| 1811 | \\Package Management Options: | ||
| 1812 | \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit | ||
| 1813 | \\ needed (Default) Lazy dependencies are fetched as needed | ||
| 1814 | \\ all Lazy dependencies are always fetched | ||
| 1815 | \\ --fork=[path] Override one or more projects from dependency tree | ||
| 1816 | \\ | ||
| 1817 | \\Advanced Options: | ||
| 1818 | \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error | ||
| 1819 | \\ -fno-reference-trace Disable reference trace | ||
| 1820 | \\ -fallow-so-scripts Allows .so files to be GNU ld scripts | ||
| 1821 | \\ -fno-allow-so-scripts (default) .so files must be ELF files | ||
| 1822 | \\ --build-file [file] Override path to build.zig | ||
| 1823 | \\ --cache-dir [path] Override path to local Zig cache directory | ||
| 1824 | \\ --global-cache-dir [path] Override path to global Zig cache directory | ||
| 1825 | \\ --zig-lib-dir [arg] Override path to Zig lib directory | ||
| 1826 | \\ --build-runner [file] Override path to build runner | ||
| 1827 | \\ --seed [integer] For shuffling dependency traversal order (default: random) | ||
| 1828 | \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries | ||
| 1829 | \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) | ||
| 1830 | \\ sha1, tree 20-byte cryptographic hash (ELF, WASM) | ||
| 1831 | \\ md5 16-byte cryptographic hash (ELF) | ||
| 1832 | \\ uuid 16-byte random UUID (ELF, WASM) | ||
| 1833 | \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM) | ||
| 1834 | \\ none (default) No build ID | ||
| 1835 | \\ --debug-log [scope] Enable debugging the compiler | ||
| 1836 | \\ --debug-pkg-config Fail if unknown pkg-config flags encountered | ||
| 1837 | \\ --debug-rt Debug compiler runtime libraries | ||
| 1838 | \\ --verbose-link Enable compiler debug output for linking | ||
| 1839 | \\ --verbose-air Enable compiler debug output for Zig AIR | ||
| 1840 | \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR | ||
| 1841 | \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC | ||
| 1842 | \\ --verbose-cimport Enable compiler debug output for C imports | ||
| 1843 | \\ --verbose-cc Enable compiler debug output for C compilation | ||
| 1844 | \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features | ||
| 1845 | \\ | ||
| 1846 | ); | ||
| 1847 | } | ||
| 1848 | }; | ||
lib/compiler/maker/Fuzz.zig deleted-606| ... | @@ -1,606 +0,0 @@ | ||
| 1 | const Fuzz = @This(); | ||
| 2 | |||
| 3 | const std = @import("std"); | ||
| 4 | const Allocator = std.mem.Allocator; | ||
| 5 | const Build = std.Build; | ||
| 6 | const Cache = std.Build.Cache; | ||
| 7 | const Coverage = std.debug.Coverage; | ||
| 8 | const Configuration = std.Build.Configuration; | ||
| 9 | const Io = std.Io; | ||
| 10 | const abi = std.Build.abi.fuzz; | ||
| 11 | const assert = std.debug.assert; | ||
| 12 | const fatal = std.process.fatal; | ||
| 13 | const log = std.log; | ||
| 14 | |||
| 15 | const maker = @import("../maker.zig"); | ||
| 16 | const WebServer = @import("WebServer.zig"); | ||
| 17 | |||
| 18 | gpa: Allocator, | ||
| 19 | io: Io, | ||
| 20 | mode: Mode, | ||
| 21 | |||
| 22 | /// Allocated into `gpa`. | ||
| 23 | run_steps: []const Configuration.Step.Index, | ||
| 24 | |||
| 25 | group: Io.Group, | ||
| 26 | root_prog_node: std.Progress.Node, | ||
| 27 | prog_node: std.Progress.Node, | ||
| 28 | |||
| 29 | /// Protects `coverage_files`. | ||
| 30 | coverage_mutex: Io.Mutex, | ||
| 31 | coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap), | ||
| 32 | |||
| 33 | queue_mutex: Io.Mutex, | ||
| 34 | queue_cond: Io.Condition, | ||
| 35 | msg_queue: std.ArrayList(Msg), | ||
| 36 | |||
| 37 | pub const Mode = union(enum) { | ||
| 38 | forever: struct { ws: *WebServer }, | ||
| 39 | limit: Limited, | ||
| 40 | |||
| 41 | pub const Limited = struct { | ||
| 42 | amount: u64, | ||
| 43 | }; | ||
| 44 | }; | ||
| 45 | |||
| 46 | const Msg = union(enum) { | ||
| 47 | coverage: struct { | ||
| 48 | id: u64, | ||
| 49 | cumulative: struct { | ||
| 50 | runs: u64, | ||
| 51 | unique: u64, | ||
| 52 | coverage: u64, | ||
| 53 | }, | ||
| 54 | run: Configuration.Step.Index, | ||
| 55 | }, | ||
| 56 | entry_point: struct { | ||
| 57 | coverage_id: u64, | ||
| 58 | addr: u64, | ||
| 59 | }, | ||
| 60 | }; | ||
| 61 | |||
| 62 | const CoverageMap = struct { | ||
| 63 | mapped_memory: []align(std.heap.page_size_min) const u8, | ||
| 64 | coverage: Coverage, | ||
| 65 | source_locations: []Coverage.SourceLocation, | ||
| 66 | /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested. | ||
| 67 | entry_points: std.ArrayList(u32), | ||
| 68 | start_timestamp: i64, | ||
| 69 | start_n_runs: u64, | ||
| 70 | |||
| 71 | fn deinit(cm: *CoverageMap, gpa: Allocator) void { | ||
| 72 | std.posix.munmap(cm.mapped_memory); | ||
| 73 | cm.coverage.deinit(gpa); | ||
| 74 | cm.* = undefined; | ||
| 75 | } | ||
| 76 | }; | ||
| 77 | |||
| 78 | pub fn init( | ||
| 79 | gpa: Allocator, | ||
| 80 | io: Io, | ||
| 81 | all_steps: []const Configuration.Step.Index, | ||
| 82 | root_prog_node: std.Progress.Node, | ||
| 83 | mode: Mode, | ||
| 84 | ) error{ OutOfMemory, Canceled }!Fuzz { | ||
| 85 | const run_steps: []const Configuration.Step.Index = steps: { | ||
| 86 | var steps: std.ArrayList(Configuration.Step.Index) = .empty; | ||
| 87 | defer steps.deinit(gpa); | ||
| 88 | const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0); | ||
| 89 | defer rebuild_node.end(); | ||
| 90 | var rebuild_group: Io.Group = .init; | ||
| 91 | defer rebuild_group.cancel(io); | ||
| 92 | |||
| 93 | for (all_steps) |step| { | ||
| 94 | if (true) @panic("TODO"); | ||
| 95 | const run = step.cast(std.Build.Step.Run) orelse continue; | ||
| 96 | if (run.producer == null) continue; | ||
| 97 | if (run.fuzz_tests.items.len == 0) continue; | ||
| 98 | try steps.append(gpa, run); | ||
| 99 | rebuild_group.async(io, rebuildTestsWorkerRun, .{ run, gpa, rebuild_node }); | ||
| 100 | } | ||
| 101 | |||
| 102 | if (steps.items.len == 0) fatal("no fuzz tests found", .{}); | ||
| 103 | rebuild_node.setEstimatedTotalItems(steps.items.len); | ||
| 104 | const run_steps = try gpa.dupe(Configuration.Step.Index, steps.items); | ||
| 105 | try rebuild_group.await(io); | ||
| 106 | break :steps run_steps; | ||
| 107 | }; | ||
| 108 | errdefer gpa.free(run_steps); | ||
| 109 | |||
| 110 | for (run_steps) |run_step_index| { | ||
| 111 | if (true) @panic("TODO"); | ||
| 112 | assert(run_step_index.fuzz_tests.items.len > 0); | ||
| 113 | if (run_step_index.rebuilt_executable == null) | ||
| 114 | fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{}); | ||
| 115 | } | ||
| 116 | |||
| 117 | return .{ | ||
| 118 | .gpa = gpa, | ||
| 119 | .io = io, | ||
| 120 | .mode = mode, | ||
| 121 | .run_steps = run_steps, | ||
| 122 | .group = .init, | ||
| 123 | .root_prog_node = root_prog_node, | ||
| 124 | .prog_node = .none, | ||
| 125 | .coverage_files = .empty, | ||
| 126 | .coverage_mutex = .init, | ||
| 127 | .queue_mutex = .init, | ||
| 128 | .queue_cond = .init, | ||
| 129 | .msg_queue = .empty, | ||
| 130 | }; | ||
| 131 | } | ||
| 132 | |||
| 133 | pub fn start(fuzz: *Fuzz) void { | ||
| 134 | const io = fuzz.io; | ||
| 135 | fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0); | ||
| 136 | |||
| 137 | if (fuzz.mode == .forever) { | ||
| 138 | // For polling messages and sending updates to subscribers. | ||
| 139 | fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err| | ||
| 140 | fatal("unable to spawn coverage task: {t}", .{err}); | ||
| 141 | } | ||
| 142 | |||
| 143 | if (true) @panic("TODO"); | ||
| 144 | |||
| 145 | for (fuzz.run_steps) |run| { | ||
| 146 | assert(run.rebuilt_executable != null); | ||
| 147 | fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run }); | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | pub fn deinit(fuzz: *Fuzz) void { | ||
| 152 | const io = fuzz.io; | ||
| 153 | fuzz.group.cancel(io); | ||
| 154 | fuzz.prog_node.end(); | ||
| 155 | fuzz.gpa.free(fuzz.run_steps); | ||
| 156 | } | ||
| 157 | |||
| 158 | fn rebuildTestsWorkerRun(run: Configuration.Step.Index, gpa: Allocator, parent_prog_node: std.Progress.Node) void { | ||
| 159 | rebuildTestsWorkerRunFallible(run, gpa, parent_prog_node) catch |err| { | ||
| 160 | const compile = run.producer.?; | ||
| 161 | log.err("step '{s}': failed to rebuild in fuzz mode: {t}", .{ compile.step.name, err }); | ||
| 162 | }; | ||
| 163 | } | ||
| 164 | |||
| 165 | fn rebuildTestsWorkerRunFallible(run: Configuration.Step.Index, gpa: Allocator, parent_prog_node: std.Progress.Node) !void { | ||
| 166 | const graph = run.step.owner.graph; | ||
| 167 | const io = graph.io; | ||
| 168 | const compile = run.producer.?; | ||
| 169 | const prog_node = parent_prog_node.start(compile.step.name, 0); | ||
| 170 | defer prog_node.end(); | ||
| 171 | |||
| 172 | const result = compile.rebuildInFuzzMode(gpa, prog_node); | ||
| 173 | |||
| 174 | const show_compile_errors = compile.step.result_error_bundle.errorMessageCount() > 0; | ||
| 175 | const show_error_msgs = compile.step.result_error_msgs.items.len > 0; | ||
| 176 | const show_stderr = compile.step.result_stderr.len > 0; | ||
| 177 | |||
| 178 | if (show_error_msgs or show_compile_errors or show_stderr) { | ||
| 179 | var buf: [256]u8 = undefined; | ||
| 180 | const stderr = try io.lockStderr(&buf, graph.stderr_mode); | ||
| 181 | defer io.unlockStderr(); | ||
| 182 | maker.printErrorMessages(gpa, &compile.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; | ||
| 183 | } | ||
| 184 | |||
| 185 | const rebuilt_bin_path = result catch |err| switch (err) { | ||
| 186 | error.MakeFailed => return, | ||
| 187 | else => |other| return other, | ||
| 188 | }; | ||
| 189 | run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename); | ||
| 190 | } | ||
| 191 | |||
| 192 | fn fuzzWorkerRun(fuzz: *Fuzz, run: Configuration.Step.Index) void { | ||
| 193 | const owner = run.step.owner; | ||
| 194 | const gpa = owner.allocator; | ||
| 195 | const graph = owner.graph; | ||
| 196 | const io = graph.io; | ||
| 197 | |||
| 198 | run.rerunInFuzzMode(fuzz, fuzz.prog_node) catch |err| switch (err) { | ||
| 199 | error.MakeFailed => { | ||
| 200 | var buf: [256]u8 = undefined; | ||
| 201 | const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) { | ||
| 202 | error.Canceled => return, | ||
| 203 | }; | ||
| 204 | defer io.unlockStderr(); | ||
| 205 | maker.printErrorMessages(gpa, &run.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; | ||
| 206 | return; | ||
| 207 | }, | ||
| 208 | else => { | ||
| 209 | log.err("step '{s}': failed to rerun in fuzz mode: {t}", .{ run.step.name, err }); | ||
| 210 | return; | ||
| 211 | }, | ||
| 212 | }; | ||
| 213 | } | ||
| 214 | |||
| 215 | pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void { | ||
| 216 | if (true) @panic("TODO"); | ||
| 217 | assert(fuzz.mode == .forever); | ||
| 218 | |||
| 219 | var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa); | ||
| 220 | defer arena_state.deinit(); | ||
| 221 | const arena = arena_state.allocator(); | ||
| 222 | |||
| 223 | const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false); | ||
| 224 | var dedup_table: DedupTable = .empty; | ||
| 225 | defer dedup_table.deinit(fuzz.gpa); | ||
| 226 | |||
| 227 | for (fuzz.run_steps) |run_step| { | ||
| 228 | const compile_inputs = run_step.producer.?.step.inputs.table; | ||
| 229 | for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| { | ||
| 230 | try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len); | ||
| 231 | for (file_list.items) |sub_path| { | ||
| 232 | if (!std.mem.endsWith(u8, sub_path, ".zig")) continue; | ||
| 233 | const joined_path = try dir_path.join(arena, sub_path); | ||
| 234 | dedup_table.putAssumeCapacity(joined_path, {}); | ||
| 235 | } | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | const deduped_paths = dedup_table.keys(); | ||
| 240 | const SortContext = struct { | ||
| 241 | pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool { | ||
| 242 | _ = this; | ||
| 243 | return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) { | ||
| 244 | .lt => true, | ||
| 245 | .gt => false, | ||
| 246 | .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path), | ||
| 247 | }; | ||
| 248 | } | ||
| 249 | }; | ||
| 250 | std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan); | ||
| 251 | return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths); | ||
| 252 | } | ||
| 253 | |||
| 254 | pub const Previous = struct { | ||
| 255 | unique_runs: usize, | ||
| 256 | entry_points: usize, | ||
| 257 | sent_source_index: bool, | ||
| 258 | pub const init: Previous = .{ | ||
| 259 | .unique_runs = 0, | ||
| 260 | .entry_points = 0, | ||
| 261 | .sent_source_index = false, | ||
| 262 | }; | ||
| 263 | }; | ||
| 264 | pub fn sendUpdate( | ||
| 265 | fuzz: *Fuzz, | ||
| 266 | socket: *std.http.Server.WebSocket, | ||
| 267 | prev: *Previous, | ||
| 268 | ) !void { | ||
| 269 | const io = fuzz.io; | ||
| 270 | |||
| 271 | try fuzz.coverage_mutex.lock(io); | ||
| 272 | defer fuzz.coverage_mutex.unlock(io); | ||
| 273 | |||
| 274 | const coverage_maps = fuzz.coverage_files.values(); | ||
| 275 | if (coverage_maps.len == 0) return; | ||
| 276 | // TODO: handle multiple fuzz steps in the WebSocket packets | ||
| 277 | const coverage_map = &coverage_maps[0]; | ||
| 278 | const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); | ||
| 279 | // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the | ||
| 280 | // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the | ||
| 281 | // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass | ||
| 282 | // this data straight to the socket with sendfile... | ||
| 283 | const seen_pcs = cov_header.seenBits(); | ||
| 284 | const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic); | ||
| 285 | const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic); | ||
| 286 | { | ||
| 287 | if (!prev.sent_source_index) { | ||
| 288 | prev.sent_source_index = true; | ||
| 289 | // We need to send initial context. | ||
| 290 | const header: abi.SourceIndexHeader = .{ | ||
| 291 | .directories_len = @intCast(coverage_map.coverage.directories.entries.len), | ||
| 292 | .files_len = @intCast(coverage_map.coverage.files.entries.len), | ||
| 293 | .source_locations_len = @intCast(coverage_map.source_locations.len), | ||
| 294 | .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len), | ||
| 295 | .start_timestamp = coverage_map.start_timestamp, | ||
| 296 | .start_n_runs = coverage_map.start_n_runs, | ||
| 297 | }; | ||
| 298 | var iovecs: [5][]const u8 = .{ | ||
| 299 | @ptrCast(&header), | ||
| 300 | @ptrCast(coverage_map.coverage.directories.keys()), | ||
| 301 | @ptrCast(coverage_map.coverage.files.keys()), | ||
| 302 | @ptrCast(coverage_map.source_locations), | ||
| 303 | coverage_map.coverage.string_bytes.items, | ||
| 304 | }; | ||
| 305 | try socket.writeMessageVec(&iovecs, .binary); | ||
| 306 | } | ||
| 307 | |||
| 308 | const header: abi.CoverageUpdateHeader = .{ | ||
| 309 | .n_runs = n_runs, | ||
| 310 | .unique_runs = unique_runs, | ||
| 311 | }; | ||
| 312 | var iovecs: [2][]const u8 = .{ | ||
| 313 | @ptrCast(&header), | ||
| 314 | @ptrCast(seen_pcs), | ||
| 315 | }; | ||
| 316 | try socket.writeMessageVec(&iovecs, .binary); | ||
| 317 | |||
| 318 | prev.unique_runs = unique_runs; | ||
| 319 | } | ||
| 320 | |||
| 321 | if (prev.entry_points != coverage_map.entry_points.items.len) { | ||
| 322 | const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len)); | ||
| 323 | var iovecs: [2][]const u8 = .{ | ||
| 324 | @ptrCast(&header), | ||
| 325 | @ptrCast(coverage_map.entry_points.items), | ||
| 326 | }; | ||
| 327 | try socket.writeMessageVec(&iovecs, .binary); | ||
| 328 | |||
| 329 | prev.entry_points = coverage_map.entry_points.items.len; | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | fn coverageRun(fuzz: *Fuzz) void { | ||
| 334 | coverageRunCancelable(fuzz) catch |err| switch (err) { | ||
| 335 | error.Canceled => return, | ||
| 336 | }; | ||
| 337 | } | ||
| 338 | |||
| 339 | fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void { | ||
| 340 | const io = fuzz.io; | ||
| 341 | |||
| 342 | try fuzz.queue_mutex.lock(io); | ||
| 343 | defer fuzz.queue_mutex.unlock(io); | ||
| 344 | |||
| 345 | while (true) { | ||
| 346 | try fuzz.queue_cond.wait(io, &fuzz.queue_mutex); | ||
| 347 | for (fuzz.msg_queue.items) |msg| switch (msg) { | ||
| 348 | .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) { | ||
| 349 | error.AlreadyReported => continue, | ||
| 350 | error.Canceled => return, | ||
| 351 | else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), | ||
| 352 | }, | ||
| 353 | .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) { | ||
| 354 | error.AlreadyReported => continue, | ||
| 355 | error.Canceled => return, | ||
| 356 | else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), | ||
| 357 | }, | ||
| 358 | }; | ||
| 359 | fuzz.msg_queue.clearRetainingCapacity(); | ||
| 360 | } | ||
| 361 | } | ||
| 362 | fn prepareTables(fuzz: *Fuzz, run_step_index: Configuration.Step.Index, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void { | ||
| 363 | if (true) @panic("TODO"); | ||
| 364 | assert(fuzz.mode == .forever); | ||
| 365 | const ws = fuzz.mode.forever.ws; | ||
| 366 | const gpa = fuzz.gpa; | ||
| 367 | const io = fuzz.io; | ||
| 368 | |||
| 369 | try fuzz.coverage_mutex.lock(io); | ||
| 370 | defer fuzz.coverage_mutex.unlock(io); | ||
| 371 | |||
| 372 | const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id); | ||
| 373 | if (gop.found_existing) { | ||
| 374 | // We are fuzzing the same executable with multiple threads. | ||
| 375 | // Perhaps the same unit test; perhaps a different one. In any | ||
| 376 | // case, since the coverage file is the same, we only have to | ||
| 377 | // notice changes to that one file in order to learn coverage for | ||
| 378 | // this particular executable. | ||
| 379 | return; | ||
| 380 | } | ||
| 381 | errdefer _ = fuzz.coverage_files.pop(); | ||
| 382 | |||
| 383 | gop.value_ptr.* = .{ | ||
| 384 | .coverage = std.debug.Coverage.init, | ||
| 385 | .mapped_memory = undefined, // populated below | ||
| 386 | .source_locations = undefined, // populated below | ||
| 387 | .entry_points = .empty, | ||
| 388 | .start_timestamp = ws.now(), | ||
| 389 | .start_n_runs = undefined, // populated below | ||
| 390 | }; | ||
| 391 | errdefer gop.value_ptr.coverage.deinit(gpa); | ||
| 392 | |||
| 393 | const rebuilt_exe_path = run_step_index.rebuilt_executable.?; | ||
| 394 | const target = run_step_index.producer.?.rootModuleTarget(); | ||
| 395 | var debug_info = std.debug.Info.load( | ||
| 396 | gpa, | ||
| 397 | io, | ||
| 398 | rebuilt_exe_path, | ||
| 399 | &gop.value_ptr.coverage, | ||
| 400 | target.ofmt, | ||
| 401 | target.cpu.arch, | ||
| 402 | ) catch |err| { | ||
| 403 | log.err("step '{s}': failed to load debug information for '{f}': {t}", .{ | ||
| 404 | run_step_index.step.name, rebuilt_exe_path, err, | ||
| 405 | }); | ||
| 406 | return error.AlreadyReported; | ||
| 407 | }; | ||
| 408 | defer debug_info.deinit(gpa); | ||
| 409 | |||
| 410 | const coverage_file_path: Build.Cache.Path = .{ | ||
| 411 | .root_dir = run_step_index.step.owner.cache_root, | ||
| 412 | .sub_path = "v/" ++ std.fmt.hex(coverage_id), | ||
| 413 | }; | ||
| 414 | var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { | ||
| 415 | log.err("step '{s}': failed to load coverage file '{f}': {t}", .{ | ||
| 416 | run_step_index.step.name, coverage_file_path, err, | ||
| 417 | }); | ||
| 418 | return error.AlreadyReported; | ||
| 419 | }; | ||
| 420 | defer coverage_file.close(io); | ||
| 421 | |||
| 422 | const file_size = coverage_file.length(io) catch |err| { | ||
| 423 | log.err("unable to check len of coverage file '{f}': {t}", .{ coverage_file_path, err }); | ||
| 424 | return error.AlreadyReported; | ||
| 425 | }; | ||
| 426 | |||
| 427 | const mapped_memory = std.posix.mmap( | ||
| 428 | null, | ||
| 429 | file_size, | ||
| 430 | .{ .READ = true }, | ||
| 431 | .{ .TYPE = .SHARED }, | ||
| 432 | coverage_file.handle, | ||
| 433 | 0, | ||
| 434 | ) catch |err| { | ||
| 435 | log.err("failed to map coverage file '{f}': {t}", .{ coverage_file_path, err }); | ||
| 436 | return error.AlreadyReported; | ||
| 437 | }; | ||
| 438 | gop.value_ptr.mapped_memory = mapped_memory; | ||
| 439 | |||
| 440 | const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); | ||
| 441 | const pcs = header.pcAddrs(); | ||
| 442 | const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len); | ||
| 443 | errdefer gpa.free(source_locations); | ||
| 444 | |||
| 445 | // Unfortunately the PCs array that LLVM gives us from the 8-bit PC | ||
| 446 | // counters feature is not sorted. | ||
| 447 | var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty; | ||
| 448 | defer sorted_pcs.deinit(gpa); | ||
| 449 | try sorted_pcs.resize(gpa, pcs.len); | ||
| 450 | @memcpy(sorted_pcs.items(.pc), pcs); | ||
| 451 | for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i); | ||
| 452 | sorted_pcs.sortUnstable(struct { | ||
| 453 | addrs: []const u64, | ||
| 454 | |||
| 455 | pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { | ||
| 456 | return ctx.addrs[a_index] < ctx.addrs[b_index]; | ||
| 457 | } | ||
| 458 | }{ .addrs = sorted_pcs.items(.pc) }); | ||
| 459 | |||
| 460 | debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| { | ||
| 461 | log.err("failed to resolve addresses to source locations: {t}", .{err}); | ||
| 462 | return error.AlreadyReported; | ||
| 463 | }; | ||
| 464 | |||
| 465 | for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl; | ||
| 466 | gop.value_ptr.source_locations = source_locations; | ||
| 467 | gop.value_ptr.start_n_runs = header.n_runs; | ||
| 468 | |||
| 469 | ws.notifyUpdate(); | ||
| 470 | } | ||
| 471 | |||
| 472 | fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void { | ||
| 473 | const io = fuzz.io; | ||
| 474 | |||
| 475 | try fuzz.coverage_mutex.lock(io); | ||
| 476 | defer fuzz.coverage_mutex.unlock(io); | ||
| 477 | |||
| 478 | const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?; | ||
| 479 | const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); | ||
| 480 | const pcs = header.pcAddrs(); | ||
| 481 | |||
| 482 | // Since this pcs list is unsorted, we must linear scan for the best index. | ||
| 483 | const index = i: { | ||
| 484 | var best: usize = 0; | ||
| 485 | for (pcs[1..], 1..) |elem_addr, i| { | ||
| 486 | if (elem_addr == addr) break :i i; | ||
| 487 | if (elem_addr > addr) continue; | ||
| 488 | if (elem_addr > pcs[best]) best = i; | ||
| 489 | } | ||
| 490 | break :i best; | ||
| 491 | }; | ||
| 492 | if (index >= pcs.len) { | ||
| 493 | log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{ | ||
| 494 | addr, pcs[0], pcs[pcs.len - 1], | ||
| 495 | }); | ||
| 496 | return error.AlreadyReported; | ||
| 497 | } | ||
| 498 | if (false) { | ||
| 499 | const sl = coverage_map.source_locations[index]; | ||
| 500 | const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename); | ||
| 501 | if (pcs.len == 1) { | ||
| 502 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{ | ||
| 503 | addr, file_name, sl.line, sl.column, | ||
| 504 | }); | ||
| 505 | } else if (index == 0) { | ||
| 506 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{ | ||
| 507 | addr, file_name, sl.line, sl.column, pcs[index + 1], | ||
| 508 | }); | ||
| 509 | } else if (index == pcs.len - 1) { | ||
| 510 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{ | ||
| 511 | addr, file_name, sl.line, sl.column, index, pcs[index - 1], | ||
| 512 | }); | ||
| 513 | } else { | ||
| 514 | log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{ | ||
| 515 | addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1], | ||
| 516 | }); | ||
| 517 | } | ||
| 518 | } | ||
| 519 | try coverage_map.entry_points.append(fuzz.gpa, @intCast(index)); | ||
| 520 | } | ||
| 521 | |||
| 522 | pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void { | ||
| 523 | if (true) @panic("TODO"); | ||
| 524 | assert(fuzz.mode == .limit); | ||
| 525 | const io = fuzz.io; | ||
| 526 | |||
| 527 | try fuzz.group.await(io); | ||
| 528 | fuzz.group = .init; | ||
| 529 | |||
| 530 | std.debug.print("======= FUZZING REPORT =======\n", .{}); | ||
| 531 | for (fuzz.msg_queue.items) |msg| { | ||
| 532 | if (msg != .coverage) continue; | ||
| 533 | |||
| 534 | const cov = msg.coverage; | ||
| 535 | const coverage_file_path: std.Build.Cache.Path = .{ | ||
| 536 | .root_dir = cov.run.step.owner.cache_root, | ||
| 537 | .sub_path = "v/" ++ std.fmt.hex(cov.id), | ||
| 538 | }; | ||
| 539 | var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { | ||
| 540 | fatal("step '{s}': failed to load coverage file '{f}': {t}", .{ | ||
| 541 | cov.run.step.name, coverage_file_path, err, | ||
| 542 | }); | ||
| 543 | }; | ||
| 544 | defer coverage_file.close(io); | ||
| 545 | |||
| 546 | const fuzz_abi = std.Build.abi.fuzz; | ||
| 547 | var rbuf: [0x1000]u8 = undefined; | ||
| 548 | var r = coverage_file.reader(io, &rbuf); | ||
| 549 | |||
| 550 | var header: fuzz_abi.SeenPcsHeader = undefined; | ||
| 551 | r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { | ||
| 552 | fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{ | ||
| 553 | cov.run.step.name, coverage_file_path, err, | ||
| 554 | }); | ||
| 555 | }; | ||
| 556 | |||
| 557 | if (header.pcs_len == 0) { | ||
| 558 | fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{ | ||
| 559 | cov.run.step.name, coverage_file_path, | ||
| 560 | }); | ||
| 561 | } | ||
| 562 | |||
| 563 | var seen_count: usize = 0; | ||
| 564 | const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len); | ||
| 565 | for (0..chunk_count) |_| { | ||
| 566 | const seen = r.interface.takeInt(usize, .little) catch |err| { | ||
| 567 | fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{ | ||
| 568 | cov.run.step.name, coverage_file_path, err, | ||
| 569 | }); | ||
| 570 | }; | ||
| 571 | seen_count += @popCount(seen); | ||
| 572 | } | ||
| 573 | |||
| 574 | const seen_f: f64 = @floatFromInt(seen_count); | ||
| 575 | const total_f: f64 = @floatFromInt(header.pcs_len); | ||
| 576 | const ratio = seen_f / total_f; | ||
| 577 | std.debug.print( | ||
| 578 | \\Step: {s} | ||
| 579 | \\Fuzz test: "{s}" ({x}) | ||
| 580 | \\Runs: {} -> {} | ||
| 581 | \\Unique runs: {} -> {} | ||
| 582 | \\Coverage: {}/{} -> {}/{} ({:.02}%) | ||
| 583 | \\ | ||
| 584 | , .{ | ||
| 585 | cov.run.step.name, | ||
| 586 | cov.run.fuzz_tests.items[0], | ||
| 587 | cov.id, | ||
| 588 | cov.cumulative.runs, | ||
| 589 | header.n_runs, | ||
| 590 | cov.cumulative.unique, | ||
| 591 | header.unique_runs, | ||
| 592 | cov.cumulative.coverage, | ||
| 593 | header.pcs_len, | ||
| 594 | seen_count, | ||
| 595 | header.pcs_len, | ||
| 596 | ratio * 100, | ||
| 597 | }); | ||
| 598 | |||
| 599 | std.debug.print("------------------------------\n", .{}); | ||
| 600 | } | ||
| 601 | std.debug.print( | ||
| 602 | \\Values are accumulated across multiple runs when preserving the cache. | ||
| 603 | \\============================== | ||
| 604 | \\ | ||
| 605 | , .{}); | ||
| 606 | } | ||
lib/compiler/maker/Graph.zig deleted-25| ... | @@ -1,25 +0,0 @@ | ||
| 1 | //! Shared maker state among all steps. | ||
| 2 | const Graph = @This(); | ||
| 3 | |||
| 4 | const std = @import("std"); | ||
| 5 | const Io = std.Io; | ||
| 6 | const Allocator = std.mem.Allocator; | ||
| 7 | const Configuration = std.Build.Configuration; | ||
| 8 | |||
| 9 | io: Io, | ||
| 10 | /// Process lifetime. | ||
| 11 | arena: Allocator, | ||
| 12 | cache: std.Build.Cache, | ||
| 13 | zig_exe: []const u8, | ||
| 14 | environ_map: std.process.Environ.Map, | ||
| 15 | global_cache_root: std.Build.Cache.Directory, | ||
| 16 | zig_lib_directory: std.Build.Cache.Directory, | ||
| 17 | |||
| 18 | debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null, | ||
| 19 | incremental: ?bool = null, | ||
| 20 | random_seed: u32 = 0, | ||
| 21 | allow_so_scripts: ?bool = null, | ||
| 22 | time_report: bool = false, | ||
| 23 | /// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also | ||
| 24 | /// respects the '--color' flag. | ||
| 25 | stderr_mode: ?Io.Terminal.Mode = null, | ||
lib/compiler/maker/Step.zig deleted-840| ... | @@ -1,840 +0,0 @@ | ||
| 1 | //! The state that maker needs in order to process a step. | ||
| 2 | const Step = @This(); | ||
| 3 | |||
| 4 | const builtin = @import("builtin"); | ||
| 5 | |||
| 6 | const std = @import("std"); | ||
| 7 | const Allocator = std.mem.Allocator; | ||
| 8 | const Cache = std.Build.Cache; | ||
| 9 | const Io = std.Io; | ||
| 10 | const LazyPath = std.Build.Configuration.LazyPath; | ||
| 11 | const Package = std.Build.Configuration.Package; | ||
| 12 | const Path = std.Build.Cache.Path; | ||
| 13 | const Configuration = std.Build.Configuration; | ||
| 14 | const assert = std.debug.assert; | ||
| 15 | |||
| 16 | const WebServer = @import("WebServer.zig"); | ||
| 17 | |||
| 18 | pub const Compile = void; // @import("Step/Compile.zig"); | ||
| 19 | pub const Run = void; // @import("Step/Run.zig"); | ||
| 20 | |||
| 21 | /// Avoid false sharing. | ||
| 22 | _: void align(std.atomic.cache_line) = {}, | ||
| 23 | |||
| 24 | state: State = .precheck_unstarted, | ||
| 25 | dependants: std.ArrayList(Configuration.Step.Index) = .empty, | ||
| 26 | /// Collects the set of files that retrigger this step to run. | ||
| 27 | /// | ||
| 28 | /// This is used by the build system's implementation of `--watch` but it can | ||
| 29 | /// also be potentially useful for IDEs to know what effects editing a | ||
| 30 | /// particular file has. | ||
| 31 | /// | ||
| 32 | /// Populated within `make`. Implementation may choose to clear and repopulate, | ||
| 33 | /// retain previous value, or update. | ||
| 34 | inputs: Inputs = .init, | ||
| 35 | pending_deps: u32 = undefined, | ||
| 36 | |||
| 37 | result_error_msgs: std.ArrayList([]const u8) = .empty, | ||
| 38 | result_error_bundle: std.zig.ErrorBundle = .empty, | ||
| 39 | result_stderr: []const u8 = "", | ||
| 40 | result_cached: bool = false, | ||
| 41 | result_duration_ns: ?u64 = null, | ||
| 42 | /// 0 means unavailable or not reported. | ||
| 43 | result_peak_rss: usize = 0, | ||
| 44 | /// If the step is failed and this field is populated, this is the command which failed. | ||
| 45 | /// This field may be populated even if the step succeeded. | ||
| 46 | result_failed_command: ?[]const u8 = null, | ||
| 47 | test_results: TestResults = .{}, | ||
| 48 | |||
| 49 | pub const State = enum { | ||
| 50 | precheck_unstarted, | ||
| 51 | precheck_started, | ||
| 52 | /// This is also used to indicate "dirty" steps that have been modified | ||
| 53 | /// after a previous build completed, in which case, the step may or may | ||
| 54 | /// not have been completed before. Either way, one or more of its direct | ||
| 55 | /// file system inputs have been modified, meaning that the step needs to | ||
| 56 | /// be re-evaluated. | ||
| 57 | precheck_done, | ||
| 58 | dependency_failure, | ||
| 59 | success, | ||
| 60 | failure, | ||
| 61 | /// This state indicates that the step did not complete, however, it also did not fail, | ||
| 62 | /// and it is safe to continue executing its dependencies. | ||
| 63 | skipped, | ||
| 64 | /// This step was skipped because it specified a max_rss that exceeded the runner's maximum. | ||
| 65 | /// It is not safe to run its dependencies. | ||
| 66 | skipped_oom, | ||
| 67 | }; | ||
| 68 | |||
| 69 | pub const Inputs = struct { | ||
| 70 | table: Table, | ||
| 71 | |||
| 72 | pub const init: Inputs = .{ | ||
| 73 | .table = .{}, | ||
| 74 | }; | ||
| 75 | |||
| 76 | pub const Table = std.ArrayHashMapUnmanaged(Cache.Path, Files, Cache.Path.TableAdapter, false); | ||
| 77 | /// The special file name "." means any changes inside the directory. | ||
| 78 | pub const Files = std.ArrayList([]const u8); | ||
| 79 | |||
| 80 | pub fn populated(inputs: *Inputs) bool { | ||
| 81 | return inputs.table.count() != 0; | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn clear(inputs: *Inputs, gpa: Allocator) void { | ||
| 85 | for (inputs.table.values()) |*files| files.deinit(gpa); | ||
| 86 | inputs.table.clearRetainingCapacity(); | ||
| 87 | } | ||
| 88 | }; | ||
| 89 | |||
| 90 | pub const TestResults = struct { | ||
| 91 | /// The total number of tests in the step. Every test has a "status" from the following: | ||
| 92 | /// * passed | ||
| 93 | /// * skipped | ||
| 94 | /// * failed cleanly | ||
| 95 | /// * crashed | ||
| 96 | /// * timed out | ||
| 97 | test_count: u32 = 0, | ||
| 98 | |||
| 99 | /// The number of tests which were skipped (`error.SkipZigTest`). | ||
| 100 | skip_count: u32 = 0, | ||
| 101 | /// The number of tests which failed cleanly. | ||
| 102 | fail_count: u32 = 0, | ||
| 103 | /// The number of tests which terminated unexpectedly, i.e. crashed. | ||
| 104 | crash_count: u32 = 0, | ||
| 105 | /// The number of tests which timed out. | ||
| 106 | timeout_count: u32 = 0, | ||
| 107 | |||
| 108 | /// The number of detected memory leaks. The associated test may still have passed; indeed, *all* | ||
| 109 | /// individual tests may have passed. However, the step as a whole fails if any test has leaks. | ||
| 110 | leak_count: u32 = 0, | ||
| 111 | /// The number of detected error logs. The associated test may still have passed; indeed, *all* | ||
| 112 | /// individual tests may have passed. However, the step as a whole fails if any test logs errors. | ||
| 113 | log_err_count: u32 = 0, | ||
| 114 | |||
| 115 | pub fn isSuccess(tr: TestResults) bool { | ||
| 116 | // all steps are success or skip | ||
| 117 | return tr.fail_count == 0 and | ||
| 118 | tr.crash_count == 0 and | ||
| 119 | tr.timeout_count == 0 and | ||
| 120 | // no (otherwise successful) step leaked memory or logged errors | ||
| 121 | tr.leak_count == 0 and | ||
| 122 | tr.log_err_count == 0; | ||
| 123 | } | ||
| 124 | |||
| 125 | /// Computes the number of tests which passed from the other values. | ||
| 126 | pub fn passCount(tr: TestResults) u32 { | ||
| 127 | return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count; | ||
| 128 | } | ||
| 129 | }; | ||
| 130 | |||
| 131 | pub const MakeOptions = struct { | ||
| 132 | progress_node: std.Progress.Node, | ||
| 133 | watch: bool, | ||
| 134 | web_server: ?*WebServer, | ||
| 135 | /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds. | ||
| 136 | unit_test_timeout_ns: ?u64, | ||
| 137 | /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`. | ||
| 138 | gpa: Allocator, | ||
| 139 | }; | ||
| 140 | |||
| 141 | pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void; | ||
| 142 | |||
| 143 | /// If the Step's `make` function reports `error.MakeFailed`, it indicates they | ||
| 144 | /// have already reported the error. Otherwise, we add a simple error report | ||
| 145 | /// here. | ||
| 146 | pub fn make(s: *Step, options: MakeOptions) error{ MakeFailed, MakeSkipped }!void { | ||
| 147 | if (true) @panic("TODO Step.make"); | ||
| 148 | const arena = s.owner.allocator; | ||
| 149 | const graph = s.owner.graph; | ||
| 150 | const io = graph.io; | ||
| 151 | |||
| 152 | var start_ts: ?Io.Timestamp = t: { | ||
| 153 | if (!graph.time_report) break :t null; | ||
| 154 | if (s.id == .compile) break :t null; | ||
| 155 | if (s.id == .run and s.cast(Run).?.stdio == .zig_test) break :t null; | ||
| 156 | break :t Io.Clock.awake.now(io); | ||
| 157 | }; | ||
| 158 | const make_result = s.makeFn(s, options); | ||
| 159 | if (start_ts) |*ts| { | ||
| 160 | const duration = ts.untilNow(io, .awake); | ||
| 161 | options.web_server.?.updateTimeReportGeneric(s, duration); | ||
| 162 | } | ||
| 163 | |||
| 164 | make_result catch |err| switch (err) { | ||
| 165 | error.MakeFailed, error.MakeSkipped => |e| return e, | ||
| 166 | else => { | ||
| 167 | s.result_error_msgs.append(arena, @errorName(err)) catch @panic("OOM"); | ||
| 168 | return error.MakeFailed; | ||
| 169 | }, | ||
| 170 | }; | ||
| 171 | |||
| 172 | if (!s.test_results.isSuccess()) { | ||
| 173 | return error.MakeFailed; | ||
| 174 | } | ||
| 175 | |||
| 176 | if (s.max_rss != 0 and s.result_peak_rss > s.max_rss) { | ||
| 177 | const msg = std.fmt.allocPrint(arena, "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", .{ | ||
| 178 | s.result_peak_rss, s.max_rss, | ||
| 179 | }) catch @panic("OOM"); | ||
| 180 | s.result_error_msgs.append(arena, msg) catch @panic("OOM"); | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | /// Implementation detail of file watching. Prepares the step for being re-evaluated. | ||
| 185 | /// Returns `true` if the step was newly invalidated, `false` if it was already invalidated. | ||
| 186 | pub fn invalidateResult(step: *Step, gpa: Allocator) bool { | ||
| 187 | if (true) @panic("TODO Step.invalidateResult"); | ||
| 188 | if (step.state == .precheck_done) return false; | ||
| 189 | assert(step.pending_deps == 0); | ||
| 190 | step.state = .precheck_done; | ||
| 191 | step.reset(gpa); | ||
| 192 | for (step.dependants.items) |dependant| { | ||
| 193 | _ = dependant.invalidateResult(gpa); | ||
| 194 | dependant.pending_deps += 1; | ||
| 195 | } | ||
| 196 | return true; | ||
| 197 | } | ||
| 198 | |||
| 199 | /// Implementation detail of file watching and forced rebuilds. Prepares the step for being re-evaluated. | ||
| 200 | pub fn reset(step: *Step, gpa: Allocator) void { | ||
| 201 | assert(step.state == .precheck_done); | ||
| 202 | |||
| 203 | if (step.result_failed_command) |cmd| gpa.free(cmd); | ||
| 204 | |||
| 205 | step.result_error_msgs.clearRetainingCapacity(); | ||
| 206 | step.result_stderr = ""; | ||
| 207 | step.result_cached = false; | ||
| 208 | step.result_duration_ns = null; | ||
| 209 | step.result_peak_rss = 0; | ||
| 210 | step.result_failed_command = null; | ||
| 211 | step.test_results = .{}; | ||
| 212 | step.clearWatchInputs(); | ||
| 213 | |||
| 214 | step.result_error_bundle.deinit(gpa); | ||
| 215 | step.result_error_bundle = std.zig.ErrorBundle.empty; | ||
| 216 | } | ||
| 217 | |||
| 218 | /// Populates `s.result_failed_command`. | ||
| 219 | pub fn captureChildProcess( | ||
| 220 | s: *Step, | ||
| 221 | gpa: Allocator, | ||
| 222 | progress_node: std.Progress.Node, | ||
| 223 | argv: []const []const u8, | ||
| 224 | ) !std.process.RunResult { | ||
| 225 | const graph = s.owner.graph; | ||
| 226 | const arena = graph.arena; | ||
| 227 | const io = graph.io; | ||
| 228 | |||
| 229 | // If an error occurs, it's happened in this command: | ||
| 230 | assert(s.result_failed_command == null); | ||
| 231 | s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv); | ||
| 232 | |||
| 233 | try handleChildProcUnsupported(s); | ||
| 234 | try handleVerbose(s, .inherit, argv); | ||
| 235 | |||
| 236 | const result = std.process.run(arena, io, .{ | ||
| 237 | .argv = argv, | ||
| 238 | .environ_map = &graph.environ_map, | ||
| 239 | .progress_node = progress_node, | ||
| 240 | }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err }); | ||
| 241 | |||
| 242 | if (result.stderr.len > 0) { | ||
| 243 | try s.result_error_msgs.append(arena, result.stderr); | ||
| 244 | } | ||
| 245 | |||
| 246 | return result; | ||
| 247 | } | ||
| 248 | |||
| 249 | pub fn fail(step: *Step, comptime fmt: []const u8, args: anytype) error{ OutOfMemory, MakeFailed } { | ||
| 250 | try step.addError(fmt, args); | ||
| 251 | return error.MakeFailed; | ||
| 252 | } | ||
| 253 | |||
| 254 | pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { | ||
| 255 | const arena = step.owner.allocator; | ||
| 256 | const msg = try std.fmt.allocPrint(arena, fmt, args); | ||
| 257 | try step.result_error_msgs.append(arena, msg); | ||
| 258 | } | ||
| 259 | |||
| 260 | pub const ZigProcess = struct { | ||
| 261 | child: std.process.Child, | ||
| 262 | multi_reader_buffer: Io.File.MultiReader.Buffer(2), | ||
| 263 | multi_reader: Io.File.MultiReader, | ||
| 264 | progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn, | ||
| 265 | |||
| 266 | pub const StreamEnum = enum { stdout, stderr }; | ||
| 267 | |||
| 268 | pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void { | ||
| 269 | zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null; | ||
| 270 | } | ||
| 271 | |||
| 272 | pub fn deinit(zp: *ZigProcess, io: Io) void { | ||
| 273 | zp.child.kill(io); | ||
| 274 | zp.multi_reader.deinit(); | ||
| 275 | zp.* = undefined; | ||
| 276 | } | ||
| 277 | }; | ||
| 278 | |||
| 279 | /// Assumes that argv contains `--listen=-` and that the process being spawned | ||
| 280 | /// is the zig compiler - the same version that compiled the build runner. | ||
| 281 | /// Populates `s.result_failed_command`. | ||
| 282 | pub fn evalZigProcess( | ||
| 283 | s: *Step, | ||
| 284 | argv: []const []const u8, | ||
| 285 | prog_node: std.Progress.Node, | ||
| 286 | watch: bool, | ||
| 287 | web_server: ?*WebServer, | ||
| 288 | gpa: Allocator, | ||
| 289 | ) !?Cache.Path { | ||
| 290 | const b = s.owner; | ||
| 291 | const io = b.graph.io; | ||
| 292 | |||
| 293 | // If an error occurs, it's happened in this command: | ||
| 294 | assert(s.result_failed_command == null); | ||
| 295 | s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv); | ||
| 296 | |||
| 297 | if (s.getZigProcess()) |zp| update: { | ||
| 298 | assert(watch); | ||
| 299 | if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index); | ||
| 300 | zp.progress_ipc_index = null; | ||
| 301 | var exited = false; | ||
| 302 | defer if (exited) { | ||
| 303 | s.cast(Compile).?.zig_process = null; | ||
| 304 | zp.deinit(io); | ||
| 305 | gpa.destroy(zp); | ||
| 306 | } else zp.saveState(prog_node); | ||
| 307 | const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) { | ||
| 308 | error.BrokenPipe, error.EndOfStream => |reason| { | ||
| 309 | std.log.info("{s} restart required: {t}", .{ argv[0], reason }); | ||
| 310 | // Process restart required. | ||
| 311 | const term = zp.child.wait(io) catch |e| { | ||
| 312 | return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); | ||
| 313 | }; | ||
| 314 | _ = term; | ||
| 315 | exited = true; | ||
| 316 | break :update; | ||
| 317 | }, | ||
| 318 | else => |e| return e, | ||
| 319 | }; | ||
| 320 | |||
| 321 | if (s.result_error_bundle.errorMessageCount() > 0) { | ||
| 322 | return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); | ||
| 323 | } | ||
| 324 | |||
| 325 | if (s.result_error_msgs.items.len > 0 and result == null) { | ||
| 326 | // Crash detected. | ||
| 327 | const term = zp.child.wait(io) catch |e| { | ||
| 328 | return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); | ||
| 329 | }; | ||
| 330 | s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 331 | exited = true; | ||
| 332 | try handleChildProcessTerm(s, term); | ||
| 333 | return error.MakeFailed; | ||
| 334 | } | ||
| 335 | |||
| 336 | return result; | ||
| 337 | } | ||
| 338 | assert(argv.len != 0); | ||
| 339 | |||
| 340 | try handleChildProcUnsupported(s); | ||
| 341 | try handleVerbose(s, .inherit, argv); | ||
| 342 | |||
| 343 | const zp = try gpa.create(ZigProcess); | ||
| 344 | defer if (!watch) gpa.destroy(zp); | ||
| 345 | |||
| 346 | zp.child = std.process.spawn(io, .{ | ||
| 347 | .argv = argv, | ||
| 348 | .environ_map = &b.graph.environ_map, | ||
| 349 | .stdin = .pipe, | ||
| 350 | .stdout = .pipe, | ||
| 351 | .stderr = .pipe, | ||
| 352 | .request_resource_usage_statistics = true, | ||
| 353 | .progress_node = prog_node, | ||
| 354 | }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); | ||
| 355 | |||
| 356 | zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{ | ||
| 357 | zp.child.stdout.?, zp.child.stderr.?, | ||
| 358 | }); | ||
| 359 | if (watch) s.cast(Compile).?.zig_process = zp; | ||
| 360 | defer if (!watch) zp.deinit(io); | ||
| 361 | |||
| 362 | const result = result: { | ||
| 363 | defer if (watch) zp.saveState(prog_node); | ||
| 364 | break :result try zigProcessUpdate(s, zp, watch, web_server, gpa); | ||
| 365 | }; | ||
| 366 | |||
| 367 | if (!watch) { | ||
| 368 | // Send EOF to stdin. | ||
| 369 | zp.child.stdin.?.close(io); | ||
| 370 | zp.child.stdin = null; | ||
| 371 | |||
| 372 | const term = zp.child.wait(io) catch |err| { | ||
| 373 | return s.fail("unable to wait for {s}: {t}", .{ argv[0], err }); | ||
| 374 | }; | ||
| 375 | s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 376 | |||
| 377 | // Special handling for Compile step that is expecting compile errors. | ||
| 378 | if (s.cast(Compile)) |compile| switch (term) { | ||
| 379 | .exited => { | ||
| 380 | // Note that the exit code may be 0 in this case due to the | ||
| 381 | // compiler server protocol. | ||
| 382 | if (compile.expect_errors != null) { | ||
| 383 | return error.NeedCompileErrorCheck; | ||
| 384 | } | ||
| 385 | }, | ||
| 386 | else => {}, | ||
| 387 | }; | ||
| 388 | |||
| 389 | try handleChildProcessTerm(s, term); | ||
| 390 | } | ||
| 391 | |||
| 392 | if (s.result_error_bundle.errorMessageCount() > 0) { | ||
| 393 | return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); | ||
| 394 | } | ||
| 395 | |||
| 396 | return result; | ||
| 397 | } | ||
| 398 | |||
| 399 | /// Wrapper around `Io.Dir.updateFile` that handles verbose and error output. | ||
| 400 | pub fn installFile(s: *Step, src_lazy_path: LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus { | ||
| 401 | const b = s.owner; | ||
| 402 | const io = b.graph.io; | ||
| 403 | const src_path = src_lazy_path.getPath3(b, s); | ||
| 404 | try handleVerbose(s, .inherit, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path }); | ||
| 405 | return Io.Dir.updateFile(src_path.root_dir.handle, io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err| | ||
| 406 | return s.fail("unable to update file from '{f}' to '{s}': {t}", .{ src_path, dest_path, err }); | ||
| 407 | } | ||
| 408 | |||
| 409 | /// Wrapper around `Io.Dir.createDirPathStatus` that handles verbose and error output. | ||
| 410 | pub fn installDir(s: *Step, dest_path: []const u8) !Io.Dir.CreatePathStatus { | ||
| 411 | const b = s.owner; | ||
| 412 | const io = b.graph.io; | ||
| 413 | try handleVerbose(s, .inherit, &.{ "install", "-d", dest_path }); | ||
| 414 | return Io.Dir.cwd().createDirPathStatus(io, dest_path, .default_dir) catch |err| | ||
| 415 | return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err }); | ||
| 416 | } | ||
| 417 | |||
| 418 | fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*WebServer, gpa: Allocator) !?Path { | ||
| 419 | const b = s.owner; | ||
| 420 | const arena = b.allocator; | ||
| 421 | const io = b.graph.io; | ||
| 422 | |||
| 423 | const start_ts = Io.Clock.awake.now(io); | ||
| 424 | |||
| 425 | try sendMessage(io, zp.child.stdin.?, .update); | ||
| 426 | if (!watch) try sendMessage(io, zp.child.stdin.?, .exit); | ||
| 427 | |||
| 428 | var result: ?Path = null; | ||
| 429 | var eos_err: error{EndOfStream}!void = {}; | ||
| 430 | |||
| 431 | const stdout = zp.multi_reader.fileReader(0); | ||
| 432 | |||
| 433 | while (true) { | ||
| 434 | const Header = std.zig.Server.Message.Header; | ||
| 435 | const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) { | ||
| 436 | error.EndOfStream => break, | ||
| 437 | error.ReadFailed => return stdout.err.?, | ||
| 438 | }; | ||
| 439 | const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) { | ||
| 440 | error.EndOfStream => |e| { | ||
| 441 | // Better to report the crash with stderr below, but we set | ||
| 442 | // this in case the child exits successfully while violating | ||
| 443 | // this protocol. | ||
| 444 | eos_err = e; | ||
| 445 | break; | ||
| 446 | }, | ||
| 447 | error.ReadFailed => return stdout.err.?, | ||
| 448 | }; | ||
| 449 | switch (header.tag) { | ||
| 450 | .zig_version => { | ||
| 451 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) { | ||
| 452 | return s.fail( | ||
| 453 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", | ||
| 454 | .{ builtin.zig_version_string, body }, | ||
| 455 | ); | ||
| 456 | } | ||
| 457 | }, | ||
| 458 | .error_bundle => { | ||
| 459 | s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); | ||
| 460 | // This message indicates the end of the update. | ||
| 461 | if (watch) break; | ||
| 462 | }, | ||
| 463 | .emit_digest => { | ||
| 464 | const EmitDigest = std.zig.Server.Message.EmitDigest; | ||
| 465 | const emit_digest: *align(1) const EmitDigest = @ptrCast(body); | ||
| 466 | s.result_cached = emit_digest.flags.cache_hit; | ||
| 467 | const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; | ||
| 468 | result = .{ | ||
| 469 | .root_dir = b.cache_root, | ||
| 470 | .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)), | ||
| 471 | }; | ||
| 472 | }, | ||
| 473 | .file_system_inputs => { | ||
| 474 | s.clearWatchInputs(); | ||
| 475 | var it = std.mem.splitScalar(u8, body, 0); | ||
| 476 | while (it.next()) |prefixed_path| { | ||
| 477 | const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1); | ||
| 478 | const sub_path = try arena.dupe(u8, prefixed_path[1..]); | ||
| 479 | const sub_path_dirname = std.fs.path.dirname(sub_path) orelse ""; | ||
| 480 | switch (prefix_index) { | ||
| 481 | .cwd => { | ||
| 482 | const path: Cache.Path = .{ | ||
| 483 | .root_dir = Cache.Directory.cwd(), | ||
| 484 | .sub_path = sub_path_dirname, | ||
| 485 | }; | ||
| 486 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 487 | }, | ||
| 488 | .zig_lib => zl: { | ||
| 489 | if (s.cast(Step.Compile)) |compile| { | ||
| 490 | if (compile.zig_lib_dir) |zig_lib_dir| { | ||
| 491 | const lp = try zig_lib_dir.join(arena, sub_path); | ||
| 492 | try addWatchInput(s, lp); | ||
| 493 | break :zl; | ||
| 494 | } | ||
| 495 | } | ||
| 496 | const path: Cache.Path = .{ | ||
| 497 | .root_dir = s.owner.graph.zig_lib_directory, | ||
| 498 | .sub_path = sub_path_dirname, | ||
| 499 | }; | ||
| 500 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 501 | }, | ||
| 502 | .local_cache => { | ||
| 503 | const path: Cache.Path = .{ | ||
| 504 | .root_dir = b.cache_root, | ||
| 505 | .sub_path = sub_path_dirname, | ||
| 506 | }; | ||
| 507 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 508 | }, | ||
| 509 | .global_cache => { | ||
| 510 | const path: Cache.Path = .{ | ||
| 511 | .root_dir = s.owner.graph.global_cache_root, | ||
| 512 | .sub_path = sub_path_dirname, | ||
| 513 | }; | ||
| 514 | try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); | ||
| 515 | }, | ||
| 516 | } | ||
| 517 | } | ||
| 518 | }, | ||
| 519 | .time_report => if (web_server) |ws| { | ||
| 520 | const TimeReport = std.zig.Server.Message.TimeReport; | ||
| 521 | const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]); | ||
| 522 | ws.updateTimeReportCompile(.{ | ||
| 523 | .compile = s.cast(Step.Compile).?, | ||
| 524 | .use_llvm = tr.flags.use_llvm, | ||
| 525 | .stats = tr.stats, | ||
| 526 | .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()), | ||
| 527 | .llvm_pass_timings_len = tr.llvm_pass_timings_len, | ||
| 528 | .files_len = tr.files_len, | ||
| 529 | .decls_len = tr.decls_len, | ||
| 530 | .trailing = body[@sizeOf(TimeReport)..], | ||
| 531 | }); | ||
| 532 | }, | ||
| 533 | else => {}, // ignore other messages | ||
| 534 | } | ||
| 535 | } | ||
| 536 | |||
| 537 | s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()); | ||
| 538 | |||
| 539 | const stderr_contents = zp.multi_reader.reader(1).buffered(); | ||
| 540 | if (stderr_contents.len > 0) { | ||
| 541 | try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents)); | ||
| 542 | } | ||
| 543 | |||
| 544 | try eos_err; | ||
| 545 | |||
| 546 | return result; | ||
| 547 | } | ||
| 548 | |||
| 549 | pub fn getZigProcess(s: *Step) ?*ZigProcess { | ||
| 550 | if (true) @panic("TODO getZigProcess"); | ||
| 551 | return switch (s.id) { | ||
| 552 | .compile => s.cast(Compile).?.zig_process, | ||
| 553 | else => null, | ||
| 554 | }; | ||
| 555 | } | ||
| 556 | |||
| 557 | fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { | ||
| 558 | const header: std.zig.Client.Message.Header = .{ | ||
| 559 | .tag = tag, | ||
| 560 | .bytes_len = 0, | ||
| 561 | }; | ||
| 562 | var w = file.writer(io, &.{}); | ||
| 563 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 564 | error.WriteFailed => return w.err.?, | ||
| 565 | }; | ||
| 566 | } | ||
| 567 | |||
| 568 | pub fn handleVerbose( | ||
| 569 | s: *Step, | ||
| 570 | arena: Allocator, | ||
| 571 | cwd: std.process.Child.Cwd, | ||
| 572 | opt_env: ?*const std.process.Environ.Map, | ||
| 573 | argv: []const []const u8, | ||
| 574 | ) error{OutOfMemory}!void { | ||
| 575 | if (!s.verbose) return; | ||
| 576 | const graph = s.graph; | ||
| 577 | // Intention of verbose is to print all sub-process command lines to | ||
| 578 | // stderr before spawning them. | ||
| 579 | const text = try allocPrintCmd(arena, cwd, if (opt_env) |env| .{ | ||
| 580 | .child = env, | ||
| 581 | .parent = &graph.environ_map, | ||
| 582 | } else null, argv); | ||
| 583 | std.log.scoped(.verbose).info("{s}", .{text}); | ||
| 584 | } | ||
| 585 | |||
| 586 | /// Asserts that the caller has already populated `s.result_failed_command`. | ||
| 587 | pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void { | ||
| 588 | if (!std.process.can_spawn) { | ||
| 589 | return s.fail("unable to spawn process: host cannot spawn child processes", .{}); | ||
| 590 | } | ||
| 591 | } | ||
| 592 | |||
| 593 | /// Asserts that the caller has already populated `s.result_failed_command`. | ||
| 594 | pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void { | ||
| 595 | assert(s.result_failed_command != null); | ||
| 596 | return switch (term) { | ||
| 597 | .exited => |code| if (code != 0) s.fail("process exited with error code {d}", .{code}), | ||
| 598 | .signal => |sig| s.fail("process terminated with signal {t}", .{sig}), | ||
| 599 | .stopped => |sig| s.fail("process stopped with signal {t}", .{sig}), | ||
| 600 | .unknown => s.fail("process terminated unexpectedly", .{}), | ||
| 601 | }; | ||
| 602 | } | ||
| 603 | |||
| 604 | /// Prefer `cacheHitAndWatch` unless you already added watch inputs | ||
| 605 | /// separately from using the cache system. | ||
| 606 | pub fn cacheHit(s: *Step, man: *Cache.Manifest) !bool { | ||
| 607 | s.result_cached = man.hit() catch |err| return failWithCacheError(s, man, err); | ||
| 608 | return s.result_cached; | ||
| 609 | } | ||
| 610 | |||
| 611 | /// Clears previous watch inputs, if any, and then populates watch inputs from | ||
| 612 | /// the full set of files picked up by the cache manifest. | ||
| 613 | /// | ||
| 614 | /// Must be accompanied with `writeManifestAndWatch`. | ||
| 615 | pub fn cacheHitAndWatch(s: *Step, man: *Cache.Manifest) !bool { | ||
| 616 | const is_hit = man.hit() catch |err| return failWithCacheError(s, man, err); | ||
| 617 | s.result_cached = is_hit; | ||
| 618 | // The above call to hit() populates the manifest with files, so in case of | ||
| 619 | // a hit, we need to populate watch inputs. | ||
| 620 | if (is_hit) try setWatchInputsFromManifest(s, man); | ||
| 621 | return is_hit; | ||
| 622 | } | ||
| 623 | |||
| 624 | fn failWithCacheError( | ||
| 625 | s: *Step, | ||
| 626 | man: *const Cache.Manifest, | ||
| 627 | err: Cache.Manifest.HitError, | ||
| 628 | ) error{ OutOfMemory, Canceled, MakeFailed } { | ||
| 629 | switch (err) { | ||
| 630 | error.CacheCheckFailed => switch (man.diagnostic) { | ||
| 631 | .none => unreachable, | ||
| 632 | .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {t} {t}", .{ | ||
| 633 | man.diagnostic, e, | ||
| 634 | }), | ||
| 635 | .file_open, .file_stat, .file_read, .file_hash => |op| { | ||
| 636 | const pp = man.files.keys()[op.file_index].prefixed_path; | ||
| 637 | const prefix = man.cache.prefixes()[pp.prefix].path orelse ""; | ||
| 638 | return s.fail("failed to check cache: '{s}{c}{s}' {t} {t}", .{ | ||
| 639 | prefix, std.fs.path.sep, pp.sub_path, man.diagnostic, op.err, | ||
| 640 | }); | ||
| 641 | }, | ||
| 642 | }, | ||
| 643 | error.OutOfMemory, error.Canceled => |e| return e, | ||
| 644 | error.InvalidFormat => return s.fail("failed to check cache: invalid manifest file format", .{}), | ||
| 645 | } | ||
| 646 | } | ||
| 647 | |||
| 648 | /// Prefer `writeManifestAndWatch` unless you already added watch inputs | ||
| 649 | /// separately from using the cache system. | ||
| 650 | pub fn writeManifest(s: *Step, man: *Cache.Manifest) !void { | ||
| 651 | if (s.test_results.isSuccess()) { | ||
| 652 | man.writeManifest() catch |err| { | ||
| 653 | try s.addError("unable to write cache manifest: {t}", .{err}); | ||
| 654 | }; | ||
| 655 | } | ||
| 656 | } | ||
| 657 | |||
| 658 | /// Clears previous watch inputs, if any, and then populates watch inputs from | ||
| 659 | /// the full set of files picked up by the cache manifest. | ||
| 660 | /// | ||
| 661 | /// Must be accompanied with `cacheHitAndWatch`. | ||
| 662 | pub fn writeManifestAndWatch(s: *Step, man: *Cache.Manifest) !void { | ||
| 663 | try writeManifest(s, man); | ||
| 664 | try setWatchInputsFromManifest(s, man); | ||
| 665 | } | ||
| 666 | |||
| 667 | fn setWatchInputsFromManifest(s: *Step, man: *Cache.Manifest) !void { | ||
| 668 | const arena = s.owner.allocator; | ||
| 669 | const prefixes = man.cache.prefixes(); | ||
| 670 | clearWatchInputs(s); | ||
| 671 | for (man.files.keys()) |file| { | ||
| 672 | // The file path data is freed when the cache manifest is cleaned up at the end of `make`. | ||
| 673 | const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path); | ||
| 674 | try addWatchInputFromPath(s, .{ | ||
| 675 | .root_dir = prefixes[file.prefixed_path.prefix], | ||
| 676 | .sub_path = std.fs.path.dirname(sub_path) orelse "", | ||
| 677 | }, std.fs.path.basename(sub_path)); | ||
| 678 | } | ||
| 679 | } | ||
| 680 | |||
| 681 | /// For steps that have a single input that never changes when re-running `make`. | ||
| 682 | pub fn singleUnchangingWatchInput(step: *Step, lazy_path: LazyPath) Allocator.Error!void { | ||
| 683 | if (!step.inputs.populated()) try step.addWatchInput(lazy_path); | ||
| 684 | } | ||
| 685 | |||
| 686 | pub fn clearWatchInputs(step: *Step) void { | ||
| 687 | const gpa = step.owner.allocator; | ||
| 688 | step.inputs.clear(gpa); | ||
| 689 | } | ||
| 690 | |||
| 691 | /// Places a *file* dependency on the path. | ||
| 692 | pub fn addWatchInput(step: *Step, lazy_file: LazyPath) Allocator.Error!void { | ||
| 693 | switch (lazy_file) { | ||
| 694 | .src_path => |src_path| try addWatchInputFromBuilder(step, src_path.owner, src_path.sub_path), | ||
| 695 | .dependency => |d| try addWatchInputFromBuilder(step, d.dependency.builder, d.sub_path), | ||
| 696 | .cwd_relative => |path_string| { | ||
| 697 | try addWatchInputFromPath(step, .{ | ||
| 698 | .root_dir = .{ | ||
| 699 | .path = null, | ||
| 700 | .handle = Io.Dir.cwd(), | ||
| 701 | }, | ||
| 702 | .sub_path = std.fs.path.dirname(path_string) orelse "", | ||
| 703 | }, std.fs.path.basename(path_string)); | ||
| 704 | }, | ||
| 705 | // Nothing to watch because this dependency edge is modeled instead via `dependants`. | ||
| 706 | .generated => {}, | ||
| 707 | } | ||
| 708 | } | ||
| 709 | |||
| 710 | /// Any changes inside the directory will trigger invalidation. | ||
| 711 | /// | ||
| 712 | /// See also `addDirectoryWatchInputFromPath` which takes a `Cache.Path` instead. | ||
| 713 | /// | ||
| 714 | /// Paths derived from this directory should also be manually added via | ||
| 715 | /// `addDirectoryWatchInputFromPath` if and only if this function returns | ||
| 716 | /// `true`. | ||
| 717 | pub fn addDirectoryWatchInput(step: *Step, lazy_directory: LazyPath) Allocator.Error!bool { | ||
| 718 | switch (lazy_directory) { | ||
| 719 | .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path), | ||
| 720 | .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path), | ||
| 721 | .cwd_relative => |path_string| { | ||
| 722 | try addDirectoryWatchInputFromPath(step, .{ | ||
| 723 | .root_dir = .{ | ||
| 724 | .path = null, | ||
| 725 | .handle = Io.Dir.cwd(), | ||
| 726 | }, | ||
| 727 | .sub_path = path_string, | ||
| 728 | }); | ||
| 729 | }, | ||
| 730 | // Nothing to watch because this dependency edge is modeled instead via `dependants`. | ||
| 731 | .generated => return false, | ||
| 732 | } | ||
| 733 | return true; | ||
| 734 | } | ||
| 735 | |||
| 736 | /// Any changes inside the directory will trigger invalidation. | ||
| 737 | /// | ||
| 738 | /// See also `addDirectoryWatchInput` which takes a `LazyPath` instead. | ||
| 739 | /// | ||
| 740 | /// This function should only be called when it has been verified that the | ||
| 741 | /// dependency on `path` is not already accounted for by a `Step` dependency. | ||
| 742 | /// In other words, before calling this function, first check that the | ||
| 743 | /// `LazyPath` which this `path` is derived from is not `generated`. | ||
| 744 | pub fn addDirectoryWatchInputFromPath(step: *Step, path: Cache.Path) !void { | ||
| 745 | return addWatchInputFromPath(step, path, "."); | ||
| 746 | } | ||
| 747 | |||
| 748 | fn addWatchInputFromBuilder(step: *Step, package: Package, sub_path: []const u8) !void { | ||
| 749 | return addWatchInputFromPath(step, .{ | ||
| 750 | .root_dir = package.build_root, | ||
| 751 | .sub_path = std.fs.path.dirname(sub_path) orelse "", | ||
| 752 | }, std.fs.path.basename(sub_path)); | ||
| 753 | } | ||
| 754 | |||
| 755 | fn addDirectoryWatchInputFromBuilder(step: *Step, package: Package, sub_path: []const u8) !void { | ||
| 756 | return addDirectoryWatchInputFromPath(step, .{ | ||
| 757 | .root_dir = package.build_root, | ||
| 758 | .sub_path = sub_path, | ||
| 759 | }); | ||
| 760 | } | ||
| 761 | |||
| 762 | fn addWatchInputFromPath(step: *Step, path: Cache.Path, basename: []const u8) !void { | ||
| 763 | const gpa = step.owner.allocator; | ||
| 764 | const gop = try step.inputs.table.getOrPut(gpa, path); | ||
| 765 | if (!gop.found_existing) gop.value_ptr.* = .empty; | ||
| 766 | try gop.value_ptr.append(gpa, basename); | ||
| 767 | } | ||
| 768 | |||
| 769 | pub fn allocPrintCmd( | ||
| 770 | gpa: Allocator, | ||
| 771 | cwd: std.process.Child.Cwd, | ||
| 772 | opt_env: ?struct { | ||
| 773 | child: *const std.process.Environ.Map, | ||
| 774 | parent: *const std.process.Environ.Map, | ||
| 775 | }, | ||
| 776 | argv: []const []const u8, | ||
| 777 | ) Allocator.Error![]u8 { | ||
| 778 | const shell = struct { | ||
| 779 | fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void { | ||
| 780 | for (string) |c| { | ||
| 781 | if (switch (c) { | ||
| 782 | else => true, | ||
| 783 | '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false, | ||
| 784 | '=' => is_argv0, | ||
| 785 | }) break; | ||
| 786 | } else return writer.writeAll(string); | ||
| 787 | |||
| 788 | try writer.writeByte('"'); | ||
| 789 | for (string) |c| { | ||
| 790 | if (switch (c) { | ||
| 791 | std.ascii.control_code.nul => break, | ||
| 792 | '!', '"', '$', '\\', '`' => true, | ||
| 793 | else => !std.ascii.isPrint(c), | ||
| 794 | }) try writer.writeByte('\\'); | ||
| 795 | switch (c) { | ||
| 796 | std.ascii.control_code.nul => unreachable, | ||
| 797 | std.ascii.control_code.bel => try writer.writeByte('a'), | ||
| 798 | std.ascii.control_code.bs => try writer.writeByte('b'), | ||
| 799 | std.ascii.control_code.ht => try writer.writeByte('t'), | ||
| 800 | std.ascii.control_code.lf => try writer.writeByte('n'), | ||
| 801 | std.ascii.control_code.vt => try writer.writeByte('v'), | ||
| 802 | std.ascii.control_code.ff => try writer.writeByte('f'), | ||
| 803 | std.ascii.control_code.cr => try writer.writeByte('r'), | ||
| 804 | std.ascii.control_code.esc => try writer.writeByte('E'), | ||
| 805 | ' '...'~' => try writer.writeByte(c), | ||
| 806 | else => try writer.print("{o:0>3}", .{c}), | ||
| 807 | } | ||
| 808 | } | ||
| 809 | try writer.writeByte('"'); | ||
| 810 | } | ||
| 811 | }; | ||
| 812 | |||
| 813 | var aw: Io.Writer.Allocating = .init(gpa); | ||
| 814 | defer aw.deinit(); | ||
| 815 | const writer = &aw.writer; | ||
| 816 | switch (cwd) { | ||
| 817 | .inherit => {}, | ||
| 818 | .path => |path| writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory, | ||
| 819 | .dir => @panic("TODO"), | ||
| 820 | } | ||
| 821 | if (opt_env) |env| { | ||
| 822 | var it = env.child.iterator(); | ||
| 823 | while (it.next()) |entry| { | ||
| 824 | const key = entry.key_ptr.*; | ||
| 825 | const value = entry.value_ptr.*; | ||
| 826 | if (env.parent.get(key)) |process_value| { | ||
| 827 | if (std.mem.eql(u8, value, process_value)) continue; | ||
| 828 | } | ||
| 829 | writer.print("{s}=", .{key}) catch return error.OutOfMemory; | ||
| 830 | shell.escape(writer, value, false) catch return error.OutOfMemory; | ||
| 831 | writer.writeByte(' ') catch return error.OutOfMemory; | ||
| 832 | } | ||
| 833 | } | ||
| 834 | shell.escape(writer, argv[0], true) catch return error.OutOfMemory; | ||
| 835 | for (argv[1..]) |arg| { | ||
| 836 | writer.writeByte(' ') catch return error.OutOfMemory; | ||
| 837 | shell.escape(writer, arg, false) catch return error.OutOfMemory; | ||
| 838 | } | ||
| 839 | return aw.toOwnedSlice(); | ||
| 840 | } | ||
lib/compiler/maker/Step/Compile.zig deleted-1200| ... | @@ -1,1200 +0,0 @@ | ||
| 1 | /// Populated during the make phase when there is a long-lived compiler process. | ||
| 2 | /// Managed by the build runner, not user build script. | ||
| 3 | zig_process: ?*Step.ZigProcess, | ||
| 4 | |||
| 5 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 6 | const b = step.owner; | ||
| 7 | const compile: *Compile = @fieldParentPtr("step", step); | ||
| 8 | |||
| 9 | const zig_args = try getZigArgs(compile, false); | ||
| 10 | |||
| 11 | const maybe_output_dir = step.evalZigProcess( | ||
| 12 | zig_args, | ||
| 13 | options.progress_node, | ||
| 14 | (b.graph.incremental == true) and (options.watch or options.web_server != null), | ||
| 15 | options.web_server, | ||
| 16 | options.gpa, | ||
| 17 | ) catch |err| switch (err) { | ||
| 18 | error.NeedCompileErrorCheck => { | ||
| 19 | assert(compile.expect_errors != null); | ||
| 20 | try checkCompileErrors(compile); | ||
| 21 | return; | ||
| 22 | }, | ||
| 23 | else => |e| return e, | ||
| 24 | }; | ||
| 25 | |||
| 26 | // Update generated files | ||
| 27 | if (maybe_output_dir) |output_dir| { | ||
| 28 | if (compile.emit_directory) |lp| { | ||
| 29 | lp.path = b.fmt("{f}", .{output_dir}); | ||
| 30 | } | ||
| 31 | |||
| 32 | // zig fmt: off | ||
| 33 | if (compile.generated_bin) |lp| lp.path = compile.outputPath(output_dir, .bin); | ||
| 34 | if (compile.generated_pdb) |lp| lp.path = compile.outputPath(output_dir, .pdb); | ||
| 35 | // hack for stage2_x86_64 + coff | ||
| 36 | if (compile.generated_compiler_rt_dyn_lib) |lp| lp.path = compile.outputPath(output_dir, .compiler_rt_dyn_lib); | ||
| 37 | if (compile.generated_implib) |lp| lp.path = compile.outputPath(output_dir, .implib); | ||
| 38 | if (compile.generated_h) |lp| lp.path = compile.outputPath(output_dir, .h); | ||
| 39 | if (compile.generated_docs) |lp| lp.path = compile.outputPath(output_dir, .docs); | ||
| 40 | if (compile.generated_asm) |lp| lp.path = compile.outputPath(output_dir, .@"asm"); | ||
| 41 | if (compile.generated_llvm_ir) |lp| lp.path = compile.outputPath(output_dir, .llvm_ir); | ||
| 42 | if (compile.generated_llvm_bc) |lp| lp.path = compile.outputPath(output_dir, .llvm_bc); | ||
| 43 | // zig fmt: on | ||
| 44 | } | ||
| 45 | |||
| 46 | if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic and | ||
| 47 | compile.version != null and compile.generated_bin != null and | ||
| 48 | std.Build.wantSharedLibSymLinks(compile.rootModuleTarget())) | ||
| 49 | { | ||
| 50 | try doAtomicSymLinks( | ||
| 51 | step, | ||
| 52 | compile.getEmittedBin().getPath2(b, step), | ||
| 53 | compile.major_only_filename.?, | ||
| 54 | compile.name_only_filename.?, | ||
| 55 | ); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { | ||
| 60 | const step = &compile.step; | ||
| 61 | const b = step.owner; | ||
| 62 | const arena = b.allocator; | ||
| 63 | |||
| 64 | var zig_args = std.array_list.Managed([]const u8).init(arena); | ||
| 65 | defer zig_args.deinit(); | ||
| 66 | |||
| 67 | try zig_args.append(b.graph.zig_exe); | ||
| 68 | |||
| 69 | const cmd = switch (compile.kind) { | ||
| 70 | .lib => "build-lib", | ||
| 71 | .exe => "build-exe", | ||
| 72 | .obj => "build-obj", | ||
| 73 | .@"test" => "test", | ||
| 74 | .test_obj => "test-obj", | ||
| 75 | }; | ||
| 76 | try zig_args.append(cmd); | ||
| 77 | |||
| 78 | if (b.reference_trace) |some| { | ||
| 79 | try zig_args.append(try std.fmt.allocPrint(arena, "-freference-trace={d}", .{some})); | ||
| 80 | } | ||
| 81 | try addFlag(&zig_args, "allow-so-scripts", compile.allow_so_scripts orelse b.graph.allow_so_scripts); | ||
| 82 | |||
| 83 | try addFlag(&zig_args, "llvm", compile.use_llvm); | ||
| 84 | try addFlag(&zig_args, "lld", compile.use_lld); | ||
| 85 | try addFlag(&zig_args, "new-linker", compile.use_new_linker); | ||
| 86 | |||
| 87 | if (compile.root_module.resolved_target.?.query.ofmt) |ofmt| { | ||
| 88 | try zig_args.append(try std.fmt.allocPrint(arena, "-ofmt={s}", .{@tagName(ofmt)})); | ||
| 89 | } | ||
| 90 | |||
| 91 | switch (compile.entry) { | ||
| 92 | .default => {}, | ||
| 93 | .disabled => try zig_args.append("-fno-entry"), | ||
| 94 | .enabled => try zig_args.append("-fentry"), | ||
| 95 | .symbol_name => |entry_name| { | ||
| 96 | try zig_args.append(try std.fmt.allocPrint(arena, "-fentry={s}", .{entry_name})); | ||
| 97 | }, | ||
| 98 | } | ||
| 99 | |||
| 100 | { | ||
| 101 | for (compile.force_undefined_symbols.keys()) |symbol_name| { | ||
| 102 | try zig_args.append("--force_undefined"); | ||
| 103 | try zig_args.append(symbol_name.*); | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | if (compile.stack_size) |stack_size| { | ||
| 108 | try zig_args.append("--stack"); | ||
| 109 | try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size})); | ||
| 110 | } | ||
| 111 | |||
| 112 | if (fuzz) { | ||
| 113 | try zig_args.append("-ffuzz"); | ||
| 114 | } | ||
| 115 | |||
| 116 | { | ||
| 117 | // Stores system libraries that have already been seen for at least one | ||
| 118 | // module, along with any arguments that need to be passed to the | ||
| 119 | // compiler for each module individually. | ||
| 120 | var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty; | ||
| 121 | var frameworks: std.StringArrayHashMapUnmanaged(Module.LinkFrameworkOptions) = .empty; | ||
| 122 | |||
| 123 | var prev_has_cflags = false; | ||
| 124 | var prev_has_rcflags = false; | ||
| 125 | var prev_search_strategy: Module.SystemLib.SearchStrategy = .paths_first; | ||
| 126 | var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic; | ||
| 127 | // Track the number of positional arguments so that a nice error can be | ||
| 128 | // emitted if there is nothing to link. | ||
| 129 | var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null); | ||
| 130 | |||
| 131 | // Fully recursive iteration including dynamic libraries to detect | ||
| 132 | // libc and libc++ linkage. | ||
| 133 | for (compile.getCompileDependencies(true)) |some_compile| { | ||
| 134 | for (some_compile.root_module.getGraph().modules) |mod| { | ||
| 135 | if (mod.link_libc == true) compile.is_linking_libc = true; | ||
| 136 | if (mod.link_libcpp == true) compile.is_linking_libcpp = true; | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | var cli_named_modules = try CliNamedModules.init(arena, compile.root_module); | ||
| 141 | |||
| 142 | // For this loop, don't chase dynamic libraries because their link | ||
| 143 | // objects are already linked. | ||
| 144 | for (compile.getCompileDependencies(false)) |dep_compile| { | ||
| 145 | for (dep_compile.root_module.getGraph().modules) |mod| { | ||
| 146 | // While walking transitive dependencies, if a given link object is | ||
| 147 | // already included in a library, it should not redundantly be | ||
| 148 | // placed on the linker line of the dependee. | ||
| 149 | const my_responsibility = dep_compile == compile; | ||
| 150 | const already_linked = !my_responsibility and dep_compile.isDynamicLibrary(); | ||
| 151 | |||
| 152 | // Inherit dependencies on darwin frameworks. | ||
| 153 | if (!already_linked) { | ||
| 154 | for (mod.frameworks.keys(), mod.frameworks.values()) |name, info| { | ||
| 155 | try frameworks.put(arena, name, info); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | // Inherit dependencies on system libraries and static libraries. | ||
| 160 | for (mod.link_objects.items) |link_object| { | ||
| 161 | switch (link_object) { | ||
| 162 | .static_path => |static_path| { | ||
| 163 | if (my_responsibility) { | ||
| 164 | try zig_args.append(static_path.getPath2(mod.owner, step)); | ||
| 165 | total_linker_objects += 1; | ||
| 166 | } | ||
| 167 | }, | ||
| 168 | .system_lib => |system_lib| { | ||
| 169 | const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name); | ||
| 170 | if (system_lib_gop.found_existing) { | ||
| 171 | try zig_args.appendSlice(system_lib_gop.value_ptr.*); | ||
| 172 | continue; | ||
| 173 | } else { | ||
| 174 | system_lib_gop.value_ptr.* = &.{}; | ||
| 175 | } | ||
| 176 | |||
| 177 | if (already_linked) | ||
| 178 | continue; | ||
| 179 | |||
| 180 | if ((system_lib.search_strategy != prev_search_strategy or | ||
| 181 | system_lib.preferred_link_mode != prev_preferred_link_mode) and | ||
| 182 | compile.linkage != .static) | ||
| 183 | { | ||
| 184 | switch (system_lib.search_strategy) { | ||
| 185 | .no_fallback => switch (system_lib.preferred_link_mode) { | ||
| 186 | .dynamic => try zig_args.append("-search_dylibs_only"), | ||
| 187 | .static => try zig_args.append("-search_static_only"), | ||
| 188 | }, | ||
| 189 | .paths_first => switch (system_lib.preferred_link_mode) { | ||
| 190 | .dynamic => try zig_args.append("-search_paths_first"), | ||
| 191 | .static => try zig_args.append("-search_paths_first_static"), | ||
| 192 | }, | ||
| 193 | .mode_first => switch (system_lib.preferred_link_mode) { | ||
| 194 | .dynamic => try zig_args.append("-search_dylibs_first"), | ||
| 195 | .static => try zig_args.append("-search_static_first"), | ||
| 196 | }, | ||
| 197 | } | ||
| 198 | prev_search_strategy = system_lib.search_strategy; | ||
| 199 | prev_preferred_link_mode = system_lib.preferred_link_mode; | ||
| 200 | } | ||
| 201 | |||
| 202 | const prefix: []const u8 = prefix: { | ||
| 203 | if (system_lib.needed) break :prefix "-needed-l"; | ||
| 204 | if (system_lib.weak) break :prefix "-weak-l"; | ||
| 205 | break :prefix "-l"; | ||
| 206 | }; | ||
| 207 | switch (system_lib.use_pkg_config) { | ||
| 208 | .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })), | ||
| 209 | .yes, .force => { | ||
| 210 | if (compile.runPkgConfig(system_lib.name)) |result| { | ||
| 211 | try zig_args.appendSlice(result.cflags); | ||
| 212 | try zig_args.appendSlice(result.libs); | ||
| 213 | try seen_system_libs.put(arena, system_lib.name, result.cflags); | ||
| 214 | } else |err| switch (err) { | ||
| 215 | error.PkgConfigInvalidOutput, | ||
| 216 | error.PkgConfigCrashed, | ||
| 217 | error.PkgConfigFailed, | ||
| 218 | error.PkgConfigNotInstalled, | ||
| 219 | error.PackageNotFound, | ||
| 220 | => switch (system_lib.use_pkg_config) { | ||
| 221 | .yes => { | ||
| 222 | // pkg-config failed, so fall back to linking the library | ||
| 223 | // by name directly. | ||
| 224 | try zig_args.append(b.fmt("{s}{s}", .{ | ||
| 225 | prefix, | ||
| 226 | system_lib.name, | ||
| 227 | })); | ||
| 228 | }, | ||
| 229 | .force => { | ||
| 230 | panic("pkg-config failed for library {s}", .{system_lib.name}); | ||
| 231 | }, | ||
| 232 | .no => unreachable, | ||
| 233 | }, | ||
| 234 | |||
| 235 | else => |e| return e, | ||
| 236 | } | ||
| 237 | }, | ||
| 238 | } | ||
| 239 | }, | ||
| 240 | .other_step => |other| { | ||
| 241 | switch (other.kind) { | ||
| 242 | .exe => return step.fail("cannot link with an executable build artifact", .{}), | ||
| 243 | .@"test" => return step.fail("cannot link with a test", .{}), | ||
| 244 | .obj, .test_obj => { | ||
| 245 | const included_in_lib_or_obj = !my_responsibility and | ||
| 246 | (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj); | ||
| 247 | if (!already_linked and !included_in_lib_or_obj) { | ||
| 248 | try zig_args.append(other.getEmittedBin().getPath2(b, step)); | ||
| 249 | total_linker_objects += 1; | ||
| 250 | } | ||
| 251 | }, | ||
| 252 | .lib => l: { | ||
| 253 | const other_produces_implib = other.producesImplib(); | ||
| 254 | const other_is_static = other_produces_implib or other.isStaticLibrary(); | ||
| 255 | |||
| 256 | if (compile.isStaticLibrary() and other_is_static) { | ||
| 257 | // Avoid putting a static library inside a static library. | ||
| 258 | break :l; | ||
| 259 | } | ||
| 260 | |||
| 261 | // For DLLs, we must link against the implib. | ||
| 262 | // For everything else, we directly link | ||
| 263 | // against the library file. | ||
| 264 | const full_path_lib = if (other_produces_implib) | ||
| 265 | try other.getGeneratedFilePath("generated_implib", &compile.step) | ||
| 266 | else | ||
| 267 | try other.getGeneratedFilePath("generated_bin", &compile.step); | ||
| 268 | |||
| 269 | try zig_args.append(full_path_lib); | ||
| 270 | total_linker_objects += 1; | ||
| 271 | |||
| 272 | if (other.linkage == .dynamic and | ||
| 273 | compile.rootModuleTarget().os.tag != .windows) | ||
| 274 | { | ||
| 275 | if (fs.path.dirname(full_path_lib)) |dirname| { | ||
| 276 | try zig_args.append("-rpath"); | ||
| 277 | try zig_args.append(dirname); | ||
| 278 | } | ||
| 279 | } | ||
| 280 | }, | ||
| 281 | } | ||
| 282 | }, | ||
| 283 | .assembly_file => |asm_file| l: { | ||
| 284 | if (!my_responsibility) break :l; | ||
| 285 | |||
| 286 | if (prev_has_cflags) { | ||
| 287 | try zig_args.append("-cflags"); | ||
| 288 | try zig_args.append("--"); | ||
| 289 | prev_has_cflags = false; | ||
| 290 | } | ||
| 291 | try zig_args.append(asm_file.getPath2(mod.owner, step)); | ||
| 292 | total_linker_objects += 1; | ||
| 293 | }, | ||
| 294 | |||
| 295 | .c_source_file => |c_source_file| l: { | ||
| 296 | if (!my_responsibility) break :l; | ||
| 297 | |||
| 298 | if (prev_has_cflags or c_source_file.flags.len != 0) { | ||
| 299 | try zig_args.append("-cflags"); | ||
| 300 | for (c_source_file.flags) |arg| { | ||
| 301 | try zig_args.append(arg); | ||
| 302 | } | ||
| 303 | try zig_args.append("--"); | ||
| 304 | } | ||
| 305 | prev_has_cflags = (c_source_file.flags.len != 0); | ||
| 306 | |||
| 307 | if (c_source_file.language) |lang| { | ||
| 308 | try zig_args.append("-x"); | ||
| 309 | try zig_args.append(lang.internalIdentifier()); | ||
| 310 | } | ||
| 311 | |||
| 312 | try zig_args.append(c_source_file.file.getPath2(mod.owner, step)); | ||
| 313 | |||
| 314 | if (c_source_file.language != null) { | ||
| 315 | try zig_args.append("-x"); | ||
| 316 | try zig_args.append("none"); | ||
| 317 | } | ||
| 318 | total_linker_objects += 1; | ||
| 319 | }, | ||
| 320 | |||
| 321 | .c_source_files => |c_source_files| l: { | ||
| 322 | if (!my_responsibility) break :l; | ||
| 323 | |||
| 324 | if (prev_has_cflags or c_source_files.flags.len != 0) { | ||
| 325 | try zig_args.append("-cflags"); | ||
| 326 | for (c_source_files.flags) |arg| { | ||
| 327 | try zig_args.append(arg); | ||
| 328 | } | ||
| 329 | try zig_args.append("--"); | ||
| 330 | } | ||
| 331 | prev_has_cflags = (c_source_files.flags.len != 0); | ||
| 332 | |||
| 333 | if (c_source_files.language) |lang| { | ||
| 334 | try zig_args.append("-x"); | ||
| 335 | try zig_args.append(lang.internalIdentifier()); | ||
| 336 | } | ||
| 337 | |||
| 338 | const root_path = c_source_files.root.getPath2(mod.owner, step); | ||
| 339 | for (c_source_files.files) |file| { | ||
| 340 | try zig_args.append(b.pathJoin(&.{ root_path, file })); | ||
| 341 | } | ||
| 342 | |||
| 343 | if (c_source_files.language != null) { | ||
| 344 | try zig_args.append("-x"); | ||
| 345 | try zig_args.append("none"); | ||
| 346 | } | ||
| 347 | |||
| 348 | total_linker_objects += c_source_files.files.len; | ||
| 349 | }, | ||
| 350 | |||
| 351 | .win32_resource_file => |rc_source_file| l: { | ||
| 352 | if (!my_responsibility) break :l; | ||
| 353 | |||
| 354 | if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) { | ||
| 355 | if (prev_has_rcflags) { | ||
| 356 | try zig_args.append("-rcflags"); | ||
| 357 | try zig_args.append("--"); | ||
| 358 | prev_has_rcflags = false; | ||
| 359 | } | ||
| 360 | } else { | ||
| 361 | try zig_args.append("-rcflags"); | ||
| 362 | for (rc_source_file.flags) |arg| { | ||
| 363 | try zig_args.append(arg); | ||
| 364 | } | ||
| 365 | for (rc_source_file.include_paths) |include_path| { | ||
| 366 | try zig_args.append("/I"); | ||
| 367 | try zig_args.append(include_path.getPath2(mod.owner, step)); | ||
| 368 | } | ||
| 369 | try zig_args.append("--"); | ||
| 370 | prev_has_rcflags = true; | ||
| 371 | } | ||
| 372 | try zig_args.append(rc_source_file.file.getPath2(mod.owner, step)); | ||
| 373 | total_linker_objects += 1; | ||
| 374 | }, | ||
| 375 | } | ||
| 376 | } | ||
| 377 | |||
| 378 | // We need to emit the --mod argument here so that the above link objects | ||
| 379 | // have the correct parent module, but only if the module is part of | ||
| 380 | // this compilation. | ||
| 381 | if (!my_responsibility) continue; | ||
| 382 | if (cli_named_modules.modules.getIndex(mod)) |module_cli_index| { | ||
| 383 | const module_cli_name = cli_named_modules.names.keys()[module_cli_index]; | ||
| 384 | try mod.appendZigProcessFlags(&zig_args, step); | ||
| 385 | |||
| 386 | // --dep arguments | ||
| 387 | try zig_args.ensureUnusedCapacity(mod.import_table.count() * 2); | ||
| 388 | for (mod.import_table.keys(), mod.import_table.values()) |name, import| { | ||
| 389 | const import_index = cli_named_modules.modules.getIndex(import).?; | ||
| 390 | const import_cli_name = cli_named_modules.names.keys()[import_index]; | ||
| 391 | zig_args.appendAssumeCapacity("--dep"); | ||
| 392 | if (std.mem.eql(u8, import_cli_name, name)) { | ||
| 393 | zig_args.appendAssumeCapacity(import_cli_name); | ||
| 394 | } else { | ||
| 395 | zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name })); | ||
| 396 | } | ||
| 397 | } | ||
| 398 | |||
| 399 | // When the CLI sees a -M argument, it determines whether it | ||
| 400 | // implies the existence of a Zig compilation unit based on | ||
| 401 | // whether there is a root source file. If there is no root | ||
| 402 | // source file, then this is not a zig compilation unit - it is | ||
| 403 | // perhaps a set of linker objects, or C source files instead. | ||
| 404 | // Linker objects are added to the CLI globally, while C source | ||
| 405 | // files must have a module parent. | ||
| 406 | if (mod.root_source_file) |lp| { | ||
| 407 | const src = lp.getPath2(mod.owner, step); | ||
| 408 | try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src })); | ||
| 409 | } else if (moduleNeedsCliArg(mod)) { | ||
| 410 | try zig_args.append(b.fmt("-M{s}", .{module_cli_name})); | ||
| 411 | } | ||
| 412 | } | ||
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | if (total_linker_objects == 0) { | ||
| 417 | return step.fail("the linker needs one or more objects to link", .{}); | ||
| 418 | } | ||
| 419 | |||
| 420 | for (frameworks.keys(), frameworks.values()) |name, info| { | ||
| 421 | if (info.needed) { | ||
| 422 | try zig_args.append("-needed_framework"); | ||
| 423 | } else if (info.weak) { | ||
| 424 | try zig_args.append("-weak_framework"); | ||
| 425 | } else { | ||
| 426 | try zig_args.append("-framework"); | ||
| 427 | } | ||
| 428 | try zig_args.append(name); | ||
| 429 | } | ||
| 430 | |||
| 431 | if (compile.is_linking_libcpp) { | ||
| 432 | try zig_args.append("-lc++"); | ||
| 433 | } | ||
| 434 | |||
| 435 | if (compile.is_linking_libc) { | ||
| 436 | try zig_args.append("-lc"); | ||
| 437 | } | ||
| 438 | } | ||
| 439 | |||
| 440 | if (compile.win32_manifest) |manifest_file| { | ||
| 441 | try zig_args.append(manifest_file.getPath2(b, step)); | ||
| 442 | } | ||
| 443 | |||
| 444 | if (compile.win32_module_definition) |module_file| { | ||
| 445 | try zig_args.append(module_file.getPath2(b, step)); | ||
| 446 | } | ||
| 447 | |||
| 448 | if (compile.image_base) |image_base| { | ||
| 449 | try zig_args.append("--image-base"); | ||
| 450 | try zig_args.append(b.fmt("0x{x}", .{image_base})); | ||
| 451 | } | ||
| 452 | |||
| 453 | for (compile.filters) |filter| { | ||
| 454 | try zig_args.append("--test-filter"); | ||
| 455 | try zig_args.append(filter); | ||
| 456 | } | ||
| 457 | |||
| 458 | if (compile.test_runner) |test_runner| { | ||
| 459 | try zig_args.append("--test-runner"); | ||
| 460 | try zig_args.append(test_runner.path.getPath2(b, step)); | ||
| 461 | } | ||
| 462 | |||
| 463 | for (b.debug_log_scopes) |log_scope| { | ||
| 464 | try zig_args.append("--debug-log"); | ||
| 465 | try zig_args.append(log_scope); | ||
| 466 | } | ||
| 467 | |||
| 468 | if (b.debug_compile_errors) { | ||
| 469 | try zig_args.append("--debug-compile-errors"); | ||
| 470 | } | ||
| 471 | |||
| 472 | if (b.debug_incremental) { | ||
| 473 | try zig_args.append("--debug-incremental"); | ||
| 474 | } | ||
| 475 | |||
| 476 | if (b.verbose_air) try zig_args.append("--verbose-air"); | ||
| 477 | if (b.verbose_llvm_ir) |path| try zig_args.append(b.fmt("--verbose-llvm-ir={s}", .{path})); | ||
| 478 | if (b.verbose_llvm_bc) |path| try zig_args.append(b.fmt("--verbose-llvm-bc={s}", .{path})); | ||
| 479 | if (b.verbose_link or compile.verbose_link) try zig_args.append("--verbose-link"); | ||
| 480 | if (b.verbose_cc or compile.verbose_cc) try zig_args.append("--verbose-cc"); | ||
| 481 | if (b.verbose_llvm_cpu_features) try zig_args.append("--verbose-llvm-cpu-features"); | ||
| 482 | if (b.graph.time_report) try zig_args.append("--time-report"); | ||
| 483 | |||
| 484 | if (compile.generated_asm != null) try zig_args.append("-femit-asm"); | ||
| 485 | if (compile.generated_bin == null) try zig_args.append("-fno-emit-bin"); | ||
| 486 | if (compile.generated_docs != null) try zig_args.append("-femit-docs"); | ||
| 487 | if (compile.generated_implib != null) try zig_args.append("-femit-implib"); | ||
| 488 | if (compile.generated_llvm_bc != null) try zig_args.append("-femit-llvm-bc"); | ||
| 489 | if (compile.generated_llvm_ir != null) try zig_args.append("-femit-llvm-ir"); | ||
| 490 | if (compile.generated_h != null) try zig_args.append("-femit-h"); | ||
| 491 | |||
| 492 | try addFlag(&zig_args, "formatted-panics", compile.formatted_panics); | ||
| 493 | |||
| 494 | switch (compile.compress_debug_sections) { | ||
| 495 | .none => {}, | ||
| 496 | .zlib => try zig_args.append("--compress-debug-sections=zlib"), | ||
| 497 | .zstd => try zig_args.append("--compress-debug-sections=zstd"), | ||
| 498 | } | ||
| 499 | |||
| 500 | if (compile.link_eh_frame_hdr) { | ||
| 501 | try zig_args.append("--eh-frame-hdr"); | ||
| 502 | } | ||
| 503 | if (compile.link_emit_relocs) { | ||
| 504 | try zig_args.append("--emit-relocs"); | ||
| 505 | } | ||
| 506 | if (compile.link_function_sections) { | ||
| 507 | try zig_args.append("-ffunction-sections"); | ||
| 508 | } | ||
| 509 | if (compile.link_data_sections) { | ||
| 510 | try zig_args.append("-fdata-sections"); | ||
| 511 | } | ||
| 512 | if (compile.link_gc_sections) |x| { | ||
| 513 | try zig_args.append(if (x) "--gc-sections" else "--no-gc-sections"); | ||
| 514 | } | ||
| 515 | if (!compile.linker_dynamicbase) { | ||
| 516 | try zig_args.append("--no-dynamicbase"); | ||
| 517 | } | ||
| 518 | if (compile.linker_allow_shlib_undefined) |x| { | ||
| 519 | try zig_args.append(if (x) "-fallow-shlib-undefined" else "-fno-allow-shlib-undefined"); | ||
| 520 | } | ||
| 521 | if (compile.link_z_notext) { | ||
| 522 | try zig_args.append("-z"); | ||
| 523 | try zig_args.append("notext"); | ||
| 524 | } | ||
| 525 | if (!compile.link_z_relro) { | ||
| 526 | try zig_args.append("-z"); | ||
| 527 | try zig_args.append("norelro"); | ||
| 528 | } | ||
| 529 | if (compile.link_z_lazy) { | ||
| 530 | try zig_args.append("-z"); | ||
| 531 | try zig_args.append("lazy"); | ||
| 532 | } | ||
| 533 | if (compile.link_z_common_page_size) |size| { | ||
| 534 | try zig_args.append("-z"); | ||
| 535 | try zig_args.append(b.fmt("common-page-size={d}", .{size})); | ||
| 536 | } | ||
| 537 | if (compile.link_z_max_page_size) |size| { | ||
| 538 | try zig_args.append("-z"); | ||
| 539 | try zig_args.append(b.fmt("max-page-size={d}", .{size})); | ||
| 540 | } | ||
| 541 | if (compile.link_z_defs) { | ||
| 542 | try zig_args.append("-z"); | ||
| 543 | try zig_args.append("defs"); | ||
| 544 | } | ||
| 545 | |||
| 546 | if (compile.libc_file) |libc_file| { | ||
| 547 | try zig_args.append("--libc"); | ||
| 548 | try zig_args.append(libc_file.getPath2(b, step)); | ||
| 549 | } else if (b.libc_file) |libc_file| { | ||
| 550 | try zig_args.append("--libc"); | ||
| 551 | try zig_args.append(libc_file); | ||
| 552 | } | ||
| 553 | |||
| 554 | try zig_args.append("--cache-dir"); | ||
| 555 | try zig_args.append(b.cache_root.path orelse "."); | ||
| 556 | |||
| 557 | try zig_args.append("--global-cache-dir"); | ||
| 558 | try zig_args.append(b.graph.global_cache_root.path orelse "."); | ||
| 559 | |||
| 560 | if (b.graph.debug_compiler_runtime_libs) |mode| | ||
| 561 | try zig_args.append(b.fmt("--debug-rt={t}", .{mode})); | ||
| 562 | |||
| 563 | try zig_args.append("--name"); | ||
| 564 | try zig_args.append(compile.name); | ||
| 565 | |||
| 566 | if (compile.linkage) |some| switch (some) { | ||
| 567 | .dynamic => try zig_args.append("-dynamic"), | ||
| 568 | .static => try zig_args.append("-static"), | ||
| 569 | }; | ||
| 570 | if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) { | ||
| 571 | if (compile.version) |version| { | ||
| 572 | try zig_args.append("--version"); | ||
| 573 | try zig_args.append(b.fmt("{f}", .{version})); | ||
| 574 | } | ||
| 575 | |||
| 576 | if (compile.rootModuleTarget().os.tag.isDarwin()) { | ||
| 577 | const install_name = compile.install_name orelse b.fmt("@rpath/{s}{s}{s}", .{ | ||
| 578 | compile.rootModuleTarget().libPrefix(), | ||
| 579 | compile.name, | ||
| 580 | compile.rootModuleTarget().dynamicLibSuffix(), | ||
| 581 | }); | ||
| 582 | try zig_args.append("-install_name"); | ||
| 583 | try zig_args.append(install_name); | ||
| 584 | } | ||
| 585 | } | ||
| 586 | |||
| 587 | if (compile.entitlements) |entitlements| { | ||
| 588 | try zig_args.appendSlice(&[_][]const u8{ "--entitlements", entitlements }); | ||
| 589 | } | ||
| 590 | if (compile.pagezero_size) |pagezero_size| { | ||
| 591 | const size = try std.fmt.allocPrint(arena, "{x}", .{pagezero_size}); | ||
| 592 | try zig_args.appendSlice(&[_][]const u8{ "-pagezero_size", size }); | ||
| 593 | } | ||
| 594 | if (compile.headerpad_size) |headerpad_size| { | ||
| 595 | const size = try std.fmt.allocPrint(arena, "{x}", .{headerpad_size}); | ||
| 596 | try zig_args.appendSlice(&[_][]const u8{ "-headerpad", size }); | ||
| 597 | } | ||
| 598 | if (compile.headerpad_max_install_names) { | ||
| 599 | try zig_args.append("-headerpad_max_install_names"); | ||
| 600 | } | ||
| 601 | if (compile.dead_strip_dylibs) { | ||
| 602 | try zig_args.append("-dead_strip_dylibs"); | ||
| 603 | } | ||
| 604 | if (compile.force_load_objc) { | ||
| 605 | try zig_args.append("-ObjC"); | ||
| 606 | } | ||
| 607 | if (compile.discard_local_symbols) { | ||
| 608 | try zig_args.append("--discard-all"); | ||
| 609 | } | ||
| 610 | |||
| 611 | try addFlag(&zig_args, "compiler-rt", compile.bundle_compiler_rt); | ||
| 612 | try addFlag(&zig_args, "ubsan-rt", compile.bundle_ubsan_rt); | ||
| 613 | try addFlag(&zig_args, "dll-export-fns", compile.dll_export_fns); | ||
| 614 | if (compile.rdynamic) { | ||
| 615 | try zig_args.append("-rdynamic"); | ||
| 616 | } | ||
| 617 | if (compile.import_memory) { | ||
| 618 | try zig_args.append("--import-memory"); | ||
| 619 | } | ||
| 620 | if (compile.export_memory) { | ||
| 621 | try zig_args.append("--export-memory"); | ||
| 622 | } | ||
| 623 | if (compile.import_symbols) { | ||
| 624 | try zig_args.append("--import-symbols"); | ||
| 625 | } | ||
| 626 | if (compile.import_table) { | ||
| 627 | try zig_args.append("--import-table"); | ||
| 628 | } | ||
| 629 | if (compile.export_table) { | ||
| 630 | try zig_args.append("--export-table"); | ||
| 631 | } | ||
| 632 | if (compile.initial_memory) |initial_memory| { | ||
| 633 | try zig_args.append(b.fmt("--initial-memory={d}", .{initial_memory})); | ||
| 634 | } | ||
| 635 | if (compile.max_memory) |max_memory| { | ||
| 636 | try zig_args.append(b.fmt("--max-memory={d}", .{max_memory})); | ||
| 637 | } | ||
| 638 | if (compile.shared_memory) { | ||
| 639 | try zig_args.append("--shared-memory"); | ||
| 640 | } | ||
| 641 | if (compile.global_base) |global_base| { | ||
| 642 | try zig_args.append(b.fmt("--global-base={d}", .{global_base})); | ||
| 643 | } | ||
| 644 | |||
| 645 | if (compile.wasi_exec_model) |model| { | ||
| 646 | try zig_args.append(b.fmt("-mexec-model={s}", .{@tagName(model)})); | ||
| 647 | } | ||
| 648 | if (compile.linker_script) |linker_script| { | ||
| 649 | try zig_args.append("--script"); | ||
| 650 | try zig_args.append(linker_script.getPath2(b, step)); | ||
| 651 | } | ||
| 652 | |||
| 653 | if (compile.version_script) |version_script| { | ||
| 654 | try zig_args.append("--version-script"); | ||
| 655 | try zig_args.append(version_script.getPath2(b, step)); | ||
| 656 | } | ||
| 657 | if (compile.linker_allow_undefined_version) |x| { | ||
| 658 | try zig_args.append(if (x) "--undefined-version" else "--no-undefined-version"); | ||
| 659 | } | ||
| 660 | |||
| 661 | if (compile.linker_enable_new_dtags) |enabled| { | ||
| 662 | try zig_args.append(if (enabled) "--enable-new-dtags" else "--disable-new-dtags"); | ||
| 663 | } | ||
| 664 | |||
| 665 | if (compile.kind == .@"test") { | ||
| 666 | if (compile.exec_cmd_args) |exec_cmd_args| { | ||
| 667 | for (exec_cmd_args) |cmd_arg| { | ||
| 668 | if (cmd_arg) |arg| { | ||
| 669 | try zig_args.append("--test-cmd"); | ||
| 670 | try zig_args.append(arg); | ||
| 671 | } else { | ||
| 672 | try zig_args.append("--test-cmd-bin"); | ||
| 673 | } | ||
| 674 | } | ||
| 675 | } | ||
| 676 | } | ||
| 677 | |||
| 678 | if (b.sysroot) |sysroot| { | ||
| 679 | try zig_args.appendSlice(&[_][]const u8{ "--sysroot", sysroot }); | ||
| 680 | } | ||
| 681 | |||
| 682 | // -I and -L arguments that appear after the last --mod argument apply to all modules. | ||
| 683 | const cwd: Io.Dir = .cwd(); | ||
| 684 | const io = b.graph.io; | ||
| 685 | |||
| 686 | for (b.search_prefixes.items) |search_prefix| { | ||
| 687 | var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| { | ||
| 688 | return step.fail("unable to open prefix directory '{s}': {s}", .{ | ||
| 689 | search_prefix, @errorName(err), | ||
| 690 | }); | ||
| 691 | }; | ||
| 692 | defer prefix_dir.close(io); | ||
| 693 | |||
| 694 | // Avoid passing -L and -I flags for nonexistent directories. | ||
| 695 | // This prevents a warning, that should probably be upgraded to an error in Zig's | ||
| 696 | // CLI parsing code, when the linker sees an -L directory that does not exist. | ||
| 697 | |||
| 698 | if (prefix_dir.access(io, "lib", .{})) |_| { | ||
| 699 | try zig_args.appendSlice(&.{ | ||
| 700 | "-L", b.pathJoin(&.{ search_prefix, "lib" }), | ||
| 701 | }); | ||
| 702 | } else |err| switch (err) { | ||
| 703 | error.FileNotFound => {}, | ||
| 704 | else => |e| return step.fail("unable to access '{s}/lib' directory: {s}", .{ | ||
| 705 | search_prefix, @errorName(e), | ||
| 706 | }), | ||
| 707 | } | ||
| 708 | |||
| 709 | if (prefix_dir.access(io, "include", .{})) |_| { | ||
| 710 | try zig_args.appendSlice(&.{ | ||
| 711 | "-I", b.pathJoin(&.{ search_prefix, "include" }), | ||
| 712 | }); | ||
| 713 | } else |err| switch (err) { | ||
| 714 | error.FileNotFound => {}, | ||
| 715 | else => |e| return step.fail("unable to access '{s}/include' directory: {s}", .{ | ||
| 716 | search_prefix, @errorName(e), | ||
| 717 | }), | ||
| 718 | } | ||
| 719 | } | ||
| 720 | |||
| 721 | if (compile.rc_includes != .any) { | ||
| 722 | try zig_args.append("-rcincludes"); | ||
| 723 | try zig_args.append(@tagName(compile.rc_includes)); | ||
| 724 | } | ||
| 725 | |||
| 726 | try addFlag(&zig_args, "each-lib-rpath", compile.each_lib_rpath); | ||
| 727 | |||
| 728 | if (compile.build_id orelse b.build_id) |build_id| { | ||
| 729 | try zig_args.append(switch (build_id) { | ||
| 730 | .hexstring => |hs| b.fmt("--build-id=0x{x}", .{hs.toSlice()}), | ||
| 731 | .none, .fast, .uuid, .sha1, .md5 => b.fmt("--build-id={s}", .{@tagName(build_id)}), | ||
| 732 | }); | ||
| 733 | } | ||
| 734 | |||
| 735 | const opt_zig_lib_dir = if (compile.zig_lib_dir) |dir| | ||
| 736 | dir.getPath2(b, step) | ||
| 737 | else if (b.graph.zig_lib_directory.path) |_| | ||
| 738 | b.fmt("{f}", .{b.graph.zig_lib_directory}) | ||
| 739 | else | ||
| 740 | null; | ||
| 741 | |||
| 742 | if (opt_zig_lib_dir) |zig_lib_dir| { | ||
| 743 | try zig_args.append("--zig-lib-dir"); | ||
| 744 | try zig_args.append(zig_lib_dir); | ||
| 745 | } | ||
| 746 | |||
| 747 | try addFlag(&zig_args, "PIE", compile.pie); | ||
| 748 | |||
| 749 | if (compile.lto) |lto| { | ||
| 750 | try zig_args.append(switch (lto) { | ||
| 751 | .full => "-flto=full", | ||
| 752 | .thin => "-flto=thin", | ||
| 753 | .none => "-fno-lto", | ||
| 754 | }); | ||
| 755 | } | ||
| 756 | |||
| 757 | try addFlag(&zig_args, "sanitize-coverage-trace-pc-guard", compile.sanitize_coverage_trace_pc_guard); | ||
| 758 | |||
| 759 | if (compile.subsystem) |subsystem| { | ||
| 760 | try zig_args.append("--subsystem"); | ||
| 761 | try zig_args.append(@tagName(subsystem)); | ||
| 762 | } | ||
| 763 | |||
| 764 | if (compile.mingw_unicode_entry_point) { | ||
| 765 | try zig_args.append("-municode"); | ||
| 766 | } | ||
| 767 | |||
| 768 | if (compile.error_limit) |err_limit| try zig_args.appendSlice(&.{ | ||
| 769 | "--error-limit", b.fmt("{d}", .{err_limit}), | ||
| 770 | }); | ||
| 771 | |||
| 772 | try addFlag(&zig_args, "incremental", b.graph.incremental); | ||
| 773 | |||
| 774 | try zig_args.append("--listen=-"); | ||
| 775 | |||
| 776 | // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux | ||
| 777 | // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and | ||
| 778 | // pass that to zig, e.g. via 'zig build-lib @args.rsp' | ||
| 779 | // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html | ||
| 780 | var args_length: usize = 0; | ||
| 781 | for (zig_args.items) |arg| { | ||
| 782 | args_length += arg.len + 1; // +1 to account for null terminator | ||
| 783 | } | ||
| 784 | if (args_length >= 30 * 1024) { | ||
| 785 | try b.cache_root.handle.createDirPath(io, "args"); | ||
| 786 | |||
| 787 | const args_to_escape = zig_args.items[2..]; | ||
| 788 | var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len); | ||
| 789 | arg_blk: for (args_to_escape) |arg| { | ||
| 790 | for (arg, 0..) |c, arg_idx| { | ||
| 791 | if (c == '\\' or c == '"') { | ||
| 792 | // Slow path for arguments that need to be escaped. We'll need to allocate and copy | ||
| 793 | var escaped: std.ArrayList(u8) = .empty; | ||
| 794 | try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1); | ||
| 795 | try escaped.appendSlice(arena, arg[0..arg_idx]); | ||
| 796 | for (arg[arg_idx..]) |to_escape| { | ||
| 797 | if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\'); | ||
| 798 | try escaped.append(arena, to_escape); | ||
| 799 | } | ||
| 800 | escaped_args.appendAssumeCapacity(escaped.items); | ||
| 801 | continue :arg_blk; | ||
| 802 | } | ||
| 803 | } | ||
| 804 | escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument | ||
| 805 | } | ||
| 806 | |||
| 807 | // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with | ||
| 808 | // other zig build commands running in parallel. | ||
| 809 | const partially_quoted = try std.mem.join(arena, "\" \"", escaped_args.items); | ||
| 810 | const args = try std.mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" }); | ||
| 811 | |||
| 812 | var args_hash: [Sha256.digest_length]u8 = undefined; | ||
| 813 | Sha256.hash(args, &args_hash, .{}); | ||
| 814 | var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined; | ||
| 815 | _ = try std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash}); | ||
| 816 | |||
| 817 | const args_file = "args" ++ fs.path.sep_str ++ args_hex_hash; | ||
| 818 | if (b.cache_root.handle.access(io, args_file, .{})) |_| { | ||
| 819 | // The args file is already present from a previous run. | ||
| 820 | } else |err| switch (err) { | ||
| 821 | error.FileNotFound => { | ||
| 822 | var af = b.cache_root.handle.createFileAtomic(io, args_file, .{ | ||
| 823 | .replace = false, | ||
| 824 | .make_path = true, | ||
| 825 | }) catch |e| return step.fail("failed creating tmp args file {f}{s}: {t}", .{ | ||
| 826 | b.cache_root, args_file, e, | ||
| 827 | }); | ||
| 828 | defer af.deinit(io); | ||
| 829 | |||
| 830 | af.file.writeStreamingAll(io, args) catch |e| { | ||
| 831 | return step.fail("failed writing args data to tmp file {f}{s}: {t}", .{ | ||
| 832 | b.cache_root, args_file, e, | ||
| 833 | }); | ||
| 834 | }; | ||
| 835 | // Note we can't clean up this file, not even after build | ||
| 836 | // success, because that might interfere with another build | ||
| 837 | // process that needs the same file. | ||
| 838 | af.link(io) catch |e| switch (e) { | ||
| 839 | error.PathAlreadyExists => { | ||
| 840 | // The args file was created by another concurrent build process. | ||
| 841 | }, | ||
| 842 | else => |other_err| return step.fail("failed linking tmp file {f}{s}: {t}", .{ | ||
| 843 | b.cache_root, args_file, other_err, | ||
| 844 | }), | ||
| 845 | }; | ||
| 846 | }, | ||
| 847 | else => |other_err| return other_err, | ||
| 848 | } | ||
| 849 | |||
| 850 | const resolved_args_file = try mem.concat(arena, u8, &.{ | ||
| 851 | "@", | ||
| 852 | try b.cache_root.join(arena, &.{args_file}), | ||
| 853 | }); | ||
| 854 | |||
| 855 | zig_args.shrinkRetainingCapacity(2); | ||
| 856 | try zig_args.append(resolved_args_file); | ||
| 857 | } | ||
| 858 | |||
| 859 | return try zig_args.toOwnedSlice(); | ||
| 860 | } | ||
| 861 | |||
| 862 | pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progress.Node) !Path { | ||
| 863 | c.step.result_error_msgs.clearRetainingCapacity(); | ||
| 864 | c.step.result_stderr = ""; | ||
| 865 | |||
| 866 | c.step.result_error_bundle.deinit(gpa); | ||
| 867 | c.step.result_error_bundle = std.zig.ErrorBundle.empty; | ||
| 868 | |||
| 869 | if (c.step.result_failed_command) |cmd| { | ||
| 870 | gpa.free(cmd); | ||
| 871 | c.step.result_failed_command = null; | ||
| 872 | } | ||
| 873 | |||
| 874 | const zig_args = try getZigArgs(c, true); | ||
| 875 | const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa); | ||
| 876 | return maybe_output_bin_path.?; | ||
| 877 | } | ||
| 878 | |||
| 879 | pub fn doAtomicSymLinks( | ||
| 880 | step: *Step, | ||
| 881 | output_path: []const u8, | ||
| 882 | filename_major_only: []const u8, | ||
| 883 | filename_name_only: []const u8, | ||
| 884 | ) !void { | ||
| 885 | const b = step.owner; | ||
| 886 | const io = b.graph.io; | ||
| 887 | const out_dir = fs.path.dirname(output_path) orelse "."; | ||
| 888 | const out_basename = fs.path.basename(output_path); | ||
| 889 | // sym link for libfoo.so.1 to libfoo.so.1.2.3 | ||
| 890 | const major_only_path = b.pathJoin(&.{ out_dir, filename_major_only }); | ||
| 891 | const cwd: Io.Dir = .cwd(); | ||
| 892 | cwd.symLinkAtomic(io, out_basename, major_only_path, .{}) catch |err| { | ||
| 893 | return step.fail("unable to symlink {s} -> {s}: {s}", .{ | ||
| 894 | major_only_path, out_basename, @errorName(err), | ||
| 895 | }); | ||
| 896 | }; | ||
| 897 | // sym link for libfoo.so to libfoo.so.1 | ||
| 898 | const name_only_path = b.pathJoin(&.{ out_dir, filename_name_only }); | ||
| 899 | cwd.symLinkAtomic(io, filename_major_only, name_only_path, .{}) catch |err| { | ||
| 900 | return step.fail("Unable to symlink {s} -> {s}: {s}", .{ | ||
| 901 | name_only_path, filename_major_only, @errorName(err), | ||
| 902 | }); | ||
| 903 | }; | ||
| 904 | } | ||
| 905 | |||
| 906 | fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg { | ||
| 907 | const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config"; | ||
| 908 | const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore); | ||
| 909 | var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator); | ||
| 910 | errdefer list.deinit(); | ||
| 911 | var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); | ||
| 912 | while (line_it.next()) |line| { | ||
| 913 | if (mem.trim(u8, line, " \t").len == 0) continue; | ||
| 914 | var tok_it = mem.tokenizeAny(u8, line, " \t"); | ||
| 915 | try list.append(PkgConfigPkg{ | ||
| 916 | .name = tok_it.next() orelse return error.PkgConfigInvalidOutput, | ||
| 917 | .desc = tok_it.rest(), | ||
| 918 | }); | ||
| 919 | } | ||
| 920 | return list.toOwnedSlice(); | ||
| 921 | } | ||
| 922 | |||
| 923 | fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg { | ||
| 924 | if (b.pkg_config_pkg_list) |res| { | ||
| 925 | return res; | ||
| 926 | } | ||
| 927 | var code: u8 = undefined; | ||
| 928 | if (execPkgConfigList(b, &code)) |list| { | ||
| 929 | b.pkg_config_pkg_list = list; | ||
| 930 | return list; | ||
| 931 | } else |err| { | ||
| 932 | const result = switch (err) { | ||
| 933 | error.ProcessTerminated => error.PkgConfigCrashed, | ||
| 934 | error.ExecNotSupported => error.PkgConfigFailed, | ||
| 935 | error.ExitCodeFailure => error.PkgConfigFailed, | ||
| 936 | error.FileNotFound => error.PkgConfigNotInstalled, | ||
| 937 | error.InvalidName => error.PkgConfigNotInstalled, | ||
| 938 | error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput, | ||
| 939 | else => return err, | ||
| 940 | }; | ||
| 941 | b.pkg_config_pkg_list = result; | ||
| 942 | return result; | ||
| 943 | } | ||
| 944 | } | ||
| 945 | |||
| 946 | fn addFlag(args: *std.array_list.Managed([]const u8), comptime name: []const u8, opt: ?bool) !void { | ||
| 947 | const cond = opt orelse return; | ||
| 948 | try args.ensureUnusedCapacity(1); | ||
| 949 | if (cond) { | ||
| 950 | args.appendAssumeCapacity("-f" ++ name); | ||
| 951 | } else { | ||
| 952 | args.appendAssumeCapacity("-fno-" ++ name); | ||
| 953 | } | ||
| 954 | } | ||
| 955 | |||
| 956 | const PkgConfigResult = struct { | ||
| 957 | cflags: []const []const u8, | ||
| 958 | libs: []const []const u8, | ||
| 959 | }; | ||
| 960 | |||
| 961 | /// Run pkg-config for the given library name and parse the output, returning the arguments | ||
| 962 | /// that should be passed to zig to link the given library. | ||
| 963 | fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult { | ||
| 964 | const wl_rpath_prefix = "-Wl,-rpath,"; | ||
| 965 | |||
| 966 | const b = compile.step.owner; | ||
| 967 | const arena = b.allocator; | ||
| 968 | const pkg_name = match: { | ||
| 969 | // First we have to map the library name to pkg config name. Unfortunately, | ||
| 970 | // there are several examples where this is not straightforward: | ||
| 971 | // -lSDL2 -> pkg-config sdl2 | ||
| 972 | // -lgdk-3 -> pkg-config gdk-3.0 | ||
| 973 | // -latk-1.0 -> pkg-config atk | ||
| 974 | // -lpulse -> pkg-config libpulse | ||
| 975 | const pkgs = try getPkgConfigList(b); | ||
| 976 | |||
| 977 | // Exact match means instant winner. | ||
| 978 | for (pkgs) |pkg| { | ||
| 979 | if (mem.eql(u8, pkg.name, lib_name)) { | ||
| 980 | break :match pkg.name; | ||
| 981 | } | ||
| 982 | } | ||
| 983 | |||
| 984 | // Next we'll try ignoring case. | ||
| 985 | for (pkgs) |pkg| { | ||
| 986 | if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) { | ||
| 987 | break :match pkg.name; | ||
| 988 | } | ||
| 989 | } | ||
| 990 | |||
| 991 | // Prefixed "lib" or suffixed ".0". | ||
| 992 | for (pkgs) |pkg| { | ||
| 993 | if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| { | ||
| 994 | const prefix = pkg.name[0..pos]; | ||
| 995 | const suffix = pkg.name[pos + lib_name.len ..]; | ||
| 996 | if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue; | ||
| 997 | if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue; | ||
| 998 | break :match pkg.name; | ||
| 999 | } | ||
| 1000 | } | ||
| 1001 | |||
| 1002 | // Trimming "-1.0". | ||
| 1003 | if (mem.endsWith(u8, lib_name, "-1.0")) { | ||
| 1004 | const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len]; | ||
| 1005 | for (pkgs) |pkg| { | ||
| 1006 | if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) { | ||
| 1007 | break :match pkg.name; | ||
| 1008 | } | ||
| 1009 | } | ||
| 1010 | } | ||
| 1011 | |||
| 1012 | return error.PackageNotFound; | ||
| 1013 | }; | ||
| 1014 | |||
| 1015 | var code: u8 = undefined; | ||
| 1016 | const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config"; | ||
| 1017 | const stdout = if (b.runAllowFail(&[_][]const u8{ | ||
| 1018 | pkg_config_exe, | ||
| 1019 | pkg_name, | ||
| 1020 | "--cflags", | ||
| 1021 | "--libs", | ||
| 1022 | }, &code, .ignore)) |stdout| stdout else |err| switch (err) { | ||
| 1023 | error.ProcessTerminated => return error.PkgConfigCrashed, | ||
| 1024 | error.ExecNotSupported => return error.PkgConfigFailed, | ||
| 1025 | error.ExitCodeFailure => return error.PkgConfigFailed, | ||
| 1026 | error.FileNotFound => return error.PkgConfigNotInstalled, | ||
| 1027 | else => return err, | ||
| 1028 | }; | ||
| 1029 | |||
| 1030 | var zig_cflags: std.ArrayList([]const u8) = .empty; | ||
| 1031 | defer zig_cflags.deinit(arena); | ||
| 1032 | var zig_libs: std.ArrayList([]const u8) = .empty; | ||
| 1033 | defer zig_libs.deinit(arena); | ||
| 1034 | |||
| 1035 | var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t"); | ||
| 1036 | while (arg_it.next()) |arg| { | ||
| 1037 | if (mem.eql(u8, arg, "-I")) { | ||
| 1038 | const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1039 | try zig_cflags.appendSlice(arena, &.{ "-I", dir }); | ||
| 1040 | } else if (mem.startsWith(u8, arg, "-I")) { | ||
| 1041 | try zig_cflags.append(arena, arg); | ||
| 1042 | } else if (mem.eql(u8, arg, "-L")) { | ||
| 1043 | const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1044 | try zig_libs.appendSlice(arena, &.{ "-L", dir }); | ||
| 1045 | } else if (mem.startsWith(u8, arg, "-L")) { | ||
| 1046 | try zig_libs.append(arena, arg); | ||
| 1047 | } else if (mem.eql(u8, arg, "-l")) { | ||
| 1048 | const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1049 | try zig_libs.appendSlice(arena, &.{ "-l", lib }); | ||
| 1050 | } else if (mem.startsWith(u8, arg, "-l")) { | ||
| 1051 | try zig_libs.append(arena, arg); | ||
| 1052 | } else if (mem.eql(u8, arg, "-D")) { | ||
| 1053 | const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput; | ||
| 1054 | try zig_cflags.appendSlice(arena, &.{ "-D", macro }); | ||
| 1055 | } else if (mem.startsWith(u8, arg, "-D")) { | ||
| 1056 | try zig_cflags.append(arena, arg); | ||
| 1057 | } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) { | ||
| 1058 | try zig_cflags.appendSlice(arena, &.{ "-rpath", arg[wl_rpath_prefix.len..] }); | ||
| 1059 | } else if (b.debug_pkg_config) { | ||
| 1060 | return compile.step.fail("unknown pkg-config flag '{s}'", .{arg}); | ||
| 1061 | } | ||
| 1062 | } | ||
| 1063 | |||
| 1064 | try zig_cflags.shrinkToLen(arena); | ||
| 1065 | try zig_libs.shrinkToLen(arena); | ||
| 1066 | |||
| 1067 | return .{ | ||
| 1068 | .cflags = zig_cflags.toOwnedSliceAssert(), | ||
| 1069 | .libs = zig_libs.toOwnedSliceAssert(), | ||
| 1070 | }; | ||
| 1071 | } | ||
| 1072 | |||
| 1073 | fn checkCompileErrors(compile: *Compile) !void { | ||
| 1074 | // Clear this field so that it does not get printed by the build runner. | ||
| 1075 | const actual_eb = compile.step.result_error_bundle; | ||
| 1076 | compile.step.result_error_bundle = .empty; | ||
| 1077 | |||
| 1078 | const arena = compile.step.owner.allocator; | ||
| 1079 | |||
| 1080 | const actual_errors = ae: { | ||
| 1081 | var aw: std.Io.Writer.Allocating = .init(arena); | ||
| 1082 | defer aw.deinit(); | ||
| 1083 | try actual_eb.renderToWriter(.{ | ||
| 1084 | .include_reference_trace = false, | ||
| 1085 | .include_source_line = false, | ||
| 1086 | }, &aw.writer); | ||
| 1087 | break :ae try aw.toOwnedSlice(); | ||
| 1088 | }; | ||
| 1089 | |||
| 1090 | // Render the expected lines into a string that we can compare verbatim. | ||
| 1091 | var expected_generated: std.ArrayList(u8) = .empty; | ||
| 1092 | const expect_errors = compile.expect_errors.?; | ||
| 1093 | |||
| 1094 | var actual_line_it = mem.splitScalar(u8, actual_errors, '\n'); | ||
| 1095 | |||
| 1096 | // TODO merge this with the testing.expectEqualStrings logic, and also CheckFile | ||
| 1097 | switch (expect_errors) { | ||
| 1098 | .starts_with => |expect_starts_with| { | ||
| 1099 | if (std.mem.startsWith(u8, actual_errors, expect_starts_with)) return; | ||
| 1100 | return compile.step.fail( | ||
| 1101 | \\ | ||
| 1102 | \\========= should start with: ============ | ||
| 1103 | \\{s} | ||
| 1104 | \\========= but not found: ================ | ||
| 1105 | \\{s} | ||
| 1106 | \\========================================= | ||
| 1107 | , .{ expect_starts_with, actual_errors }); | ||
| 1108 | }, | ||
| 1109 | .contains => |expect_line| { | ||
| 1110 | while (actual_line_it.next()) |actual_line| { | ||
| 1111 | if (!matchCompileError(actual_line, expect_line)) continue; | ||
| 1112 | return; | ||
| 1113 | } | ||
| 1114 | |||
| 1115 | return compile.step.fail( | ||
| 1116 | \\ | ||
| 1117 | \\========= should contain: =============== | ||
| 1118 | \\{s} | ||
| 1119 | \\========= but not found: ================ | ||
| 1120 | \\{s} | ||
| 1121 | \\========================================= | ||
| 1122 | , .{ expect_line, actual_errors }); | ||
| 1123 | }, | ||
| 1124 | .stderr_contains => |expect_line| { | ||
| 1125 | const actual_stderr: []const u8 = if (compile.step.result_error_msgs.items.len > 0) | ||
| 1126 | compile.step.result_error_msgs.items[0] | ||
| 1127 | else | ||
| 1128 | &.{}; | ||
| 1129 | compile.step.result_error_msgs.clearRetainingCapacity(); | ||
| 1130 | |||
| 1131 | var stderr_line_it = mem.splitScalar(u8, actual_stderr, '\n'); | ||
| 1132 | |||
| 1133 | while (stderr_line_it.next()) |actual_line| { | ||
| 1134 | if (!matchCompileError(actual_line, expect_line)) continue; | ||
| 1135 | return; | ||
| 1136 | } | ||
| 1137 | |||
| 1138 | return compile.step.fail( | ||
| 1139 | \\ | ||
| 1140 | \\========= should contain: =============== | ||
| 1141 | \\{s} | ||
| 1142 | \\========= but not found: ================ | ||
| 1143 | \\{s} | ||
| 1144 | \\========================================= | ||
| 1145 | , .{ expect_line, actual_stderr }); | ||
| 1146 | }, | ||
| 1147 | .exact => |expect_lines| { | ||
| 1148 | for (expect_lines) |expect_line| { | ||
| 1149 | const actual_line = actual_line_it.next() orelse { | ||
| 1150 | try expected_generated.appendSlice(arena, expect_line); | ||
| 1151 | try expected_generated.append(arena, '\n'); | ||
| 1152 | continue; | ||
| 1153 | }; | ||
| 1154 | if (matchCompileError(actual_line, expect_line)) { | ||
| 1155 | try expected_generated.appendSlice(arena, actual_line); | ||
| 1156 | try expected_generated.append(arena, '\n'); | ||
| 1157 | continue; | ||
| 1158 | } | ||
| 1159 | try expected_generated.appendSlice(arena, expect_line); | ||
| 1160 | try expected_generated.append(arena, '\n'); | ||
| 1161 | } | ||
| 1162 | |||
| 1163 | if (mem.eql(u8, expected_generated.items, actual_errors)) return; | ||
| 1164 | |||
| 1165 | return compile.step.fail( | ||
| 1166 | \\ | ||
| 1167 | \\========= expected: ===================== | ||
| 1168 | \\{s} | ||
| 1169 | \\========= but found: ==================== | ||
| 1170 | \\{s} | ||
| 1171 | \\========================================= | ||
| 1172 | , .{ expected_generated.items, actual_errors }); | ||
| 1173 | }, | ||
| 1174 | } | ||
| 1175 | } | ||
| 1176 | |||
| 1177 | fn matchCompileError(actual: []const u8, expected: []const u8) bool { | ||
| 1178 | if (mem.endsWith(u8, actual, expected)) return true; | ||
| 1179 | if (mem.startsWith(u8, expected, ":?:?: ")) { | ||
| 1180 | if (mem.endsWith(u8, actual, expected[":?:?: ".len..])) return true; | ||
| 1181 | } | ||
| 1182 | // We scan for /?/ in expected line and if there is a match, we match everything | ||
| 1183 | // up to and after /?/. | ||
| 1184 | const expected_trim = mem.trim(u8, expected, " "); | ||
| 1185 | if (mem.find(u8, expected_trim, "/?/")) |index| { | ||
| 1186 | const actual_trim = mem.trim(u8, actual, " "); | ||
| 1187 | const lhs = expected_trim[0..index]; | ||
| 1188 | const rhs = expected_trim[index + "/?/".len ..]; | ||
| 1189 | if (mem.startsWith(u8, actual_trim, lhs) and mem.endsWith(u8, actual_trim, rhs)) return true; | ||
| 1190 | } | ||
| 1191 | return false; | ||
| 1192 | } | ||
| 1193 | |||
| 1194 | fn moduleNeedsCliArg(mod: *const Module) bool { | ||
| 1195 | return for (mod.link_objects.items) |o| switch (o) { | ||
| 1196 | .c_source_file, .c_source_files, .assembly_file, .win32_resource_file => break true, | ||
| 1197 | else => continue, | ||
| 1198 | } else false; | ||
| 1199 | } | ||
| 1200 | |||
lib/compiler/maker/Step/InstallArtifact.zig deleted-96| ... | @@ -1,96 +0,0 @@ | ||
| 1 | |||
| 2 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 3 | _ = options; | ||
| 4 | const install_artifact: *InstallArtifact = @fieldParentPtr("step", step); | ||
| 5 | const b = step.owner; | ||
| 6 | const io = b.graph.io; | ||
| 7 | |||
| 8 | var all_cached = true; | ||
| 9 | |||
| 10 | if (install_artifact.dest_dir) |dest_dir| { | ||
| 11 | const full_dest_path = b.getInstallPath(dest_dir, install_artifact.dest_sub_path); | ||
| 12 | const p = try step.installFile(install_artifact.emitted_bin.?, full_dest_path); | ||
| 13 | all_cached = all_cached and p == .fresh; | ||
| 14 | |||
| 15 | if (install_artifact.dylib_symlinks) |dls| { | ||
| 16 | try Step.Compile.doAtomicSymLinks(step, full_dest_path, dls.major_only_filename, dls.name_only_filename); | ||
| 17 | } | ||
| 18 | |||
| 19 | install_artifact.artifact.installed_path = full_dest_path; | ||
| 20 | } | ||
| 21 | |||
| 22 | if (install_artifact.compiler_rt_dyn_lib_dir) |compiler_rt_dir| { | ||
| 23 | const full_compiler_rt_path = b.getInstallPath(compiler_rt_dir, install_artifact.emitted_compiler_rt_dyn_lib.?.basename(b, step)); | ||
| 24 | const p = try step.installFile(install_artifact.emitted_compiler_rt_dyn_lib.?, full_compiler_rt_path); | ||
| 25 | all_cached = all_cached and p == .fresh; | ||
| 26 | } | ||
| 27 | |||
| 28 | if (install_artifact.implib_dir) |implib_dir| { | ||
| 29 | const full_implib_path = b.getInstallPath(implib_dir, install_artifact.emitted_implib.?.basename(b, step)); | ||
| 30 | const p = try step.installFile(install_artifact.emitted_implib.?, full_implib_path); | ||
| 31 | all_cached = all_cached and p == .fresh; | ||
| 32 | } | ||
| 33 | |||
| 34 | if (install_artifact.pdb_dir) |pdb_dir| { | ||
| 35 | const full_pdb_path = b.getInstallPath(pdb_dir, install_artifact.emitted_pdb.?.basename(b, step)); | ||
| 36 | const p = try step.installFile(install_artifact.emitted_pdb.?, full_pdb_path); | ||
| 37 | all_cached = all_cached and p == .fresh; | ||
| 38 | } | ||
| 39 | |||
| 40 | if (install_artifact.h_dir) |h_dir| { | ||
| 41 | if (install_artifact.emitted_h) |emitted_h| { | ||
| 42 | const full_h_path = b.getInstallPath(h_dir, emitted_h.basename(b, step)); | ||
| 43 | const p = try step.installFile(emitted_h, full_h_path); | ||
| 44 | all_cached = all_cached and p == .fresh; | ||
| 45 | } | ||
| 46 | |||
| 47 | for (install_artifact.artifact.installed_headers.items) |installation| switch (installation) { | ||
| 48 | .file => |file| { | ||
| 49 | const full_h_path = b.getInstallPath(h_dir, file.dest_rel_path); | ||
| 50 | const p = try step.installFile(file.source, full_h_path); | ||
| 51 | all_cached = all_cached and p == .fresh; | ||
| 52 | }, | ||
| 53 | .directory => |dir| { | ||
| 54 | const src_dir_path = dir.source.getPath3(b, step); | ||
| 55 | const full_h_prefix = b.getInstallPath(h_dir, dir.dest_rel_path); | ||
| 56 | |||
| 57 | var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { | ||
| 58 | return step.fail("unable to open source directory '{f}': {s}", .{ | ||
| 59 | src_dir_path, @errorName(err), | ||
| 60 | }); | ||
| 61 | }; | ||
| 62 | defer src_dir.close(io); | ||
| 63 | |||
| 64 | var it = try src_dir.walk(b.allocator); | ||
| 65 | next_entry: while (try it.next(io)) |entry| { | ||
| 66 | for (dir.options.exclude_extensions) |ext| { | ||
| 67 | if (std.mem.endsWith(u8, entry.path, ext)) continue :next_entry; | ||
| 68 | } | ||
| 69 | if (dir.options.include_extensions) |incs| { | ||
| 70 | for (incs) |inc| { | ||
| 71 | if (std.mem.endsWith(u8, entry.path, inc)) break; | ||
| 72 | } else { | ||
| 73 | continue :next_entry; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | const full_dest_path = b.pathJoin(&.{ full_h_prefix, entry.path }); | ||
| 78 | switch (entry.kind) { | ||
| 79 | .directory => { | ||
| 80 | try Step.handleVerbose(b, .inherit, &.{ "install", "-d", full_dest_path }); | ||
| 81 | const p = try step.installDir(full_dest_path); | ||
| 82 | all_cached = all_cached and p == .existed; | ||
| 83 | }, | ||
| 84 | .file => { | ||
| 85 | const p = try step.installFile(try dir.source.join(b.allocator, entry.path), full_dest_path); | ||
| 86 | all_cached = all_cached and p == .fresh; | ||
| 87 | }, | ||
| 88 | else => continue, | ||
| 89 | } | ||
| 90 | } | ||
| 91 | }, | ||
| 92 | }; | ||
| 93 | } | ||
| 94 | |||
| 95 | step.result_cached = all_cached; | ||
| 96 | } | ||
lib/compiler/maker/Step/Run.zig deleted-2130| ... | @@ -1,2130 +0,0 @@ | ||
| 1 | const Run = @This(); | ||
| 2 | |||
| 3 | const builtin = @import("builtin"); | ||
| 4 | |||
| 5 | const std = @import("std"); | ||
| 6 | const Io = std.Io; | ||
| 7 | const Dir = std.Io.Dir; | ||
| 8 | const mem = std.mem; | ||
| 9 | const process = std.process; | ||
| 10 | const EnvMap = std.process.Environ.Map; | ||
| 11 | const assert = std.debug.assert; | ||
| 12 | const Cache = std.Build.Cache; | ||
| 13 | const Path = std.Build.Cache.Path; | ||
| 14 | |||
| 15 | const Step = @import("../Step.zig"); | ||
| 16 | |||
| 17 | /// If this is a Zig unit test binary, this tracks the names of the unit | ||
| 18 | /// tests that are also fuzz tests. Indexes cannot be used as they may | ||
| 19 | /// change between reruns. | ||
| 20 | fuzz_tests: std.ArrayList([]const u8), | ||
| 21 | cached_test_metadata: ?CachedTestMetadata = null, | ||
| 22 | |||
| 23 | /// Populated during the fuzz phase if this run step corresponds to a unit test | ||
| 24 | /// executable that contains fuzz tests. | ||
| 25 | rebuilt_executable: ?Path, | ||
| 26 | |||
| 27 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 28 | const b = step.owner; | ||
| 29 | const io = b.graph.io; | ||
| 30 | const arena = b.allocator; | ||
| 31 | const run: *Run = @fieldParentPtr("step", step); | ||
| 32 | const has_side_effects = run.hasSideEffects(); | ||
| 33 | |||
| 34 | var argv_list = std.array_list.Managed([]const u8).init(arena); | ||
| 35 | var output_placeholders = std.array_list.Managed(IndexedOutput).init(arena); | ||
| 36 | |||
| 37 | var man = b.graph.cache.obtain(); | ||
| 38 | defer man.deinit(); | ||
| 39 | |||
| 40 | if (run.environ_map) |environ_map| { | ||
| 41 | for (environ_map.keys(), environ_map.values()) |key, value| { | ||
| 42 | man.hash.addBytes(key); | ||
| 43 | man.hash.addBytes(value); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | man.hash.add(run.color); | ||
| 48 | man.hash.add(run.disable_zig_progress); | ||
| 49 | |||
| 50 | for (run.argv.items) |arg| { | ||
| 51 | switch (arg) { | ||
| 52 | .bytes => |bytes| { | ||
| 53 | try argv_list.append(bytes); | ||
| 54 | man.hash.addBytes(bytes); | ||
| 55 | }, | ||
| 56 | .lazy_path => |file| { | ||
| 57 | const file_path = file.lazy_path.getPath3(b, step); | ||
| 58 | try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) })); | ||
| 59 | man.hash.addBytes(file.prefix); | ||
| 60 | _ = try man.addFilePath(file_path, null); | ||
| 61 | }, | ||
| 62 | .decorated_directory => |dd| { | ||
| 63 | const file_path = dd.lazy_path.getPath3(b, step); | ||
| 64 | const resolved_arg = b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }); | ||
| 65 | try argv_list.append(resolved_arg); | ||
| 66 | man.hash.addBytes(resolved_arg); | ||
| 67 | }, | ||
| 68 | .file_content => |file_plp| { | ||
| 69 | const file_path = file_plp.lazy_path.getPath3(b, step); | ||
| 70 | |||
| 71 | var result: std.Io.Writer.Allocating = .init(arena); | ||
| 72 | errdefer result.deinit(); | ||
| 73 | result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory; | ||
| 74 | |||
| 75 | const file = file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}) catch |err| { | ||
| 76 | return step.fail( | ||
| 77 | "unable to open input file '{f}': {t}", | ||
| 78 | .{ file_path, err }, | ||
| 79 | ); | ||
| 80 | }; | ||
| 81 | defer file.close(io); | ||
| 82 | |||
| 83 | var buf: [1024]u8 = undefined; | ||
| 84 | var file_reader = file.reader(io, &buf); | ||
| 85 | _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { | ||
| 86 | error.ReadFailed => return step.fail( | ||
| 87 | "failed to read from '{f}': {t}", | ||
| 88 | .{ file_path, file_reader.err.? }, | ||
| 89 | ), | ||
| 90 | error.WriteFailed => return error.OutOfMemory, | ||
| 91 | }; | ||
| 92 | |||
| 93 | try argv_list.append(result.written()); | ||
| 94 | man.hash.addBytes(file_plp.prefix); | ||
| 95 | _ = try man.addFilePath(file_path, null); | ||
| 96 | }, | ||
| 97 | .artifact => |pa| { | ||
| 98 | const artifact = pa.artifact; | ||
| 99 | |||
| 100 | if (artifact.rootModuleTarget().os.tag == .windows) { | ||
| 101 | // On Windows we don't have rpaths so we have to add .dll search paths to PATH | ||
| 102 | run.addPathForDynLibs(artifact); | ||
| 103 | } | ||
| 104 | const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?; | ||
| 105 | |||
| 106 | try argv_list.append(b.fmt("{s}{s}", .{ | ||
| 107 | pa.prefix, | ||
| 108 | run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }), | ||
| 109 | })); | ||
| 110 | |||
| 111 | _ = try man.addFile(file_path, null); | ||
| 112 | }, | ||
| 113 | .output_file, .output_directory => |output| { | ||
| 114 | man.hash.addBytes(output.prefix); | ||
| 115 | man.hash.addBytes(output.basename); | ||
| 116 | // Add a placeholder into the argument list because we need the | ||
| 117 | // manifest hash to be updated with all arguments before the | ||
| 118 | // object directory is computed. | ||
| 119 | try output_placeholders.append(.{ | ||
| 120 | .index = argv_list.items.len, | ||
| 121 | .tag = arg, | ||
| 122 | .output = output, | ||
| 123 | }); | ||
| 124 | _ = try argv_list.addOne(); | ||
| 125 | }, | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | switch (run.stdin) { | ||
| 130 | .bytes => |bytes| { | ||
| 131 | man.hash.addBytes(bytes); | ||
| 132 | }, | ||
| 133 | .lazy_path => |lazy_path| { | ||
| 134 | const file_path = lazy_path.getPath2(b, step); | ||
| 135 | _ = try man.addFile(file_path, null); | ||
| 136 | }, | ||
| 137 | .none => {}, | ||
| 138 | } | ||
| 139 | |||
| 140 | if (run.captured_stdout) |captured| { | ||
| 141 | man.hash.addBytes(captured.output.basename); | ||
| 142 | man.hash.add(captured.trim_whitespace); | ||
| 143 | } | ||
| 144 | |||
| 145 | if (run.captured_stderr) |captured| { | ||
| 146 | man.hash.addBytes(captured.output.basename); | ||
| 147 | man.hash.add(captured.trim_whitespace); | ||
| 148 | } | ||
| 149 | |||
| 150 | hashStdIo(&man.hash, run.stdio); | ||
| 151 | |||
| 152 | for (run.file_inputs.items) |lazy_path| { | ||
| 153 | _ = try man.addFile(lazy_path.getPath2(b, step), null); | ||
| 154 | } | ||
| 155 | |||
| 156 | if (run.cwd) |cwd| { | ||
| 157 | const cwd_path = cwd.getPath3(b, step); | ||
| 158 | _ = man.hash.addBytes(try cwd_path.toString(arena)); | ||
| 159 | } | ||
| 160 | |||
| 161 | if (!has_side_effects and try step.cacheHitAndWatch(&man)) { | ||
| 162 | // cache hit, skip running command | ||
| 163 | const digest = man.final(); | ||
| 164 | |||
| 165 | try populateGeneratedPaths( | ||
| 166 | arena, | ||
| 167 | output_placeholders.items, | ||
| 168 | run.captured_stdout, | ||
| 169 | run.captured_stderr, | ||
| 170 | b.cache_root, | ||
| 171 | &digest, | ||
| 172 | ); | ||
| 173 | |||
| 174 | step.result_cached = true; | ||
| 175 | return; | ||
| 176 | } | ||
| 177 | |||
| 178 | const dep_output_file = run.dep_output_file orelse { | ||
| 179 | // We already know the final output paths, use them directly. | ||
| 180 | const digest = if (has_side_effects) | ||
| 181 | man.hash.final() | ||
| 182 | else | ||
| 183 | man.final(); | ||
| 184 | |||
| 185 | try populateGeneratedPaths( | ||
| 186 | arena, | ||
| 187 | output_placeholders.items, | ||
| 188 | run.captured_stdout, | ||
| 189 | run.captured_stderr, | ||
| 190 | b.cache_root, | ||
| 191 | &digest, | ||
| 192 | ); | ||
| 193 | |||
| 194 | const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; | ||
| 195 | for (output_placeholders.items) |placeholder| { | ||
| 196 | const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename }); | ||
| 197 | const output_sub_dir_path = switch (placeholder.tag) { | ||
| 198 | .output_file => Dir.path.dirname(output_sub_path).?, | ||
| 199 | .output_directory => output_sub_path, | ||
| 200 | else => unreachable, | ||
| 201 | }; | ||
| 202 | b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| { | ||
| 203 | return step.fail("unable to make path '{f}{s}': {s}", .{ | ||
| 204 | b.cache_root, output_sub_dir_path, @errorName(err), | ||
| 205 | }); | ||
| 206 | }; | ||
| 207 | const arg_output_path = run.convertPathArg(.{ | ||
| 208 | .root_dir = .cwd(), | ||
| 209 | .sub_path = placeholder.output.generated_file.getPath(), | ||
| 210 | }); | ||
| 211 | argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0) | ||
| 212 | arg_output_path | ||
| 213 | else | ||
| 214 | b.fmt("{s}{s}", .{ placeholder.output.prefix, arg_output_path }); | ||
| 215 | } | ||
| 216 | |||
| 217 | try runCommand(run, argv_list.items, has_side_effects, output_dir_path, options, null); | ||
| 218 | if (!has_side_effects) try step.writeManifestAndWatch(&man); | ||
| 219 | return; | ||
| 220 | }; | ||
| 221 | |||
| 222 | // We do not know the final output paths yet, use temp paths to run the command. | ||
| 223 | var rand_int: u64 = undefined; | ||
| 224 | io.random(@ptrCast(&rand_int)); | ||
| 225 | const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); | ||
| 226 | |||
| 227 | for (output_placeholders.items) |placeholder| { | ||
| 228 | const output_components = .{ tmp_dir_path, placeholder.output.basename }; | ||
| 229 | const output_sub_path = b.pathJoin(&output_components); | ||
| 230 | const output_sub_dir_path = switch (placeholder.tag) { | ||
| 231 | .output_file => Dir.path.dirname(output_sub_path).?, | ||
| 232 | .output_directory => output_sub_path, | ||
| 233 | else => unreachable, | ||
| 234 | }; | ||
| 235 | b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| { | ||
| 236 | return step.fail("unable to make path '{f}{s}': {s}", .{ | ||
| 237 | b.cache_root, output_sub_dir_path, @errorName(err), | ||
| 238 | }); | ||
| 239 | }; | ||
| 240 | const raw_output_path: Cache.Path = .{ | ||
| 241 | .root_dir = b.cache_root, | ||
| 242 | .sub_path = b.pathJoin(&output_components), | ||
| 243 | }; | ||
| 244 | placeholder.output.generated_file.path = raw_output_path.toString(b.graph.arena) catch @panic("OOM"); | ||
| 245 | argv_list.items[placeholder.index] = b.fmt("{s}{s}", .{ | ||
| 246 | placeholder.output.prefix, | ||
| 247 | run.convertPathArg(raw_output_path), | ||
| 248 | }); | ||
| 249 | } | ||
| 250 | |||
| 251 | try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, options, null); | ||
| 252 | |||
| 253 | const dep_file_dir = Dir.cwd(); | ||
| 254 | const dep_file_basename = dep_output_file.generated_file.getPath2(b, step); | ||
| 255 | if (has_side_effects) | ||
| 256 | try man.addDepFile(dep_file_dir, dep_file_basename) | ||
| 257 | else | ||
| 258 | try man.addDepFilePost(dep_file_dir, dep_file_basename); | ||
| 259 | |||
| 260 | const digest = if (has_side_effects) | ||
| 261 | man.hash.final() | ||
| 262 | else | ||
| 263 | man.final(); | ||
| 264 | |||
| 265 | const any_output = output_placeholders.items.len > 0 or | ||
| 266 | run.captured_stdout != null or run.captured_stderr != null; | ||
| 267 | |||
| 268 | // Rename into place | ||
| 269 | if (any_output) { | ||
| 270 | const o_sub_path = "o" ++ Dir.path.sep_str ++ &digest; | ||
| 271 | |||
| 272 | b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |err| switch (err) { | ||
| 273 | Dir.RenameError.DirNotEmpty => { | ||
| 274 | b.cache_root.handle.deleteTree(io, o_sub_path) catch |del_err| { | ||
| 275 | return step.fail("unable to remove dir '{f}'{s}: {t}", .{ | ||
| 276 | b.cache_root, tmp_dir_path, del_err, | ||
| 277 | }); | ||
| 278 | }; | ||
| 279 | b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |retry_err| { | ||
| 280 | return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{ | ||
| 281 | b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, retry_err, | ||
| 282 | }); | ||
| 283 | }; | ||
| 284 | }, | ||
| 285 | else => return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{ | ||
| 286 | b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, err, | ||
| 287 | }), | ||
| 288 | }; | ||
| 289 | } | ||
| 290 | |||
| 291 | if (!has_side_effects) try step.writeManifestAndWatch(&man); | ||
| 292 | |||
| 293 | try populateGeneratedPaths( | ||
| 294 | arena, | ||
| 295 | output_placeholders.items, | ||
| 296 | run.captured_stdout, | ||
| 297 | run.captured_stderr, | ||
| 298 | b.cache_root, | ||
| 299 | &digest, | ||
| 300 | ); | ||
| 301 | } | ||
| 302 | |||
| 303 | /// Reads stdout of a Zig test process until a termination condition is reached: | ||
| 304 | /// * A write fails, indicating the child unexpectedly closed stdin | ||
| 305 | /// * A test (or a response from the test runner) times out | ||
| 306 | /// * The wait fails, indicating the child closed stdout and stderr | ||
| 307 | fn waitZigTest( | ||
| 308 | run: *Run, | ||
| 309 | child: *process.Child, | ||
| 310 | options: Step.MakeOptions, | ||
| 311 | multi_reader: *Io.File.MultiReader, | ||
| 312 | opt_metadata: *?TestMetadata, | ||
| 313 | results: *Step.TestResults, | ||
| 314 | ) !union(enum) { | ||
| 315 | write_failed: anyerror, | ||
| 316 | no_poll: struct { | ||
| 317 | active_test_index: ?u32, | ||
| 318 | ns_elapsed: u64, | ||
| 319 | }, | ||
| 320 | timeout: struct { | ||
| 321 | active_test_index: ?u32, | ||
| 322 | ns_elapsed: u64, | ||
| 323 | }, | ||
| 324 | } { | ||
| 325 | const gpa = run.step.owner.allocator; | ||
| 326 | const arena = run.step.owner.allocator; | ||
| 327 | const io = run.step.owner.graph.io; | ||
| 328 | |||
| 329 | var sub_prog_node: ?std.Progress.Node = null; | ||
| 330 | defer if (sub_prog_node) |n| n.end(); | ||
| 331 | |||
| 332 | if (opt_metadata.*) |*md| { | ||
| 333 | // Previous unit test process died or was killed; we're continuing where it left off | ||
| 334 | requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; | ||
| 335 | } else { | ||
| 336 | // Running unit tests normally | ||
| 337 | run.fuzz_tests.clearRetainingCapacity(); | ||
| 338 | sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err }; | ||
| 339 | } | ||
| 340 | |||
| 341 | var active_test_index: ?u32 = null; | ||
| 342 | |||
| 343 | var last_update: Io.Clock.Timestamp = .now(io, .awake); | ||
| 344 | |||
| 345 | // This timeout is used when we're waiting on the test runner itself rather than a user-specified | ||
| 346 | // test. For instance, if the test runner leaves this much time between us requesting a test to | ||
| 347 | // start and it acknowledging the test starting, we terminate the child and raise an error. This | ||
| 348 | // *should* never happen, but could in theory be caused by some very unlucky IB in a test. | ||
| 349 | const response_timeout: Io.Clock.Duration = t: { | ||
| 350 | if (fuzz_context != null) break :t null; // don't timeout fuzz tests | ||
| 351 | const ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s); | ||
| 352 | break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) }; | ||
| 353 | }; | ||
| 354 | const test_timeout: ?Io.Clock.Duration = if (options.unit_test_timeout_ns) |ns| .{ | ||
| 355 | .clock = .awake, | ||
| 356 | .raw = .fromNanoseconds(ns), | ||
| 357 | } else null; | ||
| 358 | |||
| 359 | const stdout = multi_reader.reader(0); | ||
| 360 | const stderr = multi_reader.reader(1); | ||
| 361 | const Header = std.zig.Server.Message.Header; | ||
| 362 | |||
| 363 | while (true) { | ||
| 364 | const timeout: Io.Timeout = t: { | ||
| 365 | const opt_duration = if (active_test_index == null) response_timeout else test_timeout; | ||
| 366 | const duration = opt_duration orelse break :t .none; | ||
| 367 | break :t .{ .deadline = last_update.addDuration(duration) }; | ||
| 368 | }; | ||
| 369 | |||
| 370 | // This block is exited when `stdout` contains enough bytes for a `Header`. | ||
| 371 | header_ready: { | ||
| 372 | if (stdout.buffered().len >= @sizeOf(Header)) { | ||
| 373 | // We already have one, no need to poll! | ||
| 374 | break :header_ready; | ||
| 375 | } | ||
| 376 | |||
| 377 | multi_reader.fill(64, timeout) catch |err| switch (err) { | ||
| 378 | error.Timeout => return .{ .timeout = .{ | ||
| 379 | .active_test_index = active_test_index, | ||
| 380 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 381 | } }, | ||
| 382 | error.EndOfStream => return .{ .no_poll = .{ | ||
| 383 | .active_test_index = active_test_index, | ||
| 384 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 385 | } }, | ||
| 386 | else => |e| return e, | ||
| 387 | }; | ||
| 388 | |||
| 389 | continue; | ||
| 390 | } | ||
| 391 | // There is definitely a header available now -- read it. | ||
| 392 | const header = stdout.takeStruct(Header, .little) catch unreachable; | ||
| 393 | |||
| 394 | while (stdout.buffered().len < header.bytes_len) { | ||
| 395 | multi_reader.fill(64, timeout) catch |err| switch (err) { | ||
| 396 | error.Timeout => return .{ .timeout = .{ | ||
| 397 | .active_test_index = active_test_index, | ||
| 398 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 399 | } }, | ||
| 400 | error.EndOfStream => return .{ .no_poll = .{ | ||
| 401 | .active_test_index = active_test_index, | ||
| 402 | .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), | ||
| 403 | } }, | ||
| 404 | else => |e| return e, | ||
| 405 | }; | ||
| 406 | } | ||
| 407 | |||
| 408 | const body = stdout.take(header.bytes_len) catch unreachable; | ||
| 409 | var body_r: std.Io.Reader = .fixed(body); | ||
| 410 | switch (header.tag) { | ||
| 411 | .zig_version => { | ||
| 412 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) return run.step.fail( | ||
| 413 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", | ||
| 414 | .{ builtin.zig_version_string, body }, | ||
| 415 | ); | ||
| 416 | }, | ||
| 417 | .test_metadata => { | ||
| 418 | // `metadata` would only be populated if we'd already seen a `test_metadata`, but we | ||
| 419 | // only request it once (and importantly, we don't re-request it if we kill and | ||
| 420 | // restart the test runner). | ||
| 421 | assert(opt_metadata.* == null); | ||
| 422 | |||
| 423 | const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable; | ||
| 424 | results.test_count = tm_hdr.tests_len; | ||
| 425 | |||
| 426 | const names = try arena.alloc(u32, results.test_count); | ||
| 427 | for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; | ||
| 428 | |||
| 429 | const expected_panic_msgs = try arena.alloc(u32, results.test_count); | ||
| 430 | for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; | ||
| 431 | |||
| 432 | const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable; | ||
| 433 | |||
| 434 | options.progress_node.setEstimatedTotalItems(names.len); | ||
| 435 | opt_metadata.* = .{ | ||
| 436 | .string_bytes = try arena.dupe(u8, string_bytes), | ||
| 437 | .ns_per_test = try arena.alloc(u64, results.test_count), | ||
| 438 | .names = names, | ||
| 439 | .expected_panic_msgs = expected_panic_msgs, | ||
| 440 | .next_index = 0, | ||
| 441 | .prog_node = options.progress_node, | ||
| 442 | }; | ||
| 443 | @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64)); | ||
| 444 | |||
| 445 | active_test_index = null; | ||
| 446 | last_update = .now(io, .awake); | ||
| 447 | |||
| 448 | requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err }; | ||
| 449 | }, | ||
| 450 | .test_started => { | ||
| 451 | active_test_index = opt_metadata.*.?.next_index - 1; | ||
| 452 | last_update = .now(io, .awake); | ||
| 453 | }, | ||
| 454 | .test_results => { | ||
| 455 | const md = &opt_metadata.*.?; | ||
| 456 | |||
| 457 | const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable; | ||
| 458 | assert(tr_hdr.index == active_test_index); | ||
| 459 | |||
| 460 | switch (tr_hdr.flags.status) { | ||
| 461 | .pass => {}, | ||
| 462 | .skip => results.skip_count +|= 1, | ||
| 463 | .fail => results.fail_count +|= 1, | ||
| 464 | } | ||
| 465 | const leak_count = tr_hdr.flags.leak_count; | ||
| 466 | const log_err_count = tr_hdr.flags.log_err_count; | ||
| 467 | results.leak_count +|= leak_count; | ||
| 468 | results.log_err_count +|= log_err_count; | ||
| 469 | |||
| 470 | if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index)); | ||
| 471 | |||
| 472 | if (tr_hdr.flags.status == .fail) { | ||
| 473 | const name = md.testName(tr_hdr.index); | ||
| 474 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); | ||
| 475 | stderr.tossBuffered(); | ||
| 476 | if (stderr_bytes.len == 0) { | ||
| 477 | try run.step.addError("'{s}' failed without output", .{name}); | ||
| 478 | } else { | ||
| 479 | try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes }); | ||
| 480 | } | ||
| 481 | } else if (leak_count > 0) { | ||
| 482 | const name = md.testName(tr_hdr.index); | ||
| 483 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); | ||
| 484 | stderr.tossBuffered(); | ||
| 485 | try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes }); | ||
| 486 | } else if (log_err_count > 0) { | ||
| 487 | const name = md.testName(tr_hdr.index); | ||
| 488 | const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); | ||
| 489 | stderr.tossBuffered(); | ||
| 490 | try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes }); | ||
| 491 | } | ||
| 492 | |||
| 493 | active_test_index = null; | ||
| 494 | |||
| 495 | const now: Io.Clock.Timestamp = .now(io, .awake); | ||
| 496 | md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds); | ||
| 497 | last_update = now; | ||
| 498 | |||
| 499 | requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; | ||
| 500 | }, | ||
| 501 | else => {}, // ignore other messages | ||
| 502 | } | ||
| 503 | } | ||
| 504 | } | ||
| 505 | |||
| 506 | const FuzzTestRunner = struct { | ||
| 507 | run: *Run, | ||
| 508 | ctx: FuzzContext, | ||
| 509 | coverage_id: ?u64, | ||
| 510 | |||
| 511 | instances: []Instance, | ||
| 512 | /// The indexes of this are layed out such that it is effectively an array | ||
| 513 | /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr. | ||
| 514 | batch: Io.Batch, | ||
| 515 | /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter. | ||
| 516 | pending_broadcasts: std.ArrayList(u8), | ||
| 517 | broadcast: std.ArrayList(u8), | ||
| 518 | broadcast_undelivered: u32, | ||
| 519 | |||
| 520 | const Instance = struct { | ||
| 521 | child: process.Child, | ||
| 522 | message: std.ArrayListAligned(u8, .@"4"), | ||
| 523 | broadcast_written: usize, | ||
| 524 | stderr: std.ArrayList(u8), | ||
| 525 | stdin_vec: [1][]u8, | ||
| 526 | stdout_vec: [1][]u8, | ||
| 527 | stderr_vec: [1][]u8, | ||
| 528 | progress_node: std.Progress.Node, | ||
| 529 | |||
| 530 | fn messageHeader(instance: *Instance) InHeader { | ||
| 531 | assert(instance.message.items.len >= @sizeOf(InHeader)); | ||
| 532 | const header_ptr: *InHeader = @ptrCast(instance.message.items); | ||
| 533 | var header = header_ptr.*; | ||
| 534 | if (std.builtin.Endian.native != .little) { | ||
| 535 | std.mem.byteSwapAllFields(InHeader, &header); | ||
| 536 | } | ||
| 537 | return header; | ||
| 538 | } | ||
| 539 | }; | ||
| 540 | |||
| 541 | const PendingBroadcastFooter = struct { | ||
| 542 | from_id: u32, | ||
| 543 | body_len: u32, | ||
| 544 | }; | ||
| 545 | |||
| 546 | const InHeader = std.zig.Server.Message.Header; | ||
| 547 | const OutHeader = std.zig.Client.Message.Header; | ||
| 548 | |||
| 549 | const stdin_i = 0; | ||
| 550 | const stdout_i = 1; | ||
| 551 | const stderr_i = 2; | ||
| 552 | |||
| 553 | fn init( | ||
| 554 | run: *Run, | ||
| 555 | ctx: FuzzContext, | ||
| 556 | progress_node: std.Progress.Node, | ||
| 557 | spawn_options: process.SpawnOptions, | ||
| 558 | ) !FuzzTestRunner { | ||
| 559 | const step_owner = run.step.owner; | ||
| 560 | const gpa = step_owner.allocator; | ||
| 561 | const io = step_owner.graph.io; | ||
| 562 | |||
| 563 | const n_instances = switch (ctx.fuzz.mode) { | ||
| 564 | .forever => step_owner.graph.max_jobs orelse @min( | ||
| 565 | std.Thread.getCpuCount() catch 1, | ||
| 566 | (std.math.maxInt(u32) - 2) / 3, | ||
| 567 | ), | ||
| 568 | .limit => 1, | ||
| 569 | }; | ||
| 570 | const instances = try gpa.alloc(Instance, n_instances); | ||
| 571 | errdefer gpa.free(instances); | ||
| 572 | const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3); | ||
| 573 | errdefer gpa.free(batch_storage); | ||
| 574 | |||
| 575 | @memset(instances, .{ | ||
| 576 | .child = undefined, | ||
| 577 | .message = .empty, | ||
| 578 | .broadcast_written = undefined, | ||
| 579 | .stderr = .empty, | ||
| 580 | .stdin_vec = undefined, | ||
| 581 | .stdout_vec = undefined, | ||
| 582 | .stderr_vec = undefined, | ||
| 583 | .progress_node = undefined, | ||
| 584 | }); | ||
| 585 | for (0.., instances) |id, *instance| { | ||
| 586 | errdefer for (instances[0..id]) |*spawned| { | ||
| 587 | spawned.child.kill(io); | ||
| 588 | spawned.progress_node.end(); | ||
| 589 | }; | ||
| 590 | instance.child = try process.spawn(io, spawn_options); | ||
| 591 | instance.progress_node = progress_node.start("starting fuzzer", 0); | ||
| 592 | } | ||
| 593 | |||
| 594 | return .{ | ||
| 595 | .run = run, | ||
| 596 | .ctx = ctx, | ||
| 597 | .coverage_id = null, | ||
| 598 | |||
| 599 | .instances = instances, | ||
| 600 | .batch = .init(batch_storage), | ||
| 601 | .pending_broadcasts = .empty, | ||
| 602 | .broadcast = .empty, | ||
| 603 | .broadcast_undelivered = 0, | ||
| 604 | }; | ||
| 605 | } | ||
| 606 | |||
| 607 | fn deinit(f: *FuzzTestRunner) void { | ||
| 608 | const step_owner = f.run.step.owner; | ||
| 609 | const gpa = step_owner.allocator; | ||
| 610 | const io = step_owner.graph.io; | ||
| 611 | |||
| 612 | f.batch.cancel(io); | ||
| 613 | gpa.free(f.batch.storage); | ||
| 614 | var total_rss: usize = 0; | ||
| 615 | for (f.instances) |*instance| { | ||
| 616 | instance.child.kill(io); | ||
| 617 | instance.message.deinit(gpa); | ||
| 618 | instance.stderr.deinit(gpa); | ||
| 619 | instance.progress_node.end(); | ||
| 620 | total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 621 | } | ||
| 622 | f.run.step.result_peak_rss = @max(f.run.step.result_peak_rss, total_rss); | ||
| 623 | gpa.free(f.instances); | ||
| 624 | } | ||
| 625 | |||
| 626 | fn startInstances(f: *FuzzTestRunner) !void { | ||
| 627 | const step_owner = f.run.step.owner; | ||
| 628 | const io = step_owner.graph.io; | ||
| 629 | |||
| 630 | for (0.., f.instances) |id, *instance| { | ||
| 631 | const id32: u32 = @intCast(id); | ||
| 632 | (switch (f.ctx.fuzz.mode) { | ||
| 633 | .forever => sendRunFuzzTestMessage( | ||
| 634 | io, | ||
| 635 | instance.child.stdin.?, | ||
| 636 | f.run.fuzz_tests.items, | ||
| 637 | .forever, | ||
| 638 | id32, | ||
| 639 | ), | ||
| 640 | .limit => |limit| sendRunFuzzTestMessage( | ||
| 641 | io, | ||
| 642 | instance.child.stdin.?, | ||
| 643 | f.run.fuzz_tests.items, | ||
| 644 | .iterations, | ||
| 645 | limit.amount, | ||
| 646 | ), | ||
| 647 | }) catch |write_err| { | ||
| 648 | // The runner unexpectedly closed stdin, which means it crashed during initialization. | ||
| 649 | // Clean up everything and wait for the child to exit. | ||
| 650 | instance.child.stdin.?.close(io); | ||
| 651 | instance.child.stdin = null; | ||
| 652 | const term = try instance.child.wait(io); | ||
| 653 | return f.run.step.fail( | ||
| 654 | "unable to write stdin ({t}); test process unexpectedly {f}", | ||
| 655 | .{ write_err, fmtTerm(term) }, | ||
| 656 | ); | ||
| 657 | }; | ||
| 658 | |||
| 659 | try f.addStdoutRead(id32, @sizeOf(InHeader)); | ||
| 660 | try f.addStderrRead(id32); | ||
| 661 | } | ||
| 662 | } | ||
| 663 | |||
| 664 | fn listen(f: *FuzzTestRunner) !void { | ||
| 665 | const step_owner = f.run.step.owner; | ||
| 666 | const io = step_owner.graph.io; | ||
| 667 | |||
| 668 | while (true) { | ||
| 669 | try f.batch.awaitConcurrent(io, .none); | ||
| 670 | while (f.batch.next()) |completion| { | ||
| 671 | const id = completion.index / 3; | ||
| 672 | const result = completion.result; | ||
| 673 | switch (completion.index % 3) { | ||
| 674 | 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) { | ||
| 675 | // Avoid calling `instanceEos` until EndOfStream is seen with stderr so | ||
| 676 | // that all stderr is collected. | ||
| 677 | error.BrokenPipe => continue, | ||
| 678 | else => |write_e| return write_e, | ||
| 679 | }), | ||
| 680 | 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) { | ||
| 681 | // Avoid calling `instanceEos` until EndOfStream is seen with stderr so | ||
| 682 | // that all stderr is collected. | ||
| 683 | error.EndOfStream => continue, | ||
| 684 | else => |read_e| return read_e, | ||
| 685 | }), | ||
| 686 | 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) { | ||
| 687 | error.EndOfStream => return f.instanceEos(id), | ||
| 688 | else => |read_e| return read_e, | ||
| 689 | }), | ||
| 690 | else => unreachable, | ||
| 691 | } | ||
| 692 | } | ||
| 693 | } | ||
| 694 | } | ||
| 695 | |||
| 696 | fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void { | ||
| 697 | const step_owner = f.run.step.owner; | ||
| 698 | const gpa = step_owner.allocator; | ||
| 699 | const io = step_owner.graph.io; | ||
| 700 | const instance = &f.instances[id]; | ||
| 701 | |||
| 702 | instance.message.items.len += n; | ||
| 703 | const total_read = instance.message.items.len; | ||
| 704 | if (total_read < @sizeOf(InHeader)) { | ||
| 705 | try f.addStdoutRead(id, @sizeOf(InHeader)); | ||
| 706 | return; | ||
| 707 | } | ||
| 708 | |||
| 709 | const header = instance.messageHeader(); | ||
| 710 | const body = instance.message.items[@sizeOf(InHeader)..]; | ||
| 711 | if (body.len != header.bytes_len) { | ||
| 712 | try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len); | ||
| 713 | return; | ||
| 714 | } | ||
| 715 | |||
| 716 | switch (header.tag) { | ||
| 717 | .zig_version => { | ||
| 718 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) return f.run.step.fail( | ||
| 719 | "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", | ||
| 720 | .{ builtin.zig_version_string, body }, | ||
| 721 | ); | ||
| 722 | }, | ||
| 723 | .coverage_id => { | ||
| 724 | var body_r: Io.Reader = .fixed(body); | ||
| 725 | f.coverage_id = body_r.takeInt(u64, .little) catch unreachable; | ||
| 726 | const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable; | ||
| 727 | const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable; | ||
| 728 | const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable; | ||
| 729 | |||
| 730 | const fuzz = f.ctx.fuzz; | ||
| 731 | fuzz.queue_mutex.lockUncancelable(io); | ||
| 732 | defer fuzz.queue_mutex.unlock(io); | ||
| 733 | try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{ | ||
| 734 | .id = f.coverage_id.?, | ||
| 735 | .cumulative = .{ | ||
| 736 | .runs = cumulative_runs, | ||
| 737 | .unique = cumulative_unique, | ||
| 738 | .coverage = cumulative_coverage, | ||
| 739 | }, | ||
| 740 | .run = f.run, | ||
| 741 | } }); | ||
| 742 | fuzz.queue_cond.signal(io); | ||
| 743 | }, | ||
| 744 | .fuzz_start_addr => { | ||
| 745 | var body_r: Io.Reader = .fixed(body); | ||
| 746 | const fuzz = f.ctx.fuzz; | ||
| 747 | const addr = body_r.takeInt(u64, .little) catch unreachable; | ||
| 748 | |||
| 749 | fuzz.queue_mutex.lockUncancelable(io); | ||
| 750 | defer fuzz.queue_mutex.unlock(io); | ||
| 751 | try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{ | ||
| 752 | .addr = addr, | ||
| 753 | .coverage_id = f.coverage_id.?, | ||
| 754 | } }); | ||
| 755 | fuzz.queue_cond.signal(io); | ||
| 756 | }, | ||
| 757 | .fuzz_test_change => { | ||
| 758 | const test_i = std.mem.readInt(u32, body[0..4], .little); | ||
| 759 | instance.progress_node.setName(f.run.fuzz_tests.items[test_i]); | ||
| 760 | }, | ||
| 761 | .broadcast_fuzz_input => { | ||
| 762 | if (f.instances.len == 1) { | ||
| 763 | // No other processes to broadcast to. | ||
| 764 | } else if (f.broadcast_undelivered == 0) { | ||
| 765 | try f.instanceBroadcast(id, body); | ||
| 766 | } else { | ||
| 767 | const footer: PendingBroadcastFooter = .{ | ||
| 768 | .from_id = id, | ||
| 769 | .body_len = @intCast(body.len), | ||
| 770 | }; | ||
| 771 | // There is another broadcast in progress so add this one to the queue. | ||
| 772 | const size = @sizeOf(PendingBroadcastFooter) + body.len; | ||
| 773 | try f.pending_broadcasts.ensureUnusedCapacity(gpa, size); | ||
| 774 | f.pending_broadcasts.appendSliceAssumeCapacity(body); | ||
| 775 | f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer)); | ||
| 776 | } | ||
| 777 | }, | ||
| 778 | else => {}, // ignore other messages | ||
| 779 | } | ||
| 780 | |||
| 781 | instance.message.clearRetainingCapacity(); | ||
| 782 | try f.addStdoutRead(id, @sizeOf(InHeader)); | ||
| 783 | } | ||
| 784 | |||
| 785 | fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void { | ||
| 786 | const instance = &f.instances[id]; | ||
| 787 | instance.stderr.items.len += n; | ||
| 788 | try f.addStderrRead(id); | ||
| 789 | } | ||
| 790 | |||
| 791 | fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void { | ||
| 792 | const instance = &f.instances[id]; | ||
| 793 | |||
| 794 | instance.broadcast_written += n; | ||
| 795 | if (instance.broadcast_written == f.broadcast.items.len) { | ||
| 796 | f.broadcast_undelivered -= 1; | ||
| 797 | if (f.broadcast_undelivered == 0) { | ||
| 798 | try f.broadcastComplete(); | ||
| 799 | } | ||
| 800 | } else { | ||
| 801 | f.addStdinWrite(id); | ||
| 802 | } | ||
| 803 | } | ||
| 804 | |||
| 805 | fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void { | ||
| 806 | const step_owner = f.run.step.owner; | ||
| 807 | const gpa = step_owner.allocator; | ||
| 808 | const instance = &f.instances[id]; | ||
| 809 | |||
| 810 | try instance.message.ensureTotalCapacity(gpa, end); | ||
| 811 | const start = instance.message.items.len; | ||
| 812 | instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]}; | ||
| 813 | f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{ | ||
| 814 | .file = instance.child.stdout.?, | ||
| 815 | .data = &instance.stdout_vec, | ||
| 816 | } }); | ||
| 817 | } | ||
| 818 | |||
| 819 | fn addStderrRead(f: *FuzzTestRunner, id: u32) !void { | ||
| 820 | const step_owner = f.run.step.owner; | ||
| 821 | const gpa = step_owner.allocator; | ||
| 822 | const instance = &f.instances[id]; | ||
| 823 | |||
| 824 | try instance.stderr.ensureUnusedCapacity(gpa, 1); | ||
| 825 | instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()}; | ||
| 826 | f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{ | ||
| 827 | .file = instance.child.stderr.?, | ||
| 828 | .data = &instance.stderr_vec, | ||
| 829 | } }); | ||
| 830 | } | ||
| 831 | |||
| 832 | fn addStdinWrite(f: *FuzzTestRunner, id: u32) void { | ||
| 833 | const instance = &f.instances[id]; | ||
| 834 | |||
| 835 | assert(f.broadcast.items.len != instance.broadcast_written); | ||
| 836 | instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]}; | ||
| 837 | f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{ | ||
| 838 | .file = instance.child.stdin.?, | ||
| 839 | .data = &instance.stdin_vec, | ||
| 840 | } }); | ||
| 841 | } | ||
| 842 | |||
| 843 | fn instanceEos(f: *FuzzTestRunner, id: u32) !void { | ||
| 844 | const step_owner = f.run.step.owner; | ||
| 845 | const io = step_owner.graph.io; | ||
| 846 | const instance = &f.instances[id]; | ||
| 847 | |||
| 848 | instance.child.stdin.?.close(io); | ||
| 849 | instance.child.stdin = null; | ||
| 850 | const term = try instance.child.wait(io); | ||
| 851 | if (!termMatches(.{ .exited = 0 }, term)) { | ||
| 852 | f.run.step.result_stderr = try f.mergedStderr(); | ||
| 853 | try f.saveCrash(id, term); | ||
| 854 | return f.run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); | ||
| 855 | } | ||
| 856 | } | ||
| 857 | |||
| 858 | fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void { | ||
| 859 | const step = &f.run.step; | ||
| 860 | const b = step.owner; | ||
| 861 | const io = b.graph.io; | ||
| 862 | |||
| 863 | if (f.coverage_id == null) return; | ||
| 864 | |||
| 865 | // Search for the input file corresponding to the instance | ||
| 866 | const InputHeader = Build.abi.fuzz.MmapInputHeader; | ||
| 867 | var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined; | ||
| 868 | var in_r: Io.File.Reader = undefined; | ||
| 869 | var in_f: Io.File = undefined; | ||
| 870 | var in_name_buf: [12]u8 = undefined; | ||
| 871 | var in_name: []const u8 = undefined; | ||
| 872 | var i: u32 = 0; | ||
| 873 | const header: InputHeader = while (true) : ({ | ||
| 874 | if (i == std.math.maxInt(u32)) return; | ||
| 875 | i += 1; | ||
| 876 | }) { | ||
| 877 | const name_prefix = "f" ++ Io.Dir.path.sep_str ++ "in"; | ||
| 878 | in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable; | ||
| 879 | in_f = b.cache_root.handle.openFile(io, in_name, .{ | ||
| 880 | .lock = .exclusive, | ||
| 881 | .lock_nonblocking = true, | ||
| 882 | }) catch |e| switch (e) { | ||
| 883 | error.FileNotFound => return, | ||
| 884 | error.WouldBlock => continue, // Can not be from | ||
| 885 | // the crashed instance since it is still locked. | ||
| 886 | else => return step.fail("failed to open file '{f}{s}': {t}", .{ | ||
| 887 | b.cache_root, in_name, e, | ||
| 888 | }), | ||
| 889 | }; | ||
| 890 | |||
| 891 | in_r = in_f.readerStreaming(io, &in_r_buf); | ||
| 892 | const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| { | ||
| 893 | in_f.close(io); | ||
| 894 | switch (e) { | ||
| 895 | error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{ | ||
| 896 | b.cache_root, in_name, in_r.err.?, | ||
| 897 | }), | ||
| 898 | error.EndOfStream => continue, | ||
| 899 | } | ||
| 900 | }; | ||
| 901 | |||
| 902 | if (header.pc_digest == f.coverage_id.? and | ||
| 903 | header.instance_id == id and | ||
| 904 | header.test_i < f.run.fuzz_tests.items.len) | ||
| 905 | { | ||
| 906 | break header; | ||
| 907 | } | ||
| 908 | |||
| 909 | in_f.close(io); | ||
| 910 | }; | ||
| 911 | defer in_f.close(io); | ||
| 912 | |||
| 913 | // Save it to a seperate file | ||
| 914 | const crash_name = "f" ++ Io.Dir.path.sep_str ++ "crash"; | ||
| 915 | const out = b.cache_root.handle.createFile(io, crash_name, .{ | ||
| 916 | .lock = .exclusive, // Multiple run steps could have found a crash at the same time | ||
| 917 | }) catch |e| return step.fail("failed to create file '{f}{s}': {t}", .{ | ||
| 918 | b.cache_root, crash_name, e, | ||
| 919 | }); | ||
| 920 | defer out.close(io); | ||
| 921 | |||
| 922 | var out_w_buf: [512]u8 = undefined; | ||
| 923 | var out_w = out.writerStreaming(io, &out_w_buf); | ||
| 924 | _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) { | ||
| 925 | error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{ | ||
| 926 | b.cache_root, in_name, in_r.err.?, | ||
| 927 | }), | ||
| 928 | error.WriteFailed => return step.fail("failed to write file '{f}{s}': {t}", .{ | ||
| 929 | b.cache_root, crash_name, out_w.err.?, | ||
| 930 | }), | ||
| 931 | }; | ||
| 932 | |||
| 933 | return f.run.step.fail("test '{s}' {f}; input saved to '{f}{s}'", .{ | ||
| 934 | f.run.fuzz_tests.items[header.test_i], | ||
| 935 | fmtTerm(term), | ||
| 936 | b.cache_root, | ||
| 937 | crash_name, | ||
| 938 | }); | ||
| 939 | } | ||
| 940 | |||
| 941 | fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void { | ||
| 942 | assert(f.instances.len > 1); | ||
| 943 | assert(f.broadcast_undelivered == 0); // no other broadcast is progress | ||
| 944 | assert(f.broadcast.items.len == 0); | ||
| 945 | assert(from_id < f.instances.len); | ||
| 946 | |||
| 947 | const step_owner = f.run.step.owner; | ||
| 948 | const gpa = step_owner.allocator; | ||
| 949 | |||
| 950 | var out_header: OutHeader = .{ | ||
| 951 | .tag = .new_fuzz_input, | ||
| 952 | .bytes_len = @intCast(bytes.len), | ||
| 953 | }; | ||
| 954 | if (std.builtin.Endian.native != .little) { | ||
| 955 | std.mem.byteSwapAllFields(OutHeader, &out_header); | ||
| 956 | } | ||
| 957 | try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len); | ||
| 958 | f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header)); | ||
| 959 | f.broadcast.appendSliceAssumeCapacity(bytes); | ||
| 960 | |||
| 961 | f.broadcast_undelivered = @intCast(f.instances.len - 1); | ||
| 962 | for (0.., f.instances) |to_id, *instance| { | ||
| 963 | if (to_id == from_id) continue; | ||
| 964 | instance.broadcast_written = 0; | ||
| 965 | f.addStdinWrite(@intCast(to_id)); | ||
| 966 | } | ||
| 967 | } | ||
| 968 | |||
| 969 | fn broadcastComplete(f: *FuzzTestRunner) !void { | ||
| 970 | assert(f.instances.len > 1); | ||
| 971 | assert(f.broadcast_undelivered == 0); | ||
| 972 | f.broadcast.clearRetainingCapacity(); | ||
| 973 | |||
| 974 | const pending = &f.pending_broadcasts; | ||
| 975 | if (pending.items.len != 0) { | ||
| 976 | // Another broadcast is pending; copy it over to `broadcast` | ||
| 977 | |||
| 978 | const footer_len = @sizeOf(PendingBroadcastFooter); | ||
| 979 | const footer_bytes = pending.items[pending.items.len - footer_len ..]; | ||
| 980 | const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes); | ||
| 981 | pending.items.len -= footer_len; | ||
| 982 | |||
| 983 | const body = pending.items[pending.items.len - footer.body_len ..]; | ||
| 984 | try f.instanceBroadcast(footer.from_id, body); | ||
| 985 | pending.items.len -= body.len; | ||
| 986 | } | ||
| 987 | } | ||
| 988 | |||
| 989 | fn mergedStderr(f: *FuzzTestRunner) std.mem.Allocator.Error![]const u8 { | ||
| 990 | const step_owner = f.run.step.owner; | ||
| 991 | const arena = step_owner.allocator; | ||
| 992 | |||
| 993 | // Collect any available stderr | ||
| 994 | while (f.batch.next()) |completion| { | ||
| 995 | if (completion.index % 3 != 2) continue; | ||
| 996 | const len = completion.result.file_read_streaming catch continue; | ||
| 997 | f.instances[completion.index / 3].stderr.items.len += len; | ||
| 998 | } | ||
| 999 | |||
| 1000 | var stderr_len: usize = 0; | ||
| 1001 | for (f.instances) |*instance| stderr_len += instance.stderr.items.len; | ||
| 1002 | const stderr = try arena.alloc(u8, stderr_len); | ||
| 1003 | |||
| 1004 | stderr_len = 0; | ||
| 1005 | for (f.instances) |*instance| { | ||
| 1006 | @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items); | ||
| 1007 | stderr_len += instance.stderr.items.len; | ||
| 1008 | } | ||
| 1009 | return stderr; | ||
| 1010 | } | ||
| 1011 | }; | ||
| 1012 | |||
| 1013 | fn evalFuzzTest( | ||
| 1014 | run: *Run, | ||
| 1015 | spawn_options: process.SpawnOptions, | ||
| 1016 | options: Step.MakeOptions, | ||
| 1017 | fuzz_context: FuzzContext, | ||
| 1018 | ) !void { | ||
| 1019 | var f: FuzzTestRunner = try .init(run, fuzz_context, options.progress_node, spawn_options); | ||
| 1020 | defer f.deinit(); | ||
| 1021 | try f.startInstances(); | ||
| 1022 | try f.listen(); | ||
| 1023 | } | ||
| 1024 | |||
| 1025 | const StdioPollEnum = enum { stdout, stderr }; | ||
| 1026 | |||
| 1027 | fn evalZigTest( | ||
| 1028 | run: *Run, | ||
| 1029 | spawn_options: process.SpawnOptions, | ||
| 1030 | options: Step.MakeOptions, | ||
| 1031 | fuzz_context: ?FuzzContext, | ||
| 1032 | ) !void { | ||
| 1033 | if (fuzz_context != null) { | ||
| 1034 | try evalFuzzTest(run, spawn_options, options, fuzz_context.?); | ||
| 1035 | return; | ||
| 1036 | } | ||
| 1037 | |||
| 1038 | const step_owner = run.step.owner; | ||
| 1039 | const gpa = step_owner.allocator; | ||
| 1040 | const arena = step_owner.allocator; | ||
| 1041 | const io = step_owner.graph.io; | ||
| 1042 | |||
| 1043 | // We will update this every time a child runs. | ||
| 1044 | run.step.result_peak_rss = 0; | ||
| 1045 | |||
| 1046 | var test_results: Step.TestResults = .{ | ||
| 1047 | .test_count = 0, | ||
| 1048 | .skip_count = 0, | ||
| 1049 | .fail_count = 0, | ||
| 1050 | .crash_count = 0, | ||
| 1051 | .timeout_count = 0, | ||
| 1052 | .leak_count = 0, | ||
| 1053 | .log_err_count = 0, | ||
| 1054 | }; | ||
| 1055 | var test_metadata: ?TestMetadata = null; | ||
| 1056 | |||
| 1057 | while (true) { | ||
| 1058 | var child = try process.spawn(io, spawn_options); | ||
| 1059 | var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; | ||
| 1060 | var multi_reader: Io.File.MultiReader = undefined; | ||
| 1061 | multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? }); | ||
| 1062 | var child_killed = false; | ||
| 1063 | defer if (!child_killed) { | ||
| 1064 | child.kill(io); | ||
| 1065 | multi_reader.deinit(); | ||
| 1066 | run.step.result_peak_rss = @max( | ||
| 1067 | run.step.result_peak_rss, | ||
| 1068 | child.resource_usage_statistics.getMaxRss() orelse 0, | ||
| 1069 | ); | ||
| 1070 | }; | ||
| 1071 | |||
| 1072 | switch (try waitZigTest( | ||
| 1073 | run, | ||
| 1074 | &child, | ||
| 1075 | options, | ||
| 1076 | &multi_reader, | ||
| 1077 | &test_metadata, | ||
| 1078 | &test_results, | ||
| 1079 | )) { | ||
| 1080 | .write_failed => |err| { | ||
| 1081 | // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured | ||
| 1082 | // all available stderr to make our error output as useful as possible. | ||
| 1083 | const stderr_fr = multi_reader.fileReader(1); | ||
| 1084 | while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) { | ||
| 1085 | error.ReadFailed => return stderr_fr.err.?, | ||
| 1086 | error.EndOfStream => {}, | ||
| 1087 | } | ||
| 1088 | run.step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered()); | ||
| 1089 | |||
| 1090 | // Clean up everything and wait for the child to exit. | ||
| 1091 | child.stdin.?.close(io); | ||
| 1092 | child.stdin = null; | ||
| 1093 | multi_reader.deinit(); | ||
| 1094 | child_killed = true; | ||
| 1095 | const term = try child.wait(io); | ||
| 1096 | run.step.result_peak_rss = @max( | ||
| 1097 | run.step.result_peak_rss, | ||
| 1098 | child.resource_usage_statistics.getMaxRss() orelse 0, | ||
| 1099 | ); | ||
| 1100 | |||
| 1101 | // The individual unit test results are irrelevant: the test runner itself broke! | ||
| 1102 | // Fail immediately without populating `s.test_results`. | ||
| 1103 | return run.step.fail("unable to write stdin ({t}); test process unexpectedly {f}", .{ err, fmtTerm(term) }); | ||
| 1104 | }, | ||
| 1105 | .no_poll => |no_poll| { | ||
| 1106 | // This might be a success (we requested exit and the child dutifully closed stdout) or | ||
| 1107 | // a crash of some kind. Either way, the child will terminate by itself -- wait for it. | ||
| 1108 | const stderr_reader = multi_reader.reader(1); | ||
| 1109 | const stderr_owned = try arena.dupe(u8, stderr_reader.buffered()); | ||
| 1110 | |||
| 1111 | // Clean up everything and wait for the child to exit. | ||
| 1112 | child.stdin.?.close(io); | ||
| 1113 | child.stdin = null; | ||
| 1114 | multi_reader.deinit(); | ||
| 1115 | child_killed = true; | ||
| 1116 | const term = try child.wait(io); | ||
| 1117 | run.step.result_peak_rss = @max( | ||
| 1118 | run.step.result_peak_rss, | ||
| 1119 | child.resource_usage_statistics.getMaxRss() orelse 0, | ||
| 1120 | ); | ||
| 1121 | |||
| 1122 | if (no_poll.active_test_index) |test_index| { | ||
| 1123 | // A test was running, so this is definitely a crash. Report it against that | ||
| 1124 | // test, and continue to the next test. | ||
| 1125 | test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed; | ||
| 1126 | test_results.crash_count += 1; | ||
| 1127 | try run.step.addError("'{s}' {f}{s}{s}", .{ | ||
| 1128 | test_metadata.?.testName(test_index), | ||
| 1129 | fmtTerm(term), | ||
| 1130 | if (stderr_owned.len != 0) " with stderr:\n" else "", | ||
| 1131 | std.mem.trim(u8, stderr_owned, "\n"), | ||
| 1132 | }); | ||
| 1133 | continue; | ||
| 1134 | } | ||
| 1135 | |||
| 1136 | // Report an error if the child terminated uncleanly or if we were still trying to run more tests. | ||
| 1137 | run.step.result_stderr = stderr_owned; | ||
| 1138 | const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); | ||
| 1139 | if (!tests_done or !termMatches(.{ .exited = 0 }, term)) { | ||
| 1140 | // The individual unit test results are irrelevant: the test runner itself broke! | ||
| 1141 | // Fail immediately without populating `s.test_results`. | ||
| 1142 | return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); | ||
| 1143 | } | ||
| 1144 | |||
| 1145 | // We're done with all of the tests! Commit the test results and return. | ||
| 1146 | run.step.test_results = test_results; | ||
| 1147 | if (test_metadata) |tm| { | ||
| 1148 | run.cached_test_metadata = tm.toCachedTestMetadata(); | ||
| 1149 | if (options.web_server) |ws| { | ||
| 1150 | if (run.step.owner.graph.time_report) { | ||
| 1151 | ws.updateTimeReportRunTest( | ||
| 1152 | run, | ||
| 1153 | &run.cached_test_metadata.?, | ||
| 1154 | tm.ns_per_test, | ||
| 1155 | ); | ||
| 1156 | } | ||
| 1157 | } | ||
| 1158 | } | ||
| 1159 | return; | ||
| 1160 | }, | ||
| 1161 | .timeout => |timeout| { | ||
| 1162 | const stderr_reader = multi_reader.reader(1); | ||
| 1163 | const stderr = stderr_reader.buffered(); | ||
| 1164 | stderr_reader.tossBuffered(); | ||
| 1165 | if (timeout.active_test_index) |test_index| { | ||
| 1166 | // A test was running. Report the timeout against that test, and continue on to | ||
| 1167 | // the next test. | ||
| 1168 | test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed; | ||
| 1169 | test_results.timeout_count += 1; | ||
| 1170 | try run.step.addError("'{s}' timed out after {f}{s}{s}", .{ | ||
| 1171 | test_metadata.?.testName(test_index), | ||
| 1172 | Io.Duration{ .nanoseconds = timeout.ns_elapsed }, | ||
| 1173 | if (stderr.len != 0) " with stderr:\n" else "", | ||
| 1174 | std.mem.trim(u8, stderr, "\n"), | ||
| 1175 | }); | ||
| 1176 | continue; | ||
| 1177 | } | ||
| 1178 | // Just log an error and let the child be killed. | ||
| 1179 | run.step.result_stderr = try arena.dupe(u8, stderr); | ||
| 1180 | // The individual unit test results in `results` are irrelevant: the test runner | ||
| 1181 | // is broken! Fail immediately without populating `s.test_results`. | ||
| 1182 | return run.step.fail("test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }}); | ||
| 1183 | }, | ||
| 1184 | } | ||
| 1185 | comptime unreachable; | ||
| 1186 | } | ||
| 1187 | } | ||
| 1188 | |||
| 1189 | const TestMetadata = struct { | ||
| 1190 | names: []const u32, | ||
| 1191 | ns_per_test: []u64, | ||
| 1192 | expected_panic_msgs: []const u32, | ||
| 1193 | string_bytes: []const u8, | ||
| 1194 | next_index: u32, | ||
| 1195 | prog_node: std.Progress.Node, | ||
| 1196 | |||
| 1197 | fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { | ||
| 1198 | return .{ | ||
| 1199 | .names = tm.names, | ||
| 1200 | .string_bytes = tm.string_bytes, | ||
| 1201 | }; | ||
| 1202 | } | ||
| 1203 | |||
| 1204 | fn testName(tm: TestMetadata, index: u32) []const u8 { | ||
| 1205 | return tm.toCachedTestMetadata().testName(index); | ||
| 1206 | } | ||
| 1207 | }; | ||
| 1208 | |||
| 1209 | pub const CachedTestMetadata = struct { | ||
| 1210 | names: []const u32, | ||
| 1211 | string_bytes: []const u8, | ||
| 1212 | |||
| 1213 | pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 { | ||
| 1214 | return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0); | ||
| 1215 | } | ||
| 1216 | }; | ||
| 1217 | |||
| 1218 | fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { | ||
| 1219 | while (metadata.next_index < metadata.names.len) { | ||
| 1220 | const i = metadata.next_index; | ||
| 1221 | metadata.next_index += 1; | ||
| 1222 | |||
| 1223 | if (metadata.expected_panic_msgs[i] != 0) continue; | ||
| 1224 | |||
| 1225 | const name = metadata.testName(i); | ||
| 1226 | if (sub_prog_node.*) |n| n.end(); | ||
| 1227 | sub_prog_node.* = metadata.prog_node.start(name, 0); | ||
| 1228 | |||
| 1229 | try sendRunTestMessage(io, in, .run_test, i); | ||
| 1230 | return; | ||
| 1231 | } else { | ||
| 1232 | metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done | ||
| 1233 | try sendMessage(io, in, .exit); | ||
| 1234 | } | ||
| 1235 | } | ||
| 1236 | |||
| 1237 | fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { | ||
| 1238 | const header: std.zig.Client.Message.Header = .{ | ||
| 1239 | .tag = tag, | ||
| 1240 | .bytes_len = 0, | ||
| 1241 | }; | ||
| 1242 | var w = file.writerStreaming(io, &.{}); | ||
| 1243 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 1244 | error.WriteFailed => return w.err.?, | ||
| 1245 | }; | ||
| 1246 | } | ||
| 1247 | |||
| 1248 | fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void { | ||
| 1249 | const header: std.zig.Client.Message.Header = .{ | ||
| 1250 | .tag = tag, | ||
| 1251 | .bytes_len = 4, | ||
| 1252 | }; | ||
| 1253 | var w = file.writerStreaming(io, &.{}); | ||
| 1254 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 1255 | error.WriteFailed => return w.err.?, | ||
| 1256 | }; | ||
| 1257 | w.interface.writeInt(u32, index, .little) catch |err| switch (err) { | ||
| 1258 | error.WriteFailed => return w.err.?, | ||
| 1259 | }; | ||
| 1260 | } | ||
| 1261 | |||
| 1262 | fn sendRunFuzzTestMessage( | ||
| 1263 | io: Io, | ||
| 1264 | file: Io.File, | ||
| 1265 | test_names: []const []const u8, | ||
| 1266 | kind: std.Build.abi.fuzz.LimitKind, | ||
| 1267 | amount_or_instance: u64, | ||
| 1268 | ) !void { | ||
| 1269 | const header: std.zig.Client.Message.Header = .{ | ||
| 1270 | .tag = .start_fuzzing, | ||
| 1271 | .bytes_len = 1 + 8 + 4 + count: { | ||
| 1272 | var c: u32 = @intCast(test_names.len * 4); | ||
| 1273 | for (test_names) |name| { | ||
| 1274 | c += @intCast(name.len); | ||
| 1275 | } | ||
| 1276 | break :count c; | ||
| 1277 | }, | ||
| 1278 | }; | ||
| 1279 | var w = file.writerStreaming(io, &.{}); | ||
| 1280 | w.interface.writeStruct(header, .little) catch |err| switch (err) { | ||
| 1281 | error.WriteFailed => return w.err.?, | ||
| 1282 | }; | ||
| 1283 | w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) { | ||
| 1284 | error.WriteFailed => return w.err.?, | ||
| 1285 | }; | ||
| 1286 | w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) { | ||
| 1287 | error.WriteFailed => return w.err.?, | ||
| 1288 | }; | ||
| 1289 | w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) { | ||
| 1290 | error.WriteFailed => return w.err.?, | ||
| 1291 | }; | ||
| 1292 | for (test_names) |test_name| { | ||
| 1293 | w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) { | ||
| 1294 | error.WriteFailed => return w.err.?, | ||
| 1295 | }; | ||
| 1296 | w.interface.writeAll(test_name) catch |err| switch (err) { | ||
| 1297 | error.WriteFailed => return w.err.?, | ||
| 1298 | }; | ||
| 1299 | } | ||
| 1300 | } | ||
| 1301 | |||
| 1302 | fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult { | ||
| 1303 | const b = run.step.owner; | ||
| 1304 | const io = b.graph.io; | ||
| 1305 | const arena = b.allocator; | ||
| 1306 | const gpa = b.allocator; | ||
| 1307 | |||
| 1308 | var child = try process.spawn(io, spawn_options); | ||
| 1309 | defer child.kill(io); | ||
| 1310 | |||
| 1311 | switch (run.stdin) { | ||
| 1312 | .bytes => |bytes| { | ||
| 1313 | child.stdin.?.writeStreamingAll(io, bytes) catch |err| { | ||
| 1314 | return run.step.fail("unable to write stdin: {t}", .{err}); | ||
| 1315 | }; | ||
| 1316 | child.stdin.?.close(io); | ||
| 1317 | child.stdin = null; | ||
| 1318 | }, | ||
| 1319 | .lazy_path => |lazy_path| { | ||
| 1320 | const path = lazy_path.getPath3(b, &run.step); | ||
| 1321 | const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| { | ||
| 1322 | return run.step.fail("unable to open stdin file: {t}", .{err}); | ||
| 1323 | }; | ||
| 1324 | defer file.close(io); | ||
| 1325 | // TODO https://github.com/ziglang/zig/issues/23955 | ||
| 1326 | var read_buffer: [1024]u8 = undefined; | ||
| 1327 | var file_reader = file.reader(io, &read_buffer); | ||
| 1328 | var write_buffer: [1024]u8 = undefined; | ||
| 1329 | var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer); | ||
| 1330 | _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { | ||
| 1331 | error.ReadFailed => return run.step.fail("failed to read from {f}: {t}", .{ | ||
| 1332 | path, file_reader.err.?, | ||
| 1333 | }), | ||
| 1334 | error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{ | ||
| 1335 | stdin_writer.err.?, | ||
| 1336 | }), | ||
| 1337 | }; | ||
| 1338 | stdin_writer.interface.flush() catch |err| switch (err) { | ||
| 1339 | error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{ | ||
| 1340 | stdin_writer.err.?, | ||
| 1341 | }), | ||
| 1342 | }; | ||
| 1343 | child.stdin.?.close(io); | ||
| 1344 | child.stdin = null; | ||
| 1345 | }, | ||
| 1346 | .none => {}, | ||
| 1347 | } | ||
| 1348 | |||
| 1349 | var stdout_bytes: ?[]const u8 = null; | ||
| 1350 | var stderr_bytes: ?[]const u8 = null; | ||
| 1351 | |||
| 1352 | if (child.stdout) |stdout| { | ||
| 1353 | if (child.stderr) |stderr| { | ||
| 1354 | var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; | ||
| 1355 | var multi_reader: Io.File.MultiReader = undefined; | ||
| 1356 | multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr }); | ||
| 1357 | defer multi_reader.deinit(); | ||
| 1358 | |||
| 1359 | const stdout_reader = multi_reader.reader(0); | ||
| 1360 | const stderr_reader = multi_reader.reader(1); | ||
| 1361 | |||
| 1362 | while (multi_reader.fill(64, .none)) |_| { | ||
| 1363 | if (run.stdio_limit.toInt()) |limit| { | ||
| 1364 | if (stdout_reader.buffered().len > limit) | ||
| 1365 | return error.StdoutStreamTooLong; | ||
| 1366 | if (stderr_reader.buffered().len > limit) | ||
| 1367 | return error.StderrStreamTooLong; | ||
| 1368 | } | ||
| 1369 | } else |err| switch (err) { | ||
| 1370 | error.Timeout => unreachable, | ||
| 1371 | error.EndOfStream => {}, | ||
| 1372 | else => |e| return e, | ||
| 1373 | } | ||
| 1374 | |||
| 1375 | try multi_reader.checkAnyError(); | ||
| 1376 | |||
| 1377 | // TODO: this string can leak since alloc below can return error. | ||
| 1378 | stdout_bytes = try multi_reader.toOwnedSlice(0); | ||
| 1379 | // TODO: this string can leak since its allocated using gpa and `try child.wait(io)` below can fail. | ||
| 1380 | stderr_bytes = try multi_reader.toOwnedSlice(1); | ||
| 1381 | } else { | ||
| 1382 | var stdout_reader = stdout.readerStreaming(io, &.{}); | ||
| 1383 | stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { | ||
| 1384 | error.OutOfMemory => |e| return e, | ||
| 1385 | error.ReadFailed => return stdout_reader.err.?, | ||
| 1386 | error.StreamTooLong => return error.StdoutStreamTooLong, | ||
| 1387 | }; | ||
| 1388 | } | ||
| 1389 | } else if (child.stderr) |stderr| { | ||
| 1390 | var stderr_reader = stderr.readerStreaming(io, &.{}); | ||
| 1391 | stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { | ||
| 1392 | error.OutOfMemory => |e| return e, | ||
| 1393 | error.ReadFailed => return stderr_reader.err.?, | ||
| 1394 | error.StreamTooLong => return error.StderrStreamTooLong, | ||
| 1395 | }; | ||
| 1396 | } | ||
| 1397 | |||
| 1398 | if (stderr_bytes) |bytes| if (bytes.len > 0) { | ||
| 1399 | // Treat stderr as an error message. | ||
| 1400 | const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) { | ||
| 1401 | .check => |checks| !checksContainStderr(checks.items), | ||
| 1402 | else => true, | ||
| 1403 | }; | ||
| 1404 | if (stderr_is_diagnostic) { | ||
| 1405 | run.step.result_stderr = bytes; | ||
| 1406 | } | ||
| 1407 | }; | ||
| 1408 | |||
| 1409 | run.step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0; | ||
| 1410 | |||
| 1411 | return .{ | ||
| 1412 | .term = try child.wait(io), | ||
| 1413 | .stdout = stdout_bytes, | ||
| 1414 | .stderr = stderr_bytes, | ||
| 1415 | }; | ||
| 1416 | } | ||
| 1417 | |||
| 1418 | const IndexedOutput = struct { | ||
| 1419 | index: usize, | ||
| 1420 | tag: @typeInfo(Arg).@"union".tag_type.?, | ||
| 1421 | output: *Output, | ||
| 1422 | }; | ||
| 1423 | |||
| 1424 | pub fn rerunInFuzzMode( | ||
| 1425 | run: *Run, | ||
| 1426 | fuzz: *std.Build.Fuzz, | ||
| 1427 | prog_node: std.Progress.Node, | ||
| 1428 | ) !void { | ||
| 1429 | const step = &run.step; | ||
| 1430 | const b = step.owner; | ||
| 1431 | const io = b.graph.io; | ||
| 1432 | const arena = b.allocator; | ||
| 1433 | var argv_list: std.ArrayList([]const u8) = .empty; | ||
| 1434 | for (run.argv.items) |arg| { | ||
| 1435 | switch (arg) { | ||
| 1436 | .bytes => |bytes| { | ||
| 1437 | try argv_list.append(arena, bytes); | ||
| 1438 | }, | ||
| 1439 | .lazy_path => |file| { | ||
| 1440 | const file_path = file.lazy_path.getPath3(b, step); | ||
| 1441 | try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) })); | ||
| 1442 | }, | ||
| 1443 | .decorated_directory => |dd| { | ||
| 1444 | const file_path = dd.lazy_path.getPath3(b, step); | ||
| 1445 | try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix })); | ||
| 1446 | }, | ||
| 1447 | .file_content => |file_plp| { | ||
| 1448 | const file_path = file_plp.lazy_path.getPath3(b, step); | ||
| 1449 | |||
| 1450 | var result: std.Io.Writer.Allocating = .init(arena); | ||
| 1451 | errdefer result.deinit(); | ||
| 1452 | result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory; | ||
| 1453 | |||
| 1454 | const file = try file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}); | ||
| 1455 | defer file.close(io); | ||
| 1456 | |||
| 1457 | var buf: [1024]u8 = undefined; | ||
| 1458 | var file_reader = file.reader(io, &buf); | ||
| 1459 | _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { | ||
| 1460 | error.ReadFailed => return file_reader.err.?, | ||
| 1461 | error.WriteFailed => return error.OutOfMemory, | ||
| 1462 | }; | ||
| 1463 | |||
| 1464 | try argv_list.append(arena, result.written()); | ||
| 1465 | }, | ||
| 1466 | .artifact => |pa| { | ||
| 1467 | const artifact = pa.artifact; | ||
| 1468 | const file_path: []const u8 = p: { | ||
| 1469 | if (artifact == run.producer.?) break :p b.fmt("{f}", .{run.rebuilt_executable.?}); | ||
| 1470 | break :p artifact.installed_path orelse artifact.generated_bin.?.path.?; | ||
| 1471 | }; | ||
| 1472 | try argv_list.append(arena, b.fmt("{s}{s}", .{ | ||
| 1473 | pa.prefix, | ||
| 1474 | run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }), | ||
| 1475 | })); | ||
| 1476 | }, | ||
| 1477 | .output_file, .output_directory => unreachable, | ||
| 1478 | } | ||
| 1479 | } | ||
| 1480 | |||
| 1481 | if (run.step.result_failed_command) |cmd| { | ||
| 1482 | fuzz.gpa.free(cmd); | ||
| 1483 | run.step.result_failed_command = null; | ||
| 1484 | } | ||
| 1485 | |||
| 1486 | const has_side_effects = false; | ||
| 1487 | var rand_int: u64 = undefined; | ||
| 1488 | io.random(@ptrCast(&rand_int)); | ||
| 1489 | const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); | ||
| 1490 | try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{ | ||
| 1491 | .progress_node = prog_node, | ||
| 1492 | .watch = undefined, // not used by `runCommand` | ||
| 1493 | .web_server = null, // only needed for time reports | ||
| 1494 | .unit_test_timeout_ns = null, // don't time out fuzz tests for now | ||
| 1495 | .gpa = fuzz.gpa, | ||
| 1496 | }, .{ | ||
| 1497 | .fuzz = fuzz, | ||
| 1498 | }); | ||
| 1499 | } | ||
| 1500 | |||
| 1501 | fn populateGeneratedPaths( | ||
| 1502 | arena: std.mem.Allocator, | ||
| 1503 | output_placeholders: []const IndexedOutput, | ||
| 1504 | captured_stdout: ?*CapturedStdIo, | ||
| 1505 | captured_stderr: ?*CapturedStdIo, | ||
| 1506 | cache_root: Cache.Directory, | ||
| 1507 | digest: *const Cache.HexDigest, | ||
| 1508 | ) !void { | ||
| 1509 | for (output_placeholders) |placeholder| { | ||
| 1510 | placeholder.output.generated_file.path = try cache_root.join(arena, &.{ | ||
| 1511 | "o", digest, placeholder.output.basename, | ||
| 1512 | }); | ||
| 1513 | } | ||
| 1514 | |||
| 1515 | if (captured_stdout) |captured| { | ||
| 1516 | captured.output.generated_file.path = try cache_root.join(arena, &.{ | ||
| 1517 | "o", digest, captured.output.basename, | ||
| 1518 | }); | ||
| 1519 | } | ||
| 1520 | |||
| 1521 | if (captured_stderr) |captured| { | ||
| 1522 | captured.output.generated_file.path = try cache_root.join(arena, &.{ | ||
| 1523 | "o", digest, captured.output.basename, | ||
| 1524 | }); | ||
| 1525 | } | ||
| 1526 | } | ||
| 1527 | |||
| 1528 | fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { | ||
| 1529 | if (term) |t| switch (t) { | ||
| 1530 | .exited => |code| try w.print("exited with code {d}", .{code}), | ||
| 1531 | .signal => |sig| try w.print("terminated with signal {t}", .{sig}), | ||
| 1532 | .stopped => |sig| try w.print("stopped with signal {t}", .{sig}), | ||
| 1533 | .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), | ||
| 1534 | } else { | ||
| 1535 | try w.writeAll("exited with any code"); | ||
| 1536 | } | ||
| 1537 | } | ||
| 1538 | fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) { | ||
| 1539 | return .{ .data = term }; | ||
| 1540 | } | ||
| 1541 | |||
| 1542 | const FuzzContext = struct { | ||
| 1543 | fuzz: *std.Build.Fuzz, | ||
| 1544 | }; | ||
| 1545 | |||
| 1546 | fn runCommand( | ||
| 1547 | run: *Run, | ||
| 1548 | argv: []const []const u8, | ||
| 1549 | has_side_effects: bool, | ||
| 1550 | output_dir_path: []const u8, | ||
| 1551 | options: Step.MakeOptions, | ||
| 1552 | fuzz_context: ?FuzzContext, | ||
| 1553 | ) !void { | ||
| 1554 | const step = &run.step; | ||
| 1555 | const b = step.owner; | ||
| 1556 | const arena = b.allocator; | ||
| 1557 | const gpa = options.gpa; | ||
| 1558 | const io = b.graph.io; | ||
| 1559 | |||
| 1560 | const cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, step) } else .inherit; | ||
| 1561 | |||
| 1562 | try step.handleChildProcUnsupported(); | ||
| 1563 | try Step.handleVerbose2(step.owner, cwd, run.environ_map, argv); | ||
| 1564 | |||
| 1565 | const allow_skip = switch (run.stdio) { | ||
| 1566 | .check, .zig_test => run.skip_foreign_checks, | ||
| 1567 | else => false, | ||
| 1568 | }; | ||
| 1569 | |||
| 1570 | var interp_argv = std.array_list.Managed([]const u8).init(b.allocator); | ||
| 1571 | defer interp_argv.deinit(); | ||
| 1572 | |||
| 1573 | var environ_map: EnvMap = env: { | ||
| 1574 | const orig = run.environ_map orelse &b.graph.environ_map; | ||
| 1575 | break :env try orig.clone(gpa); | ||
| 1576 | }; | ||
| 1577 | defer environ_map.deinit(); | ||
| 1578 | |||
| 1579 | const opt_generic_result = spawnChildAndCollect(run, argv, &environ_map, has_side_effects, options, fuzz_context) catch |err| term: { | ||
| 1580 | // InvalidExe: cpu arch mismatch | ||
| 1581 | // FileNotFound: can happen with a wrong dynamic linker path | ||
| 1582 | if (err == error.InvalidExe or err == error.FileNotFound) interpret: { | ||
| 1583 | // TODO: learn the target from the binary directly rather than from | ||
| 1584 | // relying on it being a Compile step. This will make this logic | ||
| 1585 | // work even for the edge case that the binary was produced by a | ||
| 1586 | // third party. | ||
| 1587 | const exe = switch (run.argv.items[0]) { | ||
| 1588 | .artifact => |exe| exe.artifact, | ||
| 1589 | else => break :interpret, | ||
| 1590 | }; | ||
| 1591 | switch (exe.kind) { | ||
| 1592 | .exe, .@"test" => {}, | ||
| 1593 | else => break :interpret, | ||
| 1594 | } | ||
| 1595 | |||
| 1596 | const root_target = exe.rootModuleTarget(); | ||
| 1597 | const need_cross_libc = exe.is_linking_libc and | ||
| 1598 | (root_target.isGnuLibC() or (root_target.isMuslLibC() and exe.linkage == .dynamic)); | ||
| 1599 | const other_target = exe.root_module.resolved_target.?.result; | ||
| 1600 | switch (std.zig.system.getExternalExecutor(io, &b.graph.host.result, &other_target, .{ | ||
| 1601 | .qemu_fixes_dl = need_cross_libc and b.libc_runtimes_dir != null, | ||
| 1602 | .link_libc = exe.is_linking_libc, | ||
| 1603 | })) { | ||
| 1604 | .native, .rosetta => { | ||
| 1605 | if (allow_skip) return error.MakeSkipped; | ||
| 1606 | break :interpret; | ||
| 1607 | }, | ||
| 1608 | .wine => |bin_name| { | ||
| 1609 | if (b.enable_wine) { | ||
| 1610 | try interp_argv.append(bin_name); | ||
| 1611 | try interp_argv.appendSlice(argv); | ||
| 1612 | |||
| 1613 | // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but | ||
| 1614 | // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired. | ||
| 1615 | if (environ_map.get("WINEDEBUG") == null) { | ||
| 1616 | try environ_map.put("WINEDEBUG", "-all"); | ||
| 1617 | } | ||
| 1618 | } else { | ||
| 1619 | return failForeign(run, "-fwine", argv[0], exe); | ||
| 1620 | } | ||
| 1621 | }, | ||
| 1622 | .qemu => |bin_name| { | ||
| 1623 | if (b.enable_qemu) { | ||
| 1624 | try interp_argv.append(bin_name); | ||
| 1625 | |||
| 1626 | if (need_cross_libc) { | ||
| 1627 | if (b.libc_runtimes_dir) |dir| { | ||
| 1628 | try interp_argv.append("-L"); | ||
| 1629 | try interp_argv.append(b.pathJoin(&.{ | ||
| 1630 | dir, | ||
| 1631 | try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple( | ||
| 1632 | b.allocator, | ||
| 1633 | root_target.cpu.arch, | ||
| 1634 | root_target.os.tag, | ||
| 1635 | root_target.abi, | ||
| 1636 | ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple( | ||
| 1637 | b.allocator, | ||
| 1638 | root_target.cpu.arch, | ||
| 1639 | root_target.abi, | ||
| 1640 | ) else unreachable, | ||
| 1641 | })); | ||
| 1642 | } else return failForeign(run, "--libc-runtimes", argv[0], exe); | ||
| 1643 | } | ||
| 1644 | |||
| 1645 | try interp_argv.appendSlice(argv); | ||
| 1646 | } else return failForeign(run, "-fqemu", argv[0], exe); | ||
| 1647 | }, | ||
| 1648 | .darling => |bin_name| { | ||
| 1649 | if (b.enable_darling) { | ||
| 1650 | try interp_argv.append(bin_name); | ||
| 1651 | try interp_argv.appendSlice(argv); | ||
| 1652 | } else { | ||
| 1653 | return failForeign(run, "-fdarling", argv[0], exe); | ||
| 1654 | } | ||
| 1655 | }, | ||
| 1656 | .wasmtime => |bin_name| { | ||
| 1657 | if (b.enable_wasmtime) { | ||
| 1658 | try interp_argv.append(bin_name); | ||
| 1659 | try interp_argv.append("--dir=."); | ||
| 1660 | // Wasmtime doeesn't inherit environment variables from the parent process | ||
| 1661 | // by default. '-S inherit-env' was added in Wasmtime version 20. | ||
| 1662 | try interp_argv.append("-Sinherit-env"); | ||
| 1663 | try interp_argv.append(argv[0]); | ||
| 1664 | try interp_argv.appendSlice(argv[1..]); | ||
| 1665 | } else { | ||
| 1666 | return failForeign(run, "-fwasmtime", argv[0], exe); | ||
| 1667 | } | ||
| 1668 | }, | ||
| 1669 | .bad_dl => |foreign_dl| { | ||
| 1670 | if (allow_skip) return error.MakeSkipped; | ||
| 1671 | |||
| 1672 | const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)"; | ||
| 1673 | |||
| 1674 | return step.fail( | ||
| 1675 | \\the host system is unable to execute binaries from the target | ||
| 1676 | \\ because the host dynamic linker is '{s}', | ||
| 1677 | \\ while the target dynamic linker is '{s}'. | ||
| 1678 | \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step | ||
| 1679 | , .{ host_dl, foreign_dl }); | ||
| 1680 | }, | ||
| 1681 | .bad_os_or_cpu => { | ||
| 1682 | if (allow_skip) return error.MakeSkipped; | ||
| 1683 | |||
| 1684 | const host_name = try b.graph.host.result.zigTriple(b.allocator); | ||
| 1685 | const foreign_name = try root_target.zigTriple(b.allocator); | ||
| 1686 | |||
| 1687 | return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{ | ||
| 1688 | host_name, foreign_name, | ||
| 1689 | }); | ||
| 1690 | }, | ||
| 1691 | } | ||
| 1692 | |||
| 1693 | if (root_target.os.tag == .windows) { | ||
| 1694 | // On Windows we don't have rpaths so we have to add .dll search paths to PATH | ||
| 1695 | run.addPathForDynLibs(exe); | ||
| 1696 | } | ||
| 1697 | |||
| 1698 | gpa.free(step.result_failed_command.?); | ||
| 1699 | step.result_failed_command = null; | ||
| 1700 | try Step.handleVerbose2(step.owner, cwd, run.environ_map, interp_argv.items); | ||
| 1701 | |||
| 1702 | break :term spawnChildAndCollect(run, interp_argv.items, &environ_map, has_side_effects, options, fuzz_context) catch |e| { | ||
| 1703 | if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped; | ||
| 1704 | if (e == error.MakeFailed) return error.MakeFailed; // error already reported | ||
| 1705 | return step.fail("unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e }); | ||
| 1706 | }; | ||
| 1707 | } | ||
| 1708 | if (err == error.MakeFailed) return error.MakeFailed; // error already reported | ||
| 1709 | |||
| 1710 | return step.fail("failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err }); | ||
| 1711 | }; | ||
| 1712 | |||
| 1713 | const generic_result = opt_generic_result orelse { | ||
| 1714 | assert(run.stdio == .zig_test); | ||
| 1715 | // Specific errors have already been reported, and test results are populated. All we need | ||
| 1716 | // to do is report step failure if any test failed. | ||
| 1717 | if (!step.test_results.isSuccess()) return error.MakeFailed; | ||
| 1718 | return; | ||
| 1719 | }; | ||
| 1720 | |||
| 1721 | assert(fuzz_context == null); | ||
| 1722 | assert(run.stdio != .zig_test); | ||
| 1723 | |||
| 1724 | // Capture stdout and stderr to GeneratedFile objects. | ||
| 1725 | const Stream = struct { | ||
| 1726 | captured: ?*CapturedStdIo, | ||
| 1727 | bytes: ?[]const u8, | ||
| 1728 | }; | ||
| 1729 | for ([_]Stream{ | ||
| 1730 | .{ | ||
| 1731 | .captured = run.captured_stdout, | ||
| 1732 | .bytes = generic_result.stdout, | ||
| 1733 | }, | ||
| 1734 | .{ | ||
| 1735 | .captured = run.captured_stderr, | ||
| 1736 | .bytes = generic_result.stderr, | ||
| 1737 | }, | ||
| 1738 | }) |stream| { | ||
| 1739 | if (stream.captured) |captured| { | ||
| 1740 | const output_components = .{ output_dir_path, captured.output.basename }; | ||
| 1741 | const output_path = try b.cache_root.join(arena, &output_components); | ||
| 1742 | captured.output.generated_file.path = output_path; | ||
| 1743 | |||
| 1744 | const sub_path = b.pathJoin(&output_components); | ||
| 1745 | const sub_path_dirname = Dir.path.dirname(sub_path).?; | ||
| 1746 | b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| { | ||
| 1747 | return step.fail("unable to make path '{f}{s}': {s}", .{ | ||
| 1748 | b.cache_root, sub_path_dirname, @errorName(err), | ||
| 1749 | }); | ||
| 1750 | }; | ||
| 1751 | const data = switch (captured.trim_whitespace) { | ||
| 1752 | .none => stream.bytes.?, | ||
| 1753 | .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace), | ||
| 1754 | .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace), | ||
| 1755 | .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace), | ||
| 1756 | }; | ||
| 1757 | b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = data }) catch |err| { | ||
| 1758 | return step.fail("unable to write file '{f}{s}': {s}", .{ | ||
| 1759 | b.cache_root, sub_path, @errorName(err), | ||
| 1760 | }); | ||
| 1761 | }; | ||
| 1762 | } | ||
| 1763 | } | ||
| 1764 | |||
| 1765 | switch (run.stdio) { | ||
| 1766 | .zig_test => unreachable, | ||
| 1767 | .check => |checks| for (checks.items) |check| switch (check) { | ||
| 1768 | .expect_stderr_exact => |expected_bytes| { | ||
| 1769 | if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) { | ||
| 1770 | return step.fail( | ||
| 1771 | \\========= expected this stderr: ========= | ||
| 1772 | \\{s} | ||
| 1773 | \\========= but found: ==================== | ||
| 1774 | \\{s} | ||
| 1775 | , .{ | ||
| 1776 | expected_bytes, | ||
| 1777 | generic_result.stderr.?, | ||
| 1778 | }); | ||
| 1779 | } | ||
| 1780 | }, | ||
| 1781 | .expect_stderr_match => |match| { | ||
| 1782 | if (mem.find(u8, generic_result.stderr.?, match) == null) { | ||
| 1783 | return step.fail( | ||
| 1784 | \\========= expected to find in stderr: ========= | ||
| 1785 | \\{s} | ||
| 1786 | \\========= but stderr does not contain it: ===== | ||
| 1787 | \\{s} | ||
| 1788 | , .{ | ||
| 1789 | match, | ||
| 1790 | generic_result.stderr.?, | ||
| 1791 | }); | ||
| 1792 | } | ||
| 1793 | }, | ||
| 1794 | .expect_stdout_exact => |expected_bytes| { | ||
| 1795 | if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) { | ||
| 1796 | return step.fail( | ||
| 1797 | \\========= expected this stdout: ========= | ||
| 1798 | \\{s} | ||
| 1799 | \\========= but found: ==================== | ||
| 1800 | \\{s} | ||
| 1801 | , .{ | ||
| 1802 | expected_bytes, | ||
| 1803 | generic_result.stdout.?, | ||
| 1804 | }); | ||
| 1805 | } | ||
| 1806 | }, | ||
| 1807 | .expect_stdout_match => |match| { | ||
| 1808 | if (mem.find(u8, generic_result.stdout.?, match) == null) { | ||
| 1809 | return step.fail( | ||
| 1810 | \\========= expected to find in stdout: ========= | ||
| 1811 | \\{s} | ||
| 1812 | \\========= but stdout does not contain it: ===== | ||
| 1813 | \\{s} | ||
| 1814 | , .{ | ||
| 1815 | match, | ||
| 1816 | generic_result.stdout.?, | ||
| 1817 | }); | ||
| 1818 | } | ||
| 1819 | }, | ||
| 1820 | .expect_term => |expected_term| { | ||
| 1821 | if (!termMatches(expected_term, generic_result.term)) { | ||
| 1822 | return step.fail("process {f} (expected {f})", .{ | ||
| 1823 | fmtTerm(generic_result.term), | ||
| 1824 | fmtTerm(expected_term), | ||
| 1825 | }); | ||
| 1826 | } | ||
| 1827 | }, | ||
| 1828 | }, | ||
| 1829 | else => { | ||
| 1830 | // On failure, report captured stderr like normal standard error output. | ||
| 1831 | const bad_exit = switch (generic_result.term) { | ||
| 1832 | .exited => |code| code != 0, | ||
| 1833 | .signal, .stopped, .unknown => true, | ||
| 1834 | }; | ||
| 1835 | if (bad_exit) { | ||
| 1836 | if (generic_result.stderr) |bytes| { | ||
| 1837 | run.step.result_stderr = bytes; | ||
| 1838 | } | ||
| 1839 | } | ||
| 1840 | |||
| 1841 | try step.handleChildProcessTerm(generic_result.term); | ||
| 1842 | }, | ||
| 1843 | } | ||
| 1844 | } | ||
| 1845 | |||
| 1846 | const EvalGenericResult = struct { | ||
| 1847 | term: process.Child.Term, | ||
| 1848 | stdout: ?[]const u8, | ||
| 1849 | stderr: ?[]const u8, | ||
| 1850 | }; | ||
| 1851 | |||
| 1852 | fn spawnChildAndCollect( | ||
| 1853 | run: *Run, | ||
| 1854 | argv: []const []const u8, | ||
| 1855 | environ_map: *EnvMap, | ||
| 1856 | has_side_effects: bool, | ||
| 1857 | options: Step.MakeOptions, | ||
| 1858 | fuzz_context: ?FuzzContext, | ||
| 1859 | ) !?EvalGenericResult { | ||
| 1860 | const b = run.step.owner; | ||
| 1861 | const graph = b.graph; | ||
| 1862 | const io = graph.io; | ||
| 1863 | |||
| 1864 | if (fuzz_context != null) { | ||
| 1865 | assert(!has_side_effects); | ||
| 1866 | assert(run.stdio == .zig_test); | ||
| 1867 | } | ||
| 1868 | |||
| 1869 | const child_cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, &run.step) } else .inherit; | ||
| 1870 | |||
| 1871 | // If an error occurs, it's caused by this command: | ||
| 1872 | assert(run.step.result_failed_command == null); | ||
| 1873 | run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{ | ||
| 1874 | .child = environ_map, | ||
| 1875 | .parent = &graph.environ_map, | ||
| 1876 | }, argv); | ||
| 1877 | |||
| 1878 | var spawn_options: process.SpawnOptions = .{ | ||
| 1879 | .argv = argv, | ||
| 1880 | .cwd = child_cwd, | ||
| 1881 | .environ_map = environ_map, | ||
| 1882 | .request_resource_usage_statistics = true, | ||
| 1883 | .stdin = if (run.stdin != .none) s: { | ||
| 1884 | assert(run.stdio != .inherit); | ||
| 1885 | break :s .pipe; | ||
| 1886 | } else switch (run.stdio) { | ||
| 1887 | .infer_from_args => if (has_side_effects) .inherit else .ignore, | ||
| 1888 | .inherit => .inherit, | ||
| 1889 | .check => .ignore, | ||
| 1890 | .zig_test => .pipe, | ||
| 1891 | }, | ||
| 1892 | .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) { | ||
| 1893 | .infer_from_args => if (has_side_effects) .inherit else .ignore, | ||
| 1894 | .inherit => .inherit, | ||
| 1895 | .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore, | ||
| 1896 | .zig_test => .pipe, | ||
| 1897 | }, | ||
| 1898 | .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) { | ||
| 1899 | .infer_from_args => if (has_side_effects) .inherit else .pipe, | ||
| 1900 | .inherit => .inherit, | ||
| 1901 | .check => .pipe, | ||
| 1902 | .zig_test => .pipe, | ||
| 1903 | }, | ||
| 1904 | }; | ||
| 1905 | |||
| 1906 | if (run.stdio == .zig_test) { | ||
| 1907 | const started: Io.Clock.Timestamp = .now(io, .awake); | ||
| 1908 | const result = evalZigTest(run, spawn_options, options, fuzz_context) catch |err| switch (err) { | ||
| 1909 | error.Canceled => |e| return e, | ||
| 1910 | else => |e| e, | ||
| 1911 | }; | ||
| 1912 | run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); | ||
| 1913 | try result; | ||
| 1914 | return null; | ||
| 1915 | } else { | ||
| 1916 | const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; | ||
| 1917 | if (!run.disable_zig_progress and !inherit) { | ||
| 1918 | spawn_options.progress_node = options.progress_node; | ||
| 1919 | } | ||
| 1920 | const terminal_mode: Io.Terminal.Mode = if (inherit) m: { | ||
| 1921 | const stderr = try io.lockStderr(&.{}, graph.stderr_mode); | ||
| 1922 | break :m stderr.terminal_mode; | ||
| 1923 | } else .no_color; | ||
| 1924 | defer if (inherit) io.unlockStderr(); | ||
| 1925 | try setColorEnvironmentVariables(run, environ_map, terminal_mode); | ||
| 1926 | |||
| 1927 | const started: Io.Clock.Timestamp = .now(io, .awake); | ||
| 1928 | const result = evalGeneric(run, spawn_options) catch |err| switch (err) { | ||
| 1929 | error.Canceled => |e| return e, | ||
| 1930 | else => |e| e, | ||
| 1931 | }; | ||
| 1932 | run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); | ||
| 1933 | return try result; | ||
| 1934 | } | ||
| 1935 | } | ||
| 1936 | |||
| 1937 | fn hashStdIo(hh: *Cache.HashHelper, stdio: StdIo) void { | ||
| 1938 | switch (stdio) { | ||
| 1939 | .infer_from_args, .inherit, .zig_test => {}, | ||
| 1940 | .check => |checks| for (checks.items) |check| { | ||
| 1941 | hh.add(@as(std.meta.Tag(StdIo.Check), check)); | ||
| 1942 | switch (check) { | ||
| 1943 | .expect_stderr_exact, | ||
| 1944 | .expect_stderr_match, | ||
| 1945 | .expect_stdout_exact, | ||
| 1946 | .expect_stdout_match, | ||
| 1947 | => |s| hh.addBytes(s), | ||
| 1948 | |||
| 1949 | .expect_term => |term| { | ||
| 1950 | hh.add(@as(std.meta.Tag(process.Child.Term), term)); | ||
| 1951 | switch (term) { | ||
| 1952 | inline .exited, .signal, .stopped => |x| hh.add(x), | ||
| 1953 | .unknown => |x| hh.add(x), | ||
| 1954 | } | ||
| 1955 | }, | ||
| 1956 | } | ||
| 1957 | }, | ||
| 1958 | } | ||
| 1959 | } | ||
| 1960 | fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool { | ||
| 1961 | return if (expected) |e| switch (e) { | ||
| 1962 | .exited => |expected_code| switch (actual) { | ||
| 1963 | .exited => |actual_code| expected_code == actual_code, | ||
| 1964 | else => false, | ||
| 1965 | }, | ||
| 1966 | .signal => |expected_sig| switch (actual) { | ||
| 1967 | .signal => |actual_sig| expected_sig == actual_sig, | ||
| 1968 | else => false, | ||
| 1969 | }, | ||
| 1970 | .stopped => |expected_sig| switch (actual) { | ||
| 1971 | .stopped => |actual_sig| expected_sig == actual_sig, | ||
| 1972 | else => false, | ||
| 1973 | }, | ||
| 1974 | .unknown => |expected_code| switch (actual) { | ||
| 1975 | .unknown => |actual_code| expected_code == actual_code, | ||
| 1976 | else => false, | ||
| 1977 | }, | ||
| 1978 | } else switch (actual) { | ||
| 1979 | .exited => true, | ||
| 1980 | else => false, | ||
| 1981 | }; | ||
| 1982 | } | ||
| 1983 | |||
| 1984 | fn setColorEnvironmentVariables(run: *Run, environ_map: *EnvMap, terminal_mode: Io.Terminal.Mode) !void { | ||
| 1985 | color: switch (run.color) { | ||
| 1986 | .manual => {}, | ||
| 1987 | .enable => { | ||
| 1988 | try environ_map.put("CLICOLOR_FORCE", "1"); | ||
| 1989 | _ = environ_map.swapRemove("NO_COLOR"); | ||
| 1990 | }, | ||
| 1991 | .disable => { | ||
| 1992 | try environ_map.put("NO_COLOR", "1"); | ||
| 1993 | _ = environ_map.swapRemove("CLICOLOR_FORCE"); | ||
| 1994 | }, | ||
| 1995 | .inherit => switch (terminal_mode) { | ||
| 1996 | .no_color, .windows_api => continue :color .disable, | ||
| 1997 | .escape_codes => continue :color .enable, | ||
| 1998 | }, | ||
| 1999 | .auto => { | ||
| 2000 | const capture_stderr = run.captured_stderr != null or switch (run.stdio) { | ||
| 2001 | .check => |checks| checksContainStderr(checks.items), | ||
| 2002 | .infer_from_args, .inherit, .zig_test => false, | ||
| 2003 | }; | ||
| 2004 | if (capture_stderr) { | ||
| 2005 | continue :color .disable; | ||
| 2006 | } else { | ||
| 2007 | continue :color .inherit; | ||
| 2008 | } | ||
| 2009 | }, | ||
| 2010 | } | ||
| 2011 | } | ||
| 2012 | |||
| 2013 | fn checksContainStdout(checks: []const StdIo.Check) bool { | ||
| 2014 | for (checks) |check| switch (check) { | ||
| 2015 | .expect_stderr_exact, | ||
| 2016 | .expect_stderr_match, | ||
| 2017 | .expect_term, | ||
| 2018 | => continue, | ||
| 2019 | |||
| 2020 | .expect_stdout_exact, | ||
| 2021 | .expect_stdout_match, | ||
| 2022 | => return true, | ||
| 2023 | }; | ||
| 2024 | return false; | ||
| 2025 | } | ||
| 2026 | |||
| 2027 | fn checksContainStderr(checks: []const StdIo.Check) bool { | ||
| 2028 | for (checks) |check| switch (check) { | ||
| 2029 | .expect_stdout_exact, | ||
| 2030 | .expect_stdout_match, | ||
| 2031 | .expect_term, | ||
| 2032 | => continue, | ||
| 2033 | |||
| 2034 | .expect_stderr_exact, | ||
| 2035 | .expect_stderr_match, | ||
| 2036 | => return true, | ||
| 2037 | }; | ||
| 2038 | return false; | ||
| 2039 | } | ||
| 2040 | |||
| 2041 | /// Returns whether the Run step has side effects *other than* updating the output arguments. | ||
| 2042 | fn hasSideEffects(run: Run) bool { | ||
| 2043 | if (run.has_side_effects) return true; | ||
| 2044 | return switch (run.stdio) { | ||
| 2045 | .infer_from_args => !run.hasAnyOutputArgs(), | ||
| 2046 | .inherit => true, | ||
| 2047 | .check => false, | ||
| 2048 | .zig_test => false, | ||
| 2049 | }; | ||
| 2050 | } | ||
| 2051 | |||
| 2052 | fn hasAnyOutputArgs(run: Run) bool { | ||
| 2053 | if (run.captured_stdout != null) return true; | ||
| 2054 | if (run.captured_stderr != null) return true; | ||
| 2055 | for (run.argv.items) |arg| switch (arg) { | ||
| 2056 | .output_file, .output_directory => return true, | ||
| 2057 | else => continue, | ||
| 2058 | }; | ||
| 2059 | return false; | ||
| 2060 | } | ||
| 2061 | |||
| 2062 | /// If `path` is cwd-relative, make it relative to the cwd of the child instead. | ||
| 2063 | /// | ||
| 2064 | /// Whenever a path is included in the argv of a child, it should be put through this function first | ||
| 2065 | /// to make sure the child doesn't see paths relative to a cwd other than its own. | ||
| 2066 | fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 { | ||
| 2067 | const b = run.step.owner; | ||
| 2068 | const graph = b.graph; | ||
| 2069 | const arena = graph.arena; | ||
| 2070 | |||
| 2071 | const path_str = path.toString(arena) catch @panic("OOM"); | ||
| 2072 | if (Dir.path.isAbsolute(path_str)) { | ||
| 2073 | // Absolute paths don't need changing. | ||
| 2074 | return path_str; | ||
| 2075 | } | ||
| 2076 | const child_cwd_rel: []const u8 = rel: { | ||
| 2077 | const child_lazy_cwd = run.cwd orelse break :rel path_str; | ||
| 2078 | const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(arena) catch @panic("OOM"); | ||
| 2079 | // Convert it from relative to *our* cwd, to relative to the *child's* cwd. | ||
| 2080 | break :rel Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str) catch @panic("OOM"); | ||
| 2081 | }; | ||
| 2082 | // Not every path can be made relative, e.g. if the path and the child cwd are on different | ||
| 2083 | // disk designators on Windows. In that case, `relative` will return an absolute path which we can | ||
| 2084 | // just return. | ||
| 2085 | if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel; | ||
| 2086 | |||
| 2087 | // We're not done yet. In some cases this path must be prefixed with './': | ||
| 2088 | // * On POSIX, the executable name cannot be a single component like 'foo' | ||
| 2089 | // * Some executables might treat a leading '-' like a flag, which we must avoid | ||
| 2090 | // There's no harm in it, so just *always* apply this prefix. | ||
| 2091 | return Dir.path.join(arena, &.{ ".", child_cwd_rel }) catch @panic("OOM"); | ||
| 2092 | } | ||
| 2093 | |||
| 2094 | fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void { | ||
| 2095 | const b = run.step.owner; | ||
| 2096 | const compiles = artifact.getCompileDependencies(true); | ||
| 2097 | for (compiles) |compile| { | ||
| 2098 | if (compile.root_module.resolved_target.?.result.os.tag == .windows and | ||
| 2099 | compile.isDynamicLibrary()) | ||
| 2100 | { | ||
| 2101 | addPathDir(run, Dir.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?); | ||
| 2102 | } | ||
| 2103 | } | ||
| 2104 | } | ||
| 2105 | |||
| 2106 | fn failForeign( | ||
| 2107 | run: *Run, | ||
| 2108 | suggested_flag: []const u8, | ||
| 2109 | argv0: []const u8, | ||
| 2110 | exe: *Step.Compile, | ||
| 2111 | ) error{ MakeFailed, MakeSkipped, OutOfMemory } { | ||
| 2112 | switch (run.stdio) { | ||
| 2113 | .check, .zig_test => { | ||
| 2114 | if (run.skip_foreign_checks) | ||
| 2115 | return error.MakeSkipped; | ||
| 2116 | |||
| 2117 | const b = run.step.owner; | ||
| 2118 | const host_name = try b.graph.host.result.zigTriple(b.allocator); | ||
| 2119 | const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator); | ||
| 2120 | |||
| 2121 | return run.step.fail( | ||
| 2122 | \\unable to spawn foreign binary '{s}' ({s}) on host system ({s}) | ||
| 2123 | \\ consider using {s} or enabling skip_foreign_checks in the Run step | ||
| 2124 | , .{ argv0, foreign_name, host_name, suggested_flag }); | ||
| 2125 | }, | ||
| 2126 | else => { | ||
| 2127 | return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0}); | ||
| 2128 | }, | ||
| 2129 | } | ||
| 2130 | } | ||
lib/compiler/maker/Step/WriteFile.zig deleted-206| ... | @@ -1,206 +0,0 @@ | ||
| 1 | |||
| 2 | fn make(step: *Step, options: Step.MakeOptions) !void { | ||
| 3 | _ = options; | ||
| 4 | const b = step.owner; | ||
| 5 | const graph = b.graph; | ||
| 6 | const io = graph.io; | ||
| 7 | const arena = b.allocator; | ||
| 8 | const gpa = graph.cache.gpa; | ||
| 9 | const write_file: *WriteFile = @fieldParentPtr("step", step); | ||
| 10 | |||
| 11 | const open_dir_cache = try arena.alloc(Io.Dir, write_file.directories.items.len); | ||
| 12 | var open_dirs_count: usize = 0; | ||
| 13 | defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]); | ||
| 14 | |||
| 15 | switch (write_file.mode) { | ||
| 16 | .whole_cached => { | ||
| 17 | step.clearWatchInputs(); | ||
| 18 | |||
| 19 | // The cache is used here not really as a way to speed things up - because writing | ||
| 20 | // the data to a file would probably be very fast - but as a way to find a canonical | ||
| 21 | // location to put build artifacts. | ||
| 22 | |||
| 23 | // If, for example, a hard-coded path was used as the location to put WriteFile | ||
| 24 | // files, then two WriteFiles executing in parallel might clobber each other. | ||
| 25 | |||
| 26 | var man = b.graph.cache.obtain(); | ||
| 27 | defer man.deinit(); | ||
| 28 | |||
| 29 | for (write_file.files.items) |file| { | ||
| 30 | man.hash.addBytes(file.sub_path); | ||
| 31 | |||
| 32 | switch (file.contents) { | ||
| 33 | .bytes => |bytes| { | ||
| 34 | man.hash.addBytes(bytes); | ||
| 35 | }, | ||
| 36 | .copy => |lazy_path| { | ||
| 37 | const path = lazy_path.getPath3(b, step); | ||
| 38 | _ = try man.addFilePath(path, null); | ||
| 39 | try step.addWatchInput(lazy_path); | ||
| 40 | }, | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| { | ||
| 45 | man.hash.addBytes(dir.sub_path); | ||
| 46 | for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext); | ||
| 47 | if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc); | ||
| 48 | |||
| 49 | const need_derived_inputs = try step.addDirectoryWatchInput(dir.source); | ||
| 50 | const src_dir_path = dir.source.getPath3(b, step); | ||
| 51 | |||
| 52 | var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { | ||
| 53 | return step.fail("unable to open source directory '{f}': {s}", .{ | ||
| 54 | src_dir_path, @errorName(err), | ||
| 55 | }); | ||
| 56 | }; | ||
| 57 | open_dir_cache_elem.* = src_dir; | ||
| 58 | open_dirs_count += 1; | ||
| 59 | |||
| 60 | var it = try src_dir.walk(gpa); | ||
| 61 | defer it.deinit(); | ||
| 62 | while (try it.next(io)) |entry| { | ||
| 63 | if (!dir.options.pathIncluded(entry.path)) continue; | ||
| 64 | |||
| 65 | switch (entry.kind) { | ||
| 66 | .directory => { | ||
| 67 | if (need_derived_inputs) { | ||
| 68 | const entry_path = try src_dir_path.join(arena, entry.path); | ||
| 69 | try step.addDirectoryWatchInputFromPath(entry_path); | ||
| 70 | } | ||
| 71 | }, | ||
| 72 | .file => { | ||
| 73 | const entry_path = try src_dir_path.join(arena, entry.path); | ||
| 74 | _ = try man.addFilePath(entry_path, null); | ||
| 75 | }, | ||
| 76 | else => continue, | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | if (try step.cacheHit(&man)) { | ||
| 82 | const digest = man.final(); | ||
| 83 | write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest }); | ||
| 84 | assert(step.result_cached); | ||
| 85 | return; | ||
| 86 | } | ||
| 87 | |||
| 88 | const digest = man.final(); | ||
| 89 | const cache_path = "o" ++ Dir.path.sep_str ++ digest; | ||
| 90 | |||
| 91 | write_file.generated_directory.path = try b.cache_root.join(arena, &.{cache_path}); | ||
| 92 | |||
| 93 | try operate(write_file, open_dir_cache, .{ | ||
| 94 | .root_dir = b.cache_root, | ||
| 95 | .sub_path = cache_path, | ||
| 96 | }); | ||
| 97 | |||
| 98 | try step.writeManifest(&man); | ||
| 99 | }, | ||
| 100 | .tmp => { | ||
| 101 | step.result_cached = false; | ||
| 102 | |||
| 103 | var rand_int: u64 = undefined; | ||
| 104 | io.random(@ptrCast(&rand_int)); | ||
| 105 | const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); | ||
| 106 | |||
| 107 | write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path}); | ||
| 108 | |||
| 109 | try operate(write_file, open_dir_cache, .{ | ||
| 110 | .root_dir = b.cache_root, | ||
| 111 | .sub_path = tmp_dir_sub_path, | ||
| 112 | }); | ||
| 113 | }, | ||
| 114 | .mutate => |lp| { | ||
| 115 | step.result_cached = false; | ||
| 116 | const root_path = try lp.getPath4(b, step); | ||
| 117 | write_file.generated_directory.path = try root_path.toString(arena); | ||
| 118 | try operate(write_file, open_dir_cache, root_path); | ||
| 119 | }, | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | fn operate(write_file: *WriteFile, open_dir_cache: []const Io.Dir, root_path: std.Build.Cache.Path) !void { | ||
| 124 | const step = &write_file.step; | ||
| 125 | const b = step.owner; | ||
| 126 | const io = b.graph.io; | ||
| 127 | const gpa = b.graph.cache.gpa; | ||
| 128 | const arena = b.allocator; | ||
| 129 | |||
| 130 | var cache_dir = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err| | ||
| 131 | return step.fail("unable to make path {f}: {t}", .{ root_path, err }); | ||
| 132 | defer cache_dir.close(io); | ||
| 133 | |||
| 134 | for (write_file.files.items) |file| { | ||
| 135 | if (Dir.path.dirname(file.sub_path)) |dirname| { | ||
| 136 | cache_dir.createDirPath(io, dirname) catch |err| { | ||
| 137 | return step.fail("unable to make path '{f}{c}{s}': {t}", .{ | ||
| 138 | root_path, Dir.path.sep, dirname, err, | ||
| 139 | }); | ||
| 140 | }; | ||
| 141 | } | ||
| 142 | switch (file.contents) { | ||
| 143 | .bytes => |bytes| { | ||
| 144 | cache_dir.writeFile(io, .{ .sub_path = file.sub_path, .data = bytes }) catch |err| { | ||
| 145 | return step.fail("unable to write file '{f}{c}{s}': {t}", .{ | ||
| 146 | root_path, Dir.path.sep, file.sub_path, err, | ||
| 147 | }); | ||
| 148 | }; | ||
| 149 | }, | ||
| 150 | .copy => |file_source| { | ||
| 151 | const source_path = file_source.getPath2(b, step); | ||
| 152 | const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir, file.sub_path, .{}) catch |err| { | ||
| 153 | return step.fail("unable to update file from '{s}' to '{f}{c}{s}': {t}", .{ | ||
| 154 | source_path, root_path, Dir.path.sep, file.sub_path, err, | ||
| 155 | }); | ||
| 156 | }; | ||
| 157 | // At this point we already will mark the step as a cache miss. | ||
| 158 | // But this is kind of a partial cache hit since individual | ||
| 159 | // file copies may be avoided. Oh well, this information is | ||
| 160 | // discarded. | ||
| 161 | _ = prev_status; | ||
| 162 | }, | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| { | ||
| 167 | const src_dir_path = dir.source.getPath3(b, step); | ||
| 168 | const dest_dirname = dir.sub_path; | ||
| 169 | |||
| 170 | if (dest_dirname.len != 0) { | ||
| 171 | cache_dir.createDirPath(io, dest_dirname) catch |err| { | ||
| 172 | return step.fail("unable to make path '{f}{c}{s}': {t}", .{ | ||
| 173 | root_path, Dir.path.sep, dest_dirname, err, | ||
| 174 | }); | ||
| 175 | }; | ||
| 176 | } | ||
| 177 | |||
| 178 | var it = try already_open_dir.walk(gpa); | ||
| 179 | defer it.deinit(); | ||
| 180 | while (try it.next(io)) |entry| { | ||
| 181 | if (!dir.options.pathIncluded(entry.path)) continue; | ||
| 182 | |||
| 183 | const src_entry_path = try src_dir_path.join(arena, entry.path); | ||
| 184 | const dest_path = b.pathJoin(&.{ dest_dirname, entry.path }); | ||
| 185 | switch (entry.kind) { | ||
| 186 | .directory => try cache_dir.createDirPath(io, dest_path), | ||
| 187 | .file => { | ||
| 188 | const prev_status = Io.Dir.updateFile( | ||
| 189 | src_entry_path.root_dir.handle, | ||
| 190 | io, | ||
| 191 | src_entry_path.sub_path, | ||
| 192 | cache_dir, | ||
| 193 | dest_path, | ||
| 194 | .{}, | ||
| 195 | ) catch |err| { | ||
| 196 | return step.fail("unable to update file from '{f}' to '{f}{c}{s}': {t}", .{ | ||
| 197 | src_entry_path, root_path, Dir.path.sep, dest_path, err, | ||
| 198 | }); | ||
| 199 | }; | ||
| 200 | _ = prev_status; | ||
| 201 | }, | ||
| 202 | else => continue, | ||
| 203 | } | ||
| 204 | } | ||
| 205 | } | ||
| 206 | } | ||
lib/compiler/maker/Watch.zig deleted-976| ... | @@ -1,976 +0,0 @@ | ||
| 1 | const Watch = @This(); | ||
| 2 | const builtin = @import("builtin"); | ||
| 3 | |||
| 4 | const std = @import("std"); | ||
| 5 | const Io = std.Io; | ||
| 6 | const Allocator = std.mem.Allocator; | ||
| 7 | const assert = std.debug.assert; | ||
| 8 | const fatal = std.process.fatal; | ||
| 9 | const Configuration = std.Build.Configuration; | ||
| 10 | |||
| 11 | const FsEvents = @import("Watch/FsEvents.zig"); | ||
| 12 | const Step = @import("Step.zig"); | ||
| 13 | |||
| 14 | os: Os, | ||
| 15 | /// The number to show as the number of directories being watched. | ||
| 16 | dir_count: usize, | ||
| 17 | // These fields are common to most implementations so are kept here for simplicity. | ||
| 18 | // They are `undefined` on implementations which do not utilize then. | ||
| 19 | dir_table: DirTable, | ||
| 20 | generation: Generation, | ||
| 21 | configuration: *const Configuration, | ||
| 22 | make_steps: []Step, | ||
| 23 | |||
| 24 | pub const have_impl = Os != void; | ||
| 25 | |||
| 26 | /// Key is the directory to watch which contains one or more files we are | ||
| 27 | /// interested in noticing changes to. | ||
| 28 | /// | ||
| 29 | /// Value is generation. | ||
| 30 | const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false); | ||
| 31 | |||
| 32 | /// Special key of "." means any changes in this directory trigger the steps. | ||
| 33 | const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet); | ||
| 34 | const StepSet = std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, Generation); | ||
| 35 | |||
| 36 | const Generation = u8; | ||
| 37 | |||
| 38 | const Hash = std.hash.Wyhash; | ||
| 39 | const Cache = std.Build.Cache; | ||
| 40 | |||
| 41 | const Os = switch (builtin.os.tag) { | ||
| 42 | .linux => struct { | ||
| 43 | const posix = std.posix; | ||
| 44 | |||
| 45 | /// Keyed differently but indexes correspond 1:1 with `dir_table`. | ||
| 46 | handle_table: HandleTable, | ||
| 47 | /// fanotify file descriptors are keyed by mount id since marks | ||
| 48 | /// are limited to a single filesystem. | ||
| 49 | poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd), | ||
| 50 | |||
| 51 | const MountId = i32; | ||
| 52 | const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false); | ||
| 53 | |||
| 54 | const fan_mask: std.os.linux.fanotify.MarkMask = .{ | ||
| 55 | .CLOSE_WRITE = true, | ||
| 56 | .CREATE = true, | ||
| 57 | .DELETE = true, | ||
| 58 | .DELETE_SELF = true, | ||
| 59 | .EVENT_ON_CHILD = true, | ||
| 60 | .MOVED_FROM = true, | ||
| 61 | .MOVED_TO = true, | ||
| 62 | .MOVE_SELF = true, | ||
| 63 | .ONDIR = true, | ||
| 64 | }; | ||
| 65 | |||
| 66 | const FileHandle = struct { | ||
| 67 | handle: *align(1) std.os.linux.file_handle, | ||
| 68 | |||
| 69 | fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle { | ||
| 70 | const bytes = lfh.slice(); | ||
| 71 | const new_ptr = try gpa.alignedAlloc( | ||
| 72 | u8, | ||
| 73 | .of(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: FileHandle = .{ .handle = new_header }; | ||
| 79 | @memcpy(new.slice(), lfh.slice()); | ||
| 80 | return new; | ||
| 81 | } | ||
| 82 | |||
| 83 | fn destroy(lfh: FileHandle, 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 | fn slice(lfh: FileHandle) []u8 { | ||
| 90 | const ptr: [*]u8 = &lfh.handle.f_handle; | ||
| 91 | return ptr[0..lfh.handle.handle_bytes]; | ||
| 92 | } | ||
| 93 | |||
| 94 | const Adapter = struct { | ||
| 95 | pub fn hash(self: Adapter, a: FileHandle) 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: FileHandle, b: FileHandle, 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 | fn init(cwd_path: []const u8, configuration: *const Configuration, make_steps: []Step) !Watch { | ||
| 109 | _ = cwd_path; | ||
| 110 | return .{ | ||
| 111 | .dir_table = .{}, | ||
| 112 | .dir_count = 0, | ||
| 113 | .os = switch (builtin.os.tag) { | ||
| 114 | .linux => .{ | ||
| 115 | .handle_table = .{}, | ||
| 116 | .poll_fds = .{}, | ||
| 117 | }, | ||
| 118 | else => {}, | ||
| 119 | }, | ||
| 120 | .generation = 0, | ||
| 121 | .make_steps = make_steps, | ||
| 122 | .configuration = configuration, | ||
| 123 | }; | ||
| 124 | } | ||
| 125 | |||
| 126 | fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle { | ||
| 127 | var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; | ||
| 128 | var buf: [std.fs.max_path_bytes]u8 = undefined; | ||
| 129 | const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ | ||
| 130 | path.sub_path, | ||
| 131 | }) catch return error.NameTooLong; | ||
| 132 | const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); | ||
| 133 | stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); | ||
| 134 | try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID); | ||
| 135 | const stack_lfh: FileHandle = .{ .handle = stack_ptr }; | ||
| 136 | return stack_lfh.clone(gpa); | ||
| 137 | } | ||
| 138 | |||
| 139 | fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool { | ||
| 140 | const fanotify = std.os.linux.fanotify; | ||
| 141 | const M = fanotify.event_metadata; | ||
| 142 | var events_buf: [256 + 4096]u8 = undefined; | ||
| 143 | var any_dirty = false; | ||
| 144 | while (true) { | ||
| 145 | var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) { | ||
| 146 | error.WouldBlock => return any_dirty, | ||
| 147 | else => |e| return e, | ||
| 148 | }; | ||
| 149 | var meta: [*]align(1) M = @ptrCast(&events_buf); | ||
| 150 | while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ | ||
| 151 | len -= meta[0].event_len; | ||
| 152 | meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); | ||
| 153 | }) { | ||
| 154 | assert(meta[0].vers == M.VERSION); | ||
| 155 | if (meta[0].mask.Q_OVERFLOW) { | ||
| 156 | any_dirty = true; | ||
| 157 | std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); | ||
| 158 | markAllFilesDirty(w, gpa); | ||
| 159 | return true; | ||
| 160 | } | ||
| 161 | const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); | ||
| 162 | switch (fid.hdr.info_type) { | ||
| 163 | .DFID_NAME => { | ||
| 164 | const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); | ||
| 165 | const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); | ||
| 166 | const file_name = std.mem.span(file_name_z); | ||
| 167 | const lfh: FileHandle = .{ .handle = file_handle }; | ||
| 168 | if (w.os.handle_table.getPtr(lfh)) |value| { | ||
| 169 | if (value.reaction_set.getPtr(".")) |glob_set| | ||
| 170 | any_dirty = markStepSetDirty(gpa, w.make_steps, glob_set, any_dirty); | ||
| 171 | if (value.reaction_set.getPtr(file_name)) |step_set| | ||
| 172 | any_dirty = markStepSetDirty(gpa, w.make_steps, step_set, any_dirty); | ||
| 173 | } | ||
| 174 | }, | ||
| 175 | else => |t| std.log.warn("unexpected fanotify event '{t}'", .{t}), | ||
| 176 | } | ||
| 177 | } | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 182 | // Add missing marks and note persisted ones. | ||
| 183 | for (steps) |step_index| { | ||
| 184 | const step = &w.make_steps[@intFromEnum(step_index)]; | ||
| 185 | for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { | ||
| 186 | const reaction_set = rs: { | ||
| 187 | const gop = try w.dir_table.getOrPut(gpa, path); | ||
| 188 | if (!gop.found_existing) { | ||
| 189 | var mount_id: MountId = undefined; | ||
| 190 | const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) { | ||
| 191 | error.FileNotFound => { | ||
| 192 | std.debug.assert(w.dir_table.swapRemove(path)); | ||
| 193 | continue; | ||
| 194 | }, | ||
| 195 | else => return err, | ||
| 196 | }; | ||
| 197 | const fan_fd = blk: { | ||
| 198 | const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id); | ||
| 199 | if (!fd_gop.found_existing) { | ||
| 200 | const fan_fd = std.posix.fanotify_init(.{ | ||
| 201 | .CLASS = .NOTIF, | ||
| 202 | .CLOEXEC = true, | ||
| 203 | .NONBLOCK = true, | ||
| 204 | .REPORT_NAME = true, | ||
| 205 | .REPORT_DIR_FID = true, | ||
| 206 | .REPORT_FID = true, | ||
| 207 | .REPORT_TARGET_FID = true, | ||
| 208 | }, 0) catch |err| switch (err) { | ||
| 209 | error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}), | ||
| 210 | else => |e| return e, | ||
| 211 | }; | ||
| 212 | fd_gop.value_ptr.* = .{ | ||
| 213 | .fd = fan_fd, | ||
| 214 | .events = std.posix.POLL.IN, | ||
| 215 | .revents = undefined, | ||
| 216 | }; | ||
| 217 | } | ||
| 218 | break :blk fd_gop.value_ptr.*.fd; | ||
| 219 | }; | ||
| 220 | // `dir_handle` may already be present in the table in | ||
| 221 | // the case that we have multiple Cache.Path instances | ||
| 222 | // that compare inequal but ultimately point to the same | ||
| 223 | // directory on the file system. | ||
| 224 | // In such case, we must revert adding this directory, but keep | ||
| 225 | // the additions to the step set. | ||
| 226 | const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle); | ||
| 227 | if (dh_gop.found_existing) { | ||
| 228 | _ = w.dir_table.pop(); | ||
| 229 | } else { | ||
| 230 | assert(dh_gop.index == gop.index); | ||
| 231 | dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} }; | ||
| 232 | posix.fanotify_mark(fan_fd, .{ | ||
| 233 | .ADD = true, | ||
| 234 | .ONLYDIR = true, | ||
| 235 | }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| { | ||
| 236 | fatal("unable to watch {f}: {s}", .{ path, @errorName(err) }); | ||
| 237 | }; | ||
| 238 | } | ||
| 239 | break :rs &dh_gop.value_ptr.reaction_set; | ||
| 240 | } | ||
| 241 | break :rs &w.os.handle_table.values()[gop.index].reaction_set; | ||
| 242 | }; | ||
| 243 | for (files.items) |basename| { | ||
| 244 | const gop = try reaction_set.getOrPut(gpa, basename); | ||
| 245 | if (!gop.found_existing) gop.value_ptr.* = .{}; | ||
| 246 | try gop.value_ptr.put(gpa, step_index, w.generation); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | { | ||
| 252 | // Remove marks for files that are no longer inputs. | ||
| 253 | var i: usize = 0; | ||
| 254 | while (i < w.os.handle_table.entries.len) { | ||
| 255 | { | ||
| 256 | const reaction_set = &w.os.handle_table.values()[i].reaction_set; | ||
| 257 | var step_set_i: usize = 0; | ||
| 258 | while (step_set_i < reaction_set.entries.len) { | ||
| 259 | const step_set = &reaction_set.values()[step_set_i]; | ||
| 260 | var dirent_i: usize = 0; | ||
| 261 | while (dirent_i < step_set.entries.len) { | ||
| 262 | const generations = step_set.values(); | ||
| 263 | if (generations[dirent_i] == w.generation) { | ||
| 264 | dirent_i += 1; | ||
| 265 | continue; | ||
| 266 | } | ||
| 267 | step_set.swapRemoveAt(dirent_i); | ||
| 268 | } | ||
| 269 | if (step_set.entries.len > 0) { | ||
| 270 | step_set_i += 1; | ||
| 271 | continue; | ||
| 272 | } | ||
| 273 | reaction_set.swapRemoveAt(step_set_i); | ||
| 274 | } | ||
| 275 | if (reaction_set.entries.len > 0) { | ||
| 276 | i += 1; | ||
| 277 | continue; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | |||
| 281 | const path = w.dir_table.keys()[i]; | ||
| 282 | |||
| 283 | const mount_id = w.os.handle_table.values()[i].mount_id; | ||
| 284 | const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd; | ||
| 285 | posix.fanotify_mark(fan_fd, .{ | ||
| 286 | .REMOVE = true, | ||
| 287 | .ONLYDIR = true, | ||
| 288 | }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) { | ||
| 289 | error.FileNotFound => {}, // Expected, harmless. | ||
| 290 | else => |e| std.log.warn("unable to unwatch '{f}': {s}", .{ path, @errorName(e) }), | ||
| 291 | }; | ||
| 292 | |||
| 293 | w.dir_table.swapRemoveAt(i); | ||
| 294 | w.os.handle_table.swapRemoveAt(i); | ||
| 295 | } | ||
| 296 | w.generation +%= 1; | ||
| 297 | } | ||
| 298 | w.dir_count = w.dir_table.count(); | ||
| 299 | } | ||
| 300 | |||
| 301 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 302 | _ = io; | ||
| 303 | const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms()); | ||
| 304 | if (events_len == 0) | ||
| 305 | return .timeout; | ||
| 306 | 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, gpa, poll_fd.fd)) | ||
| 308 | return .dirty; | ||
| 309 | } | ||
| 310 | return .clean; | ||
| 311 | } | ||
| 312 | }, | ||
| 313 | .windows => struct { | ||
| 314 | const windows = std.os.windows; | ||
| 315 | |||
| 316 | /// Keyed differently but indexes correspond 1:1 with `dir_table`. | ||
| 317 | handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false), | ||
| 318 | ready_dirs: std.DoublyLinkedList, | ||
| 319 | |||
| 320 | const FileId = struct { | ||
| 321 | volumeSerialNumber: windows.ULONG, | ||
| 322 | indexNumber: windows.LARGE_INTEGER, | ||
| 323 | }; | ||
| 324 | |||
| 325 | const Directory = struct { | ||
| 326 | reaction_set: ReactionSet, | ||
| 327 | id: FileId, | ||
| 328 | file: Io.File, | ||
| 329 | state: enum { idle, listening, ready }, | ||
| 330 | iosb: windows.IO_STATUS_BLOCK, | ||
| 331 | // 64 KB is the packet size limit when monitoring over a network. | ||
| 332 | // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks | ||
| 333 | buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)), | ||
| 334 | ready_node: std.DoublyLinkedList.Node, | ||
| 335 | |||
| 336 | /// Start listening for events, buffer field will be overwritten eventually. | ||
| 337 | fn startListening(dir: *Directory, w: *Watch) !void { | ||
| 338 | assert(dir.file.flags.nonblocking); | ||
| 339 | assert(dir.state == .idle); | ||
| 340 | switch (windows.ntdll.NtNotifyChangeDirectoryFileEx( | ||
| 341 | dir.file.handle, | ||
| 342 | null, | ||
| 343 | &notifyApc, | ||
| 344 | w, | ||
| 345 | &dir.iosb, | ||
| 346 | &dir.buffer, | ||
| 347 | dir.buffer.len, | ||
| 348 | .{ | ||
| 349 | .FILE_NAME = true, | ||
| 350 | .DIR_NAME = true, | ||
| 351 | .SIZE = true, | ||
| 352 | .LAST_WRITE = true, | ||
| 353 | .CREATION = true, | ||
| 354 | }, | ||
| 355 | .FALSE, | ||
| 356 | .Notify, | ||
| 357 | )) { | ||
| 358 | .SUCCESS, .PENDING => dir.state = .listening, | ||
| 359 | .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported, | ||
| 360 | else => |status| return windows.unexpectedStatus(status), | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void { | ||
| 365 | const w: *Watch = @ptrCast(@alignCast(apc_context)); | ||
| 366 | const dir: *Directory = @fieldParentPtr("iosb", iosb); | ||
| 367 | assert(iosb.u.Status != .PENDING); | ||
| 368 | assert(dir.state == .listening); | ||
| 369 | w.os.ready_dirs.append(&dir.ready_node); | ||
| 370 | dir.state = .ready; | ||
| 371 | } | ||
| 372 | |||
| 373 | fn init(gpa: Allocator, path: Cache.Path) !*Directory { | ||
| 374 | // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW) | ||
| 375 | // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW. | ||
| 376 | var dir_handle: windows.HANDLE = undefined; | ||
| 377 | const root_fd = path.root_dir.handle.handle; | ||
| 378 | const sub_path = path.subPathOrDot(); | ||
| 379 | const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call | ||
| 380 | var iosb: windows.IO_STATUS_BLOCK = undefined; | ||
| 381 | switch (windows.ntdll.NtCreateFile( | ||
| 382 | &dir_handle, | ||
| 383 | .{ | ||
| 384 | .SPECIFIC = .{ .FILE_DIRECTORY = .{ | ||
| 385 | .LIST = true, | ||
| 386 | } }, | ||
| 387 | .STANDARD = .{ .SYNCHRONIZE = true }, | ||
| 388 | .GENERIC = .{ .READ = true }, | ||
| 389 | }, | ||
| 390 | &.{ | ||
| 391 | .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd, | ||
| 392 | .ObjectName = @constCast(&sub_path_w.string()), | ||
| 393 | }, | ||
| 394 | &iosb, | ||
| 395 | null, | ||
| 396 | .{}, | ||
| 397 | .VALID_FLAGS, | ||
| 398 | .OPEN, | ||
| 399 | .{ | ||
| 400 | .DIRECTORY_FILE = true, | ||
| 401 | .IO = .ASYNCHRONOUS, | ||
| 402 | .OPEN_FOR_BACKUP_INTENT = true, | ||
| 403 | }, | ||
| 404 | null, | ||
| 405 | 0, | ||
| 406 | )) { | ||
| 407 | .SUCCESS => {}, | ||
| 408 | .OBJECT_NAME_INVALID => return error.BadPathName, | ||
| 409 | .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, | ||
| 410 | .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, | ||
| 411 | .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, | ||
| 412 | .NOT_A_DIRECTORY => return error.NotDir, | ||
| 413 | // This can happen if the directory has 'List folder contents' permission set to 'Deny' | ||
| 414 | .ACCESS_DENIED => return error.AccessDenied, | ||
| 415 | .INVALID_PARAMETER => unreachable, | ||
| 416 | else => |rc| return windows.unexpectedStatus(rc), | ||
| 417 | } | ||
| 418 | assert(dir_handle != windows.INVALID_HANDLE_VALUE); | ||
| 419 | errdefer windows.CloseHandle(dir_handle); | ||
| 420 | |||
| 421 | const dir_id = try getFileId(dir_handle); | ||
| 422 | |||
| 423 | const dir = try gpa.create(Directory); | ||
| 424 | dir.* = .{ | ||
| 425 | .reaction_set = .empty, | ||
| 426 | .id = dir_id, | ||
| 427 | .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } }, | ||
| 428 | .state = .idle, | ||
| 429 | .iosb = undefined, | ||
| 430 | .buffer = undefined, | ||
| 431 | .ready_node = undefined, | ||
| 432 | }; | ||
| 433 | return dir; | ||
| 434 | } | ||
| 435 | |||
| 436 | fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void { | ||
| 437 | state: switch (dir.state) { | ||
| 438 | .idle => {}, | ||
| 439 | .listening => { | ||
| 440 | var cancel_iosb: windows.IO_STATUS_BLOCK = undefined; | ||
| 441 | _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb); | ||
| 442 | while (switch (dir.state) { | ||
| 443 | .idle => unreachable, | ||
| 444 | .listening => true, | ||
| 445 | .ready => false, | ||
| 446 | }) Io.Threaded.waitForApcOrAlert(); | ||
| 447 | continue :state .ready; | ||
| 448 | }, | ||
| 449 | .ready => w.os.ready_dirs.remove(&dir.ready_node), | ||
| 450 | } | ||
| 451 | windows.CloseHandle(dir.file.handle); | ||
| 452 | gpa.destroy(dir); | ||
| 453 | } | ||
| 454 | |||
| 455 | /// Useful to make `*Directory` a key in `std.ArrayHashMap`. | ||
| 456 | const TableAdapter = struct { | ||
| 457 | pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 { | ||
| 458 | return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber))); | ||
| 459 | } | ||
| 460 | pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool { | ||
| 461 | _ = rhs_index; | ||
| 462 | return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and | ||
| 463 | lhs_dir.id.indexNumber == rhs_dir.id.indexNumber; | ||
| 464 | } | ||
| 465 | }; | ||
| 466 | }; | ||
| 467 | |||
| 468 | fn init(cwd_path: []const u8) !Watch { | ||
| 469 | _ = cwd_path; | ||
| 470 | return .{ | ||
| 471 | .dir_table = .{}, | ||
| 472 | .dir_count = 0, | ||
| 473 | .os = switch (builtin.os.tag) { | ||
| 474 | .windows => .{ | ||
| 475 | .handle_table = .empty, | ||
| 476 | .ready_dirs = .{}, | ||
| 477 | }, | ||
| 478 | else => {}, | ||
| 479 | }, | ||
| 480 | .generation = 0, | ||
| 481 | }; | ||
| 482 | } | ||
| 483 | |||
| 484 | fn getFileId(handle: windows.HANDLE) !FileId { | ||
| 485 | var file_id: FileId = undefined; | ||
| 486 | var io_status: windows.IO_STATUS_BLOCK = undefined; | ||
| 487 | var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined; | ||
| 488 | switch (windows.ntdll.NtQueryVolumeInformationFile( | ||
| 489 | handle, | ||
| 490 | &io_status, | ||
| 491 | &volume_info, | ||
| 492 | @sizeOf(windows.FILE.FS_VOLUME_INFORMATION), | ||
| 493 | .Volume, | ||
| 494 | )) { | ||
| 495 | .SUCCESS => {}, | ||
| 496 | // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer | ||
| 497 | // size provided. This is treated as success because the type of variable-length information that this would be relevant for | ||
| 498 | // (name, volume name, etc) we don't care about. | ||
| 499 | .BUFFER_OVERFLOW => {}, | ||
| 500 | else => |rc| return windows.unexpectedStatus(rc), | ||
| 501 | } | ||
| 502 | file_id.volumeSerialNumber = volume_info.VolumeSerialNumber; | ||
| 503 | var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined; | ||
| 504 | switch (windows.ntdll.NtQueryInformationFile( | ||
| 505 | handle, | ||
| 506 | &io_status, | ||
| 507 | &internal_info, | ||
| 508 | @sizeOf(windows.FILE.INTERNAL_INFORMATION), | ||
| 509 | .Internal, | ||
| 510 | )) { | ||
| 511 | .SUCCESS => {}, | ||
| 512 | else => |rc| return windows.unexpectedStatus(rc), | ||
| 513 | } | ||
| 514 | file_id.indexNumber = internal_info.IndexNumber; | ||
| 515 | return file_id; | ||
| 516 | } | ||
| 517 | |||
| 518 | fn markDirtySteps(w: *Watch, gpa: Allocator, dir: *Directory) !bool { | ||
| 519 | var any_dirty = false; | ||
| 520 | const bytes_returned = dir.iosb.Information; | ||
| 521 | if (bytes_returned == 0) { | ||
| 522 | std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); | ||
| 523 | markAllFilesDirty(w, gpa); | ||
| 524 | try dir.startListening(w); | ||
| 525 | return true; | ||
| 526 | } | ||
| 527 | var file_name_buf: [std.fs.max_path_bytes]u8 = undefined; | ||
| 528 | var offset: usize = 0; | ||
| 529 | while (true) { | ||
| 530 | const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset])); | ||
| 531 | const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())]; | ||
| 532 | if (dir.reaction_set.getPtr(".")) |glob_set| | ||
| 533 | any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); | ||
| 534 | if (dir.reaction_set.getPtr(file_name)) |step_set| | ||
| 535 | any_dirty = markStepSetDirty(gpa, step_set, any_dirty); | ||
| 536 | if (notify.NextEntryOffset == 0) | ||
| 537 | break; | ||
| 538 | |||
| 539 | offset += notify.NextEntryOffset; | ||
| 540 | } | ||
| 541 | |||
| 542 | // We call this now since at this point we have finished reading dir.buffer. | ||
| 543 | try dir.startListening(w); | ||
| 544 | return any_dirty; | ||
| 545 | } | ||
| 546 | |||
| 547 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 548 | // Add missing marks and note persisted ones. | ||
| 549 | for (steps) |step| { | ||
| 550 | for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { | ||
| 551 | const dir = dir: { | ||
| 552 | const gop = try w.dir_table.getOrPut(gpa, path); | ||
| 553 | if (!gop.found_existing) { | ||
| 554 | const dir: *Directory = try .init(gpa, path); | ||
| 555 | errdefer dir.deinit(gpa, w); | ||
| 556 | // `dir.id` may already be present in the table in | ||
| 557 | // the case that we have multiple Cache.Path instances | ||
| 558 | // that compare inequal but ultimately point to the same | ||
| 559 | // directory on the file system. | ||
| 560 | // In such case, we must revert adding this directory, but keep | ||
| 561 | // the additions to the step set. | ||
| 562 | const dh_gop = try w.os.handle_table.getOrPut(gpa, dir); | ||
| 563 | if (dh_gop.found_existing) { | ||
| 564 | dir.deinit(gpa, w); | ||
| 565 | _ = w.dir_table.pop(); | ||
| 566 | break :dir w.os.handle_table.keys()[dh_gop.index]; | ||
| 567 | } else { | ||
| 568 | assert(dh_gop.index == gop.index); | ||
| 569 | try dir.startListening(w); | ||
| 570 | break :dir dir; | ||
| 571 | } | ||
| 572 | } | ||
| 573 | break :dir w.os.handle_table.keys()[gop.index]; | ||
| 574 | }; | ||
| 575 | for (files.items) |basename| { | ||
| 576 | const gop = try dir.reaction_set.getOrPut(gpa, basename); | ||
| 577 | if (!gop.found_existing) gop.value_ptr.* = .{}; | ||
| 578 | try gop.value_ptr.put(gpa, step, w.generation); | ||
| 579 | } | ||
| 580 | } | ||
| 581 | } | ||
| 582 | |||
| 583 | { | ||
| 584 | // Remove marks for files that are no longer inputs. | ||
| 585 | var i: usize = 0; | ||
| 586 | while (i < w.os.handle_table.entries.len) { | ||
| 587 | const dir = w.os.handle_table.keys()[i]; | ||
| 588 | { | ||
| 589 | var step_set_i: usize = 0; | ||
| 590 | while (step_set_i < dir.reaction_set.entries.len) { | ||
| 591 | const step_set = &dir.reaction_set.values()[step_set_i]; | ||
| 592 | var dirent_i: usize = 0; | ||
| 593 | while (dirent_i < step_set.entries.len) { | ||
| 594 | const generations = step_set.values(); | ||
| 595 | if (generations[dirent_i] == w.generation) { | ||
| 596 | dirent_i += 1; | ||
| 597 | continue; | ||
| 598 | } | ||
| 599 | step_set.swapRemoveAt(dirent_i); | ||
| 600 | } | ||
| 601 | if (step_set.entries.len > 0) { | ||
| 602 | step_set_i += 1; | ||
| 603 | continue; | ||
| 604 | } | ||
| 605 | dir.reaction_set.swapRemoveAt(step_set_i); | ||
| 606 | } | ||
| 607 | if (dir.reaction_set.entries.len > 0) { | ||
| 608 | i += 1; | ||
| 609 | continue; | ||
| 610 | } | ||
| 611 | } | ||
| 612 | |||
| 613 | w.dir_table.swapRemoveAt(i); | ||
| 614 | w.os.handle_table.swapRemoveAt(i); | ||
| 615 | dir.deinit(gpa, w); | ||
| 616 | } | ||
| 617 | w.generation +%= 1; | ||
| 618 | } | ||
| 619 | w.dir_count = w.dir_table.count(); | ||
| 620 | } | ||
| 621 | |||
| 622 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 623 | for (0..2) |attempt| { | ||
| 624 | while (w.os.ready_dirs.popFirst()) |ready_node| { | ||
| 625 | const dir: *Directory = @fieldParentPtr("ready_node", ready_node); | ||
| 626 | assert(dir.state == .ready); | ||
| 627 | dir.state = .idle; | ||
| 628 | switch (dir.iosb.u.Status) { | ||
| 629 | .SUCCESS => return if (try markDirtySteps(w, gpa, dir)) .dirty else .clean, | ||
| 630 | .PENDING => unreachable, | ||
| 631 | .CANCELLED => {}, | ||
| 632 | else => |status| return windows.unexpectedStatus(status), | ||
| 633 | } | ||
| 634 | try dir.startListening(w); | ||
| 635 | } | ||
| 636 | try io.checkCancel(); | ||
| 637 | if (attempt == 1) return .timeout; | ||
| 638 | const delay_interval: windows.LARGE_INTEGER = switch (timeout) { | ||
| 639 | .none => std.math.minInt(windows.LARGE_INTEGER), | ||
| 640 | .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100), | ||
| 641 | }; | ||
| 642 | _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval); | ||
| 643 | } else unreachable; | ||
| 644 | } | ||
| 645 | }, | ||
| 646 | .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct { | ||
| 647 | const posix = std.posix; | ||
| 648 | |||
| 649 | kq_fd: i32, | ||
| 650 | /// Indexes correspond 1:1 with `dir_table`. | ||
| 651 | handles: std.MultiArrayList(struct { | ||
| 652 | rs: ReactionSet, | ||
| 653 | /// If the corresponding dir_table Path has sub_path == "", then it | ||
| 654 | /// suffices as the open directory handle, and this value will be | ||
| 655 | /// -1. Otherwise, it needs to be opened in update(), and will be | ||
| 656 | /// stored here. | ||
| 657 | dir_fd: i32, | ||
| 658 | }), | ||
| 659 | |||
| 660 | const dir_open_flags: posix.O = f: { | ||
| 661 | var f: posix.O = .{ | ||
| 662 | .ACCMODE = .RDONLY, | ||
| 663 | .NOFOLLOW = false, | ||
| 664 | .DIRECTORY = true, | ||
| 665 | .CLOEXEC = true, | ||
| 666 | }; | ||
| 667 | if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true; | ||
| 668 | if (@hasField(posix.O, "PATH")) f.PATH = true; | ||
| 669 | break :f f; | ||
| 670 | }; | ||
| 671 | |||
| 672 | const EV = std.c.EV; | ||
| 673 | const NOTE = std.c.NOTE; | ||
| 674 | |||
| 675 | fn init(cwd_path: []const u8) !Watch { | ||
| 676 | _ = cwd_path; | ||
| 677 | return .{ | ||
| 678 | .dir_table = .{}, | ||
| 679 | .dir_count = 0, | ||
| 680 | .os = .{ | ||
| 681 | .kq_fd = try Io.Kqueue.createFileDescriptor(), | ||
| 682 | .handles = .empty, | ||
| 683 | }, | ||
| 684 | .generation = 0, | ||
| 685 | }; | ||
| 686 | } | ||
| 687 | |||
| 688 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 689 | const handles = &w.os.handles; | ||
| 690 | for (steps) |step| { | ||
| 691 | for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { | ||
| 692 | const reaction_set = rs: { | ||
| 693 | const gop = try w.dir_table.getOrPut(gpa, path); | ||
| 694 | if (!gop.found_existing) { | ||
| 695 | const skip_open_dir = path.sub_path.len == 0; | ||
| 696 | const dir_fd = if (skip_open_dir) | ||
| 697 | path.root_dir.handle.handle | ||
| 698 | else | ||
| 699 | posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| { | ||
| 700 | fatal("failed to open directory {f}: {t}", .{ path, err }); | ||
| 701 | }; | ||
| 702 | // Empirically the dir has to stay open or else no events are triggered. | ||
| 703 | errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd); | ||
| 704 | const changes = [1]posix.Kevent{.{ | ||
| 705 | .ident = @bitCast(@as(isize, dir_fd)), | ||
| 706 | .filter = std.c.EVFILT.VNODE, | ||
| 707 | .flags = EV.ADD | EV.ENABLE | EV.CLEAR, | ||
| 708 | .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, | ||
| 709 | .data = 0, | ||
| 710 | .udata = gop.index, | ||
| 711 | }}; | ||
| 712 | _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null); | ||
| 713 | assert(handles.len == gop.index); | ||
| 714 | try handles.append(gpa, .{ | ||
| 715 | .rs = .{}, | ||
| 716 | .dir_fd = if (skip_open_dir) -1 else dir_fd, | ||
| 717 | }); | ||
| 718 | } | ||
| 719 | |||
| 720 | break :rs &handles.items(.rs)[gop.index]; | ||
| 721 | }; | ||
| 722 | for (files.items) |basename| { | ||
| 723 | const gop = try reaction_set.getOrPut(gpa, basename); | ||
| 724 | if (!gop.found_existing) gop.value_ptr.* = .{}; | ||
| 725 | try gop.value_ptr.put(gpa, step, w.generation); | ||
| 726 | } | ||
| 727 | } | ||
| 728 | } | ||
| 729 | |||
| 730 | { | ||
| 731 | // Remove marks for files that are no longer inputs. | ||
| 732 | var i: usize = 0; | ||
| 733 | while (i < handles.len) { | ||
| 734 | { | ||
| 735 | const reaction_set = &handles.items(.rs)[i]; | ||
| 736 | var step_set_i: usize = 0; | ||
| 737 | while (step_set_i < reaction_set.entries.len) { | ||
| 738 | const step_set = &reaction_set.values()[step_set_i]; | ||
| 739 | var dirent_i: usize = 0; | ||
| 740 | while (dirent_i < step_set.entries.len) { | ||
| 741 | const generations = step_set.values(); | ||
| 742 | if (generations[dirent_i] == w.generation) { | ||
| 743 | dirent_i += 1; | ||
| 744 | continue; | ||
| 745 | } | ||
| 746 | step_set.swapRemoveAt(dirent_i); | ||
| 747 | } | ||
| 748 | if (step_set.entries.len > 0) { | ||
| 749 | step_set_i += 1; | ||
| 750 | continue; | ||
| 751 | } | ||
| 752 | reaction_set.swapRemoveAt(step_set_i); | ||
| 753 | } | ||
| 754 | if (reaction_set.entries.len > 0) { | ||
| 755 | i += 1; | ||
| 756 | continue; | ||
| 757 | } | ||
| 758 | } | ||
| 759 | |||
| 760 | // If the sub_path == "" then this patch has already the | ||
| 761 | // dir fd that we need to use as the ident to remove the | ||
| 762 | // event. If it was opened above with openat() then we need | ||
| 763 | // to access that data via the dir_fd field. | ||
| 764 | const path = w.dir_table.keys()[i]; | ||
| 765 | const dir_fd = if (path.sub_path.len == 0) | ||
| 766 | path.root_dir.handle.handle | ||
| 767 | else | ||
| 768 | handles.items(.dir_fd)[i]; | ||
| 769 | assert(dir_fd != -1); | ||
| 770 | |||
| 771 | // The changelist also needs to update the udata field of the last | ||
| 772 | // event, since we are doing a swap remove, and we store the dir_table | ||
| 773 | // index in the udata field. | ||
| 774 | const last_dir_fd = fd: { | ||
| 775 | const last_path = w.dir_table.keys()[handles.len - 1]; | ||
| 776 | const last_dir_fd = if (last_path.sub_path.len == 0) | ||
| 777 | last_path.root_dir.handle.handle | ||
| 778 | else | ||
| 779 | handles.items(.dir_fd)[handles.len - 1]; | ||
| 780 | assert(last_dir_fd != -1); | ||
| 781 | break :fd last_dir_fd; | ||
| 782 | }; | ||
| 783 | const changes = [_]posix.Kevent{ | ||
| 784 | .{ | ||
| 785 | .ident = @bitCast(@as(isize, dir_fd)), | ||
| 786 | .filter = std.c.EVFILT.VNODE, | ||
| 787 | .flags = EV.DELETE, | ||
| 788 | .fflags = 0, | ||
| 789 | .data = 0, | ||
| 790 | .udata = i, | ||
| 791 | }, | ||
| 792 | .{ | ||
| 793 | .ident = @bitCast(@as(isize, last_dir_fd)), | ||
| 794 | .filter = std.c.EVFILT.VNODE, | ||
| 795 | .flags = EV.ADD, | ||
| 796 | .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, | ||
| 797 | .data = 0, | ||
| 798 | .udata = i, | ||
| 799 | }, | ||
| 800 | }; | ||
| 801 | const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes; | ||
| 802 | _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null); | ||
| 803 | if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd); | ||
| 804 | |||
| 805 | w.dir_table.swapRemoveAt(i); | ||
| 806 | handles.swapRemove(i); | ||
| 807 | } | ||
| 808 | w.generation +%= 1; | ||
| 809 | } | ||
| 810 | w.dir_count = w.dir_table.count(); | ||
| 811 | } | ||
| 812 | |||
| 813 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 814 | _ = io; | ||
| 815 | var timespec_buffer: posix.timespec = undefined; | ||
| 816 | var event_buffer: [100]posix.Kevent = undefined; | ||
| 817 | var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer)); | ||
| 818 | if (n == 0) return .timeout; | ||
| 819 | const reaction_sets = w.os.handles.items(.rs); | ||
| 820 | var any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], false); | ||
| 821 | timespec_buffer = .{ .sec = 0, .nsec = 0 }; | ||
| 822 | while (n == event_buffer.len) { | ||
| 823 | n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer); | ||
| 824 | if (n == 0) break; | ||
| 825 | any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], any_dirty); | ||
| 826 | } | ||
| 827 | return if (any_dirty) .dirty else .clean; | ||
| 828 | } | ||
| 829 | |||
| 830 | fn markDirtySteps( | ||
| 831 | gpa: Allocator, | ||
| 832 | reaction_sets: []ReactionSet, | ||
| 833 | events: []const std.c.Kevent, | ||
| 834 | start_any_dirty: bool, | ||
| 835 | ) bool { | ||
| 836 | var any_dirty = start_any_dirty; | ||
| 837 | for (events) |event| { | ||
| 838 | const index: usize = @intCast(event.udata); | ||
| 839 | const reaction_set = &reaction_sets[index]; | ||
| 840 | // If we knew the basename of the changed file, here we would | ||
| 841 | // mark only the step set dirty, and possibly the glob set: | ||
| 842 | //if (reaction_set.getPtr(".")) |glob_set| | ||
| 843 | // any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); | ||
| 844 | //if (reaction_set.getPtr(file_name)) |step_set| | ||
| 845 | // any_dirty = markStepSetDirty(gpa, step_set, any_dirty); | ||
| 846 | // However we don't know the file name so just mark all the | ||
| 847 | // sets dirty for this directory. | ||
| 848 | for (reaction_set.values()) |*step_set| { | ||
| 849 | any_dirty = markStepSetDirty(gpa, step_set, any_dirty); | ||
| 850 | } | ||
| 851 | } | ||
| 852 | return any_dirty; | ||
| 853 | } | ||
| 854 | }, | ||
| 855 | .macos => struct { | ||
| 856 | fse: FsEvents, | ||
| 857 | |||
| 858 | fn init(cwd_path: []const u8) !Watch { | ||
| 859 | return .{ | ||
| 860 | .os = .{ .fse = try .init(cwd_path) }, | ||
| 861 | .dir_count = 0, | ||
| 862 | .dir_table = undefined, | ||
| 863 | .generation = undefined, | ||
| 864 | }; | ||
| 865 | } | ||
| 866 | fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 867 | try w.os.fse.setPaths(gpa, steps); | ||
| 868 | w.dir_count = w.os.fse.watch_roots.len; | ||
| 869 | } | ||
| 870 | fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 871 | _ = io; | ||
| 872 | return w.os.fse.wait(gpa, switch (timeout) { | ||
| 873 | .none => null, | ||
| 874 | .ms => |ms| @as(u64, ms) * std.time.ns_per_ms, | ||
| 875 | }); | ||
| 876 | } | ||
| 877 | }, | ||
| 878 | else => void, | ||
| 879 | }; | ||
| 880 | |||
| 881 | pub fn init(cwd_path: []const u8, configuration: *const Configuration, make_steps: []Step) !Watch { | ||
| 882 | return Os.init(cwd_path, configuration, make_steps); | ||
| 883 | } | ||
| 884 | |||
| 885 | pub const Match = struct { | ||
| 886 | /// Relative to the watched directory, the file path that triggers this | ||
| 887 | /// match. | ||
| 888 | basename: []const u8, | ||
| 889 | /// The step to re-run when file corresponding to `basename` is changed. | ||
| 890 | step_index: Configuration.Step.Index, | ||
| 891 | |||
| 892 | pub const Context = struct { | ||
| 893 | pub fn hash(self: Context, a: Match) u32 { | ||
| 894 | _ = self; | ||
| 895 | var hasher = Hash.init(@intFromEnum(a.step_index)); | ||
| 896 | hasher.update(a.basename); | ||
| 897 | return @truncate(hasher.final()); | ||
| 898 | } | ||
| 899 | pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool { | ||
| 900 | _ = self; | ||
| 901 | _ = b_index; | ||
| 902 | return a.step_index == b.step_index and std.mem.eql(u8, a.basename, b.basename); | ||
| 903 | } | ||
| 904 | }; | ||
| 905 | }; | ||
| 906 | |||
| 907 | fn markAllFilesDirty(w: *Watch, gpa: Allocator) void { | ||
| 908 | for (switch (builtin.os.tag) { | ||
| 909 | .windows => w.os.handle_table.keys(), | ||
| 910 | else => w.os.handle_table.values(), | ||
| 911 | }) |item| { | ||
| 912 | const reaction_set = switch (builtin.os.tag) { | ||
| 913 | .linux, .windows => item.reaction_set, | ||
| 914 | else => item, | ||
| 915 | }; | ||
| 916 | for (reaction_set.values()) |step_set| { | ||
| 917 | for (step_set.keys()) |step_index| { | ||
| 918 | const step = &w.make_steps[@intFromEnum(step_index)]; | ||
| 919 | _ = step.invalidateResult(gpa); | ||
| 920 | } | ||
| 921 | } | ||
| 922 | } | ||
| 923 | } | ||
| 924 | |||
| 925 | fn markStepSetDirty(gpa: Allocator, make_steps: []Step, step_set: *StepSet, any_dirty: bool) bool { | ||
| 926 | var this_any_dirty = false; | ||
| 927 | for (step_set.keys()) |step_index| { | ||
| 928 | const step = &make_steps[@intFromEnum(step_index)]; | ||
| 929 | if (step.invalidateResult(gpa)) this_any_dirty = true; | ||
| 930 | } | ||
| 931 | return any_dirty or this_any_dirty; | ||
| 932 | } | ||
| 933 | |||
| 934 | pub fn update(w: *Watch, gpa: Allocator, steps: []const Configuration.Step.Index) !void { | ||
| 935 | return Os.update(w, gpa, steps); | ||
| 936 | } | ||
| 937 | |||
| 938 | pub const Timeout = union(enum) { | ||
| 939 | none, | ||
| 940 | ms: u16, | ||
| 941 | |||
| 942 | pub fn to_i32_ms(t: Timeout) i32 { | ||
| 943 | return switch (t) { | ||
| 944 | .none => -1, | ||
| 945 | .ms => |ms| ms, | ||
| 946 | }; | ||
| 947 | } | ||
| 948 | |||
| 949 | pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec { | ||
| 950 | return switch (t) { | ||
| 951 | .none => null, | ||
| 952 | .ms => |ms_u16| { | ||
| 953 | const ms: isize = ms_u16; | ||
| 954 | buf.* = .{ | ||
| 955 | .sec = @divTrunc(ms, std.time.ms_per_s), | ||
| 956 | .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms, | ||
| 957 | }; | ||
| 958 | return buf; | ||
| 959 | }, | ||
| 960 | }; | ||
| 961 | } | ||
| 962 | }; | ||
| 963 | |||
| 964 | pub const WaitResult = enum { | ||
| 965 | timeout, | ||
| 966 | /// File system watching triggered on files that were marked as inputs to at least one Step. | ||
| 967 | /// Relevant steps have been marked dirty. | ||
| 968 | dirty, | ||
| 969 | /// File system watching triggered but none of the events were relevant to | ||
| 970 | /// what we are listening to. There is nothing to do. | ||
| 971 | clean, | ||
| 972 | }; | ||
| 973 | |||
| 974 | pub fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { | ||
| 975 | return Os.wait(w, gpa, io, timeout); | ||
| 976 | } | ||
lib/compiler/maker/Watch/FsEvents.zig deleted-479| ... | @@ -1,479 +0,0 @@ | ||
| 1 | //! An implementation of file-system watching based on the `FSEventStream` API in macOS. | ||
| 2 | //! While macOS supports kqueue, it does not allow detecting changes to files without | ||
| 3 | //! placing watches on each individual file, meaning FD limits are reached incredibly | ||
| 4 | //! quickly. The File System Events API works differently: it implements *recursive* | ||
| 5 | //! directory watches, managed by a system service. Rather than being in libc, the API is | ||
| 6 | //! exposed by the CoreServices framework. To avoid a compile dependency on the framework | ||
| 7 | //! bundle, we dynamically load CoreServices with `std.DynLib`. | ||
| 8 | //! | ||
| 9 | //! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been | ||
| 10 | //! made to keep that specialization to a minimum. Other use cases could be served with | ||
| 11 | //! relatively minimal modifications to the `watch_paths` field and its usages (in | ||
| 12 | //! particular the `setPaths` function). We avoid using the global GCD dispatch queue in | ||
| 13 | //! favour of creating our own and synchronizing with an explicit semaphore, meaning this | ||
| 14 | //! logic is thread-safe and does not affect process-global state. | ||
| 15 | //! | ||
| 16 | //! In theory, this API is quite good at avoiding filesystem race conditions. In practice, | ||
| 17 | //! the logic that would avoid them is currently disabled, because the build system kind | ||
| 18 | //! of relies on them at the time of writing to avoid redundant work -- see the comment at | ||
| 19 | //! the top of `wait` for details. | ||
| 20 | |||
| 21 | const enable_debug_logs = false; | ||
| 22 | |||
| 23 | core_services: std.DynLib, | ||
| 24 | resolved_symbols: ResolvedSymbols, | ||
| 25 | |||
| 26 | paths_arena: std.heap.ArenaAllocator.State, | ||
| 27 | /// The roots of the recursive watches. FSEvents has relatively small limits on the number | ||
| 28 | /// of watched paths, so this slice must not be too long. The paths themselves are allocated | ||
| 29 | /// into `paths_arena`, but this slice is allocated into the GPA. | ||
| 30 | watch_roots: [][:0]const u8, | ||
| 31 | /// All of the paths being watched. Value is the set of steps which depend on the file/directory. | ||
| 32 | /// Keys and values are in `paths_arena`, but this map is allocated into the GPA. | ||
| 33 | watch_paths: std.StringArrayHashMapUnmanaged([]const *std.Build.Step), | ||
| 34 | |||
| 35 | /// The semaphore we use to block the thread calling `wait` until the callback determines a relevant | ||
| 36 | /// event has occurred. This is retained across `wait` calls for simplicity and efficiency. | ||
| 37 | waiting_semaphore: dispatch.semaphore_t, | ||
| 38 | /// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the | ||
| 39 | /// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained | ||
| 40 | /// across `wait` calls for simplicity and efficiency. | ||
| 41 | dispatch_queue: dispatch.queue_t, | ||
| 42 | /// In theory, this field avoids race conditions. In practice, it is essentially unused at the time | ||
| 43 | /// of writing. See the comment at the start of `wait` for details. | ||
| 44 | since_event: FSEventStreamEventId, | ||
| 45 | |||
| 46 | cwd_path: []const u8, | ||
| 47 | |||
| 48 | /// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols | ||
| 49 | /// is not present, `init` will close the framework and return an error. | ||
| 50 | const ResolvedSymbols = struct { | ||
| 51 | FSEventStreamCreate: *const fn ( | ||
| 52 | allocator: CFAllocatorRef, | ||
| 53 | callback: FSEventStreamCallback, | ||
| 54 | ctx: ?*const FSEventStreamContext, | ||
| 55 | paths_to_watch: CFArrayRef, | ||
| 56 | since_when: FSEventStreamEventId, | ||
| 57 | latency: CFTimeInterval, | ||
| 58 | flags: FSEventStreamCreateFlags, | ||
| 59 | ) callconv(.c) FSEventStreamRef, | ||
| 60 | FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void, | ||
| 61 | FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool, | ||
| 62 | FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void, | ||
| 63 | FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void, | ||
| 64 | FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void, | ||
| 65 | FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId, | ||
| 66 | FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId, | ||
| 67 | CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void, | ||
| 68 | CFArrayCreate: *const fn ( | ||
| 69 | allocator: CFAllocatorRef, | ||
| 70 | values: [*]const usize, | ||
| 71 | num_values: CFIndex, | ||
| 72 | call_backs: ?*const CFArrayCallBacks, | ||
| 73 | ) callconv(.c) CFArrayRef, | ||
| 74 | CFStringCreateWithCString: *const fn ( | ||
| 75 | alloc: CFAllocatorRef, | ||
| 76 | c_str: [*:0]const u8, | ||
| 77 | encoding: CFStringEncoding, | ||
| 78 | ) callconv(.c) CFStringRef, | ||
| 79 | CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef, | ||
| 80 | kCFAllocatorUseContext: *const CFAllocatorRef, | ||
| 81 | }; | ||
| 82 | |||
| 83 | pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents { | ||
| 84 | var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch | ||
| 85 | return error.OpenFrameworkFailed; | ||
| 86 | errdefer core_services.close(); | ||
| 87 | |||
| 88 | var resolved_symbols: ResolvedSymbols = undefined; | ||
| 89 | inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| { | ||
| 90 | @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol; | ||
| 91 | } | ||
| 92 | |||
| 93 | return .{ | ||
| 94 | .core_services = core_services, | ||
| 95 | .resolved_symbols = resolved_symbols, | ||
| 96 | .paths_arena = .{}, | ||
| 97 | .watch_roots = &.{}, | ||
| 98 | .watch_paths = .empty, | ||
| 99 | .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources, | ||
| 100 | .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources, | ||
| 101 | // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order | ||
| 102 | // to notice any changes which happened during said work. | ||
| 103 | .since_event = resolved_symbols.FSEventsGetCurrentEventId(), | ||
| 104 | .cwd_path = cwd_path, | ||
| 105 | }; | ||
| 106 | } | ||
| 107 | |||
| 108 | pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void { | ||
| 109 | fse.waiting_semaphore.as_object().release(); | ||
| 110 | fse.dispatch_queue.as_object().release(); | ||
| 111 | fse.core_services.close(io); | ||
| 112 | |||
| 113 | gpa.free(fse.watch_roots); | ||
| 114 | fse.watch_paths.deinit(gpa); | ||
| 115 | { | ||
| 116 | var paths_arena = fse.paths_arena.promote(gpa); | ||
| 117 | paths_arena.deinit(); | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) !void { | ||
| 122 | var paths_arena_instance = fse.paths_arena.promote(gpa); | ||
| 123 | defer fse.paths_arena = paths_arena_instance.state; | ||
| 124 | const paths_arena = paths_arena_instance.allocator(); | ||
| 125 | |||
| 126 | var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty; | ||
| 127 | defer need_dirs.deinit(gpa); | ||
| 128 | |||
| 129 | fse.watch_paths.clearRetainingCapacity(); | ||
| 130 | |||
| 131 | // We take `step` by pointer for a slight memory optimization in a moment. | ||
| 132 | for (steps) |*step| { | ||
| 133 | for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| { | ||
| 134 | const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{ | ||
| 135 | fse.cwd_path, path.root_dir.path orelse ".", path.sub_path, | ||
| 136 | }); | ||
| 137 | try need_dirs.put(gpa, resolved_dir, {}); | ||
| 138 | for (files.items) |file_name| { | ||
| 139 | const watch_path = if (std.mem.eql(u8, file_name, ".")) | ||
| 140 | resolved_dir | ||
| 141 | else | ||
| 142 | try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name }); | ||
| 143 | const gop = try fse.watch_paths.getOrPut(gpa, watch_path); | ||
| 144 | if (gop.found_existing) { | ||
| 145 | const old_steps = gop.value_ptr.*; | ||
| 146 | const new_steps = try paths_arena.alloc(*std.Build.Step, old_steps.len + 1); | ||
| 147 | @memcpy(new_steps[0..old_steps.len], old_steps); | ||
| 148 | new_steps[old_steps.len] = step.*; | ||
| 149 | gop.value_ptr.* = new_steps; | ||
| 150 | } else { | ||
| 151 | // This is why we captured `step` by pointer! We can avoid allocating a slice of one | ||
| 152 | // step in the arena in the common case where a file is referenced by only one step. | ||
| 153 | gop.value_ptr.* = step[0..1]; | ||
| 154 | } | ||
| 155 | } | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | { | ||
| 160 | // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar"). | ||
| 161 | // To eliminate these, we'll re-add directories in order of path length with a redundancy check. | ||
| 162 | const old_dirs = try gpa.dupe([]const u8, need_dirs.keys()); | ||
| 163 | defer gpa.free(old_dirs); | ||
| 164 | std.mem.sort([]const u8, old_dirs, {}, struct { | ||
| 165 | fn lessThan(ctx: void, a: []const u8, b: []const u8) bool { | ||
| 166 | ctx; | ||
| 167 | return std.mem.lessThan(u8, a, b); | ||
| 168 | } | ||
| 169 | }.lessThan); | ||
| 170 | need_dirs.clearRetainingCapacity(); | ||
| 171 | for (old_dirs) |dir_path| { | ||
| 172 | var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path); | ||
| 173 | while (it.next()) |component| { | ||
| 174 | if (need_dirs.contains(component.path)) { | ||
| 175 | // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added | ||
| 176 | break; | ||
| 177 | } | ||
| 178 | } else { | ||
| 179 | need_dirs.putAssumeCapacityNoClobber(dir_path, {}); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very | ||
| 185 | // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/` | ||
| 186 | // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit | ||
| 187 | // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be | ||
| 188 | // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below* | ||
| 189 | // that known limit. | ||
| 190 | if (need_dirs.count() > 2048) { | ||
| 191 | // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P | ||
| 192 | if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{}); | ||
| 193 | fse.watch_roots = try gpa.realloc(fse.watch_roots, 1); | ||
| 194 | fse.watch_roots[0] = "/"; | ||
| 195 | } else { | ||
| 196 | fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count()); | ||
| 197 | for (fse.watch_roots, need_dirs.keys()) |*out, in| { | ||
| 198 | out.* = try paths_arena.dupeSentinel(u8, in, 0); | ||
| 199 | } | ||
| 200 | } | ||
| 201 | if (enable_debug_logs) { | ||
| 202 | watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len }); | ||
| 203 | for (fse.watch_roots) |dir_path| { | ||
| 204 | watch_log.debug("- '{s}'", .{dir_path}); | ||
| 205 | } | ||
| 206 | } | ||
| 207 | } | ||
| 208 | |||
| 209 | pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!std.Build.Watch.WaitResult { | ||
| 210 | if (fse.watch_roots.len == 0) @panic("nothing to watch"); | ||
| 211 | |||
| 212 | const rs = fse.resolved_symbols; | ||
| 213 | |||
| 214 | // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds | ||
| 215 | // to occur, because one step modifies a file which is an input to another step. The solution | ||
| 216 | // to this problem will probably be either: | ||
| 217 | // | ||
| 218 | // a) Don't include the output of one step as a watch input of another; only mark external | ||
| 219 | // files as watch inputs. Or... | ||
| 220 | // | ||
| 221 | // b) Note the current event ID when a step begins, and disregard events preceding that ID | ||
| 222 | // when considering whether to dirty that step in `eventCallback`. | ||
| 223 | // | ||
| 224 | // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does | ||
| 225 | // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those | ||
| 226 | // too at the time of writing, so this is kind of expected. | ||
| 227 | fse.since_event = .since_now; | ||
| 228 | |||
| 229 | const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{ | ||
| 230 | .version = 0, | ||
| 231 | .info = @constCast(&gpa), | ||
| 232 | .retain = null, | ||
| 233 | .release = null, | ||
| 234 | .copy_description = null, | ||
| 235 | .allocate = &cf_alloc_callbacks.allocate, | ||
| 236 | .reallocate = &cf_alloc_callbacks.reallocate, | ||
| 237 | .deallocate = &cf_alloc_callbacks.deallocate, | ||
| 238 | .preferred_size = null, | ||
| 239 | }) orelse return error.OutOfMemory; | ||
| 240 | defer rs.CFRelease(cf_allocator); | ||
| 241 | |||
| 242 | const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len); | ||
| 243 | @memset(cf_paths, null); | ||
| 244 | defer { | ||
| 245 | for (cf_paths) |o| if (o) |p| rs.CFRelease(p); | ||
| 246 | gpa.free(cf_paths); | ||
| 247 | } | ||
| 248 | for (fse.watch_roots, cf_paths) |raw_path, *cf_path| { | ||
| 249 | cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8); | ||
| 250 | } | ||
| 251 | const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null); | ||
| 252 | defer rs.CFRelease(cf_paths_array); | ||
| 253 | |||
| 254 | const callback_ctx: EventCallbackCtx = .{ | ||
| 255 | .fse = fse, | ||
| 256 | .gpa = gpa, | ||
| 257 | }; | ||
| 258 | const event_stream = rs.FSEventStreamCreate( | ||
| 259 | null, | ||
| 260 | &eventCallback, | ||
| 261 | &.{ | ||
| 262 | .version = 0, | ||
| 263 | .info = @constCast(&callback_ctx), | ||
| 264 | .retain = null, | ||
| 265 | .release = null, | ||
| 266 | .copy_description = null, | ||
| 267 | }, | ||
| 268 | cf_paths_array, | ||
| 269 | fse.since_event, | ||
| 270 | 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events | ||
| 271 | .{ .watch_root = true, .file_events = true }, | ||
| 272 | ); | ||
| 273 | defer rs.FSEventStreamRelease(event_stream); | ||
| 274 | rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue); | ||
| 275 | defer rs.FSEventStreamInvalidate(event_stream); | ||
| 276 | if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed; | ||
| 277 | defer rs.FSEventStreamStop(event_stream); | ||
| 278 | const result = fse.waiting_semaphore.wait(timeout: { | ||
| 279 | const ns = timeout_ns orelse break :timeout .FOREVER; | ||
| 280 | break :timeout .time(.NOW, @intCast(ns)); | ||
| 281 | }); | ||
| 282 | return switch (result) { | ||
| 283 | 0 => .dirty, | ||
| 284 | else => .timeout, | ||
| 285 | }; | ||
| 286 | } | ||
| 287 | |||
| 288 | const cf_alloc_callbacks = struct { | ||
| 289 | const log = std.log.scoped(.cf_alloc); | ||
| 290 | fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { | ||
| 291 | if (enable_debug_logs) log.debug("allocate {d}", .{size}); | ||
| 292 | _ = hint; | ||
| 293 | const gpa: *const Allocator = @ptrCast(@alignCast(info)); | ||
| 294 | const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null; | ||
| 295 | const metadata: *usize = @ptrCast(mem); | ||
| 296 | metadata.* = @intCast(size); | ||
| 297 | return mem[@sizeOf(usize)..].ptr; | ||
| 298 | } | ||
| 299 | fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { | ||
| 300 | if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size }); | ||
| 301 | _ = hint; | ||
| 302 | if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL | ||
| 303 | const gpa: *const Allocator = @ptrCast(@alignCast(info)); | ||
| 304 | const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); | ||
| 305 | const old_size = @as(*const usize, @ptrCast(old_base)).*; | ||
| 306 | const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; | ||
| 307 | const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null; | ||
| 308 | const metadata: *usize = @ptrCast(new_mem); | ||
| 309 | metadata.* = @intCast(new_size); | ||
| 310 | return new_mem[@sizeOf(usize)..].ptr; | ||
| 311 | } | ||
| 312 | fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void { | ||
| 313 | if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr}); | ||
| 314 | const gpa: *const Allocator = @ptrCast(@alignCast(info)); | ||
| 315 | const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); | ||
| 316 | const old_size = @as(*const usize, @ptrCast(old_base)).*; | ||
| 317 | const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; | ||
| 318 | gpa.free(old_mem); | ||
| 319 | } | ||
| 320 | }; | ||
| 321 | |||
| 322 | const EventCallbackCtx = struct { | ||
| 323 | fse: *FsEvents, | ||
| 324 | gpa: Allocator, | ||
| 325 | }; | ||
| 326 | |||
| 327 | fn eventCallback( | ||
| 328 | stream: ConstFSEventStreamRef, | ||
| 329 | client_callback_info: ?*anyopaque, | ||
| 330 | num_events: usize, | ||
| 331 | events_paths_ptr: *anyopaque, | ||
| 332 | events_flags_ptr: [*]const FSEventStreamEventFlags, | ||
| 333 | events_ids_ptr: [*]const FSEventStreamEventId, | ||
| 334 | ) callconv(.c) void { | ||
| 335 | const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info)); | ||
| 336 | const fse = ctx.fse; | ||
| 337 | const gpa = ctx.gpa; | ||
| 338 | const rs = fse.resolved_symbols; | ||
| 339 | const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr)); | ||
| 340 | const events_paths = events_paths_ptr_casted[0..num_events]; | ||
| 341 | const events_ids = events_ids_ptr[0..num_events]; | ||
| 342 | const events_flags = events_flags_ptr[0..num_events]; | ||
| 343 | var any_dirty = false; | ||
| 344 | for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| { | ||
| 345 | _ = event_id; | ||
| 346 | if (event_flags.history_done) continue; // sentinel | ||
| 347 | const event_path = std.mem.span(event_path_nts); | ||
| 348 | switch (event_flags.must_scan_sub_dirs) { | ||
| 349 | false => { | ||
| 350 | if (fse.watch_paths.get(event_path)) |steps| { | ||
| 351 | assert(steps.len > 0); | ||
| 352 | for (steps) |s| { | ||
| 353 | if (s.invalidateResult(gpa)) any_dirty = true; | ||
| 354 | } | ||
| 355 | } | ||
| 356 | if (std.fs.path.dirname(event_path)) |event_dirname| { | ||
| 357 | // Modifying '/foo/bar' triggers the watch on '/foo'. | ||
| 358 | if (fse.watch_paths.get(event_dirname)) |steps| { | ||
| 359 | assert(steps.len > 0); | ||
| 360 | for (steps) |s| { | ||
| 361 | if (s.invalidateResult(gpa)) any_dirty = true; | ||
| 362 | } | ||
| 363 | } | ||
| 364 | } | ||
| 365 | }, | ||
| 366 | true => { | ||
| 367 | // This is unlikely, but can occasionally happen when bottlenecked: events have been | ||
| 368 | // coalesced into one. We want to see if any of these events are actually relevant | ||
| 369 | // to us. The only way we can reasonably do that in this rare edge case is iterate | ||
| 370 | // the watch paths and see if any is under this directory. That's acceptable because | ||
| 371 | // we would otherwise kick off a rebuild which would be clearing those paths anyway. | ||
| 372 | const changed_path = std.fs.path.dirname(event_path) orelse event_path; | ||
| 373 | for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| { | ||
| 374 | if (dirStartsWith(watching_path, changed_path)) { | ||
| 375 | for (steps) |s| { | ||
| 376 | if (s.invalidateResult(gpa)) any_dirty = true; | ||
| 377 | } | ||
| 378 | } | ||
| 379 | } | ||
| 380 | }, | ||
| 381 | } | ||
| 382 | } | ||
| 383 | if (any_dirty) { | ||
| 384 | fse.since_event = rs.FSEventStreamGetLatestEventId(stream); | ||
| 385 | _ = fse.waiting_semaphore.signal(); | ||
| 386 | } | ||
| 387 | } | ||
| 388 | fn dirStartsWith(path: []const u8, prefix: []const u8) bool { | ||
| 389 | if (std.mem.eql(u8, path, prefix)) return true; | ||
| 390 | if (!std.mem.startsWith(u8, path, prefix)) return false; | ||
| 391 | if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar` | ||
| 392 | return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar` | ||
| 393 | } | ||
| 394 | |||
| 395 | const CFAllocatorRef = ?*const opaque {}; | ||
| 396 | const CFArrayRef = *const opaque {}; | ||
| 397 | const CFStringRef = *const opaque {}; | ||
| 398 | const CFTimeInterval = f64; | ||
| 399 | const CFIndex = i32; | ||
| 400 | const CFOptionFlags = enum(u32) { _ }; | ||
| 401 | const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque; | ||
| 402 | const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void; | ||
| 403 | const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef; | ||
| 404 | const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; | ||
| 405 | const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; | ||
| 406 | const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void; | ||
| 407 | const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex; | ||
| 408 | const CFAllocatorContext = extern struct { | ||
| 409 | version: CFIndex, | ||
| 410 | info: ?*anyopaque, | ||
| 411 | retain: ?CFAllocatorRetainCallBack, | ||
| 412 | release: ?CFAllocatorReleaseCallBack, | ||
| 413 | copy_description: ?CFAllocatorCopyDescriptionCallBack, | ||
| 414 | allocate: CFAllocatorAllocateCallBack, | ||
| 415 | reallocate: ?CFAllocatorReallocateCallBack, | ||
| 416 | deallocate: ?CFAllocatorDeallocateCallBack, | ||
| 417 | preferred_size: ?CFAllocatorPreferredSizeCallBack, | ||
| 418 | }; | ||
| 419 | const CFArrayCallBacks = opaque {}; | ||
| 420 | const CFStringEncoding = enum(u32) { | ||
| 421 | invalid_id = std.math.maxInt(u32), | ||
| 422 | mac_roman = 0, | ||
| 423 | windows_latin_1 = 0x500, | ||
| 424 | iso_latin_1 = 0x201, | ||
| 425 | next_step_latin = 0xB01, | ||
| 426 | ascii = 0x600, | ||
| 427 | unicode = 0x100, | ||
| 428 | utf8 = 0x8000100, | ||
| 429 | non_lossy_ascii = 0xBFF, | ||
| 430 | }; | ||
| 431 | |||
| 432 | const FSEventStreamRef = *opaque {}; | ||
| 433 | const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child; | ||
| 434 | const FSEventStreamCallback = *const fn ( | ||
| 435 | stream: ConstFSEventStreamRef, | ||
| 436 | client_callback_info: ?*anyopaque, | ||
| 437 | num_events: usize, | ||
| 438 | event_paths: *anyopaque, | ||
| 439 | event_flags: [*]const FSEventStreamEventFlags, | ||
| 440 | event_ids: [*]const FSEventStreamEventId, | ||
| 441 | ) callconv(.c) void; | ||
| 442 | const FSEventStreamContext = extern struct { | ||
| 443 | version: CFIndex, | ||
| 444 | info: ?*anyopaque, | ||
| 445 | retain: ?CFAllocatorRetainCallBack, | ||
| 446 | release: ?CFAllocatorReleaseCallBack, | ||
| 447 | copy_description: ?CFAllocatorCopyDescriptionCallBack, | ||
| 448 | }; | ||
| 449 | const FSEventStreamEventId = enum(u64) { | ||
| 450 | since_now = std.math.maxInt(u64), | ||
| 451 | _, | ||
| 452 | }; | ||
| 453 | const FSEventStreamCreateFlags = packed struct(u32) { | ||
| 454 | use_cf_types: bool = false, | ||
| 455 | no_defer: bool = false, | ||
| 456 | watch_root: bool = false, | ||
| 457 | ignore_self: bool = false, | ||
| 458 | file_events: bool = false, | ||
| 459 | _: u27 = 0, | ||
| 460 | }; | ||
| 461 | const FSEventStreamEventFlags = packed struct(u32) { | ||
| 462 | must_scan_sub_dirs: bool, | ||
| 463 | user_dropped: bool, | ||
| 464 | kernel_dropped: bool, | ||
| 465 | event_ids_wrapped: bool, | ||
| 466 | history_done: bool, | ||
| 467 | root_changed: bool, | ||
| 468 | mount: bool, | ||
| 469 | unmount: bool, | ||
| 470 | _: u24 = 0, | ||
| 471 | }; | ||
| 472 | |||
| 473 | const dispatch = std.c.dispatch; | ||
| 474 | const std = @import("std"); | ||
| 475 | const Io = std.Io; | ||
| 476 | const assert = std.debug.assert; | ||
| 477 | const Allocator = std.mem.Allocator; | ||
| 478 | const watch_log = std.log.scoped(.watch); | ||
| 479 | const FsEvents = @This(); | ||
lib/compiler/maker/WebServer.zig deleted-940| ... | @@ -1,940 +0,0 @@ | ||
| 1 | const WebServer = @This(); | ||
| 2 | |||
| 3 | const builtin = @import("builtin"); | ||
| 4 | |||
| 5 | const std = @import("std"); | ||
| 6 | const Allocator = std.mem.Allocator; | ||
| 7 | const Cache = std.Build.Cache; | ||
| 8 | const Configuration = std.Build.Configuration; | ||
| 9 | const Io = std.Io; | ||
| 10 | const abi = std.Build.abi; | ||
| 11 | const assert = std.debug.assert; | ||
| 12 | const http = std.http; | ||
| 13 | const log = std.log.scoped(.web_server); | ||
| 14 | const mem = std.mem; | ||
| 15 | const net = std.Io.net; | ||
| 16 | |||
| 17 | const Fuzz = @import("Fuzz.zig"); | ||
| 18 | const Graph = @import("Graph.zig"); | ||
| 19 | const Step = @import("Step.zig"); | ||
| 20 | |||
| 21 | gpa: Allocator, | ||
| 22 | graph: *const Graph, | ||
| 23 | all_steps: []const Configuration.Step.Index, | ||
| 24 | listen_address: net.IpAddress, | ||
| 25 | root_prog_node: std.Progress.Node, | ||
| 26 | watch: bool, | ||
| 27 | |||
| 28 | tcp_server: ?net.Server, | ||
| 29 | serve_task: ?Io.Future(Io.Cancelable!void), | ||
| 30 | |||
| 31 | /// Uses `Io.Clock.awake`. | ||
| 32 | base_timestamp: Io.Timestamp, | ||
| 33 | /// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`. | ||
| 34 | step_names_trailing: []u8, | ||
| 35 | |||
| 36 | /// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps. | ||
| 37 | /// Accessed atomically. | ||
| 38 | step_status_bits: []u8, | ||
| 39 | |||
| 40 | fuzz: ?Fuzz, | ||
| 41 | time_report_mutex: Io.Mutex, | ||
| 42 | time_report_msgs: [][]u8, | ||
| 43 | time_report_update_times: []i64, | ||
| 44 | |||
| 45 | build_status: std.atomic.Value(abi.BuildStatus), | ||
| 46 | /// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate` | ||
| 47 | /// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so | ||
| 48 | /// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it | ||
| 49 | /// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For | ||
| 50 | /// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes, | ||
| 51 | /// because this value changes quickly so this would result in constantly spamming all clients with | ||
| 52 | /// an unreasonable number of packets. | ||
| 53 | update_id: std.atomic.Value(u32), | ||
| 54 | |||
| 55 | runner_request_mutex: Io.Mutex, | ||
| 56 | runner_request_ready_cond: Io.Condition, | ||
| 57 | runner_request_empty_cond: Io.Condition, | ||
| 58 | runner_request: ?RunnerRequest, | ||
| 59 | |||
| 60 | /// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates | ||
| 61 | /// on a fixed interval of this many milliseconds. | ||
| 62 | const default_update_interval_ms = 500; | ||
| 63 | |||
| 64 | pub const base_clock: Io.Clock = .awake; | ||
| 65 | |||
| 66 | /// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`. | ||
| 67 | pub fn notifyUpdate(ws: *WebServer) void { | ||
| 68 | _ = ws.update_id.rmw(.Add, 1, .release); | ||
| 69 | ws.graph.io.futexWake(u32, &ws.update_id.raw, 16); | ||
| 70 | } | ||
| 71 | |||
| 72 | pub const Options = struct { | ||
| 73 | gpa: Allocator, | ||
| 74 | graph: *const Graph, | ||
| 75 | all_steps: []const Configuration.Step.Index, | ||
| 76 | root_prog_node: std.Progress.Node, | ||
| 77 | watch: bool, | ||
| 78 | listen_address: net.IpAddress, | ||
| 79 | base_timestamp: Io.Clock.Timestamp, | ||
| 80 | configuration: *const Configuration, | ||
| 81 | }; | ||
| 82 | pub fn init(opts: Options) WebServer { | ||
| 83 | // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent` | ||
| 84 | // instead of threads, so that the web server can function in single-threaded builds. | ||
| 85 | comptime assert(!builtin.single_threaded); | ||
| 86 | assert(opts.base_timestamp.clock == base_clock); | ||
| 87 | |||
| 88 | const all_steps = opts.all_steps; | ||
| 89 | const c = opts.configuration; | ||
| 90 | |||
| 91 | const step_names_trailing = opts.gpa.alloc(u8, len: { | ||
| 92 | var name_bytes: usize = 0; | ||
| 93 | for (all_steps) |step_index| name_bytes += step_index.ptr(c).name.slice(c).len; | ||
| 94 | break :len name_bytes + all_steps.len * 4; | ||
| 95 | }) catch @panic("out of memory"); | ||
| 96 | { | ||
| 97 | const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]); | ||
| 98 | var idx: usize = all_steps.len * 4; | ||
| 99 | for (all_steps, step_name_lens) |step_index, *name_len| { | ||
| 100 | const step_name = step_index.ptr(c).name.slice(c); | ||
| 101 | name_len.* = @intCast(step_name.len); | ||
| 102 | @memcpy(step_names_trailing[idx..][0..step_name.len], step_name); | ||
| 103 | idx += step_name.len; | ||
| 104 | } | ||
| 105 | assert(idx == step_names_trailing.len); | ||
| 106 | } | ||
| 107 | |||
| 108 | const step_status_bits = opts.gpa.alloc( | ||
| 109 | u8, | ||
| 110 | std.math.divCeil(usize, all_steps.len, 4) catch unreachable, | ||
| 111 | ) catch @panic("out of memory"); | ||
| 112 | @memset(step_status_bits, 0); | ||
| 113 | |||
| 114 | const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0; | ||
| 115 | const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory"); | ||
| 116 | const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory"); | ||
| 117 | @memset(time_report_msgs, &.{}); | ||
| 118 | @memset(time_report_update_times, std.math.minInt(i64)); | ||
| 119 | |||
| 120 | return .{ | ||
| 121 | .gpa = opts.gpa, | ||
| 122 | .graph = opts.graph, | ||
| 123 | .all_steps = all_steps, | ||
| 124 | .listen_address = opts.listen_address, | ||
| 125 | .root_prog_node = opts.root_prog_node, | ||
| 126 | .watch = opts.watch, | ||
| 127 | |||
| 128 | .tcp_server = null, | ||
| 129 | .serve_task = null, | ||
| 130 | |||
| 131 | .base_timestamp = opts.base_timestamp.raw, | ||
| 132 | .step_names_trailing = step_names_trailing, | ||
| 133 | |||
| 134 | .step_status_bits = step_status_bits, | ||
| 135 | |||
| 136 | .fuzz = null, | ||
| 137 | .time_report_mutex = .init, | ||
| 138 | .time_report_msgs = time_report_msgs, | ||
| 139 | .time_report_update_times = time_report_update_times, | ||
| 140 | |||
| 141 | .build_status = .init(.idle), | ||
| 142 | .update_id = .init(0), | ||
| 143 | |||
| 144 | .runner_request_mutex = .init, | ||
| 145 | .runner_request_ready_cond = .init, | ||
| 146 | .runner_request_empty_cond = .init, | ||
| 147 | .runner_request = null, | ||
| 148 | }; | ||
| 149 | } | ||
| 150 | pub fn deinit(ws: *WebServer) void { | ||
| 151 | const gpa = ws.gpa; | ||
| 152 | const io = ws.graph.io; | ||
| 153 | |||
| 154 | gpa.free(ws.step_names_trailing); | ||
| 155 | gpa.free(ws.step_status_bits); | ||
| 156 | |||
| 157 | if (ws.fuzz) |*f| f.deinit(); | ||
| 158 | for (ws.time_report_msgs) |msg| gpa.free(msg); | ||
| 159 | gpa.free(ws.time_report_msgs); | ||
| 160 | gpa.free(ws.time_report_update_times); | ||
| 161 | |||
| 162 | if (ws.serve_task) |t| { | ||
| 163 | if (ws.tcp_server) |*s| s.stream.close(io); | ||
| 164 | t.await(); | ||
| 165 | } | ||
| 166 | if (ws.tcp_server) |*s| s.deinit(); | ||
| 167 | |||
| 168 | gpa.free(ws.step_names_trailing); | ||
| 169 | } | ||
| 170 | pub fn start(ws: *WebServer) error{AlreadyReported}!void { | ||
| 171 | assert(ws.tcp_server == null); | ||
| 172 | assert(ws.serve_task == null); | ||
| 173 | const io = ws.graph.io; | ||
| 174 | |||
| 175 | ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| { | ||
| 176 | log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err }); | ||
| 177 | return error.AlreadyReported; | ||
| 178 | }; | ||
| 179 | ws.serve_task = io.concurrent(serve, .{ws}) catch |err| { | ||
| 180 | log.err("unable to spawn web server thread: {t}", .{err}); | ||
| 181 | ws.tcp_server.?.deinit(io); | ||
| 182 | ws.tcp_server = null; | ||
| 183 | return error.AlreadyReported; | ||
| 184 | }; | ||
| 185 | |||
| 186 | log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address}); | ||
| 187 | if (ws.listen_address.getPort() == 0) { | ||
| 188 | log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address}); | ||
| 189 | } | ||
| 190 | } | ||
| 191 | fn serve(ws: *WebServer) Io.Cancelable!void { | ||
| 192 | const io = ws.graph.io; | ||
| 193 | var group: Io.Group = .init; | ||
| 194 | defer group.cancel(io); | ||
| 195 | while (true) { | ||
| 196 | var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) { | ||
| 197 | error.Canceled => |e| return e, | ||
| 198 | else => |e| { | ||
| 199 | log.err("failed to accept connection: {t}", .{e}); | ||
| 200 | return; | ||
| 201 | }, | ||
| 202 | }; | ||
| 203 | group.concurrent(io, accept, .{ ws, stream }) catch |err| { | ||
| 204 | log.err("unable to spawn connection thread: {t}", .{err}); | ||
| 205 | stream.close(io); | ||
| 206 | continue; | ||
| 207 | }; | ||
| 208 | } | ||
| 209 | } | ||
| 210 | |||
| 211 | pub fn startBuild(ws: *WebServer) void { | ||
| 212 | if (ws.fuzz) |*fuzz| { | ||
| 213 | fuzz.deinit(); | ||
| 214 | ws.fuzz = null; | ||
| 215 | } | ||
| 216 | for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic); | ||
| 217 | ws.build_status.store(.running, .monotonic); | ||
| 218 | ws.notifyUpdate(); | ||
| 219 | } | ||
| 220 | |||
| 221 | pub fn updateStepStatus( | ||
| 222 | ws: *WebServer, | ||
| 223 | step_index: Configuration.Step.Index, | ||
| 224 | new_status: abi.StepUpdate.Status, | ||
| 225 | ) void { | ||
| 226 | // TODO don't do linear search, especially in a hot loop like this | ||
| 227 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 228 | if (s == step_index) break @intCast(i); | ||
| 229 | } else unreachable; | ||
| 230 | const ptr = &ws.step_status_bits[step_idx / 4]; | ||
| 231 | const bit_offset: u3 = @intCast((step_idx % 4) * 2); | ||
| 232 | const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset); | ||
| 233 | const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset; | ||
| 234 | _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic); | ||
| 235 | ws.notifyUpdate(); | ||
| 236 | } | ||
| 237 | |||
| 238 | pub fn finishBuild(ws: *WebServer, opts: struct { | ||
| 239 | fuzz: bool, | ||
| 240 | }) void { | ||
| 241 | if (opts.fuzz) { | ||
| 242 | switch (builtin.os.tag) { | ||
| 243 | // Current implementation depends on two things that need to be ported to Windows: | ||
| 244 | // * Memory-mapping to share data between the fuzzer and build runner. | ||
| 245 | // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving | ||
| 246 | // many addresses to source locations). | ||
| 247 | .windows => std.process.fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}), | ||
| 248 | else => {}, | ||
| 249 | } | ||
| 250 | if (@bitSizeOf(usize) != 64) { | ||
| 251 | // Current implementation depends on posix.mmap()'s second | ||
| 252 | // parameter, `length: usize`, being compatible with file system's | ||
| 253 | // u64 return value. This is not the case on 32-bit platforms. | ||
| 254 | // Affects or affected by issues #5185, #22523, and #22464. | ||
| 255 | std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); | ||
| 256 | } | ||
| 257 | |||
| 258 | assert(ws.fuzz == null); | ||
| 259 | |||
| 260 | ws.build_status.store(.fuzz_init, .monotonic); | ||
| 261 | ws.notifyUpdate(); | ||
| 262 | |||
| 263 | ws.fuzz = Fuzz.init( | ||
| 264 | ws.gpa, | ||
| 265 | ws.graph.io, | ||
| 266 | ws.all_steps, | ||
| 267 | ws.root_prog_node, | ||
| 268 | .{ .forever = .{ .ws = ws } }, | ||
| 269 | ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)}); | ||
| 270 | ws.fuzz.?.start(); | ||
| 271 | } | ||
| 272 | |||
| 273 | ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic); | ||
| 274 | ws.notifyUpdate(); | ||
| 275 | } | ||
| 276 | |||
| 277 | pub fn now(s: *const WebServer) i64 { | ||
| 278 | const io = s.graph.io; | ||
| 279 | const ts = base_clock.now(io); | ||
| 280 | return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds()); | ||
| 281 | } | ||
| 282 | |||
| 283 | fn accept(ws: *WebServer, stream: net.Stream) void { | ||
| 284 | const io = ws.graph.io; | ||
| 285 | defer { | ||
| 286 | // `net.Stream.close` wants to helpfully overwrite `stream` with | ||
| 287 | // `undefined`, but it cannot do so since it is an immutable parameter. | ||
| 288 | var copy = stream; | ||
| 289 | copy.close(io); | ||
| 290 | } | ||
| 291 | var send_buffer: [4096]u8 = undefined; | ||
| 292 | var recv_buffer: [4096]u8 = undefined; | ||
| 293 | var connection_reader = stream.reader(io, &recv_buffer); | ||
| 294 | var connection_writer = stream.writer(io, &send_buffer); | ||
| 295 | var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); | ||
| 296 | |||
| 297 | while (true) { | ||
| 298 | var request = server.receiveHead() catch |err| switch (err) { | ||
| 299 | error.HttpConnectionClosing => return, | ||
| 300 | else => return log.err("failed to receive http request: {t}", .{err}), | ||
| 301 | }; | ||
| 302 | switch (request.upgradeRequested()) { | ||
| 303 | .websocket => |opt_key| { | ||
| 304 | const key = opt_key orelse return log.err("missing websocket key", .{}); | ||
| 305 | var web_socket = request.respondWebSocket(.{ .key = key }) catch { | ||
| 306 | return log.err("failed to respond web socket: {t}", .{connection_writer.err.?}); | ||
| 307 | }; | ||
| 308 | ws.serveWebSocket(&web_socket) catch |err| { | ||
| 309 | log.err("failed to serve websocket: {t}", .{err}); | ||
| 310 | return; | ||
| 311 | }; | ||
| 312 | comptime unreachable; | ||
| 313 | }, | ||
| 314 | .other => |name| return log.err("unknown upgrade request: {s}", .{name}), | ||
| 315 | .none => { | ||
| 316 | ws.serveRequest(&request) catch |err| switch (err) { | ||
| 317 | error.AlreadyReported => return, | ||
| 318 | else => { | ||
| 319 | log.err("failed to serve '{s}': {t}", .{ request.head.target, err }); | ||
| 320 | return; | ||
| 321 | }, | ||
| 322 | }; | ||
| 323 | }, | ||
| 324 | } | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn { | ||
| 329 | const io = ws.graph.io; | ||
| 330 | |||
| 331 | var prev_build_status = ws.build_status.load(.monotonic); | ||
| 332 | |||
| 333 | const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len); | ||
| 334 | defer ws.gpa.free(prev_step_status_bits); | ||
| 335 | for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| { | ||
| 336 | copy.* = @atomicLoad(u8, shared, .monotonic); | ||
| 337 | } | ||
| 338 | |||
| 339 | var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock }); | ||
| 340 | defer recv_thread.cancel(io); | ||
| 341 | |||
| 342 | { | ||
| 343 | const hello_header: abi.Hello = .{ | ||
| 344 | .status = prev_build_status, | ||
| 345 | .flags = .{ | ||
| 346 | .time_report = ws.graph.time_report, | ||
| 347 | }, | ||
| 348 | .timestamp = ws.now(), | ||
| 349 | .steps_len = @intCast(ws.all_steps.len), | ||
| 350 | }; | ||
| 351 | var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits }; | ||
| 352 | try sock.writeMessageVec(&bufs, .binary); | ||
| 353 | } | ||
| 354 | |||
| 355 | var prev_fuzz: Fuzz.Previous = .init; | ||
| 356 | var prev_time: i64 = std.math.minInt(i64); | ||
| 357 | while (true) { | ||
| 358 | const start_time = ws.now(); | ||
| 359 | const start_update_id = ws.update_id.load(.acquire); | ||
| 360 | |||
| 361 | if (ws.fuzz) |*fuzz| { | ||
| 362 | try fuzz.sendUpdate(sock, &prev_fuzz); | ||
| 363 | } | ||
| 364 | |||
| 365 | { | ||
| 366 | try ws.time_report_mutex.lock(io); | ||
| 367 | defer ws.time_report_mutex.unlock(io); | ||
| 368 | for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| { | ||
| 369 | if (update_time <= prev_time) continue; | ||
| 370 | // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so | ||
| 371 | // that we don't hold up the build system on the client accepting this packet. | ||
| 372 | const owned_msg = try ws.gpa.dupe(u8, msg); | ||
| 373 | defer ws.gpa.free(owned_msg); | ||
| 374 | // Temporarily unlock, then re-lock after the message is sent. | ||
| 375 | ws.time_report_mutex.unlock(io); | ||
| 376 | defer ws.time_report_mutex.lockUncancelable(io); | ||
| 377 | try sock.writeMessage(owned_msg, .binary); | ||
| 378 | } | ||
| 379 | } | ||
| 380 | |||
| 381 | { | ||
| 382 | const build_status = ws.build_status.load(.monotonic); | ||
| 383 | if (build_status != prev_build_status) { | ||
| 384 | prev_build_status = build_status; | ||
| 385 | const msg: abi.StatusUpdate = .{ .new = build_status }; | ||
| 386 | try sock.writeMessage(@ptrCast(&msg), .binary); | ||
| 387 | } | ||
| 388 | } | ||
| 389 | |||
| 390 | for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| { | ||
| 391 | const cur_byte = @atomicLoad(u8, shared, .monotonic); | ||
| 392 | if (prev_byte.* == cur_byte) continue; | ||
| 393 | const cur: [4]abi.StepUpdate.Status = .{ | ||
| 394 | @enumFromInt(@as(u2, @truncate(cur_byte >> 0))), | ||
| 395 | @enumFromInt(@as(u2, @truncate(cur_byte >> 2))), | ||
| 396 | @enumFromInt(@as(u2, @truncate(cur_byte >> 4))), | ||
| 397 | @enumFromInt(@as(u2, @truncate(cur_byte >> 6))), | ||
| 398 | }; | ||
| 399 | const prev: [4]abi.StepUpdate.Status = .{ | ||
| 400 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))), | ||
| 401 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))), | ||
| 402 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))), | ||
| 403 | @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))), | ||
| 404 | }; | ||
| 405 | for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| { | ||
| 406 | const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } }; | ||
| 407 | if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary); | ||
| 408 | } | ||
| 409 | prev_byte.* = cur_byte; | ||
| 410 | } | ||
| 411 | |||
| 412 | prev_time = start_time; | ||
| 413 | |||
| 414 | const old_cp = io.swapCancelProtection(.blocked); | ||
| 415 | defer _ = io.swapCancelProtection(old_cp); | ||
| 416 | io.futexWaitTimeout( | ||
| 417 | u32, | ||
| 418 | &ws.update_id.raw, | ||
| 419 | start_update_id, | ||
| 420 | .{ .duration = .{ | ||
| 421 | .clock = .awake, | ||
| 422 | .raw = .fromMilliseconds(default_update_interval_ms), | ||
| 423 | } }, | ||
| 424 | ) catch |err| switch (err) { | ||
| 425 | error.Canceled => unreachable, | ||
| 426 | }; | ||
| 427 | } | ||
| 428 | } | ||
| 429 | fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void { | ||
| 430 | const io = ws.graph.io; | ||
| 431 | |||
| 432 | while (true) { | ||
| 433 | const msg = sock.readSmallMessage() catch return; | ||
| 434 | if (msg.opcode != .binary) continue; | ||
| 435 | if (msg.data.len == 0) continue; | ||
| 436 | const tag: abi.ToServerTag = @enumFromInt(msg.data[0]); | ||
| 437 | switch (tag) { | ||
| 438 | _ => continue, | ||
| 439 | .rebuild => while (true) { | ||
| 440 | ws.runner_request_mutex.lock(io) catch |err| switch (err) { | ||
| 441 | error.Canceled => return, | ||
| 442 | }; | ||
| 443 | defer ws.runner_request_mutex.unlock(io); | ||
| 444 | if (ws.runner_request == null) { | ||
| 445 | ws.runner_request = .rebuild; | ||
| 446 | ws.runner_request_ready_cond.signal(io); | ||
| 447 | break; | ||
| 448 | } | ||
| 449 | ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return; | ||
| 450 | }, | ||
| 451 | } | ||
| 452 | } | ||
| 453 | } | ||
| 454 | |||
| 455 | fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void { | ||
| 456 | // Strip an optional leading '/debug' component from the request. | ||
| 457 | const target: []const u8, const debug: bool = target: { | ||
| 458 | if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true }; | ||
| 459 | if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true }; | ||
| 460 | if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true }; | ||
| 461 | break :target .{ req.head.target, false }; | ||
| 462 | }; | ||
| 463 | |||
| 464 | if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html"); | ||
| 465 | if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript"); | ||
| 466 | if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css"); | ||
| 467 | if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css"); | ||
| 468 | if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast); | ||
| 469 | |||
| 470 | if (ws.fuzz) |*fuzz| { | ||
| 471 | if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req); | ||
| 472 | } | ||
| 473 | |||
| 474 | try req.respond("not found", .{ | ||
| 475 | .status = .not_found, | ||
| 476 | .extra_headers = &.{ | ||
| 477 | .{ .name = "Content-Type", .value = "text/plain" }, | ||
| 478 | }, | ||
| 479 | }); | ||
| 480 | } | ||
| 481 | |||
| 482 | fn serveLibFile( | ||
| 483 | ws: *WebServer, | ||
| 484 | request: *http.Server.Request, | ||
| 485 | sub_path: []const u8, | ||
| 486 | content_type: []const u8, | ||
| 487 | ) !void { | ||
| 488 | return serveFile(ws, request, .{ | ||
| 489 | .root_dir = ws.graph.zig_lib_directory, | ||
| 490 | .sub_path = sub_path, | ||
| 491 | }, content_type); | ||
| 492 | } | ||
| 493 | fn serveClientWasm( | ||
| 494 | ws: *WebServer, | ||
| 495 | req: *http.Server.Request, | ||
| 496 | optimize_mode: std.builtin.OptimizeMode, | ||
| 497 | ) !void { | ||
| 498 | var arena_state: std.heap.ArenaAllocator = .init(ws.gpa); | ||
| 499 | defer arena_state.deinit(); | ||
| 500 | const arena = arena_state.allocator(); | ||
| 501 | |||
| 502 | // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page. | ||
| 503 | const bin_path = try buildClientWasm(ws, arena, optimize_mode); | ||
| 504 | return serveFile(ws, req, bin_path, "application/wasm"); | ||
| 505 | } | ||
| 506 | |||
| 507 | pub fn serveFile( | ||
| 508 | ws: *WebServer, | ||
| 509 | request: *http.Server.Request, | ||
| 510 | path: Cache.Path, | ||
| 511 | content_type: []const u8, | ||
| 512 | ) !void { | ||
| 513 | const gpa = ws.gpa; | ||
| 514 | const io = ws.graph.io; | ||
| 515 | // The desired API is actually sendfile, which will require enhancing http.Server. | ||
| 516 | // We load the file with every request so that the user can make changes to the file | ||
| 517 | // and refresh the HTML page without restarting this server. | ||
| 518 | const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| { | ||
| 519 | log.err("failed to read '{f}': {t}", .{ path, err }); | ||
| 520 | return error.AlreadyReported; | ||
| 521 | }; | ||
| 522 | defer gpa.free(file_contents); | ||
| 523 | try request.respond(file_contents, .{ | ||
| 524 | .extra_headers = &.{ | ||
| 525 | .{ .name = "Content-Type", .value = content_type }, | ||
| 526 | cache_control_header, | ||
| 527 | }, | ||
| 528 | }); | ||
| 529 | } | ||
| 530 | pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void { | ||
| 531 | const graph = ws.graph; | ||
| 532 | const io = graph.io; | ||
| 533 | |||
| 534 | var send_buffer: [0x4000]u8 = undefined; | ||
| 535 | var response = try request.respondStreaming(&send_buffer, .{ | ||
| 536 | .respond_options = .{ | ||
| 537 | .extra_headers = &.{ | ||
| 538 | .{ .name = "Content-Type", .value = "application/x-tar" }, | ||
| 539 | cache_control_header, | ||
| 540 | }, | ||
| 541 | }, | ||
| 542 | }); | ||
| 543 | |||
| 544 | var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer }; | ||
| 545 | |||
| 546 | for (paths) |path| { | ||
| 547 | var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| { | ||
| 548 | log.err("failed to open '{f}': {s}", .{ path, @errorName(err) }); | ||
| 549 | continue; | ||
| 550 | }; | ||
| 551 | defer file.close(io); | ||
| 552 | const stat = try file.stat(io); | ||
| 553 | var read_buffer: [1024]u8 = undefined; | ||
| 554 | var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size); | ||
| 555 | |||
| 556 | // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can | ||
| 557 | // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI: | ||
| 558 | // it turns out the WASM treats the first path component as the module name, typically | ||
| 559 | // resulting in modules named "" and "src". The compiler needs to tell the build system | ||
| 560 | // about the module graph so that the build system can correctly encode this information in | ||
| 561 | // the tar file. | ||
| 562 | // | ||
| 563 | // Additionally, this needs to ensure that all path separators for both prefix and | ||
| 564 | // sub_path are using the POSIX-style `/` on platforms that don't use it as their native | ||
| 565 | // path separator. | ||
| 566 | archiver.prefix = path.root_dir.path orelse graph.cache.cwd; | ||
| 567 | try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); | ||
| 568 | } | ||
| 569 | |||
| 570 | // intentionally not calling `archiver.finishPedantically` | ||
| 571 | try response.end(); | ||
| 572 | } | ||
| 573 | |||
| 574 | fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path { | ||
| 575 | const root_name = "build-web"; | ||
| 576 | const arch_os_abi = "wasm32-freestanding"; | ||
| 577 | const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; | ||
| 578 | |||
| 579 | const gpa = ws.gpa; | ||
| 580 | const graph = ws.graph; | ||
| 581 | const io = graph.io; | ||
| 582 | |||
| 583 | const main_src_path: Cache.Path = .{ | ||
| 584 | .root_dir = graph.zig_lib_directory, | ||
| 585 | .sub_path = "build-web/main.zig", | ||
| 586 | }; | ||
| 587 | const walk_src_path: Cache.Path = .{ | ||
| 588 | .root_dir = graph.zig_lib_directory, | ||
| 589 | .sub_path = "docs/wasm/Walk.zig", | ||
| 590 | }; | ||
| 591 | const html_render_src_path: Cache.Path = .{ | ||
| 592 | .root_dir = graph.zig_lib_directory, | ||
| 593 | .sub_path = "docs/wasm/html_render.zig", | ||
| 594 | }; | ||
| 595 | |||
| 596 | var argv: std.ArrayList([]const u8) = .empty; | ||
| 597 | |||
| 598 | try argv.appendSlice(arena, &.{ | ||
| 599 | graph.zig_exe, "build-exe", // | ||
| 600 | "-fno-entry", // | ||
| 601 | "-O", @tagName(optimize), // | ||
| 602 | "-target", arch_os_abi, // | ||
| 603 | "-mcpu", cpu_features, // | ||
| 604 | "--cache-dir", graph.global_cache_root.path orelse ".", // | ||
| 605 | "--global-cache-dir", graph.global_cache_root.path orelse ".", // | ||
| 606 | "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", // | ||
| 607 | "--name", root_name, // | ||
| 608 | "-rdynamic", // | ||
| 609 | "-fsingle-threaded", // | ||
| 610 | "--dep", "Walk", // | ||
| 611 | "--dep", "html_render", // | ||
| 612 | try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), // | ||
| 613 | try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), // | ||
| 614 | "--dep", "Walk", // | ||
| 615 | try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), // | ||
| 616 | "--listen=-", | ||
| 617 | }); | ||
| 618 | |||
| 619 | var child = try std.process.spawn(io, .{ | ||
| 620 | .argv = argv.items, | ||
| 621 | .environ_map = &graph.environ_map, | ||
| 622 | .stdin = .pipe, | ||
| 623 | .stdout = .pipe, | ||
| 624 | .stderr = .pipe, | ||
| 625 | }); | ||
| 626 | defer child.kill(io); | ||
| 627 | |||
| 628 | var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited }); | ||
| 629 | defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {}; | ||
| 630 | |||
| 631 | var stdout_buffer: [512]u8 = undefined; | ||
| 632 | var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer); | ||
| 633 | const stdout = &stdout_reader.interface; | ||
| 634 | |||
| 635 | { | ||
| 636 | var w = child.stdin.?.writer(io, &.{}); | ||
| 637 | w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) { | ||
| 638 | error.WriteFailed => return w.err.?, | ||
| 639 | }; | ||
| 640 | w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) { | ||
| 641 | error.WriteFailed => return w.err.?, | ||
| 642 | }; | ||
| 643 | } | ||
| 644 | |||
| 645 | const Header = std.zig.Server.Message.Header; | ||
| 646 | |||
| 647 | var result: ?Cache.Path = null; | ||
| 648 | var result_error_bundle = std.zig.ErrorBundle.empty; | ||
| 649 | var body_buffer: std.ArrayList(u8) = .empty; | ||
| 650 | defer body_buffer.deinit(gpa); | ||
| 651 | |||
| 652 | while (true) { | ||
| 653 | const header = stdout.takeStruct(Header, .little) catch |err| switch (err) { | ||
| 654 | error.ReadFailed => |e| return e, | ||
| 655 | error.EndOfStream => break, | ||
| 656 | }; | ||
| 657 | body_buffer.clearRetainingCapacity(); | ||
| 658 | try stdout.appendExact(gpa, &body_buffer, header.bytes_len); | ||
| 659 | const body = body_buffer.items; | ||
| 660 | |||
| 661 | switch (header.tag) { | ||
| 662 | .zig_version => { | ||
| 663 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) { | ||
| 664 | return error.ZigProtocolVersionMismatch; | ||
| 665 | } | ||
| 666 | }, | ||
| 667 | .error_bundle => { | ||
| 668 | result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body); | ||
| 669 | }, | ||
| 670 | .emit_digest => { | ||
| 671 | const EmitDigest = std.zig.Server.Message.EmitDigest; | ||
| 672 | const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body); | ||
| 673 | if (!ebp_hdr.flags.cache_hit) { | ||
| 674 | log.info("source changes detected; rebuilt wasm component", .{}); | ||
| 675 | } | ||
| 676 | const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; | ||
| 677 | result = .{ | ||
| 678 | .root_dir = graph.global_cache_root, | ||
| 679 | .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)), | ||
| 680 | }; | ||
| 681 | }, | ||
| 682 | else => {}, // ignore other messages | ||
| 683 | } | ||
| 684 | } | ||
| 685 | |||
| 686 | const stderr_contents = try stderr_task.await(io); | ||
| 687 | if (stderr_contents.len > 0) { | ||
| 688 | std.debug.print("{s}", .{stderr_contents}); | ||
| 689 | } | ||
| 690 | |||
| 691 | // Send EOF to stdin. | ||
| 692 | child.stdin.?.close(io); | ||
| 693 | child.stdin = null; | ||
| 694 | |||
| 695 | switch (try child.wait(io)) { | ||
| 696 | .exited => |code| { | ||
| 697 | if (code != 0) { | ||
| 698 | log.err( | ||
| 699 | "the following command exited with error code {d}:\n{s}", | ||
| 700 | .{ code, try Step.allocPrintCmd(arena, .inherit, null, argv.items) }, | ||
| 701 | ); | ||
| 702 | return error.WasmCompilationFailed; | ||
| 703 | } | ||
| 704 | }, | ||
| 705 | .signal => |sig| { | ||
| 706 | log.err( | ||
| 707 | "the following command terminated with signal {t}:\n{s}", | ||
| 708 | .{ sig, try Step.allocPrintCmd(arena, .inherit, null, argv.items) }, | ||
| 709 | ); | ||
| 710 | return error.WasmCompilationFailed; | ||
| 711 | }, | ||
| 712 | .stopped => |sig| { | ||
| 713 | log.err( | ||
| 714 | "the following command stopped unexpectedly with signal {t}:\n{s}", | ||
| 715 | .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, | ||
| 716 | ); | ||
| 717 | return error.WasmCompilationFailed; | ||
| 718 | }, | ||
| 719 | .unknown => { | ||
| 720 | log.err( | ||
| 721 | "the following command terminated unexpectedly:\n{s}", | ||
| 722 | .{try Step.allocPrintCmd(arena, .inherit, null, argv.items)}, | ||
| 723 | ); | ||
| 724 | return error.WasmCompilationFailed; | ||
| 725 | }, | ||
| 726 | } | ||
| 727 | |||
| 728 | if (result_error_bundle.errorMessageCount() > 0) { | ||
| 729 | try result_error_bundle.renderToStderr(io, .{}, .auto); | ||
| 730 | log.err("the following command failed with {d} compilation errors:\n{s}", .{ | ||
| 731 | result_error_bundle.errorMessageCount(), | ||
| 732 | try Step.allocPrintCmd(arena, .inherit, null, argv.items), | ||
| 733 | }); | ||
| 734 | return error.WasmCompilationFailed; | ||
| 735 | } | ||
| 736 | |||
| 737 | const base_path = result orelse { | ||
| 738 | log.err("child process failed to report result\n{s}", .{ | ||
| 739 | try Step.allocPrintCmd(arena, .inherit, null, argv.items), | ||
| 740 | }); | ||
| 741 | return error.WasmCompilationFailed; | ||
| 742 | }; | ||
| 743 | const bin_name = try std.zig.binNameAlloc(arena, .{ | ||
| 744 | .root_name = root_name, | ||
| 745 | .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ | ||
| 746 | .arch_os_abi = arch_os_abi, | ||
| 747 | .cpu_features = cpu_features, | ||
| 748 | }) catch unreachable) catch unreachable), | ||
| 749 | .output_mode = .Exe, | ||
| 750 | }); | ||
| 751 | return base_path.join(arena, bin_name); | ||
| 752 | } | ||
| 753 | |||
| 754 | fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 { | ||
| 755 | var file_reader: Io.File.Reader = .initStreaming(file, io, &.{}); | ||
| 756 | return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) { | ||
| 757 | error.ReadFailed => return file_reader.err.?, | ||
| 758 | else => |e| return e, | ||
| 759 | }; | ||
| 760 | } | ||
| 761 | |||
| 762 | pub fn updateTimeReportCompile(ws: *WebServer, opts: struct { | ||
| 763 | compile_step: Configuration.Step.Index, | ||
| 764 | |||
| 765 | use_llvm: bool, | ||
| 766 | stats: abi.time_report.CompileResult.Stats, | ||
| 767 | ns_total: u64, | ||
| 768 | |||
| 769 | llvm_pass_timings_len: u32, | ||
| 770 | files_len: u32, | ||
| 771 | decls_len: u32, | ||
| 772 | |||
| 773 | /// The trailing data of `abi.time_report.CompileResult`, except the step name. | ||
| 774 | trailing: []const u8, | ||
| 775 | }) void { | ||
| 776 | const gpa = ws.gpa; | ||
| 777 | const io = ws.graph.io; | ||
| 778 | |||
| 779 | // TODO don't do linear search | ||
| 780 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 781 | if (s == opts.compile_step) break @intCast(i); | ||
| 782 | } else unreachable; | ||
| 783 | |||
| 784 | const old_buf = old: { | ||
| 785 | ws.time_report_mutex.lock(io) catch return; | ||
| 786 | defer ws.time_report_mutex.unlock(io); | ||
| 787 | const old = ws.time_report_msgs[step_idx]; | ||
| 788 | ws.time_report_msgs[step_idx] = &.{}; | ||
| 789 | break :old old; | ||
| 790 | }; | ||
| 791 | const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory"); | ||
| 792 | |||
| 793 | const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]); | ||
| 794 | out_header.* = .{ | ||
| 795 | .step_idx = step_idx, | ||
| 796 | .flags = .{ | ||
| 797 | .use_llvm = opts.use_llvm, | ||
| 798 | }, | ||
| 799 | .stats = opts.stats, | ||
| 800 | .ns_total = opts.ns_total, | ||
| 801 | .llvm_pass_timings_len = opts.llvm_pass_timings_len, | ||
| 802 | .files_len = opts.files_len, | ||
| 803 | .decls_len = opts.decls_len, | ||
| 804 | }; | ||
| 805 | @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing); | ||
| 806 | |||
| 807 | { | ||
| 808 | ws.time_report_mutex.lock(io) catch return; | ||
| 809 | defer ws.time_report_mutex.unlock(io); | ||
| 810 | assert(ws.time_report_msgs[step_idx].len == 0); | ||
| 811 | ws.time_report_msgs[step_idx] = buf; | ||
| 812 | ws.time_report_update_times[step_idx] = ws.now(); | ||
| 813 | } | ||
| 814 | ws.notifyUpdate(); | ||
| 815 | } | ||
| 816 | |||
| 817 | pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.Index, duration: Io.Duration) void { | ||
| 818 | const gpa = ws.gpa; | ||
| 819 | const io = ws.graph.io; | ||
| 820 | |||
| 821 | // TODO don't do linear search | ||
| 822 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 823 | if (s == step_index) break @intCast(i); | ||
| 824 | } else unreachable; | ||
| 825 | |||
| 826 | const old_buf = old: { | ||
| 827 | ws.time_report_mutex.lock(io) catch return; | ||
| 828 | defer ws.time_report_mutex.unlock(io); | ||
| 829 | const old = ws.time_report_msgs[step_idx]; | ||
| 830 | ws.time_report_msgs[step_idx] = &.{}; | ||
| 831 | break :old old; | ||
| 832 | }; | ||
| 833 | const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory"); | ||
| 834 | const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf); | ||
| 835 | out.* = .{ | ||
| 836 | .step_idx = step_idx, | ||
| 837 | .ns_total = @intCast(duration.toNanoseconds()), | ||
| 838 | }; | ||
| 839 | { | ||
| 840 | ws.time_report_mutex.lock(io) catch return; | ||
| 841 | defer ws.time_report_mutex.unlock(io); | ||
| 842 | assert(ws.time_report_msgs[step_idx].len == 0); | ||
| 843 | ws.time_report_msgs[step_idx] = buf; | ||
| 844 | ws.time_report_update_times[step_idx] = ws.now(); | ||
| 845 | } | ||
| 846 | ws.notifyUpdate(); | ||
| 847 | } | ||
| 848 | |||
| 849 | pub fn updateTimeReportRunTest( | ||
| 850 | ws: *WebServer, | ||
| 851 | run_step_index: Configuration.Step.Index, | ||
| 852 | tests: *const Step.Run.CachedTestMetadata, | ||
| 853 | ns_per_test: []const u64, | ||
| 854 | ) void { | ||
| 855 | const gpa = ws.gpa; | ||
| 856 | const io = ws.graph.io; | ||
| 857 | |||
| 858 | // TODO don't do linear search | ||
| 859 | const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { | ||
| 860 | if (s == run_step_index) break @intCast(i); | ||
| 861 | } else unreachable; | ||
| 862 | |||
| 863 | assert(tests.names.len == ns_per_test.len); | ||
| 864 | const tests_len: u32 = @intCast(tests.names.len); | ||
| 865 | |||
| 866 | const new_len: u64 = len: { | ||
| 867 | var names_len: u64 = 0; | ||
| 868 | for (0..tests_len) |i| { | ||
| 869 | names_len += tests.testName(@intCast(i)).len + 1; | ||
| 870 | } | ||
| 871 | break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len; | ||
| 872 | }; | ||
| 873 | const old_buf = old: { | ||
| 874 | ws.time_report_mutex.lock(io) catch return; | ||
| 875 | defer ws.time_report_mutex.unlock(io); | ||
| 876 | const old = ws.time_report_msgs[step_idx]; | ||
| 877 | ws.time_report_msgs[step_idx] = &.{}; | ||
| 878 | break :old old; | ||
| 879 | }; | ||
| 880 | const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory"); | ||
| 881 | |||
| 882 | const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]); | ||
| 883 | out_header.* = .{ | ||
| 884 | .step_idx = step_idx, | ||
| 885 | .tests_len = tests_len, | ||
| 886 | }; | ||
| 887 | var offset: usize = @sizeOf(abi.time_report.RunTestResult); | ||
| 888 | const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]); | ||
| 889 | @memcpy(ns_per_test_out, ns_per_test); | ||
| 890 | offset += tests_len * 8; | ||
| 891 | for (0..tests_len) |i| { | ||
| 892 | const name = tests.testName(@intCast(i)); | ||
| 893 | @memcpy(buf[offset..][0..name.len], name); | ||
| 894 | buf[offset..][name.len] = 0; | ||
| 895 | offset += name.len + 1; | ||
| 896 | } | ||
| 897 | assert(offset == buf.len); | ||
| 898 | |||
| 899 | { | ||
| 900 | ws.time_report_mutex.lock(io) catch return; | ||
| 901 | defer ws.time_report_mutex.unlock(io); | ||
| 902 | assert(ws.time_report_msgs[step_idx].len == 0); | ||
| 903 | ws.time_report_msgs[step_idx] = buf; | ||
| 904 | ws.time_report_update_times[step_idx] = ws.now(); | ||
| 905 | } | ||
| 906 | ws.notifyUpdate(); | ||
| 907 | } | ||
| 908 | |||
| 909 | const RunnerRequest = union(enum) { | ||
| 910 | rebuild, | ||
| 911 | }; | ||
| 912 | pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest { | ||
| 913 | const io = ws.graph.io; | ||
| 914 | ws.runner_request_mutex.lock(io) catch return; | ||
| 915 | defer ws.runner_request_mutex.unlock(io); | ||
| 916 | if (ws.runner_request) |req| { | ||
| 917 | ws.runner_request = null; | ||
| 918 | ws.runner_request_empty_cond.signal(); | ||
| 919 | return req; | ||
| 920 | } | ||
| 921 | return null; | ||
| 922 | } | ||
| 923 | pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest { | ||
| 924 | const io = ws.graph.io; | ||
| 925 | try ws.runner_request_mutex.lock(io); | ||
| 926 | defer ws.runner_request_mutex.unlock(io); | ||
| 927 | while (true) { | ||
| 928 | if (ws.runner_request) |req| { | ||
| 929 | ws.runner_request = null; | ||
| 930 | ws.runner_request_empty_cond.signal(io); | ||
| 931 | return req; | ||
| 932 | } | ||
| 933 | try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex); | ||
| 934 | } | ||
| 935 | } | ||
| 936 | |||
| 937 | const cache_control_header: http.Header = .{ | ||
| 938 | .name = "Cache-Control", | ||
| 939 | .value = "max-age=0, must-revalidate", | ||
| 940 | }; | ||
src/main.zig+1-1| ... | @@ -5799,7 +5799,7 @@ fn compileMakeRunner(gpa: Allocator, arena: Allocator, io: Io, options: MakeRunn | ... | @@ -5799,7 +5799,7 @@ fn compileMakeRunner(gpa: Allocator, arena: Allocator, io: Io, options: MakeRunn |
| 5799 | 5799 | ||
| 5800 | const main_mod_paths: Package.Module.CreateOptions.Paths = .{ | 5800 | const main_mod_paths: Package.Module.CreateOptions.Paths = .{ |
| 5801 | .root = try .fromRoot(arena, options.dirs, .zig_lib, "compiler"), | 5801 | .root = try .fromRoot(arena, options.dirs, .zig_lib, "compiler"), |
| 5802 | .root_src_path = "maker.zig", | 5802 | .root_src_path = "Maker.zig", |
| 5803 | }; | 5803 | }; |
| 5804 | 5804 | ||
| 5805 | const config = try Compilation.Config.resolve(.{ | 5805 | const config = try Compilation.Config.resolve(.{ |