authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-08-24 20:08:32-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-08-24 20:08:32-07:00
log5f3d30d6ed900e7e424f61eb9417df683867319e
tree281af7102b177f441ac722666ecad3443de481c3
parent978edbfc0ffe982169325c9de2d47d6ee2e46292

std.Build.Cache: implement is_directory and metadata_only


1 files changed, 317 insertions(+), 97 deletions(-)

lib/std/Build/Cache.zig+317-97
......@@ -49,7 +49,6 @@ pub fn obtain(cache: *Cache) Manifest {
4949 .hash = cache.hash,
5050 .manifest_file = null,
5151 .manifest_dirty = false,
52 .hex_digest = undefined,
5352 };
5453}
5554
......@@ -251,7 +250,7 @@ pub const HashHelper = struct {
251250
252251pub fn binToHex(bin_digest: BinDigest) HexDigest {
253252 var out_digest: HexDigest = undefined;
254 var w: std.Io.Writer = .fixed(&out_digest);
253 var w: Io.Writer = .fixed(&out_digest);
255254 w.printHex(&bin_digest, .lower) catch unreachable;
256255 return out_digest;
257256}
......@@ -278,7 +277,6 @@ pub const Manifest = struct {
278277 cache: *Cache,
279278 /// Current state for incremental hashing.
280279 hash: HashHelper,
281 hex_digest: HexDigest,
282280 /// When this is null, `Manifest` is in "pre-check" phase. Otherwise it is in "post-check" phase.
283281 manifest_file: ?Io.File,
284282 manifest_dirty: bool,
......@@ -309,9 +307,34 @@ pub const Manifest = struct {
309307 /// All contents from all `input_files` whose contents were requested,
310308 /// concatenated. Total byte size will be less than `max_input_content_len`
311309 /// otherwise an error is returned.
310 ///
311 /// Data is invalidated when `addFilePost` is called.
312312 all_input_content: std.ArrayList(u8) = .empty,
313313 max_input_content_len: usize = std.math.maxInt(u32),
314314
315 /// State that exists only during check.
316 pub const Check = struct {
317 /// Protects `Manifest.diagnostic` from data races.
318 diagnostic_lock: bool = false,
319 status: Status = .hit,
320
321 pub const Status = enum { hit, miss };
322
323 pub const Error = error{
324 /// Unable to check the cache for a reason that has been recorded into
325 /// the `diagnostic` field.
326 CacheCheckFailed,
327 /// A cache manifest file exists however it could not be parsed.
328 InvalidFormat,
329 } || Allocator.Error || Io.Cancelable;
330
331 fn fail(c: *Check, m: *Manifest, diagnostic: Diagnostic) void {
332 if (!@atomicRmw(bool, &c.diagnostic_lock, .Xchg, true, .unordered)) {
333 m.diagnostic = diagnostic;
334 }
335 }
336 };
337
315338 pub const Files = std.array_hash_map.Custom(File.Offset, void, File.HashContext, false);
316339
317340 /// Source files whose prefix and relative path are included when computing
......@@ -328,7 +351,7 @@ pub const Manifest = struct {
328351 have_stat: bool,
329352 /// Determines whether `File.digest` is populated.
330353 have_digest: bool,
331 contents: enum (usize) {
354 contents: enum(usize) {
332355 requested = std.math.maxInt(u32) - 1,
333356 not_requested = std.math.maxInt(u32),
334357 /// Byte offset index into `Manifest.all_input_content`.
......@@ -356,12 +379,29 @@ pub const Manifest = struct {
356379 /// Terminated by zero byte, then followed by padding until 8-byte aligned.
357380 path_start: [0]u8,
358381
359 pub const Flags = packed struct (u8) {
382 pub const Flags = packed struct(u8) {
360383 is_directory: bool,
361384 metadata_only: bool,
362385 prefix: u6,
363386 };
364387
388 /// Prefixes path names in encoded directory contents. Starts numbering
389 /// at `1` so that null byte can be used unambiguously as entry
390 /// separator.
391 pub const Kind = enum(u8) {
392 file = 1,
393 directory = 2,
394 other = 3,
395
396 pub fn fromStat(kind: Io.File.Kind) @This() {
397 return switch (kind) {
398 .file => .file,
399 .directory => .directory,
400 else => .other,
401 };
402 }
403 };
404
365405 /// Byte index within `Manifest.contents` where the entry starts.
366406 pub const Offset = enum(u32) {
367407 _,
......@@ -392,7 +432,6 @@ pub const Manifest = struct {
392432 }
393433 };
394434
395
396435 pub fn path(file: *const File) [:0]const u8 {
397436 return pathFallible(file) catch unreachable;
398437 }
......@@ -408,7 +447,7 @@ pub const Manifest = struct {
408447 const path_len = mem.findScalar(u8, path_ptr, 0).?;
409448 comptime assert(@offsetOf(File, "path_start") - @offsetOf(File, "flags") == 1);
410449 // Includes flags and sentinel.
411 const hash_string = (path_ptr - 1)[0..path_len + 2];
450 const hash_string = (path_ptr - 1)[0 .. path_len + 2];
412451 hasher.update(hash_string);
413452 }
414453
......@@ -424,9 +463,20 @@ pub const Manifest = struct {
424463 }
425464 }
426465
466 /// Returns true if the stat was changed. Updates the `file` with the new stat value.
467 fn setStatChanged(file: *File, m: *Manifest, stat: Stat) Io.Cancelable!bool {
468 if (stat.size == file.size and
469 stat.mtime.nanoseconds == file.mtime and
470 stat.inode == file.inode)
471 {
472 return false;
473 } else {
474 setStat(file, m, stat);
475 return true;
476 }
477 }
427478 };
428479
429
430480 pub const Diagnostic = union(enum) {
431481 none,
432482 manifest_create: Io.File.OpenError,
......@@ -438,7 +488,7 @@ pub const Manifest = struct {
438488 file_hash: FileOp,
439489
440490 pub const FileOp = struct {
441 file_index: usize,
491 file_offset: File.Offset,
442492 err: anyerror,
443493 };
444494 };
......@@ -450,16 +500,24 @@ pub const Manifest = struct {
450500 };
451501
452502 pub const AddInputFileOptions = struct {
503 /// If `is_directory` is true, this handle must be opened with
504 /// iteration capability.
453505 handle: ?Io.File = null,
454506 stat: ?Stat = null,
455507 request_handle: bool = false,
508 /// Can request file or directory contents depending on `is_directory`.
456509 request_contents: bool = false,
510 /// Contents of a directory are considered to be the sorted list of
511 /// file names of direct entries, separated by null byte. Each file name
512 /// is prefixed by `Io.File.Kind` byte, +1 so that the zero tag is
513 /// not aliased by the entry separator.
457514 is_directory: bool = false,
515 /// Content hashing skipped; any difference in metadata implies cache
516 /// miss.
458517 metadata_only: bool = false,
459
460518 };
461519
462 pub const AddInputFileError = error {
520 pub const AddInputFileError = error{
463521 /// The same file path has been added to the cache manifest both as a
464522 /// directory and as a normal file, making the intended caching
465523 /// behavior ambiguous.
......@@ -544,16 +602,6 @@ pub const Manifest = struct {
544602 _ = try addInputFile(m, opt_path orelse return, options);
545603 }
546604
547 pub const CheckError = error{
548 /// Unable to check the cache for a reason that has been recorded into
549 /// the `diagnostic` field.
550 CacheCheckFailed,
551 /// A cache manifest file exists however it could not be parsed.
552 InvalidFormat,
553 } || Allocator.Error || Io.Cancelable;
554
555 pub const CheckStatus = enum { hit, miss };
556
557605 /// Check the cache to see if the input exists in it.
558606 /// A hex encoding of its hash is available by calling `final`.
559607 ///
......@@ -566,13 +614,13 @@ pub const Manifest = struct {
566614 /// The lock on the manifest file is released when `deinit` is called. As another
567615 /// option, one may call `toOwnedLock` to obtain a smaller object which can represent
568616 /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called.
569 pub fn check(man: *Manifest, parent_progress_node: std.Progress.Node) CheckError!CheckStatus {
617 pub fn check(man: *Manifest, parent_progress_node: std.Progress.Node) Check.Error!Check.Status {
570618 const node = parent_progress_node.start("Reusing Cache Artifacts", 0);
571619 defer node.end();
572620 return checkProgressless(man);
573621 }
574622
575 pub fn checkProgressless(man: *Manifest) CheckError!CheckStatus {
623 pub fn checkProgressless(man: *Manifest) Check.Error!Check.Status {
576624 assert(man.manifest_file == null);
577625
578626 for (man.files.keys()[0..man.input_files.items.len]) |file_off| {
......@@ -583,9 +631,8 @@ pub const Manifest = struct {
583631
584632 var bin_digest: BinDigest = undefined;
585633 man.hash.hasher.final(&bin_digest);
586 man.hex_digest = binToHex(bin_digest);
587
588 const manifest_file_path = &man.hex_digest;
634 const hex_digest = binToHex(bin_digest);
635 const manifest_file_path = &hex_digest;
589636 const io = man.cache.io;
590637
591638 // We'll try to open the cache with an exclusive lock, but if that would block
......@@ -724,7 +771,7 @@ pub const Manifest = struct {
724771
725772 /// Assumes that `self.hash.hasher` has been updated only with the original digest and that
726773 /// `self.files` contains only the original input files.
727 fn checkLocked(m: *Manifest) CheckError!CheckStatus {
774 fn checkLocked(m: *Manifest) Check.Error!Check.Status {
728775 const gpa = m.cache.gpa;
729776 const io = m.cache.io;
730777
......@@ -750,11 +797,12 @@ pub const Manifest = struct {
750797
751798 // This group we would like to cancel as soon as a cache miss is discovered.
752799 const PostResult = union(enum) {
753 checkFile: CheckFileResult,
800 checkFile: Check.Status,
754801 };
755802 var post_select_buffer: [10]PostResult = undefined;
756803 var post_select: Io.Select(PostResult) = .init(&post_select_buffer);
757804 var post_select_remaining: usize = 0;
805 var c: Check = .{};
758806 defer post_select.cancel(io);
759807
760808 while (off + 1 < m.contents.len) {
......@@ -767,11 +815,11 @@ pub const Manifest = struct {
767815 if (file_index < m.input_files.items.len) {
768816 if (m.files.keys()[file_index] != file_off) return error.InvalidFormat;
769817
770 input_group.async(io, checkFile, .{m.cache, file, path});
818 input_group.async(io, checkInputFile, .{ m, &c, file_off, path });
771819 } else {
772820 try m.files.put(gpa, file_off);
773821
774 post_select.async(.checkFile, checkFile, .{m.cache, file, path});
822 post_select.async(.checkFile, checkFile, .{ m, &c, file_off, path });
775823 post_select_remaining += 1;
776824 }
777825
......@@ -794,6 +842,18 @@ pub const Manifest = struct {
794842 while (post_select_remaining > 0) {
795843 const n = try post_select.awaitMany(&post_await_buffer, 1);
796844 post_select_remaining -= n;
845
846 // Detect if input group already had a miss. In this case we still wait
847 // for those digests to be updated, but cancel the non input group.
848 switch (@atomicLoad(Check.Status, &c.status, .unordered)) {
849 .miss => {
850 post_select.cancelDiscard();
851 try input_group.await(io);
852 return .miss;
853 },
854 .hit => continue,
855 }
856
797857 for (post_await_buffer[0..n]) |u| switch (u) {
798858 .checkFile => |result| switch (result) {
799859 .hit => continue,
......@@ -811,6 +871,7 @@ pub const Manifest = struct {
811871 }
812872
813873 try input_group.await(io);
874 if (c.status == .miss) return .miss;
814875
815876 for (m.files.keys()) |file_off| {
816877 m.hash.hasher.update(&file_off.get(m).digest);
......@@ -819,54 +880,121 @@ pub const Manifest = struct {
819880 return .hit;
820881 }
821882
822 const CheckFileResult = union(enum) {
823 hit,
824 miss,
825 fail: Diagnostic,
826 };
883 fn checkInputFile(m: *Manifest, c: *Check, file_off: File.Offset, file_path: [:0]const u8) Io.Cancelable!void {
884 // TODO use already open handle
885 // TODO use already provided stat
886 // TODO implement request_handle
887 // TODO implement request_contents
888 switch (try checkFile(m, c, file_off, file_path)) {
889 .hit => return,
890 .miss => @atomicStore(Check.Status, &c.status, .miss, .unordered),
891 }
892 }
827893
828894 /// Runs concurrently with other `checkFile`.
829 fn checkFile(cache: *const Cache, file: *File, file_path: [:0]const u8) Io.Cancelable!CheckFileResult {
895 fn checkFile(
896 m: *Manifest,
897 c: *Check,
898 file_off: File.Offset,
899 file_path: [:0]const u8,
900 ) Io.Cancelable!Check.Status {
901 const file = file_off.get(m);
902 const cache = m.cache;
903 const gpa = cache.gpa;
830904 const io = cache.io;
831 const dir = cache.prefixes()[file.flags.prefix].handle;
905 const parent_dir = cache.prefixes()[file.flags.prefix].handle;
906
907 if (file.flags.metadata_only) {
908 const actual_stat = parent_dir.statFile() catch |err| switch (err) {
909 error.FileNotFound => return .miss,
910 error.Canceled => |e| return e,
911 else => |e| return c.fail(m, .{ .file_stat = .{
912 .file_offset = file_off,
913 .err = e,
914 } }),
915 };
916
917 const actual_is_directory = actual_stat.kind == .directory;
918 if (actual_is_directory != file.flags.is_directory) return .miss;
919
920 if (try file.setStatChanged(m, actual_stat)) return .miss;
921
922 return .hit;
923 }
832924
833 const this_file = dir.openFile(io, file_path, .{ .mode = .read_only }) catch |err| switch (err) {
834 error.FileNotFound => return .miss,
925 if (file.flags.is_directory) {
926 const opened_dir = parent_dir.openDir(io, file_path, .{
927 .iterate = true,
928 .access_sub_paths = false,
929 }) catch |err| switch (err) {
930 error.FileNotFound, error.NotDir => return .miss,
931 error.Canceled => |e| return e,
932 else => |e| return c.fail(m, .{ .file_open = .{
933 .file_offset = file_off,
934 .err = e,
935 } }),
936 };
937 defer opened_dir.close(io);
938
939 const actual_stat = opened_dir.stat(io) catch |err| switch (err) {
940 error.Canceled => |e| return e,
941 else => |e| return c.fail(m, .{ .file_stat = .{
942 .file_offset = file_off,
943 .err = e,
944 } }),
945 };
946 if (try file.setStatChanged(m, actual_stat)) {
947 const prev_digest: BinDigest = file.digest;
948 var contents: std.ArrayList(u8) = .empty;
949 defer contents.deinit(gpa);
950 hashDir(gpa, io, opened_dir, &file.digest, &contents) catch |err| switch (err) {
951 error.Canceled => |e| return e,
952 else => |e| return c.fail(m, .{ .file_read = .{
953 .file_offset = file_off,
954 .err = e,
955 } }),
956 };
957
958 if (!mem.eql(u8, &file.digest, &prev_digest)) return .miss;
959 }
960 return .hit;
961 }
962
963 const opened_file = parent_dir.openFile(io, file_path, .{ .mode = .read_only }) catch |err| switch (err) {
964 error.FileNotFound, error.IsDir => return .miss,
835965 error.Canceled => |e| return e,
836 else => |e| return .{ .fail = .{ .file_open = .{
837 .file_index = file_index,
966 else => |e| return c.fail(m, .{ .file_open = .{
967 .file_offset = file_off,
838968 .err = e,
839 } }},
969 } }),
840970 };
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 }
971 defer opened_file.close(io);
972
973 const actual_stat = opened_file.stat(io) catch |err| switch (err) {
974 error.Canceled => |e| return e,
975 else => |e| return c.fail(m, .{ .file_stat = .{
976 .file_offset = file_off,
977 .err = e,
978 } }),
979 };
980
981 if (try file.setStatChanged(m, actual_stat)) {
982 const prev_digest: BinDigest = file.digest;
983 hashFile(io, opened_file, &file.digest) catch |err| switch (err) {
984 error.Canceled => |e| return e,
985 else => |e| return c.fail(m, .{ .file_read = .{
986 .file_offset = file_off,
987 .err = e,
988 } }),
989 };
990
991 if (!mem.eql(u8, &file.digest, &prev_digest)) return .miss;
864992 }
865993
866994 return .hit;
867995 }
868996
869 /// Reset `man.hash.hasher` to the state it should be in after `hit` returns `CheckStatus.miss`.
997 /// Reset `man.hash.hasher` to the state it should be in after `hit` returns `Check.Status.miss`.
870998 /// The hasher contains the original input digest, and all original input file digests (i.e.
871999 /// not including post files).
8721000 ///
......@@ -931,16 +1059,18 @@ pub const Manifest = struct {
9311059 dir: ?Io.Dir,
9321060 } = .{ .file = null },
9331061 stat: ?Stat = null,
1062 /// If it is a directory, there is a special encoding required for contents, which
1063 /// is null-separated sorted entries, each one prefixed with `File.Kind`.
9341064 contents: ?[]const u8 = null,
9351065 metadata_only: bool = false,
9361066 };
9371067
938 pub const AddFilePostError = error {
1068 pub const AddFilePostError = error{
9391069 /// The same file path has been added to the cache manifest both as a
9401070 /// directory and as a normal file, making the intended caching
9411071 /// behavior ambiguous.
9421072 IsDirectoryAmbiguous,
943 } || Allocator.Error;
1073 } || Io.Cancelable || Allocator.Error;
9441074
9451075 /// Add a file as a dependency of process being cached, after cache miss
9461076 /// occurs.
......@@ -1006,7 +1136,10 @@ pub const Manifest = struct {
10061136 try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only);
10071137 } else {
10081138 const dir = cache.prefixes()[header.flags.prefix].handle;
1009 const handle = try dir.openDir(io, header.path(), .{ .access_sub_paths = false, .iterate = true, });
1139 const handle = try dir.openDir(io, header.path(), .{
1140 .access_sub_paths = false,
1141 .iterate = true,
1142 });
10101143 defer handle.close(io);
10111144 try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only);
10121145 },
......@@ -1022,7 +1155,14 @@ pub const Manifest = struct {
10221155 }
10231156 }
10241157
1025 fn populateFile(m: *Manifest, file: *File, need_stat: bool, handle: Io.File, contents: ?[]const u8, metadata_only: bool,) !void {
1158 fn populateFile(
1159 m: *Manifest,
1160 file: *File,
1161 need_stat: bool,
1162 handle: Io.File,
1163 contents: ?[]const u8,
1164 metadata_only: bool,
1165 ) !void {
10261166 const io = m.cache.io;
10271167
10281168 if (need_stat) {
......@@ -1039,14 +1179,32 @@ pub const Manifest = struct {
10391179 }
10401180 }
10411181
1042 fn populateDirectory(m: *Manifest, file: *File, need_stat: bool, handle: Io.File, contents: ?[]const u8, metadata_only: bool,) !void {
1043 _ = m;
1044 _ = file;
1045 _ = need_stat;
1046 _ = handle;
1047 _ = contents;
1048 _ = metadata_only;
1049 @panic("TODO");
1182 fn populateDirectory(
1183 m: *Manifest,
1184 file: *File,
1185 need_stat: bool,
1186 handle: Io.Dir,
1187 contents: ?[]const u8,
1188 metadata_only: bool,
1189 ) !void {
1190 const cache = m.cache;
1191 const io = cache.io;
1192 const gpa = cache.gpa;
1193
1194 if (need_stat) {
1195 const stat = try handle.stat(io);
1196 try file.setStat(m, stat);
1197 }
1198 if (metadata_only) return;
1199 if (contents) |bytes| {
1200 var hasher = hasher_init;
1201 hasher.update(bytes);
1202 hasher.final(&file.digest);
1203 } else {
1204 const prev_contents_len = m.all_input_content.items.len;
1205 defer m.all_input_content.shrinkRetainingCapacity(prev_contents_len);
1206 try hashDir(gpa, io, handle, &file.digest, &m.all_input_content);
1207 }
10501208 }
10511209
10521210 pub fn addDepFile(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
......@@ -1062,7 +1220,7 @@ pub const Manifest = struct {
10621220 fn addDepFileMaybePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void {
10631221 const gpa = self.cache.gpa;
10641222 const io = self.cache.io;
1065 const dep_file_contents = try dir.readFileAlloc(io, dep_file_sub_path, gpa, .limited(file_size_max));
1223 const dep_file_contents = try dir.readFileAlloc(io, dep_file_sub_path, gpa, .unlimited);
10661224 defer gpa.free(dep_file_contents);
10671225
10681226 var error_buf: std.ArrayList(u8) = .empty;
......@@ -1124,7 +1282,6 @@ pub const Manifest = struct {
11241282 const io = m.cache.io;
11251283 const manifest_file = m.manifest_file.?;
11261284 if (m.manifest_dirty) {
1127
11281285 m.contents.appendAssumeCapacity(0);
11291286 defer _ = m.contents.pop().?;
11301287
......@@ -1264,22 +1421,85 @@ pub const Manifest = struct {
12641421 other_file.prefix = prefix_map[other_file.prefix];
12651422 }
12661423 }
1267};
12681424
1269fn hashFile(io: Io, file: Io.File, bin_digest: *[Hasher.mac_length]u8) Io.File.ReadPositionalError!void {
1270 var buffer: [2048]u8 = undefined;
1271 var hasher = hasher_init;
1272 var offset: u64 = 0;
1273 while (true) {
1274 const n = try file.readPositional(io, &.{&buffer}, offset);
1275 if (n == 0) break;
1276 hasher.update(buffer[0..n]);
1277 offset += n;
1425 fn hashFile(io: Io, file: Io.File, bin_digest: *[Hasher.mac_length]u8) Io.File.ReadPositionalError!void {
1426 var buffer: [2048]u8 = undefined;
1427 var hasher = hasher_init;
1428 var offset: u64 = 0;
1429 while (true) {
1430 const n = try file.readPositional(io, &.{&buffer}, offset);
1431 if (n == 0) break;
1432 hasher.update(buffer[0..n]);
1433 offset += n;
1434 }
1435 hasher.final(bin_digest);
12781436 }
1279 hasher.final(bin_digest);
1280}
12811437
1282// Create/Write a file, close it, then grab its stat.mtime timestamp.
1438 const HashDirError = Io.Dir.Reader.Error || Allocator.Error;
1439
1440 /// Appends the sorted, encoded directory entries to `contents`.
1441 fn hashDir(
1442 gpa: Allocator,
1443 io: Io,
1444 dir: Io.Dir,
1445 bin_digest: *[Hasher.mac_length]u8,
1446 contents: *std.ArrayList(u8),
1447 ) HashDirError!void {
1448 var buffer: [@max(2048, Io.Dir.Reader.min_buffer_len)]u8 align(@alignOf(usize)) = undefined;
1449 var reader: Io.Dir.Reader = .init(dir, &buffer);
1450 var entry_buffer: [16]Io.Dir.Entry = undefined;
1451
1452 const contents_start = contents.items.len;
1453 errdefer contents.shrinkRetainingCapacity(contents_start);
1454
1455 // Each index points into `contents`.
1456 var entries_list: std.ArrayList(u32) = .empty;
1457 defer entries_list.deinit(gpa);
1458
1459 while (true) {
1460 const entries = entry_buffer[0..try reader.read(io, &entry_buffer)];
1461 for (try entries_list.addManyAsSlice(gpa, entries.len), entries) |*off, entry| {
1462 off.* = contents.items.len;
1463 // As an optimization, make the reservation also count the duplication
1464 // of the contents buffer that will be required after sorting.
1465 try contents.ensureUnusedCapacity(gpa, (contents.items.len + entry.name.len + 2 - contents_start) * 2);
1466 contents.appendAssumeCapacity(@backingInt(Manifest.File.Kind.fromStat(entry.kind)));
1467 contents.appendSliceAssumeCapacity(entry.name);
1468 contents.appendAssumeCapacity(0);
1469 }
1470 }
1471
1472 const Sort = struct {
1473 contents: []const u8,
1474 pub fn lessThan(this: @This(), lhs: u32, rhs: u32) bool {
1475 return mem.lessThanZ(u8, this.contents[lhs + 1 ..], this.contents[rhs + 1 ..]); // +1 for kind byte
1476 }
1477 };
1478 mem.sortUnstable(u32, entries_list.items, @as(Sort, .{ .contents = contents.items }), Sort.lessThan);
1479
1480 // Duplicate the contents such that we may refer to it while creating a
1481 // sorted copy in the original position (at contents_start). We will then
1482 // offset all the entries_list offsets by contents len when reading from the unsorted copy.
1483 const contents_len = contents.items.len - contents_start;
1484 @memcpy(contents.addManyAsSliceAssumeCapacity(contents_len), contents.items[contents_start..][0..contents_len]);
1485
1486 var new_offset: usize = contents_start;
1487 for (entries_list.items) |wrong_offset| {
1488 const offset = wrong_offset + contents_len;
1489 // Includes the kind prefix which we also want to copy.
1490 const entry: [*:0]const u8 = @ptrCast(contents.items[offset..]);
1491 new_offset += mem.copySentinel(u8, 0, contents.items[new_offset..], entry);
1492 }
1493 assert(new_offset == contents_start + contents_len);
1494 contents.shrinkRetainingCapacity(contents_start + contents_len);
1495
1496 var hasher = hasher_init;
1497 hasher.update(contents.items[contents_start..][0..contents_len]);
1498 hasher.final(bin_digest);
1499 }
1500};
1501
1502/// Create/Write a file, close it, then grab its stat.mtime timestamp.
12831503fn testGetCurrentFileTimestamp(io: Io, dir: Io.Dir) !Io.Timestamp {
12841504 const test_out_file = "test-filetimestamp.tmp";
12851505
......@@ -1312,7 +1532,7 @@ test "cache file and then recall it" {
13121532 // Wait for file timestamps to tick
13131533 const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir);
13141534 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time.nanoseconds) {
1315 try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
1535 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
13161536 }
13171537
13181538 var digest1: HexDigest = undefined;
......@@ -1382,7 +1602,7 @@ test "check that changing a file makes cache fail" {
13821602 // Wait for file timestamps to tick
13831603 const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir);
13841604 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time.nanoseconds) {
1385 try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
1605 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
13861606 }
13871607
13881608 var digest1: HexDigest = undefined;
......@@ -1508,7 +1728,7 @@ test "Manifest with files added after initial hash work" {
15081728 // Wait for file timestamps to tick
15091729 const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir);
15101730 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time.nanoseconds) {
1511 try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
1731 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
15121732 }
15131733
15141734 var digest1: HexDigest = undefined;
......@@ -1560,7 +1780,7 @@ test "Manifest with files added after initial hash work" {
15601780 // Wait for file timestamps to tick
15611781 const initial_time2 = try testGetCurrentFileTimestamp(io, tmp.dir);
15621782 while ((try testGetCurrentFileTimestamp(io, tmp.dir)).nanoseconds == initial_time2.nanoseconds) {
1563 try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
1783 try Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io);
15641784 }
15651785
15661786 {