authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-08-23 21:42:17-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-09-04 17:41:27-07:00
logfe409bad656a866ea17de13595a11fbbe2eea225
tree176bb7ce2d547498b0ea7217869d5407296b381f
parent1436d488b9d64af6d7087c0070d18df3d23604c5

WIP: rework std.Build.Cache

- implement std.Build functions: - `dependOnDirectoryMetadata` - `dependOnDirectoryContents` - `dependOnFileMetadata` - `dependOnFileContents` - switch cache manifest to binary format. saves about 25% of file size, which is not a ton, but this is in the really hot path, so any amount we can reduce file system cache pressure is going to help a lot. - also means saving and loading is simpler - just copying bytes in and out of memory plus a tiny bit of validation. - batch cache manifest memory allocations together. one big allocation for the entire manifest file contents, with the files hash map only tracking starting offsets for each entry, and the input files as a simple array list with the extra information needed for input files (so they can be lazily loaded only on cache miss) - remove the manifest file size limit - input files that have requested content to be loaded is loaded together into one big allocation rather than separately tracked. - each file entry in the cache now tracks two additional bits: - kind: file or directory - tracking mode: contents or metadata - when tracking a directory, "contents" are considered to be the sorted list of file names contained in the directory (non recursive) - rename "hit" to "check" and make it return enum instead of bool - merge most "add...File" function variants together into one that has an "options bag" parameter. - take advantage of async I/O when checking for cache hit to compute hashes concurrently. Care is taken to never cancel useful work, and never wait for unnecessary work to complete. bonus: - build.zig: use std.log for printing log messages

7 files changed, 626 insertions(+), 626 deletions(-)

build.zig+12-9
...@@ -263,8 +263,8 @@ pub fn build(b: *std.Build) !void {...@@ -263,8 +263,8 @@ pub fn build(b: *std.Build) !void {
263 const opt_version_string = b.option([]const u8, "version-string", "Override Zig version string. Default is to find out with git.");263 const opt_version_string = b.option([]const u8, "version-string", "Override Zig version string. Default is to find out with git.");
264 const version_slice = if (opt_version_string) |version| version else v: {264 const version_slice = if (opt_version_string) |version| version else v: {
265 if (!std.process.can_spawn) {265 if (!std.process.can_spawn) {
266 std.debug.print("error: version info cannot be retrieved from git. Zig version must be provided using -Dversion-string\n", .{});266 std.log.info("version info can be provided explicitly via \"-Dversion-string\"", .{});
267 std.process.exit(1);267 std.process.fatal("version info cannot be retrieved from git", .{});
268 }268 }
269269
270 // Ensure git version changes get picked up.270 // Ensure git version changes get picked up.
...@@ -310,8 +310,9 @@ pub fn build(b: *std.Build) !void {...@@ -310,8 +310,9 @@ pub fn build(b: *std.Build) !void {
310 0 => {310 0 => {
311 // Tagged release version (e.g. 0.10.0).311 // Tagged release version (e.g. 0.10.0).
312 if (!mem.eql(u8, git_describe, version_string)) {312 if (!mem.eql(u8, git_describe, version_string)) {
313 std.debug.print("Zig version '{s}' does not match Git tag '{s}'\n", .{ version_string, git_describe });313 std.process.fatal("zig version {q} does not match Git tag {q}", .{
314 std.process.exit(1);314 version_string, git_describe,
315 });
315 }316 }
316 break :v version_string;317 break :v version_string;
317 },318 },
...@@ -324,13 +325,14 @@ pub fn build(b: *std.Build) !void {...@@ -324,13 +325,14 @@ pub fn build(b: *std.Build) !void {
324325
325 const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor);326 const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor);
326 if (zig_version.order(ancestor_ver) != .gt) {327 if (zig_version.order(ancestor_ver) != .gt) {
327 std.debug.print("Zig version '{f}' must be greater than tagged ancestor '{f}'\n", .{ zig_version, ancestor_ver });328 std.process.fatal("zig version {f} must be greater than tagged ancestor {qf}", .{
328 std.process.exit(1);329 zig_version, ancestor_ver,
330 });
329 }331 }
330332
331 // Check that the commit hash is prefixed with a 'g' (a Git convention).333 // Check that the commit hash is prefixed with a 'g' (a Git convention).
332 if (commit_id.len < 1 or commit_id[0] != 'g') {334 if (commit_id.len < 1 or commit_id[0] != 'g') {
333 std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe});335 std.log.warn("unexpected \"git describe\" output: {s}", .{git_describe});
334 break :v version_string;336 break :v version_string;
335 }337 }
336338
...@@ -338,7 +340,7 @@ pub fn build(b: *std.Build) !void {...@@ -338,7 +340,7 @@ pub fn build(b: *std.Build) !void {
338 break :v b.fmt("{s}-dev.{s}+{s}", .{ version_string, commit_height, commit_id[1..] });340 break :v b.fmt("{s}-dev.{s}+{s}", .{ version_string, commit_height, commit_id[1..] });
339 },341 },
340 else => {342 else => {
341 std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe});343 std.log.warn("unexpected \"git describe\" output: {s}", .{git_describe});
342 break :v version_string;344 break :v version_string;
343 },345 },
344 }346 }
...@@ -354,7 +356,8 @@ pub fn build(b: *std.Build) !void {...@@ -354,7 +356,8 @@ pub fn build(b: *std.Build) !void {
354 const file_contents = cwd.readFileAlloc(io, config_h_path, arena, .limited(max_config_h_bytes)) catch unreachable;356 const file_contents = cwd.readFileAlloc(io, config_h_path, arena, .limited(max_config_h_bytes)) catch unreachable;
355 break :blk parseConfigH(b, file_contents);357 break :blk parseConfigH(b, file_contents);
356 } else {358 } else {
357 std.log.warn("config.h could not be located automatically. Consider providing it explicitly via \"-Dconfig_h\"", .{});359 std.log.warn("config.h could not be located automatically", .{});
360 std.log.info("config.h can be provided explicitly via \"-Dconfig_h\"", .{});
358 break :blk null;361 break :blk null;
359 }362 }
360 };363 };
lib/std/Build.zig+34-2
...@@ -2636,6 +2636,35 @@ pub fn dependOnFileMetadata(b: *Build, lazy_path: LazyPath) void {...@@ -2636,6 +2636,35 @@ pub fn dependOnFileMetadata(b: *Build, lazy_path: LazyPath) void {
2636/// This is an alternative to `Graph.poisonCache` that avoids making every invocation2636/// This is an alternative to `Graph.poisonCache` that avoids making every invocation
2637/// of `zig build` into a cache miss.2637/// of `zig build` into a cache miss.
2638///2638///
2639/// If any file is created, deleted, or renamed in this directory, leaving the
2640/// directory in a different state than last configuration with respect to
2641/// existence and naming of entries, the configure phase will be repeated.
2642///
2643/// Only a subset of `LazyPath` are supported:
2644/// - Relative to cwd
2645/// - Relative to any package root
2646/// - Relative to zig cache or zig installation
2647///
2648/// If the directory would be inside one of the search prefixes, then the dependency
2649/// cannot be tracked; `Graph.poisonCache` must be used instead.
2650///
2651/// Not recursive.
2652pub fn dependOnDirectoryContents(b: *Build, lazy_path: LazyPath) void {
2653 validateConfigureDependency(lazy_path);
2654 const graph = b.graph;
2655 graph.configure_dependencies.append(graph.arena, .{
2656 .lazy_path = lazy_path.dupe(graph),
2657 .is_directory = true,
2658 .metadata_only = false,
2659 }) catch @panic("OOM");
2660}
2661
2662/// Indicates that the build.zig logic depends on a particular directory's last
2663/// modification date.
2664///
2665/// This is an alternative to `Graph.poisonCache` that avoids making every invocation
2666/// of `zig build` into a cache miss.
2667///
2639/// If any file is created, deleted, or renamed in this directory, the2668/// If any file is created, deleted, or renamed in this directory, the
2640/// configure phase will be repeated.2669/// configure phase will be repeated.
2641///2670///
...@@ -2646,12 +2675,15 @@ pub fn dependOnFileMetadata(b: *Build, lazy_path: LazyPath) void {...@@ -2646,12 +2675,15 @@ pub fn dependOnFileMetadata(b: *Build, lazy_path: LazyPath) void {
2646///2675///
2647/// If the directory would be inside one of the search prefixes, then the dependency2676/// If the directory would be inside one of the search prefixes, then the dependency
2648/// cannot be tracked; `Graph.poisonCache` must be used instead.2677/// cannot be tracked; `Graph.poisonCache` must be used instead.
2649pub fn dependOnDirectory(b: *Build, lazy_path: LazyPath) void {2678///
2679/// Not recursive.
2680pub fn dependOnDirectoryMetadata(b: *Build, lazy_path: LazyPath) void {
2650 validateConfigureDependency(lazy_path);2681 validateConfigureDependency(lazy_path);
2651 const graph = b.graph;2682 const graph = b.graph;
2652 graph.configure_dependencies.append(graph.arena, .{2683 graph.configure_dependencies.append(graph.arena, .{
2653 .lazy_path = lazy_path.dupe(graph),2684 .lazy_path = lazy_path.dupe(graph),
2654 .mode = .directory,2685 .is_directory = true,
2686 .metadata_only = true,
2655 }) catch @panic("OOM");2687 }) catch @panic("OOM");
2656}2688}
26572689
lib/std/Build/Cache.zig+565-605
...@@ -62,7 +62,7 @@ pub const PrefixedPath = struct {...@@ -62,7 +62,7 @@ pub const PrefixedPath = struct {
62 sub_path: []const u8,62 sub_path: []const u8,
6363
64 fn eql(a: PrefixedPath, b: PrefixedPath) bool {64 fn eql(a: PrefixedPath, b: PrefixedPath) bool {
65 return a.prefix == b.prefix and std.mem.eql(u8, a.sub_path, b.sub_path);65 return a.prefix == b.prefix and mem.eql(u8, a.sub_path, b.sub_path);
66 }66 }
6767
68 fn hash(pp: PrefixedPath) u32 {68 fn hash(pp: PrefixedPath) u32 {
...@@ -118,7 +118,7 @@ fn getPrefixSubpath(gpa: Allocator, cwd: []const u8, prefix: []const u8, path: [...@@ -118,7 +118,7 @@ fn getPrefixSubpath(gpa: Allocator, cwd: []const u8, prefix: []const u8, path: [
118 return error.NotASubPath;118 return error.NotASubPath;
119 }119 }
120 const first_component = component_iterator.first();120 const first_component = component_iterator.first();
121 if (first_component != null and std.mem.eql(u8, first_component.?.name, "..")) {121 if (first_component != null and mem.eql(u8, first_component.?.name, "..")) {
122 return error.NotASubPath;122 return error.NotASubPath;
123 }123 }
124 return relative;124 return relative;
...@@ -130,10 +130,6 @@ pub const hex_digest_len = bin_digest_len * 2;...@@ -130,10 +130,6 @@ pub const hex_digest_len = bin_digest_len * 2;
130pub const BinDigest = [bin_digest_len]u8;130pub const BinDigest = [bin_digest_len]u8;
131pub const HexDigest = [hex_digest_len]u8;131pub const HexDigest = [hex_digest_len]u8;
132132
133/// This is currently just an arbitrary non-empty string that can't match another manifest line.
134const manifest_header = "0";
135pub const manifest_file_size_max = 100 * 1024 * 1024;
136
137/// The type used for hashing file contents. Currently, this is SipHash128(1, 3), because it133/// The type used for hashing file contents. Currently, this is SipHash128(1, 3), because it
138/// provides enough collision resistance for the Manifest use cases, while being one of our134/// provides enough collision resistance for the Manifest use cases, while being one of our
139/// fastest options right now.135/// fastest options right now.
...@@ -149,50 +145,6 @@ pub const hasher_init: Hasher = Hasher.init(&.{...@@ -149,50 +145,6 @@ pub const hasher_init: Hasher = Hasher.init(&.{
149 0x77, 0xd6, 0xf0, 0x60,145 0x77, 0xd6, 0xf0, 0x60,
150});146});
151147
152pub const File = struct {
153 prefixed_path: PrefixedPath,
154 max_file_size: ?usize,
155 /// Populated if the user calls `addOpenedFile`.
156 /// The handle is not owned here.
157 handle: ?Io.File,
158 stat: Stat,
159 bin_digest: BinDigest,
160 contents: ?[]const u8,
161
162 pub const Stat = struct {
163 inode: Io.File.INode,
164 size: u64,
165 mtime: Io.Timestamp,
166
167 pub fn fromFs(fs_stat: Io.File.Stat) Stat {
168 return .{
169 .inode = fs_stat.inode,
170 .size = fs_stat.size,
171 .mtime = fs_stat.mtime,
172 };
173 }
174 };
175
176 pub fn deinit(self: *File, gpa: Allocator) void {
177 gpa.free(self.prefixed_path.sub_path);
178 if (self.contents) |contents| {
179 gpa.free(contents);
180 self.contents = null;
181 }
182 self.* = undefined;
183 }
184
185 pub fn updateMaxSize(file: *File, new_max_size: ?usize) void {
186 const new = new_max_size orelse return;
187 file.max_file_size = if (file.max_file_size) |old| @max(old, new) else new;
188 }
189
190 pub fn updateHandle(file: *File, new_handle: ?Io.File) void {
191 const handle = new_handle orelse return;
192 file.handle = handle;
193 }
194};
195
196pub const HashHelper = struct {148pub const HashHelper = struct {
197 hasher: Hasher = hasher_init,149 hasher: Hasher = hasher_init,
198150
...@@ -319,10 +271,15 @@ pub const Lock = struct {...@@ -319,10 +271,15 @@ pub const Lock = struct {
319 }271 }
320};272};
321273
274/// Format: a series of consecutive `Manifest.File`, followed by a final
275/// terminating zero byte to distinguish empty manifest file from manifest with
276/// zero files.
322pub const Manifest = struct {277pub const Manifest = struct {
323 cache: *Cache,278 cache: *Cache,
324 /// Current state for incremental hashing.279 /// Current state for incremental hashing.
325 hash: HashHelper,280 hash: HashHelper,
281 hex_digest: HexDigest,
282 /// When this is null, `Manifest` is in "pre-check" phase. Otherwise it is in "post-check" phase.
326 manifest_file: ?Io.File,283 manifest_file: ?Io.File,
327 manifest_dirty: bool,284 manifest_dirty: bool,
328 /// Set this flag to true before calling hit() in order to indicate that285 /// Set this flag to true before calling hit() in order to indicate that
...@@ -335,12 +292,140 @@ pub const Manifest = struct {...@@ -335,12 +292,140 @@ pub const Manifest = struct {
335 // order to obtain a problematic timestamp for the next call. Calls after that292 // order to obtain a problematic timestamp for the next call. Calls after that
336 // will then use the same timestamp, to avoid unnecessary filesystem writes.293 // will then use the same timestamp, to avoid unnecessary filesystem writes.
337 want_refresh_timestamp: bool = true,294 want_refresh_timestamp: bool = true,
338 files: Files = .{},295 /// Uses `Cache.gpa`.
339 hex_digest: HexDigest,296 files: Files = .empty,
297 /// Indexes line up with `files`, but only up until `hit` is called. Uses
298 /// `Cache.gpa`.
299 input_files: std.ArrayList(InputFile) = .empty,
340 diagnostic: Diagnostic = .none,300 diagnostic: Diagnostic = .none,
341 /// Keeps track of the last time we performed a file system write to observe301 /// Keeps track of the last time we performed a file system write to observe
342 /// what time the file system thinks it is, according to its own granularity.302 /// what time the file system thinks it is, according to its own granularity.
343 recent_problematic_timestamp: Io.Timestamp = .zero,303 recent_problematic_timestamp: Io.Timestamp = .zero,
304 /// The entire manifest file contents, except for the final terminating
305 /// zero byte. However maintains always at least 1 unused capacity so the
306 /// final terminating byte can be added without allocation. Uses
307 /// `Cache.gpa`.
308 contents: std.ArrayList(u8) = .empty,
309 /// All contents from all `input_files` whose contents were requested,
310 /// concatenated. Total byte size will be less than `max_input_content_len`
311 /// otherwise an error is returned.
312 all_input_content: std.ArrayList(u8) = .empty,
313 max_input_content_len: usize = std.math.maxInt(u32),
314
315 pub const Files = std.array_hash_map.Custom(File.Offset, void, File.HashContext, false);
316
317 /// Source files whose prefix and relative path are included when computing
318 /// the cache manifest digest. It's the information needed to lazily hash
319 /// the input files only when a cache miss occurs.
320 ///
321 /// `File.prefix`, `File.path`, and `File.mode` will be always populated,
322 /// but the other fields of `File` will be populated depending on the
323 /// fields of `InputFile`.
324 pub const InputFile = struct {
325 request_handle: bool,
326 have_handle: bool,
327 /// Determines whether `File.size`, `File.inode`, and `File.mtime` are populated.
328 have_stat: bool,
329 /// Determines whether `File.digest` is populated.
330 have_digest: bool,
331 contents: enum (usize) {
332 requested = std.math.maxInt(u32) - 1,
333 not_requested = std.math.maxInt(u32),
334 /// Byte offset index into `Manifest.all_input_content`.
335 _,
336 },
337 /// `have_handle` determines whether this is populated.
338 handle: Io.File,
339
340 /// Index into `Manifest.input_files`.
341 pub const Index = enum(u32) {
342 _,
343 };
344 };
345
346 /// The data per tracked input file that is stored in the manifest file.
347 pub const File = extern struct {
348 size: u64,
349 inode: u64,
350 digest: BinDigest,
351 /// Nanoseconds.
352 mtime: i64,
353 /// Starting with this field and continuing into the path, excluding the null byte,
354 /// is the string that is hashed for the manifest digest.
355 flags: Flags,
356 /// Terminated by zero byte, then followed by padding until 8-byte aligned.
357 path_start: [0]u8,
358
359 pub const Flags = packed struct (u8) {
360 is_directory: bool,
361 metadata_only: bool,
362 prefix: u6,
363 };
364
365 /// Byte index within `Manifest.contents` where the entry starts.
366 pub const Offset = enum(u32) {
367 _,
368
369 pub fn get(offset: Offset, m: *const Manifest) *File {
370 return @ptrCast(m.contents.items[@backingInt(offset)..][0..@sizeOf(File)]);
371 }
372
373 pub fn getFallible(offset: Offset, m: *const Manifest) error{EndOfStream}!*File {
374 if (@backingInt(offset) + @sizeOf(File) >= m.contents.len) return error.EndOfStream;
375 return get(offset, m);
376 }
377 };
378
379 pub const HashContext = struct {
380 manifest: *const Manifest,
381
382 pub fn hash(this: @This(), off: Offset) u32 {
383 const file = off.get(this.manifest);
384 return @truncate(std.hash.Wyhash.hash(file.prefix, file.path()));
385 }
386
387 pub fn eql(this: @This(), a_off: Offset, b_off: Offset, b_index: usize) bool {
388 _ = b_index;
389 const a = a_off.get(this.manifest);
390 const b = b_off.get(this.manifest);
391 return a.prefix == b.prefix and mem.eql(u8, a.path(), b.path());
392 }
393 };
394
395
396 pub fn path(file: *const File) [:0]const u8 {
397 return pathFallible(file) catch unreachable;
398 }
399
400 pub fn pathFallible(file: *const File) error{EndOfStream}![:0]const u8 {
401 const ptr: [*]u8 = &file.path_start;
402 const len = mem.findScalar(u8, ptr, 0) orelse return error.EndOfStream;
403 return ptr[0..len :0];
404 }
405
406 fn manifestDigestHash(file: *const File, hasher: *Hasher) void {
407 const path_ptr: [*]u8 = &file.path_start;
408 const path_len = mem.findScalar(u8, path_ptr, 0).?;
409 comptime assert(@offsetOf(File, "path_start") - @offsetOf(File, "flags") == 1);
410 // Includes flags and sentinel.
411 const hash_string = (path_ptr - 1)[0..path_len + 2];
412 hasher.update(hash_string);
413 }
414
415 fn setStat(file: *File, m: *Manifest, stat: Stat) Io.Cancelable!void {
416 file.size = stat.size;
417 file.inode = stat.inode;
418 file.mtime = stat.mtime;
419
420 if (try m.isProblematicTimestamp(stat.mtime)) {
421 // The actual file has an unreliable timestamp; force it to be hashed.
422 file.stat.mtime = 0;
423 file.stat.inode = 0;
424 }
425 }
426
427 };
428
344429
345 pub const Diagnostic = union(enum) {430 pub const Diagnostic = union(enum) {
346 none,431 none,
...@@ -358,110 +443,118 @@ pub const Manifest = struct {...@@ -358,110 +443,118 @@ pub const Manifest = struct {
358 };443 };
359 };444 };
360445
361 pub const Files = std.array_hash_map.Custom(File, void, FilesContext, false);446 pub const Stat = struct {
362447 size: u64,
363 pub const FilesContext = struct {448 inode: Io.File.INode,
364 pub fn hash(fc: FilesContext, file: File) u32 {449 mtime: Io.Timestamp,
365 _ = fc;
366 return file.prefixed_path.hash();
367 }
368
369 pub fn eql(fc: FilesContext, a: File, b: File, b_index: usize) bool {
370 _ = fc;
371 _ = b_index;
372 return a.prefixed_path.eql(b.prefixed_path);
373 }
374 };450 };
375451
376 const FilesAdapter = struct {452 pub const AddInputFileOptions = struct {
377 pub fn eql(context: @This(), a: PrefixedPath, b: File, b_index: usize) bool {453 handle: ?Io.File = null,
378 _ = context;454 stat: ?Stat = null,
379 _ = b_index;455 request_handle: bool = false,
380 return a.eql(b.prefixed_path);456 request_contents: bool = false,
381 }457 is_directory: bool = false,
458 metadata_only: bool = false,
382459
383 pub fn hash(context: @This(), key: PrefixedPath) u32 {
384 _ = context;
385 return key.hash();
386 }
387 };460 };
388461
462 pub const AddInputFileError = error {
463 /// The same file path has been added to the cache manifest both as a
464 /// directory and as a normal file, making the intended caching
465 /// behavior ambiguous.
466 IsDirectoryAmbiguous,
467 } || Allocator.Error;
468
389 /// Add a file as a dependency of process being cached. When `hit` is469 /// Add a file as a dependency of process being cached. When `hit` is
390 /// called, the file's contents will be checked to ensure that it matches470 /// called, the file's contents will be checked to ensure that it matches
391 /// the contents from previous times.471 /// the contents from previous times.
392 ///472 ///
393 /// Max file size will be used to determine the amount of space the file contents473 /// The contents of the input file may be requested and subsequently
394 /// are allowed to take up in memory. If max_file_size is null, then the contents474 /// obtained via methods of the returned `InputFile.Index` after calling
395 /// will not be loaded into memory.475 /// `hit`.
396 ///476 pub fn addInputFile(m: *Manifest, path: Path, options: AddInputFileOptions) Allocator.Error!InputFile.Index {
397 /// Returns the index of the entry in the `files` array list. You can use it
398 /// to access the contents of the file after calling `hit()` like so:
399 ///
400 /// ```
401 /// var file_contents = cache_hash.files.keys()[file_index].contents.?;
402 /// ```
403 pub fn addFilePath(m: *Manifest, file_path: Path, max_file_size: ?usize) !usize {
404 return addOpenedFile(m, file_path, null, max_file_size);
405 }
406
407 /// Same as `addFilePath` except the file has already been opened.
408 pub fn addOpenedFile(m: *Manifest, path: Path, handle: ?Io.File, max_file_size: ?usize) !usize {
409 const gpa = m.cache.gpa;477 const gpa = m.cache.gpa;
410 try m.files.ensureUnusedCapacity(gpa, 1);478 try m.files.ensureUnusedCapacity(gpa, 1);
411 const resolved_path = try std.fs.path.resolve(gpa, &.{479 try m.input_files.ensureUnusedCapacity(gpa, 1);
412 path.root_dir.path orelse ".",
413 path.subPathOrDot(),
414 });
415 errdefer gpa.free(resolved_path);
416 const prefixed_path = try m.cache.findPrefixResolved(resolved_path);
417 return addFileInner(m, prefixed_path, handle, max_file_size);
418 }
419
420 fn addFileInner(self: *Manifest, prefixed_path: PrefixedPath, handle: ?Io.File, max_file_size: ?usize) usize {
421 const gop = self.files.getOrPutAssumeCapacityAdapted(prefixed_path, FilesAdapter{});
422 if (gop.found_existing) {
423 self.cache.gpa.free(prefixed_path.sub_path);
424 gop.key_ptr.updateMaxSize(max_file_size);
425 gop.key_ptr.updateHandle(handle);
426 return gop.index;
427 }
428 gop.key_ptr.* = .{
429 .prefixed_path = prefixed_path,
430 .contents = null,
431 .max_file_size = max_file_size,
432 .stat = undefined,
433 .bin_digest = undefined,
434 .handle = handle,
435 };
436480
437 self.hash.add(prefixed_path.prefix);481 const prev_contents_len = m.contents.items.len;
438 self.hash.addBytes(prefixed_path.sub_path);482 const header: *File = @ptrCast(try m.contents.addManyAsSlice(gpa, @sizeOf(File)));
483 errdefer m.contents.shrinkRetainingCapacity(prev_contents_len);
439484
440 return gop.index;485 header.* = .{
441 }486 .flags = .{
487 .prefix = try m.cache.findAppendPrefixedPath(&m.contents, path),
488 .is_directory = options.is_directory,
489 .metadata_only = options.metadata_only,
490 },
491 .size = undefined,
492 .inode = undefined,
493 .mtime = undefined,
494 .digest = undefined,
495 };
496 assert(m.contents.items.len % @alignOf(File) == 0);
442497
443 pub fn addOptionalFilePath(self: *Manifest, optional_file_path: ?Path) !void {498 const gop = try m.files.getOrPutAssumeCapacityContext(@fromBackingInt(prev_contents_len), .{
444 self.hash.add(optional_file_path != null);499 .manifest = m,
445 const file_path = optional_file_path orelse return;500 });
446 _ = try self.addFilePath(file_path, null);501 if (gop.found_existing) {
502 m.contents.shrinkRetainingCapacity(prev_contents_len);
503 const existing_input_file = &m.input_files.items[gop.index];
504 if (options.handle) |handle| {
505 existing_input_file.handle = handle;
506 existing_input_file.have_handle = true;
507 }
508 if (options.request_contents) switch (existing_input_file.contents) {
509 .requested, .not_requested => existing_input_file.contents = .requested,
510 _ => {},
511 };
512 const existing_header = &m.files.keys()[gop.index];
513 if (options.stat) |stat| {
514 existing_input_file.have_stat = true;
515 existing_header.size = stat.size;
516 existing_header.inode = stat.inode;
517 existing_header.mtime = stat.mtime;
518 }
519 if (existing_header.flags.is_directory != options.is_directory)
520 return error.IsDirectoryAmbiguous;
521 if (!options.metadata_only)
522 existing_header.flags.metadata_only = false;
523 } else {
524 m.input_files.appendAssumeCapacity(.{
525 .request_handle = options.request_handle,
526 .have_handle = options.handle != null,
527 .handle = if (options.handle) |handle| handle else undefined,
528 .contents = if (options.request_contents) .requested else .not_requested,
529 .have_digest = false,
530 .have_stat = options.stat != null,
531 });
532 assert(m.input_files.items.len - 1 == gop.index);
533 if (options.stat) |stat| {
534 header.size = stat.size;
535 header.inode = stat.inode;
536 header.mtime = stat.mtime;
537 }
538 }
539 return @fromBackingInt(gop.index);
447 }540 }
448541
449 pub fn addDepFile(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {542 pub fn addInputFileOptional(m: *Manifest, opt_path: ?Path, options: AddInputFileOptions) Allocator.Error!void {
450 assert(self.manifest_file == null);543 m.hash.add(opt_path != null);
451 return self.addDepFileMaybePost(dir, dep_file_sub_path);544 _ = try addInputFile(m, opt_path orelse return, options);
452 }545 }
453546
454 pub const HitError = error{547 pub const CheckError = error{
455 /// Unable to check the cache for a reason that has been recorded into548 /// Unable to check the cache for a reason that has been recorded into
456 /// the `diagnostic` field.549 /// the `diagnostic` field.
457 CacheCheckFailed,550 CacheCheckFailed,
458 /// A cache manifest file exists however it could not be parsed.551 /// A cache manifest file exists however it could not be parsed.
459 InvalidFormat,552 InvalidFormat,
460 OutOfMemory,553 } || Allocator.Error || Io.Cancelable;
461 Canceled,554
462 };555 pub const CheckStatus = enum { hit, miss };
463556
464 /// Check the cache to see if the input exists in it. If it exists, returns `true`.557 /// Check the cache to see if the input exists in it.
465 /// A hex encoding of its hash is available by calling `final`.558 /// A hex encoding of its hash is available by calling `final`.
466 ///559 ///
467 /// This function will also acquire an exclusive lock to the manifest file. This means560 /// This function will also acquire an exclusive lock to the manifest file. This means
...@@ -473,50 +566,48 @@ pub const Manifest = struct {...@@ -473,50 +566,48 @@ pub const Manifest = struct {
473 /// The lock on the manifest file is released when `deinit` is called. As another566 /// The lock on the manifest file is released when `deinit` is called. As another
474 /// option, one may call `toOwnedLock` to obtain a smaller object which can represent567 /// option, one may call `toOwnedLock` to obtain a smaller object which can represent
475 /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called.568 /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called.
476 pub fn hit(man: *Manifest, parent_progress_node: std.Progress.Node) HitError!bool {569 pub fn check(man: *Manifest, parent_progress_node: std.Progress.Node) CheckError!CheckStatus {
477 const node = parent_progress_node.start("Reusing Cache Artifacts", 0);570 const node = parent_progress_node.start("Reusing Cache Artifacts", 0);
478 defer node.end();571 defer node.end();
479 return hitInner(man);572 return checkProgressless(man);
480 }573 }
481574
482 pub fn hitInner(self: *Manifest) HitError!bool {575 pub fn checkProgressless(man: *Manifest) CheckError!CheckStatus {
483 assert(self.manifest_file == null);576 assert(man.manifest_file == null);
484577
485 self.diagnostic = .none;578 for (man.files.keys()[0..man.input_files.items.len]) |file_off| {
579 file_off.get(man).manifestDigestHash(&man.hash.hasher);
580 }
486581
487 const ext = ".txt";582 man.diagnostic = .none;
488 var manifest_file_path: [hex_digest_len + ext.len]u8 = undefined;
489583
490 var bin_digest: BinDigest = undefined;584 var bin_digest: BinDigest = undefined;
491 self.hash.hasher.final(&bin_digest);585 man.hash.hasher.final(&bin_digest);
492586 man.hex_digest = binToHex(bin_digest);
493 self.hex_digest = binToHex(bin_digest);
494587
495 @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest);588 const manifest_file_path = &man.hex_digest;
496 manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*;589 const io = man.cache.io;
497
498 const io = self.cache.io;
499590
500 // We'll try to open the cache with an exclusive lock, but if that would block591 // We'll try to open the cache with an exclusive lock, but if that would block
501 // and `want_shared_lock` is set, a shared lock might be sufficient, so we'll592 // and `want_shared_lock` is set, a shared lock might be sufficient, so we'll
502 // open with a shared lock instead.593 // open with a shared lock instead.
503 while (true) {594 while (true) {
504 if (self.cache.manifest_dir.createFile(io, &manifest_file_path, .{595 if (man.cache.manifest_dir.createFile(io, manifest_file_path, .{
505 .read = true,596 .read = true,
506 .truncate = false,597 .truncate = false,
507 .lock = .exclusive,598 .lock = .exclusive,
508 .lock_nonblocking = self.want_shared_lock,599 .lock_nonblocking = man.want_shared_lock,
509 })) |manifest_file| {600 })) |manifest_file| {
510 self.manifest_file = manifest_file;601 man.manifest_file = manifest_file;
511 self.have_exclusive_lock = true;602 man.have_exclusive_lock = true;
512 break;603 break;
513 } else |err| switch (err) {604 } else |err| switch (err) {
514 error.WouldBlock => {605 error.WouldBlock => {
515 self.manifest_file = self.cache.manifest_dir.openFile(io, &manifest_file_path, .{606 man.manifest_file = man.cache.manifest_dir.openFile(io, manifest_file_path, .{
516 .mode = .read_write,607 .mode = .read_write,
517 .lock = .shared,608 .lock = .shared,
518 }) catch |e| {609 }) catch |e| {
519 self.diagnostic = .{ .manifest_create = e };610 man.diagnostic = .{ .manifest_create = e };
520 return error.CacheCheckFailed;611 return error.CacheCheckFailed;
521 };612 };
522 break;613 break;
...@@ -532,315 +623,262 @@ pub const Manifest = struct {...@@ -532,315 +623,262 @@ pub const Manifest = struct {
532 // failure was a race, or ENOENT, indicating deletion of623 // failure was a race, or ENOENT, indicating deletion of
533 // the directory of our open handle.624 // the directory of our open handle.
534 if (!builtin.os.tag.isDarwin()) {625 if (!builtin.os.tag.isDarwin()) {
535 self.diagnostic = .{ .manifest_create = error.FileNotFound };626 man.diagnostic = .{ .manifest_create = error.FileNotFound };
536 return error.CacheCheckFailed;627 return error.CacheCheckFailed;
537 }628 }
538629
539 if (self.cache.manifest_dir.createFile(io, &manifest_file_path, .{630 if (man.cache.manifest_dir.createFile(io, manifest_file_path, .{
540 .read = true,631 .read = true,
541 .truncate = false,632 .truncate = false,
542 .lock = .exclusive,633 .lock = .exclusive,
543 .lock_nonblocking = self.want_shared_lock,634 .lock_nonblocking = man.want_shared_lock,
544 .exclusive = true,635 .exclusive = true,
545 })) |manifest_file| {636 })) |manifest_file| {
546 self.manifest_file = manifest_file;637 man.manifest_file = manifest_file;
547 self.have_exclusive_lock = true;638 man.have_exclusive_lock = true;
548 break;639 break;
549 } else |excl_err| switch (excl_err) {640 } else |excl_err| switch (excl_err) {
550 error.WouldBlock, error.PathAlreadyExists => continue,641 error.WouldBlock, error.PathAlreadyExists => continue,
551 error.FileNotFound => {642 error.FileNotFound => {
552 self.diagnostic = .{ .manifest_create = error.FileNotFound };643 man.diagnostic = .{ .manifest_create = error.FileNotFound };
553 return error.CacheCheckFailed;644 return error.CacheCheckFailed;
554 },645 },
555 error.Canceled => |e| return e,646 error.Canceled => |e| return e,
556 else => |e| {647 else => |e| {
557 self.diagnostic = .{ .manifest_create = e };648 man.diagnostic = .{ .manifest_create = e };
558 return error.CacheCheckFailed;649 return error.CacheCheckFailed;
559 },650 },
560 }651 }
561 },652 },
562 error.Canceled => |e| return e,653 error.Canceled => |e| return e,
563 else => |e| {654 else => |e| {
564 self.diagnostic = .{ .manifest_create = e };655 man.diagnostic = .{ .manifest_create = e };
565 return error.CacheCheckFailed;656 return error.CacheCheckFailed;
566 },657 },
567 }658 }
568 }659 }
569660
570 self.want_refresh_timestamp = true;661 man.want_refresh_timestamp = true;
571
572 const input_file_count = self.files.entries.len;
573662
574 // We're going to construct a second hash. Its input will begin with the digest we've663 // We're going to construct a second hash. Its input will begin with the digest we've
575 // already computed (`bin_digest`), and then it'll have the digests of each input file,664 // already computed (`bin_digest`), and then it'll have the digests of each input file,
576 // including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post"665 // including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post"
577 // files from the manifest on disk. If this is a miss, we'll learn those from future calls666 // files from the manifest on disk. If this is a miss, we'll learn those from future calls
578 // to `addFilePost` etc. As such, the state of `self.hash.hasher` after this function667 // to `addFilePost` etc. As such, the state of `man.hash.hasher` after this function
579 // depends on whether this is a hit or a miss.668 // depends on whether this is a hit or a miss.
580 //669 //
581 // If we return `true` indicating a cache hit, then `self.hash.hasher` must already include670 // If we return `CacheStatus.hit`, then `man.hash.hasher` must already include
582 // the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache671 // the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache
583 // miss, `self.hash.hasher` will include the digests of all non-"post" files -- that is,672 // miss, `man.hash.hasher` will include the digests of all non-"post" files -- that is,
584 // the ones we've already been told about. The rest will be discovered through calls to673 // the ones we've already been told about. The rest will be discovered through calls to
585 // `addFilePost` etc, which will update the hasher. After all files are added, the user can674 // `addFilePost` etc, which will update the hasher. After all files are added, the user can
586 // use `final`, and will at some point `writeManifest` the file list to disk.675 // use `final`, and will at some point `writeManifest` the file list to disk.
587676
588 self.hash.hasher = hasher_init;677 man.hash.hasher = hasher_init;
589 self.hash.hasher.update(&bin_digest);678 man.hash.hasher.update(&bin_digest);
590679
591 hit: {680 hit: {
592 const file_digests_populated: usize = digests: {681 digests: {
593 switch (try self.hitWithCurrentLock()) {682 switch (try man.checkLocked()) {
594 .hit => break :hit,683 .hit => break :hit,
595 .miss => |m| if (!try self.upgradeToExclusiveLock()) {684 .miss => if (!try man.upgradeToExclusiveLock()) break :digests,
596 break :digests m.file_digests_populated;
597 },
598 }685 }
599 // We've just had a miss with the shared lock, and upgraded to an exclusive lock. Someone686 // We've just had a miss with the shared lock, and upgraded to an exclusive lock. Someone
600 // else might have modified the digest, so we need to check again before deciding to miss.687 // else might have modified the digest, so we need to check again before deciding to miss.
601 // Before trying again, we must reset `self.hash.hasher` and `self.files`.688 // Before trying again, we must reset `man.hash.hasher` and `man.files`.
602 // This is basically just the first half of `unhit`.689 // This is basically just the first half of `unhit`.
603 self.hash.hasher = hasher_init;690 man.hash.hasher = hasher_init;
604 self.hash.hasher.update(&bin_digest);691 man.hash.hasher.update(&bin_digest);
605 while (self.files.count() != input_file_count) {692 man.shrinkFilesToInput();
606 var file = self.files.pop().?;693 switch (try man.checkLocked()) {
607 file.key.deinit(self.cache.gpa);
608 }
609 switch (try self.hitWithCurrentLock()) {
610 .hit => break :hit,694 .hit => break :hit,
611 .miss => |m| break :digests m.file_digests_populated,695 .miss => break :digests,
612 }
613 };
614
615 // This is a guaranteed cache miss. We're almost ready to return `false`, but there's a
616 // little bookkeeping to do first. The first `file_digests_populated` entries in `files`
617 // have their `bin_digest` populated; there may be some left in `input_file_count` which
618 // we'll need to populate ourselves. Other than that, this is basically `unhit`.
619 self.manifest_dirty = true;
620 self.hash.hasher = hasher_init;
621 self.hash.hasher.update(&bin_digest);
622 while (self.files.count() != input_file_count) {
623 var file = self.files.pop().?;
624 file.key.deinit(self.cache.gpa);
625 }
626 for (self.files.keys(), 0..) |*file, idx| {
627 if (idx < file_digests_populated) {
628 // `bin_digest` is already populated by `hitWithCurrentLock`, so we can use it directly.
629 self.hash.hasher.update(&file.bin_digest);
630 } else {
631 self.populateFileHash(file) catch |err| {
632 self.diagnostic = .{ .file_hash = .{
633 .file_index = idx,
634 .err = err,
635 } };
636 return error.CacheCheckFailed;
637 };
638 }696 }
639 }697 }
640 return false;698
699 // Cache miss. `checkLocked` guarantees that all input files have their digests populated
700 // unless it returns an error.
701 man.manifest_dirty = true;
702 // All input file digests are already populated by `checkLocked`, so we can call `unhit` directly.
703 unhit(man, &bin_digest);
704 return .miss;
641 }705 }
642706
643 if (self.want_shared_lock) {707 if (man.want_shared_lock) {
644 self.downgradeToSharedLock() catch |err| {708 man.downgradeToSharedLock() catch |err| {
645 self.diagnostic = .{ .manifest_lock = err };709 man.diagnostic = .{ .manifest_lock = err };
646 return error.CacheCheckFailed;710 return error.CacheCheckFailed;
647 };711 };
648 }712 }
649713
650 return true;714 return .hit;
715 }
716
717 fn shrinkFilesToInput(m: *Manifest) void {
718 if (m.files.count() <= m.input_files.items.len) return;
719 const off = m.files.keys()[m.input_files.items.len];
720 m.contents.shrinkRetainingCapacity(@backingInt(off));
721 assert(m.contents.len % @alignOf(File) == 0);
722 m.files.shrinkRetainingCapacity(m.input_files.items.len);
651 }723 }
652724
653 /// Assumes that `self.hash.hasher` has been updated only with the original digest and that725 /// Assumes that `self.hash.hasher` has been updated only with the original digest and that
654 /// `self.files` contains only the original input files.726 /// `self.files` contains only the original input files.
655 fn hitWithCurrentLock(self: *Manifest) HitError!union(enum) {727 fn checkLocked(m: *Manifest) CheckError!CheckStatus {
656 hit,728 const gpa = m.cache.gpa;
657 miss: struct {729 const io = m.cache.io;
658 file_digests_populated: usize,730
659 },731 var manifest_reader = m.manifest_file.?.reader(io, &.{}); // Reads positionally from zero.
660 } {732 m.contents.clearRetainingCapacity();
661 const gpa = self.cache.gpa;733 manifest_reader.interface.appendRemainingUnlimited(gpa, &m.contents) catch |err| switch (err) {
662 const io = self.cache.io;
663 const input_file_count = self.files.entries.len;
664 var tiny_buffer: [1]u8 = undefined; // allows allocRemaining to detect limit exceeded
665 var manifest_reader = self.manifest_file.?.reader(io, &tiny_buffer); // Reads positionally from zero.
666 const limit: std.Io.Limit = .limited(manifest_file_size_max);
667 const file_contents = manifest_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
668 error.OutOfMemory => |e| return e,734 error.OutOfMemory => |e| return e,
669 error.StreamTooLong => return error.OutOfMemory,
670 error.ReadFailed => {735 error.ReadFailed => {
671 self.diagnostic = .{ .manifest_read = manifest_reader.err.? };736 m.diagnostic = .{ .manifest_read = manifest_reader.err.? };
672 return error.CacheCheckFailed;737 return error.CacheCheckFailed;
673 },738 },
674 };739 };
675 defer gpa.free(file_contents);740
676741 // Guess number of files based on manifest contents len to reduce allocations.
677 var any_file_changed = false;742 try m.files.ensureUnusedCapacity(gpa, m.contents.len / (@sizeOf(File) + 32));
678 var line_iter = mem.tokenizeScalar(u8, file_contents, '\n');743
679 var idx: usize = 0;744 var file_index: usize = 0;
680 const header_valid = valid: {745 var off: usize = 0;
681 const line = line_iter.next() orelse break :valid false;746
682 break :valid std.mem.eql(u8, line, manifest_header);747 // This group we always want to compute the hash digests, even on a cache miss.
748 var input_group: Io.Group = .init;
749 defer input_group.cancel(io);
750
751 // This group we would like to cancel as soon as a cache miss is discovered.
752 const PostResult = union(enum) {
753 checkFile: CheckFileResult,
683 };754 };
684 if (!header_valid) {755 var post_select_buffer: [10]PostResult = undefined;
685 return .{ .miss = .{ .file_digests_populated = 0 } };756 var post_select: Io.Select(PostResult) = .init(&post_select_buffer);
757 var post_select_remaining: usize = 0;
758 defer post_select.cancel(io);
759
760 while (off + 1 < m.contents.len) {
761 const file_off: File.Offset = @fromBackingInt(off);
762 const file = try File.getFallible(file_off, m);
763 if (file.flags.prefix >= m.cache.prefixes_len) return error.InvalidFormat;
764 const path = try file.pathFallible();
765 if (path.len == 0) return error.InvalidFormat;
766
767 if (file_index < m.input_files.items.len) {
768 if (m.files.keys()[file_index] != file_off) return error.InvalidFormat;
769
770 input_group.async(io, checkFile, .{m.cache, file, path});
771 } else {
772 try m.files.put(gpa, file_off);
773
774 post_select.async(.checkFile, checkFile, .{m.cache, file, path});
775 post_select_remaining += 1;
776 }
777
778 file_index += 1;
779 off += @sizeOf(File) + path.len + 1;
686 }780 }
687 while (line_iter.next()) |line| {
688 defer idx += 1;
689
690 var iter = mem.tokenizeScalar(u8, line, ' ');
691 const size = iter.next() orelse return error.InvalidFormat;
692 const inode = iter.next() orelse return error.InvalidFormat;
693 const mtime_nsec_str = iter.next() orelse return error.InvalidFormat;
694 const digest_str = iter.next() orelse return error.InvalidFormat;
695 const prefix_str = iter.next() orelse return error.InvalidFormat;
696 const file_path = iter.rest();
697
698 const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat;
699 const stat_inode = fmt.parseInt(Io.File.INode, inode, 10) catch return error.InvalidFormat;
700 const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
701 const file_bin_digest = b: {
702 if (digest_str.len != hex_digest_len) return error.InvalidFormat;
703 var bd: BinDigest = undefined;
704 _ = fmt.hexToBytes(&bd, digest_str) catch return error.InvalidFormat;
705 break :b bd;
706 };
707781
708 const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat;782 // Final terminating zero byte to distinguish empty manifest file from
709 if (prefix >= self.cache.prefixes_len) return error.InvalidFormat;783 // manifest with zero files.
710784 const file_valid = off + 1 == m.contents.len and m.contents[off] == 0;
711 if (file_path.len == 0) return error.InvalidFormat;785 if (!file_valid or file_index < m.input_files.items.len) {
712786 try input_group.await(io);
713 const cache_hash_file = f: {787 return .miss;
714 const prefixed_path: PrefixedPath = .{788 }
715 .prefix = prefix,
716 .sub_path = file_path, // expires with file_contents
717 };
718 if (idx < input_file_count) {
719 const file = &self.files.keys()[idx];
720 if (!file.prefixed_path.eql(prefixed_path))
721 return error.InvalidFormat;
722
723 file.stat = .{
724 .size = stat_size,
725 .inode = stat_inode,
726 .mtime = .{ .nanoseconds = stat_mtime },
727 };
728 file.bin_digest = file_bin_digest;
729 break :f file;
730 }
731 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
732 errdefer _ = self.files.pop();
733 if (!gop.found_existing) {
734 gop.key_ptr.* = .{
735 .prefixed_path = .{
736 .prefix = prefix,
737 .sub_path = try gpa.dupe(u8, file_path),
738 },
739 .contents = null,
740 .max_file_size = null,
741 .handle = null,
742 .stat = .{
743 .size = stat_size,
744 .inode = stat_inode,
745 .mtime = .{ .nanoseconds = stat_mtime },
746 },
747 .bin_digest = file_bin_digest,
748 };
749 }
750 break :f gop.key_ptr;
751 };
752789
753 const pp = cache_hash_file.prefixed_path;790 // Don't track the trailing zero byte in contents.
754 const dir = self.cache.prefixes()[pp.prefix].handle;791 m.contents.len -= 1;
755 const this_file = dir.openFile(io, pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) {792
756 error.FileNotFound => {793 var post_await_buffer: [10]PostResult = undefined;
757 // Every digest before this one has been populated successfully.794 while (post_select_remaining > 0) {
758 return .{ .miss = .{ .file_digests_populated = idx } };795 const n = try post_select.awaitMany(&post_await_buffer, 1);
759 },796 post_select_remaining -= n;
760 error.Canceled => |e| return e,797 for (post_await_buffer[0..n]) |u| switch (u) {
761 else => |e| {798 .checkFile => |result| switch (result) {
762 self.diagnostic = .{ .file_open = .{799 .hit => continue,
763 .file_index = idx,800 .miss => {
764 .err = e,801 post_select.cancelDiscard();
765 } };802 try input_group.await(io);
766 return error.CacheCheckFailed;803 return .miss;
804 },
805 .fail => |diagnostic| {
806 m.diagnostic = diagnostic;
807 return error.CacheCheckFailed;
808 },
767 },809 },
768 };810 };
769 defer this_file.close(io);811 }
770812
771 const actual_stat = this_file.stat(io) catch |err| {813 try input_group.await(io);
772 self.diagnostic = .{ .file_stat = .{
773 .file_index = idx,
774 .err = err,
775 } };
776 return error.CacheCheckFailed;
777 };
778 const size_match = actual_stat.size == cache_hash_file.stat.size;
779 const mtime_match = actual_stat.mtime.nanoseconds == cache_hash_file.stat.mtime.nanoseconds;
780 const inode_match = actual_stat.inode == cache_hash_file.stat.inode;
781
782 if (!size_match or !mtime_match or !inode_match) {
783 cache_hash_file.stat = .{
784 .size = actual_stat.size,
785 .mtime = actual_stat.mtime,
786 .inode = actual_stat.inode,
787 };
788
789 if (try self.isProblematicTimestamp(cache_hash_file.stat.mtime)) {
790 // The actual file has an unreliable timestamp, force it to be hashed
791 cache_hash_file.stat.mtime = .zero;
792 cache_hash_file.stat.inode = 0;
793 }
794814
795 var actual_digest: BinDigest = undefined;815 for (m.files.keys()) |file_off| {
796 hashFile(io, this_file, &actual_digest) catch |err| {816 m.hash.hasher.update(&file_off.get(m).digest);
797 self.diagnostic = .{ .file_read = .{817 }
798 .file_index = idx,
799 .err = err,
800 } };
801 return error.CacheCheckFailed;
802 };
803818
804 if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) {819 return .hit;
805 cache_hash_file.bin_digest = actual_digest;820 }
806 // keep going until we have the input file digests
807 any_file_changed = true;
808 }
809 }
810821
811 if (!any_file_changed) {822 const CheckFileResult = union(enum) {
812 self.hash.hasher.update(&cache_hash_file.bin_digest);823 hit,
813 }824 miss,
814 }825 fail: Diagnostic,
826 };
815827
816 // If the manifest was somehow missing one of our input files, or if any file hash has changed,828 /// Runs concurrently with other `checkFile`.
817 // then this is a cache miss. However, we have successfully populated some or all of the file829 fn checkFile(cache: *const Cache, file: *File, file_path: [:0]const u8) Io.Cancelable!CheckFileResult {
818 // digests.830 const io = cache.io;
819 if (any_file_changed or idx < input_file_count) {831 const dir = cache.prefixes()[file.flags.prefix].handle;
820 return .{ .miss = .{ .file_digests_populated = idx } };832
833 const this_file = dir.openFile(io, file_path, .{ .mode = .read_only }) catch |err| switch (err) {
834 error.FileNotFound => return .miss,
835 error.Canceled => |e| return e,
836 else => |e| return .{ .fail = .{ .file_open = .{
837 .file_index = file_index,
838 .err = e,
839 } }},
840 };
841 defer this_file.close(io);
842
843 const actual_stat = this_file.stat(io) catch |err| return .{ .fail = .{ .file_stat = .{
844 .file_index = file_index,
845 .err = err,
846 } }};
847 const size_match = actual_stat.size == file.size;
848 const mtime_match = actual_stat.mtime.nanoseconds == file.mtime;
849 const inode_match = actual_stat.inode == file.inode;
850
851 if (!size_match or !mtime_match or !inode_match) {
852 try file.setStat(actual_stat);
853
854 var actual_digest: BinDigest = undefined;
855 hashFile(io, this_file, &actual_digest) catch |err| return .{ .fail = .{ .file_read = .{
856 .file_index = file_index,
857 .err = err,
858 } }};
859
860 if (!mem.eql(u8, &file.digest, &actual_digest)) {
861 file.digest = actual_digest;
862 return .miss;
863 }
821 }864 }
822865
823 return .hit;866 return .hit;
824 }867 }
825868
826 /// Reset `self.hash.hasher` to the state it should be in after `hit` returns `false`.869 /// Reset `man.hash.hasher` to the state it should be in after `hit` returns `CheckStatus.miss`.
827 /// The hasher contains the original input digest, and all original input file digests (i.e.870 /// The hasher contains the original input digest, and all original input file digests (i.e.
828 /// not including post files).871 /// not including post files).
829 /// Assumes that `bin_digest` is populated for all files up to `input_file_count`. As such,872 ///
830 /// this is not necessarily safe to call within `hit`.873 /// Assumes that `bin_digest` is populated for all input files.
831 pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void {874 pub fn unhit(man: *Manifest, bin_digest: BinDigest) void {
832 // Reset the hash.875 // Reset the hash.
833 self.hash.hasher = hasher_init;876 man.hash.hasher = hasher_init;
834 self.hash.hasher.update(&bin_digest);877 man.hash.hasher.update(&bin_digest);
835878 man.shrinkFilesToInput();
836 // Remove files not in the initial hash.879 for (man.files.keys()) |off| {
837 while (self.files.count() != input_file_count) {880 const file = off.get(man);
838 var file = self.files.pop().?;881 man.hash.hasher.update(&file.digest);
839 file.key.deinit(self.cache.gpa);
840 }
841
842 for (self.files.keys()) |file| {
843 self.hash.hasher.update(&file.bin_digest);
844 }882 }
845 }883 }
846884
...@@ -887,203 +925,133 @@ pub const Manifest = struct {...@@ -887,203 +925,133 @@ pub const Manifest = struct {
887 return timestamp.nanoseconds >= man.recent_problematic_timestamp.nanoseconds;925 return timestamp.nanoseconds >= man.recent_problematic_timestamp.nanoseconds;
888 }926 }
889927
890 fn populateFileHash(self: *Manifest, ch_file: *File) !void {928 pub const AddFilePostOptions = struct {
891 const io = self.cache.io;929 handle: union(enum) {
892930 file: ?Io.File,
893 if (ch_file.handle) |handle| {931 dir: ?Io.Dir,
894 return populateFileHashHandle(self, ch_file, handle);932 } = .{ .file = null },
895 } else {933 stat: ?Stat = null,
896 const pp = ch_file.prefixed_path;934 contents: ?[]const u8 = null,
897 const dir = self.cache.prefixes()[pp.prefix].handle;935 metadata_only: bool = false,
898 const handle = try dir.openFile(io, pp.sub_path, .{});936 };
899 defer handle.close(io);
900 return populateFileHashHandle(self, ch_file, handle);
901 }
902 }
903
904 fn populateFileHashHandle(self: *Manifest, ch_file: *File, io_file: Io.File) !void {
905 const io = self.cache.io;
906 const gpa = self.cache.gpa;
907937
908 const actual_stat = try io_file.stat(io);938 pub const AddFilePostError = error {
909 ch_file.stat = .{939 /// The same file path has been added to the cache manifest both as a
910 .size = actual_stat.size,940 /// directory and as a normal file, making the intended caching
911 .mtime = actual_stat.mtime,941 /// behavior ambiguous.
912 .inode = actual_stat.inode,942 IsDirectoryAmbiguous,
913 };943 } || Allocator.Error;
944
945 /// Add a file as a dependency of process being cached, after cache miss
946 /// occurs.
947 pub fn addFilePost(m: *Manifest, path: Path, options: AddFilePostOptions) AddFilePostError!void {
948 assert(m.manifest_file != null);
949 const cache = m.cache;
950 const gpa = cache.gpa;
951 const io = cache.io;
952 const is_directory = options.handle == .dir;
914953
915 if (try self.isProblematicTimestamp(ch_file.stat.mtime)) {954 try m.files.ensureUnusedCapacity(gpa, 1);
916 // The actual file has an unreliable timestamp, force it to be hashed
917 ch_file.stat.mtime = .zero;
918 ch_file.stat.inode = 0;
919 }
920955
921 if (ch_file.max_file_size) |max_file_size| {956 const prev_contents_len = m.contents.items.len;
922 if (ch_file.stat.size > max_file_size) return error.FileTooBig;957 const new_header: *File = @ptrCast(try m.contents.addManyAsSlice(gpa, @sizeOf(File)));
958 errdefer m.contents.shrinkRetainingCapacity(prev_contents_len);
923959
924 // Hash while reading from disk, to keep the contents in the cpu960 new_header.* = .{
925 // cache while doing hashing.961 .flags = .{
926 const contents = try gpa.alloc(u8, @intCast(ch_file.stat.size));962 .prefix = try cache.findAppendPrefixedPath(&m.contents, path),
927 errdefer gpa.free(contents);963 .is_directory = is_directory,
964 .metadata_only = options.metadata_only,
965 },
966 .size = undefined,
967 .inode = undefined,
968 .mtime = undefined,
969 .digest = @splat(0),
970 };
971 assert(m.contents.items.len % @alignOf(File) == 0);
928972
929 var hasher = hasher_init;973 const gop = m.files.getOrPutAssumeCapacity(@fromBackingInt(prev_contents_len), .{
930 var off: usize = 0;974 .manifest = m,
931 while (true) {975 });
932 const bytes_read = try io_file.readPositional(io, &.{contents[off..]}, off);976 m.files.lockPointers();
933 if (bytes_read == 0) break;977 defer m.files.unlockPointers();
934 hasher.update(contents[off..][0..bytes_read]);978
935 off += bytes_read;979 const header = if (gop.found_existing) h: {
980 m.contents.shrinkRetainingCapacity(prev_contents_len);
981 const existing_off = gop.key_ptr.*;
982 const header = existing_off.get(m);
983 if (header.flags.is_directory != is_directory)
984 return error.IsDirectoryAmbiguous;
985 if (!options.metadata_only)
986 header.flags.metadata_only = false;
987 break :h header;
988 } else new_header;
989
990 if (options.stat) |stat| {
991 try header.setStat(m, stat);
992 if (header.metadata_only) {
993 return;
994 } else if (options.contents) |contents| {
995 var hasher = hasher_init;
996 hasher.update(contents);
997 hasher.final(&header.digest);
998 return;
936 }999 }
937 hasher.final(&ch_file.bin_digest);
938
939 ch_file.contents = contents;
940 } else {
941 try hashFile(io, io_file, &ch_file.bin_digest);
942 }1000 }
9431001
944 self.hash.hasher.update(&ch_file.bin_digest);1002 const need_stat = options.stat == null;
945 }
9461003
947 /// Add a file as a dependency of process being cached, after the initial hash has been1004 switch (options.handle) {
948 /// calculated. This is useful for processes that don't know all the files that1005 .dir => |opt_handle| if (opt_handle) |handle| {
949 /// are depended on ahead of time. For example, a source file that can import other files1006 try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only);
950 /// will need to be recompiled if the imported file is changed.1007 } else {
951 pub fn addFilePostFetch(self: *Manifest, file_path: []const u8, max_file_size: usize) ![]const u8 {1008 const dir = cache.prefixes()[header.flags.prefix].handle;
952 assert(self.manifest_file != null);1009 const handle = try dir.openDir(io, header.path(), .{ .access_sub_paths = false, .iterate = true, });
9531010 defer handle.close(io);
954 const gpa = self.cache.gpa;1011 try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only);
955 const prefixed_path = try self.cache.findPrefix(file_path);1012 },
956 errdefer gpa.free(prefixed_path.sub_path);
957
958 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
959 errdefer _ = self.files.pop();
9601013
961 if (gop.found_existing) {1014 .file => |opt_handle| if (opt_handle) |handle| {
962 gpa.free(prefixed_path.sub_path);1015 try populateFile(m, header, need_stat, handle, options.contents, header.metadata_only);
963 return gop.key_ptr.contents.?;1016 } else {
1017 const dir = cache.prefixes()[header.flags.prefix].handle;
1018 const handle = try dir.openFile(io, header.path(), .{ .mode = .read_only });
1019 defer handle.close(io);
1020 try populateFile(m, header, need_stat, handle, options.contents, header.metadata_only);
1021 },
964 }1022 }
965
966 gop.key_ptr.* = .{
967 .prefixed_path = prefixed_path,
968 .max_file_size = max_file_size,
969 .stat = undefined,
970 .bin_digest = undefined,
971 .contents = null,
972 .handle = null,
973 };
974
975 self.files.lockPointers();
976 defer self.files.unlockPointers();
977
978 try self.populateFileHash(gop.key_ptr);
979 return gop.key_ptr.contents.?;
980 }
981
982 /// Add a file as a dependency of process being cached, after the initial hash has been
983 /// calculated.
984 ///
985 /// This is useful for processes that don't know the all the files that are
986 /// depended on ahead of time. For example, a source file that can import
987 /// other files will need to be recompiled if the imported file is changed.
988 pub fn addFilePost(man: *Manifest, file_path: []const u8) !void {
989 assert(man.manifest_file != null);
990 const gpa = man.cache.gpa;
991 const prefixed_path = try man.cache.findPrefix(file_path);
992 var keep = false;
993 defer if (!keep) gpa.free(prefixed_path.sub_path);
994 keep = try addPrefixedPathPost(man, prefixed_path);
995 }1023 }
9961024
997 pub fn addPathPost(man: *Manifest, path: Path) !void {1025 fn populateFile(m: *Manifest, file: *File, need_stat: bool, handle: Io.File, contents: ?[]const u8, metadata_only: bool,) !void {
998 assert(man.manifest_file != null);1026 const io = m.cache.io;
999 const gpa = man.cache.gpa;
1000 const prefixed_path: PrefixedPath = try man.cache.findPrefixPath(path);
1001 var keep = false;
1002 defer if (!keep) gpa.free(prefixed_path.sub_path);
1003 keep = try addPrefixedPathPost(man, prefixed_path);
1004 }
1005
1006 /// Low level function. `prefixed_path` references cloned memory. Returns
1007 /// whether or not `prefixed_path.sub_path` should be kept.
1008 pub fn addPrefixedPathPost(man: *Manifest, prefixed_path: PrefixedPath) !bool {
1009 assert(man.manifest_file != null);
1010 const gpa = man.cache.gpa;
1011
1012 const gop = try man.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
1013 errdefer _ = man.files.pop();
1014
1015 if (gop.found_existing) return false;
1016
1017 gop.key_ptr.* = .{
1018 .prefixed_path = prefixed_path,
1019 .max_file_size = null,
1020 .handle = null,
1021 .stat = undefined,
1022 .bin_digest = undefined,
1023 .contents = null,
1024 };
1025
1026 man.files.lockPointers();
1027 defer man.files.unlockPointers();
1028
1029 try man.populateFileHash(gop.key_ptr);
1030 return true;
1031 }
1032
1033 /// Like `addFilePost` but when the file contents have already been loaded from disk.
1034 pub fn addFilePostContents(
1035 man: *Manifest,
1036 file_path: []const u8,
1037 bytes: []const u8,
1038 stat: File.Stat,
1039 ) !void {
1040 assert(man.manifest_file != null);
1041 const gpa = man.cache.gpa;
1042 const prefixed_path = try man.cache.findPrefix(file_path);
1043 var keep = false;
1044 defer if (!keep) gpa.free(prefixed_path.sub_path);
1045 keep = try addPrefixedPathPostContents(man, prefixed_path, bytes, stat);
1046 }
1047
1048 /// Low level function. `prefixed_path` references cloned memory. Returns
1049 /// whether or not `prefixed_path.sub_path` should be kept.
1050 pub fn addPrefixedPathPostContents(
1051 man: *Manifest,
1052 prefixed_path: PrefixedPath,
1053 bytes: []const u8,
1054 stat: File.Stat,
1055 ) !bool {
1056 const gpa = man.cache.gpa;
1057 const gop = try man.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
1058 errdefer _ = man.files.pop();
10591027
1060 if (gop.found_existing) return false;1028 if (need_stat) {
10611029 const stat = try handle.stat(io);
1062 const new_file = gop.key_ptr;1030 try file.setStat(m, stat);
1063
1064 new_file.* = .{
1065 .prefixed_path = prefixed_path,
1066 .max_file_size = null,
1067 .handle = null,
1068 .stat = stat,
1069 .bin_digest = undefined,
1070 .contents = null,
1071 };
1072
1073 if (try man.isProblematicTimestamp(new_file.stat.mtime)) {
1074 // The actual file has an unreliable timestamp, force it to be hashed
1075 new_file.stat.mtime = .zero;
1076 new_file.stat.inode = 0;
1077 }1031 }
10781032 if (metadata_only) return;
1079 {1033 if (contents) |bytes| {
1080 var hasher = hasher_init;1034 var hasher = hasher_init;
1081 hasher.update(bytes);1035 hasher.update(bytes);
1082 hasher.final(&new_file.bin_digest);1036 hasher.final(&file.digest);
1037 } else {
1038 try hashFile(io, handle, &file.digest);
1083 }1039 }
1040 }
10841041
1085 man.hash.hasher.update(&new_file.bin_digest);1042 fn populateDirectory(m: *Manifest, file: *File, need_stat: bool, handle: Io.File, contents: ?[]const u8, metadata_only: bool,) !void {
1086 return true;1043 _ = m;
1044 _ = file;
1045 _ = need_stat;
1046 _ = handle;
1047 _ = contents;
1048 _ = metadata_only;
1049 @panic("TODO");
1050 }
1051
1052 pub fn addDepFile(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
1053 assert(self.manifest_file == null);
1054 return self.addDepFileMaybePost(dir, dep_file_sub_path);
1087 }1055 }
10881056
1089 pub fn addDepFilePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {1057 pub fn addDepFilePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
...@@ -1094,7 +1062,7 @@ pub const Manifest = struct {...@@ -1094,7 +1062,7 @@ pub const Manifest = struct {
1094 fn addDepFileMaybePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {1062 fn addDepFileMaybePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
1095 const gpa = self.cache.gpa;1063 const gpa = self.cache.gpa;
1096 const io = self.cache.io;1064 const io = self.cache.io;
1097 const dep_file_contents = try dir.readFileAlloc(io, dep_file_sub_path, gpa, .limited(manifest_file_size_max));1065 const dep_file_contents = try dir.readFileAlloc(io, dep_file_sub_path, gpa, .limited(file_size_max));
1098 defer gpa.free(dep_file_contents);1066 defer gpa.free(dep_file_contents);
10991067
1100 var error_buf: std.ArrayList(u8) = .empty;1068 var error_buf: std.ArrayList(u8) = .empty;
...@@ -1151,39 +1119,24 @@ pub const Manifest = struct {...@@ -1151,39 +1119,24 @@ pub const Manifest = struct {
11511119
1152 /// If `want_shared_lock` is true, this function automatically downgrades the1120 /// If `want_shared_lock` is true, this function automatically downgrades the
1153 /// lock from exclusive to shared.1121 /// lock from exclusive to shared.
1154 pub fn writeManifest(self: *Manifest) !void {1122 pub fn writeManifest(m: *Manifest) !void {
1155 assert(self.have_exclusive_lock);1123 assert(m.have_exclusive_lock);
1156 const io = self.cache.io;1124 const io = m.cache.io;
1157 const manifest_file = self.manifest_file.?;1125 const manifest_file = m.manifest_file.?;
1158 if (self.manifest_dirty) {1126 if (m.manifest_dirty) {
1159 self.manifest_dirty = false;1127
11601128 m.contents.appendAssumeCapacity(0);
1161 var buffer: [4000]u8 = undefined;1129 defer _ = m.contents.pop().?;
1162 var fw = manifest_file.writer(io, &buffer);1130
1163 writeDirtyManifestToStream(self, &fw) catch |err| switch (err) {1131 try manifest_file.setLength(io, m.contents.items.len);
1164 error.WriteFailed => return fw.err.?,1132 try manifest_file.writePositionalAll(io, m.contents.items, 0);
1165 else => |e| return e,
1166 };
1167 }
11681133
1169 if (self.want_shared_lock) {1134 m.manifest_dirty = false;
1170 try self.downgradeToSharedLock();
1171 }1135 }
1172 }
11731136
1174 fn writeDirtyManifestToStream(self: *Manifest, fw: *Io.File.Writer) !void {1137 if (m.want_shared_lock) {
1175 try fw.interface.writeAll(manifest_header ++ "\n");1138 try m.downgradeToSharedLock();
1176 for (self.files.keys()) |file| {
1177 try fw.interface.print("{d} {d} {d} {x} {d} {s}\n", .{
1178 file.stat.size,
1179 file.stat.inode,
1180 file.stat.mtime,
1181 &file.bin_digest,
1182 file.prefixed_path.prefix,
1183 file.prefixed_path.sub_path,
1184 });
1185 }1139 }
1186 try fw.end();
1187 }1140 }
11881141
1189 fn downgradeToSharedLock(self: *Manifest) !void {1142 fn downgradeToSharedLock(self: *Manifest) !void {
...@@ -1275,33 +1228,40 @@ pub const Manifest = struct {...@@ -1275,33 +1228,40 @@ pub const Manifest = struct {
12751228
1276 pub fn populateOtherManifest(man: *Manifest, other: *Manifest, prefix_map: [5]u8) Allocator.Error!void {1229 pub fn populateOtherManifest(man: *Manifest, other: *Manifest, prefix_map: [5]u8) Allocator.Error!void {
1277 const gpa = other.cache.gpa;1230 const gpa = other.cache.gpa;
1231 assert(other.manifest_file != null);
1278 assert(@typeInfo(std.zig.Server.Message.PathPrefix).@"enum".field_names.len == man.cache.prefixes_len);1232 assert(@typeInfo(std.zig.Server.Message.PathPrefix).@"enum".field_names.len == man.cache.prefixes_len);
1279 assert(man.cache.prefixes_len == 5);1233 assert(man.cache.prefixes_len == 5);
1280 for (man.files.keys()) |file| {
1281 const prefixed_path: PrefixedPath = .{
1282 .prefix = prefix_map[file.prefixed_path.prefix],
1283 .sub_path = try gpa.dupe(u8, file.prefixed_path.sub_path),
1284 };
1285 errdefer gpa.free(prefixed_path.sub_path);
12861234
1287 const gop = try other.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});1235 const orig_files_len = other.files.count();
1288 errdefer _ = other.files.pop();1236 const orig_contents_len = other.contents.items.len;
1237 errdefer {
1238 other.files.shrinkRetainingCapacity(orig_files_len);
1239 other.contents.shrinkRetainingCapacity(orig_contents_len);
1240 }
1241
1242 for (man.files.keys(), 0..) |off, file_index| {
1243 try other.files.ensureUnusedCapacity(gpa, 1);
1244
1245 const next_off = if (file_index < man.files.count())
1246 @backingInt(man.files.keys()[file_index + 1])
1247 else
1248 man.contents.items.len;
1249
1250 const copy_bytes = man.contents.items[@backingInt(off)..next_off];
1251 const prev_contents_len = other.contents.items.len;
1252 try other.contents.appendSlice(gpa, copy_bytes);
1253
1254 const gop = other.files.getOrPutAssumeCapacity(@fromBackingInt(prev_contents_len), .{
1255 .manifest = other,
1256 });
12891257
1290 if (gop.found_existing) {1258 if (gop.found_existing) {
1291 gpa.free(prefixed_path.sub_path);1259 other.contents.shrinkRetainingCapacity(prev_contents_len);
1292 continue;1260 continue;
1293 }1261 }
12941262
1295 gop.key_ptr.* = .{1263 const other_file = File.get(@fromBackingInt(prev_contents_len));
1296 .prefixed_path = prefixed_path,1264 other_file.prefix = prefix_map[other_file.prefix];
1297 .max_file_size = file.max_file_size,
1298 .handle = file.handle,
1299 .stat = file.stat,
1300 .bin_digest = file.bin_digest,
1301 .contents = null,
1302 };
1303
1304 other.hash.hasher.update(&gop.key_ptr.bin_digest);
1305 }1265 }
1306 }1266 }
1307};1267};
lib/std/Build/Configuration.zig+2-3
...@@ -1872,12 +1872,11 @@ pub const PathDep = extern struct {...@@ -1872,12 +1872,11 @@ pub const PathDep = extern struct {
1872 pkg: Package.OptionalIndex,1872 pkg: Package.OptionalIndex,
18731873
1874 pub const Flags = packed struct(u32) {1874 pub const Flags = packed struct(u32) {
1875 mode: Mode,1875 is_directory: bool,
1876 metadata_only: bool,
1876 base: LazyPath.Relative.Base,1877 base: LazyPath.Relative.Base,
1877 _: u16 = 0,1878 _: u16 = 0,
1878 };1879 };
1879
1880 pub const Mode = enum(u8) { directory, contents, metadata };
1881};1880};
18821881
1883pub const InstallDestDir = enum(u32) {1882pub const InstallDestDir = enum(u32) {
lib/std/fs/path.zig+7
...@@ -1178,6 +1178,13 @@ pub fn resolvePosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![...@@ -1178,6 +1178,13 @@ pub fn resolvePosix(gpa: Allocator, paths: []const []const u8) Allocator.Error![
1178 }1178 }
1179}1179}
11801180
1181pub fn resolvePosix2(gpa: Allocator, al: *std.ArrayList(u8), paths: []const []const u8) Allocator.Error!void {
1182 _ = gpa;
1183 _ = al;
1184 _ = paths;
1185 @panic("TODO");
1186}
1187
1181test resolve {1188test resolve {
1182 try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");1189 try testResolveWindows(&[_][]const u8{ "a", "..\\..\\.." }, "..\\..");
1183 try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");1190 try testResolveWindows(&[_][]const u8{ "..", "", "..\\..\\foo" }, "..\\..\\..\\foo");
src/Zcu/PerThread.zig+5-6
...@@ -348,11 +348,8 @@ pub fn update(...@@ -348,11 +348,8 @@ pub fn update(
348 }348 }
349}349}
350fn workerUpdateBuiltinFile(comp: *Compilation, file: *Zcu.File) void {350fn workerUpdateBuiltinFile(comp: *Compilation, file: *Zcu.File) void {
351 Builtin.updateFileOnDisk(file, comp) catch |err| comp.lockAndSetMiscFailure(351 Builtin.updateFileOnDisk(file, comp) catch |err|
352 .write_builtin_zig,352 comp.lockAndSetMiscFailure(.write_builtin_zig, "unable to write {qf}: {t}", .{ file.path.fmt(comp), err });
353 "unable to write '{f}': {s}",
354 .{ file.path.fmt(comp), @errorName(err) },
355 );
356}353}
357fn workerUpdateFile(354fn workerUpdateFile(
358 comp: *Compilation,355 comp: *Compilation,
...@@ -371,7 +368,9 @@ fn workerUpdateFile(...@@ -371,7 +368,9 @@ fn workerUpdateFile(
371 const active = comp.zcu.?.activate(tid);368 const active = comp.zcu.?.activate(tid);
372 defer active.deactivate();369 defer active.deactivate();
373 active.pt.updateFile(file_index, file) catch |err| {370 active.pt.updateFile(file_index, file) catch |err| {
374 active.pt.reportRetryableFileError(file_index, "unable to load '{s}': {s}", .{ std.fs.path.basename(file.path.sub_path), @errorName(err) }) catch |oom| switch (oom) {371 active.pt.reportRetryableFileError(file_index, "unable to load {q}: {t}", .{
372 std.fs.path.basename(file.path.sub_path), err,
373 }) catch |oom| switch (oom) {
375 error.OutOfMemory => {374 error.OutOfMemory => {
376 comp.mutex.lockUncancelable(io);375 comp.mutex.lockUncancelable(io);
377 defer comp.mutex.unlock(io);376 defer comp.mutex.unlock(io);
test/src/Cases.zig+1-1
...@@ -319,7 +319,7 @@ pub fn addCompile(...@@ -319,7 +319,7 @@ pub fn addCompile(
319pub fn addFromDir(ctx: *Cases, dir: Io.Dir, path_from_root: []const u8, b: *std.Build) void {319pub fn addFromDir(ctx: *Cases, dir: Io.Dir, path_from_root: []const u8, b: *std.Build) void {
320 var current_file: []const u8 = "none";320 var current_file: []const u8 = "none";
321 ctx.addFromDirInner(dir, path_from_root, &current_file, b) catch |err| {321 ctx.addFromDirInner(dir, path_from_root, &current_file, b) catch |err| {
322 std.debug.panicExtra(@returnAddress(), "test harness failed to process file {q}: {t}\n", .{322 std.debug.panicExtra(@returnAddress(), "test harness failed to process file {q}: {t}", .{
323 current_file, err,323 current_file, err,
324 });324 });
325 };325 };