zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

blob acaa5edc (24757B) - Raw


      1 const std = @import("std.zig");
      2 const Blake3 = std.crypto.Blake3;
      3 const fs = std.fs;
      4 const base64 = std.base64;
      5 const ArrayList = std.ArrayList;
      6 const assert = std.debug.assert;
      7 const testing = std.testing;
      8 const mem = std.mem;
      9 const fmt = std.fmt;
     10 const Allocator = std.mem.Allocator;
     11 
     12 const base64_encoder = fs.base64_encoder;
     13 const base64_decoder = fs.base64_decoder;
     14 /// This is 70 more bits than UUIDs. For an analysis of probability of collisions, see:
     15 /// https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions
     16 const BIN_DIGEST_LEN = 24;
     17 const BASE64_DIGEST_LEN = base64.Base64Encoder.calcSize(BIN_DIGEST_LEN);
     18 
     19 const MANIFEST_FILE_SIZE_MAX = 50 * 1024 * 1024;
     20 
     21 pub const File = struct {
     22     path: ?[]const u8,
     23     max_file_size: ?usize,
     24     stat: fs.File.Stat,
     25     bin_digest: [BIN_DIGEST_LEN]u8,
     26     contents: ?[]const u8,
     27 
     28     pub fn deinit(self: *File, allocator: *Allocator) void {
     29         if (self.path) |owned_slice| {
     30             allocator.free(owned_slice);
     31             self.path = null;
     32         }
     33         if (self.contents) |contents| {
     34             allocator.free(contents);
     35             self.contents = null;
     36         }
     37         self.* = undefined;
     38     }
     39 };
     40 
     41 pub const CacheHash = struct {
     42     allocator: *Allocator,
     43     blake3: Blake3,
     44     manifest_dir: fs.Dir,
     45     manifest_file: ?fs.File,
     46     manifest_dirty: bool,
     47     files: ArrayList(File),
     48     b64_digest: [BASE64_DIGEST_LEN]u8,
     49 
     50     /// Be sure to call release after successful initialization.
     51     pub fn init(allocator: *Allocator, dir: fs.Dir, manifest_dir_path: []const u8) !CacheHash {
     52         return CacheHash{
     53             .allocator = allocator,
     54             .blake3 = Blake3.init(),
     55             .manifest_dir = try dir.makeOpenPath(manifest_dir_path, .{}),
     56             .manifest_file = null,
     57             .manifest_dirty = false,
     58             .files = ArrayList(File).init(allocator),
     59             .b64_digest = undefined,
     60         };
     61     }
     62 
     63     /// Record a slice of bytes as an dependency of the process being cached
     64     pub fn addSlice(self: *CacheHash, val: []const u8) void {
     65         assert(self.manifest_file == null);
     66 
     67         self.blake3.update(val);
     68         self.blake3.update(&[_]u8{0});
     69     }
     70 
     71     /// Convert the input value into bytes and record it as a dependency of the
     72     /// process being cached
     73     pub fn add(self: *CacheHash, val: anytype) void {
     74         assert(self.manifest_file == null);
     75 
     76         const valPtr = switch (@typeInfo(@TypeOf(val))) {
     77             .Int => &val,
     78             .Pointer => val,
     79             else => &val,
     80         };
     81 
     82         self.addSlice(mem.asBytes(valPtr));
     83     }
     84 
     85     /// Add a file as a dependency of process being cached. When `CacheHash.hit` is
     86     /// called, the file's contents will be checked to ensure that it matches
     87     /// the contents from previous times.
     88     ///
     89     /// Max file size will be used to determine the amount of space to the file contents
     90     /// are allowed to take up in memory. If max_file_size is null, then the contents
     91     /// will not be loaded into memory.
     92     ///
     93     /// Returns the index of the entry in the `CacheHash.files` ArrayList. You can use it
     94     /// to access the contents of the file after calling `CacheHash.hit()` like so:
     95     ///
     96     /// ```
     97     /// var file_contents = cache_hash.files.items[file_index].contents.?;
     98     /// ```
     99     pub fn addFile(self: *CacheHash, file_path: []const u8, max_file_size: ?usize) !usize {
    100         assert(self.manifest_file == null);
    101 
    102         try self.files.ensureCapacity(self.files.items.len + 1);
    103         const resolved_path = try fs.path.resolve(self.allocator, &[_][]const u8{file_path});
    104 
    105         const idx = self.files.items.len;
    106         self.files.addOneAssumeCapacity().* = .{
    107             .path = resolved_path,
    108             .contents = null,
    109             .max_file_size = max_file_size,
    110             .stat = undefined,
    111             .bin_digest = undefined,
    112         };
    113 
    114         self.addSlice(resolved_path);
    115 
    116         return idx;
    117     }
    118 
    119     /// Check the cache to see if the input exists in it. If it exists, a base64 encoding
    120     /// of it's hash will be returned; otherwise, null will be returned.
    121     ///
    122     /// This function will also acquire an exclusive lock to the manifest file. This means
    123     /// that a process holding a CacheHash will block any other process attempting to
    124     /// acquire the lock.
    125     ///
    126     /// The lock on the manifest file is released when `CacheHash.release` is called.
    127     pub fn hit(self: *CacheHash) !?[BASE64_DIGEST_LEN]u8 {
    128         assert(self.manifest_file == null);
    129 
    130         var bin_digest: [BIN_DIGEST_LEN]u8 = undefined;
    131         self.blake3.final(&bin_digest);
    132 
    133         base64_encoder.encode(self.b64_digest[0..], &bin_digest);
    134 
    135         self.blake3 = Blake3.init();
    136         self.blake3.update(&bin_digest);
    137 
    138         const manifest_file_path = try fmt.allocPrint(self.allocator, "{}.txt", .{self.b64_digest});
    139         defer self.allocator.free(manifest_file_path);
    140 
    141         if (self.files.items.len != 0) {
    142             self.manifest_file = try self.manifest_dir.createFile(manifest_file_path, .{
    143                 .read = true,
    144                 .truncate = false,
    145                 .lock = .Exclusive,
    146             });
    147         } else {
    148             // If there are no file inputs, we check if the manifest file exists instead of
    149             // comparing the hashes on the files used for the cached item
    150             self.manifest_file = self.manifest_dir.openFile(manifest_file_path, .{
    151                 .read = true,
    152                 .write = true,
    153                 .lock = .Exclusive,
    154             }) catch |err| switch (err) {
    155                 error.FileNotFound => {
    156                     self.manifest_dirty = true;
    157                     self.manifest_file = try self.manifest_dir.createFile(manifest_file_path, .{
    158                         .read = true,
    159                         .truncate = false,
    160                         .lock = .Exclusive,
    161                     });
    162                     return null;
    163                 },
    164                 else => |e| return e,
    165             };
    166         }
    167 
    168         const file_contents = try self.manifest_file.?.inStream().readAllAlloc(self.allocator, MANIFEST_FILE_SIZE_MAX);
    169         defer self.allocator.free(file_contents);
    170 
    171         const input_file_count = self.files.items.len;
    172         var any_file_changed = false;
    173         var line_iter = mem.tokenize(file_contents, "\n");
    174         var idx: usize = 0;
    175         while (line_iter.next()) |line| {
    176             defer idx += 1;
    177 
    178             const cache_hash_file = if (idx < input_file_count) &self.files.items[idx] else blk: {
    179                 const new = try self.files.addOne();
    180                 new.* = .{
    181                     .path = null,
    182                     .contents = null,
    183                     .max_file_size = null,
    184                     .stat = undefined,
    185                     .bin_digest = undefined,
    186                 };
    187                 break :blk new;
    188             };
    189 
    190             var iter = mem.tokenize(line, " ");
    191             const inode = iter.next() orelse return error.InvalidFormat;
    192             const mtime_nsec_str = iter.next() orelse return error.InvalidFormat;
    193             const digest_str = iter.next() orelse return error.InvalidFormat;
    194             const file_path = iter.rest();
    195 
    196             cache_hash_file.stat.inode = fmt.parseInt(fs.File.INode, mtime_nsec_str, 10) catch return error.InvalidFormat;
    197             cache_hash_file.stat.mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat;
    198             base64_decoder.decode(&cache_hash_file.bin_digest, digest_str) catch return error.InvalidFormat;
    199 
    200             if (file_path.len == 0) {
    201                 return error.InvalidFormat;
    202             }
    203             if (cache_hash_file.path) |p| {
    204                 if (!mem.eql(u8, file_path, p)) {
    205                     return error.InvalidFormat;
    206                 }
    207             }
    208 
    209             if (cache_hash_file.path == null) {
    210                 cache_hash_file.path = try self.allocator.dupe(u8, file_path);
    211             }
    212 
    213             const this_file = fs.cwd().openFile(cache_hash_file.path.?, .{ .read = true }) catch {
    214                 return error.CacheUnavailable;
    215             };
    216             defer this_file.close();
    217 
    218             const actual_stat = try this_file.stat();
    219             const mtime_match = actual_stat.mtime == cache_hash_file.stat.mtime;
    220             const inode_match = actual_stat.inode == cache_hash_file.stat.inode;
    221 
    222             if (!mtime_match or !inode_match) {
    223                 self.manifest_dirty = true;
    224 
    225                 cache_hash_file.stat = actual_stat;
    226 
    227                 if (isProblematicTimestamp(cache_hash_file.stat.mtime)) {
    228                     cache_hash_file.stat.mtime = 0;
    229                     cache_hash_file.stat.inode = 0;
    230                 }
    231 
    232                 var actual_digest: [BIN_DIGEST_LEN]u8 = undefined;
    233                 try hashFile(this_file, &actual_digest);
    234 
    235                 if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) {
    236                     cache_hash_file.bin_digest = actual_digest;
    237                     // keep going until we have the input file digests
    238                     any_file_changed = true;
    239                 }
    240             }
    241 
    242             if (!any_file_changed) {
    243                 self.blake3.update(&cache_hash_file.bin_digest);
    244             }
    245         }
    246 
    247         if (any_file_changed) {
    248             // cache miss
    249             // keep the manifest file open
    250             // reset the hash
    251             self.blake3 = Blake3.init();
    252             self.blake3.update(&bin_digest);
    253 
    254             // Remove files not in the initial hash
    255             for (self.files.items[input_file_count..]) |*file| {
    256                 file.deinit(self.allocator);
    257             }
    258             self.files.shrink(input_file_count);
    259 
    260             for (self.files.items) |file| {
    261                 self.blake3.update(&file.bin_digest);
    262             }
    263             return null;
    264         }
    265 
    266         if (idx < input_file_count) {
    267             self.manifest_dirty = true;
    268             while (idx < input_file_count) : (idx += 1) {
    269                 const ch_file = &self.files.items[idx];
    270                 try self.populateFileHash(ch_file);
    271             }
    272             return null;
    273         }
    274 
    275         return self.final();
    276     }
    277 
    278     fn populateFileHash(self: *CacheHash, ch_file: *File) !void {
    279         const file = try fs.cwd().openFile(ch_file.path.?, .{});
    280         defer file.close();
    281 
    282         ch_file.stat = try file.stat();
    283 
    284         if (isProblematicTimestamp(ch_file.stat.mtime)) {
    285             ch_file.stat.mtime = 0;
    286             ch_file.stat.inode = 0;
    287         }
    288 
    289         if (ch_file.max_file_size) |max_file_size| {
    290             if (ch_file.stat.size > max_file_size) {
    291                 return error.FileTooBig;
    292             }
    293 
    294             const contents = try self.allocator.alloc(u8, @intCast(usize, ch_file.stat.size));
    295             errdefer self.allocator.free(contents);
    296 
    297             // Hash while reading from disk, to keep the contents in the cpu cache while
    298             // doing hashing.
    299             var blake3 = Blake3.init();
    300             var off: usize = 0;
    301             while (true) {
    302                 // give me everything you've got, captain
    303                 const bytes_read = try file.read(contents[off..]);
    304                 if (bytes_read == 0) break;
    305                 blake3.update(contents[off..][0..bytes_read]);
    306                 off += bytes_read;
    307             }
    308             blake3.final(&ch_file.bin_digest);
    309 
    310             ch_file.contents = contents;
    311         } else {
    312             try hashFile(file, &ch_file.bin_digest);
    313         }
    314 
    315         self.blake3.update(&ch_file.bin_digest);
    316     }
    317 
    318     /// Add a file as a dependency of process being cached, after the initial hash has been
    319     /// calculated. This is useful for processes that don't know the all the files that
    320     /// are depended on ahead of time. For example, a source file that can import other files
    321     /// will need to be recompiled if the imported file is changed.
    322     pub fn addFilePostFetch(self: *CacheHash, file_path: []const u8, max_file_size: usize) ![]u8 {
    323         assert(self.manifest_file != null);
    324 
    325         const resolved_path = try fs.path.resolve(self.allocator, &[_][]const u8{file_path});
    326         errdefer self.allocator.free(resolved_path);
    327 
    328         const new_ch_file = try self.files.addOne();
    329         new_ch_file.* = .{
    330             .path = resolved_path,
    331             .max_file_size = max_file_size,
    332             .stat = undefined,
    333             .bin_digest = undefined,
    334             .contents = null,
    335         };
    336         errdefer self.files.shrink(self.files.items.len - 1);
    337 
    338         try self.populateFileHash(new_ch_file);
    339 
    340         return new_ch_file.contents.?;
    341     }
    342 
    343     /// Add a file as a dependency of process being cached, after the initial hash has been
    344     /// calculated. This is useful for processes that don't know the all the files that
    345     /// are depended on ahead of time. For example, a source file that can import other files
    346     /// will need to be recompiled if the imported file is changed.
    347     pub fn addFilePost(self: *CacheHash, file_path: []const u8) !void {
    348         assert(self.manifest_file != null);
    349 
    350         const resolved_path = try fs.path.resolve(self.allocator, &[_][]const u8{file_path});
    351         errdefer self.allocator.free(resolved_path);
    352 
    353         const new_ch_file = try self.files.addOne();
    354         new_ch_file.* = .{
    355             .path = resolved_path,
    356             .max_file_size = null,
    357             .stat = undefined,
    358             .bin_digest = undefined,
    359             .contents = null,
    360         };
    361         errdefer self.files.shrink(self.files.items.len - 1);
    362 
    363         try self.populateFileHash(new_ch_file);
    364     }
    365 
    366     /// Returns a base64 encoded hash of the inputs.
    367     pub fn final(self: *CacheHash) [BASE64_DIGEST_LEN]u8 {
    368         assert(self.manifest_file != null);
    369 
    370         // We don't close the manifest file yet, because we want to
    371         // keep it locked until the API user is done using it.
    372         // We also don't write out the manifest yet, because until
    373         // cache_release is called we still might be working on creating
    374         // the artifacts to cache.
    375 
    376         var bin_digest: [BIN_DIGEST_LEN]u8 = undefined;
    377         self.blake3.final(&bin_digest);
    378 
    379         var out_digest: [BASE64_DIGEST_LEN]u8 = undefined;
    380         base64_encoder.encode(&out_digest, &bin_digest);
    381 
    382         return out_digest;
    383     }
    384 
    385     pub fn writeManifest(self: *CacheHash) !void {
    386         assert(self.manifest_file != null);
    387 
    388         var encoded_digest: [BASE64_DIGEST_LEN]u8 = undefined;
    389         var contents = ArrayList(u8).init(self.allocator);
    390         var outStream = contents.outStream();
    391         defer contents.deinit();
    392 
    393         for (self.files.items) |file| {
    394             base64_encoder.encode(encoded_digest[0..], &file.bin_digest);
    395             try outStream.print("{} {} {} {}\n", .{ file.stat.inode, file.stat.mtime, encoded_digest[0..], file.path });
    396         }
    397 
    398         try self.manifest_file.?.pwriteAll(contents.items, 0);
    399         self.manifest_dirty = false;
    400     }
    401 
    402     /// Releases the manifest file and frees any memory the CacheHash was using.
    403     /// `CacheHash.hit` must be called first.
    404     ///
    405     /// Will also attempt to write to the manifest file if the manifest is dirty.
    406     /// Writing to the manifest file can fail, but this function ignores those errors.
    407     /// To detect failures from writing the manifest, one may explicitly call
    408     /// `writeManifest` before `release`.
    409     pub fn release(self: *CacheHash) void {
    410         if (self.manifest_file) |file| {
    411             if (self.manifest_dirty) {
    412                 // To handle these errors, API users should call
    413                 // writeManifest before release().
    414                 self.writeManifest() catch {};
    415             }
    416 
    417             file.close();
    418         }
    419 
    420         for (self.files.items) |*file| {
    421             file.deinit(self.allocator);
    422         }
    423         self.files.deinit();
    424         self.manifest_dir.close();
    425     }
    426 };
    427 
    428 fn hashFile(file: fs.File, bin_digest: []u8) !void {
    429     var blake3 = Blake3.init();
    430     var buf: [1024]u8 = undefined;
    431 
    432     while (true) {
    433         const bytes_read = try file.read(&buf);
    434         if (bytes_read == 0) break;
    435         blake3.update(buf[0..bytes_read]);
    436     }
    437 
    438     blake3.final(bin_digest);
    439 }
    440 
    441 /// If the wall clock time, rounded to the same precision as the
    442 /// mtime, is equal to the mtime, then we cannot rely on this mtime
    443 /// yet. We will instead save an mtime value that indicates the hash
    444 /// must be unconditionally computed.
    445 /// This function recognizes the precision of mtime by looking at trailing
    446 /// zero bits of the seconds and nanoseconds.
    447 fn isProblematicTimestamp(fs_clock: i128) bool {
    448     const wall_clock = std.time.nanoTimestamp();
    449 
    450     // We have to break the nanoseconds into seconds and remainder nanoseconds
    451     // to detect precision of seconds, because looking at the zero bits in base
    452     // 2 would not detect precision of the seconds value.
    453     const fs_sec = @intCast(i64, @divFloor(fs_clock, std.time.ns_per_s));
    454     const fs_nsec = @intCast(i64, @mod(fs_clock, std.time.ns_per_s));
    455     var wall_sec = @intCast(i64, @divFloor(wall_clock, std.time.ns_per_s));
    456     var wall_nsec = @intCast(i64, @mod(wall_clock, std.time.ns_per_s));
    457 
    458     // First make all the least significant zero bits in the fs_clock, also zero bits in the wall clock.
    459     if (fs_nsec == 0) {
    460         wall_nsec = 0;
    461         if (fs_sec == 0) {
    462             wall_sec = 0;
    463         } else {
    464             wall_sec &= @as(i64, -1) << @intCast(u6, @ctz(i64, fs_sec));
    465         }
    466     } else {
    467         wall_nsec &= @as(i64, -1) << @intCast(u6, @ctz(i64, fs_nsec));
    468     }
    469     return wall_nsec == fs_nsec and wall_sec == fs_sec;
    470 }
    471 
    472 test "cache file and then recall it" {
    473     if (std.Target.current.os.tag == .wasi) {
    474         // https://github.com/ziglang/zig/issues/5437
    475         return error.SkipZigTest;
    476     }
    477     const cwd = fs.cwd();
    478 
    479     const temp_file = "test.txt";
    480     const temp_manifest_dir = "temp_manifest_dir";
    481 
    482     try cwd.writeFile(temp_file, "Hello, world!\n");
    483 
    484     while (isProblematicTimestamp(std.time.nanoTimestamp())) {
    485         std.time.sleep(1);
    486     }
    487 
    488     var digest1: [BASE64_DIGEST_LEN]u8 = undefined;
    489     var digest2: [BASE64_DIGEST_LEN]u8 = undefined;
    490 
    491     {
    492         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    493         defer ch.release();
    494 
    495         ch.add(true);
    496         ch.add(@as(u16, 1234));
    497         ch.add("1234");
    498         _ = try ch.addFile(temp_file, null);
    499 
    500         // There should be nothing in the cache
    501         testing.expectEqual(@as(?[32]u8, null), try ch.hit());
    502 
    503         digest1 = ch.final();
    504     }
    505     {
    506         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    507         defer ch.release();
    508 
    509         ch.add(true);
    510         ch.add(@as(u16, 1234));
    511         ch.add("1234");
    512         _ = try ch.addFile(temp_file, null);
    513 
    514         // Cache hit! We just "built" the same file
    515         digest2 = (try ch.hit()).?;
    516     }
    517 
    518     testing.expectEqual(digest1, digest2);
    519 
    520     try cwd.deleteTree(temp_manifest_dir);
    521     try cwd.deleteFile(temp_file);
    522 }
    523 
    524 test "give problematic timestamp" {
    525     var fs_clock = std.time.nanoTimestamp();
    526     // to make it problematic, we make it only accurate to the second
    527     fs_clock = @divTrunc(fs_clock, std.time.ns_per_s);
    528     fs_clock *= std.time.ns_per_s;
    529     testing.expect(isProblematicTimestamp(fs_clock));
    530 }
    531 
    532 test "give nonproblematic timestamp" {
    533     testing.expect(!isProblematicTimestamp(std.time.nanoTimestamp() - std.time.ns_per_s));
    534 }
    535 
    536 test "check that changing a file makes cache fail" {
    537     if (std.Target.current.os.tag == .wasi) {
    538         // https://github.com/ziglang/zig/issues/5437
    539         return error.SkipZigTest;
    540     }
    541     const cwd = fs.cwd();
    542 
    543     const temp_file = "cache_hash_change_file_test.txt";
    544     const temp_manifest_dir = "cache_hash_change_file_manifest_dir";
    545     const original_temp_file_contents = "Hello, world!\n";
    546     const updated_temp_file_contents = "Hello, world; but updated!\n";
    547 
    548     try cwd.writeFile(temp_file, original_temp_file_contents);
    549 
    550     while (isProblematicTimestamp(std.time.nanoTimestamp())) {
    551         std.time.sleep(1);
    552     }
    553 
    554     var digest1: [BASE64_DIGEST_LEN]u8 = undefined;
    555     var digest2: [BASE64_DIGEST_LEN]u8 = undefined;
    556 
    557     {
    558         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    559         defer ch.release();
    560 
    561         ch.add("1234");
    562         const temp_file_idx = try ch.addFile(temp_file, 100);
    563 
    564         // There should be nothing in the cache
    565         testing.expectEqual(@as(?[32]u8, null), try ch.hit());
    566 
    567         testing.expect(mem.eql(u8, original_temp_file_contents, ch.files.items[temp_file_idx].contents.?));
    568 
    569         digest1 = ch.final();
    570     }
    571 
    572     try cwd.writeFile(temp_file, updated_temp_file_contents);
    573 
    574     while (isProblematicTimestamp(std.time.nanoTimestamp())) {
    575         std.time.sleep(1);
    576     }
    577 
    578     {
    579         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    580         defer ch.release();
    581 
    582         ch.add("1234");
    583         const temp_file_idx = try ch.addFile(temp_file, 100);
    584 
    585         // A file that we depend on has been updated, so the cache should not contain an entry for it
    586         testing.expectEqual(@as(?[32]u8, null), try ch.hit());
    587 
    588         // The cache system does not keep the contents of re-hashed input files.
    589         testing.expect(ch.files.items[temp_file_idx].contents == null);
    590 
    591         digest2 = ch.final();
    592     }
    593 
    594     testing.expect(!mem.eql(u8, digest1[0..], digest2[0..]));
    595 
    596     try cwd.deleteTree(temp_manifest_dir);
    597     try cwd.deleteFile(temp_file);
    598 }
    599 
    600 test "no file inputs" {
    601     if (std.Target.current.os.tag == .wasi) {
    602         // https://github.com/ziglang/zig/issues/5437
    603         return error.SkipZigTest;
    604     }
    605     const cwd = fs.cwd();
    606     const temp_manifest_dir = "no_file_inputs_manifest_dir";
    607     defer cwd.deleteTree(temp_manifest_dir) catch unreachable;
    608 
    609     var digest1: [BASE64_DIGEST_LEN]u8 = undefined;
    610     var digest2: [BASE64_DIGEST_LEN]u8 = undefined;
    611 
    612     {
    613         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    614         defer ch.release();
    615 
    616         ch.add("1234");
    617 
    618         // There should be nothing in the cache
    619         testing.expectEqual(@as(?[32]u8, null), try ch.hit());
    620 
    621         digest1 = ch.final();
    622     }
    623     {
    624         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    625         defer ch.release();
    626 
    627         ch.add("1234");
    628 
    629         digest2 = (try ch.hit()).?;
    630     }
    631 
    632     testing.expectEqual(digest1, digest2);
    633 }
    634 
    635 test "CacheHashes with files added after initial hash work" {
    636     if (std.Target.current.os.tag == .wasi) {
    637         // https://github.com/ziglang/zig/issues/5437
    638         return error.SkipZigTest;
    639     }
    640     const cwd = fs.cwd();
    641 
    642     const temp_file1 = "cache_hash_post_file_test1.txt";
    643     const temp_file2 = "cache_hash_post_file_test2.txt";
    644     const temp_manifest_dir = "cache_hash_post_file_manifest_dir";
    645 
    646     try cwd.writeFile(temp_file1, "Hello, world!\n");
    647     try cwd.writeFile(temp_file2, "Hello world the second!\n");
    648 
    649     while (isProblematicTimestamp(std.time.nanoTimestamp())) {
    650         std.time.sleep(1);
    651     }
    652 
    653     var digest1: [BASE64_DIGEST_LEN]u8 = undefined;
    654     var digest2: [BASE64_DIGEST_LEN]u8 = undefined;
    655     var digest3: [BASE64_DIGEST_LEN]u8 = undefined;
    656 
    657     {
    658         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    659         defer ch.release();
    660 
    661         ch.add("1234");
    662         _ = try ch.addFile(temp_file1, null);
    663 
    664         // There should be nothing in the cache
    665         testing.expectEqual(@as(?[32]u8, null), try ch.hit());
    666 
    667         _ = try ch.addFilePost(temp_file2);
    668 
    669         digest1 = ch.final();
    670     }
    671     {
    672         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    673         defer ch.release();
    674 
    675         ch.add("1234");
    676         _ = try ch.addFile(temp_file1, null);
    677 
    678         digest2 = (try ch.hit()).?;
    679     }
    680     testing.expect(mem.eql(u8, &digest1, &digest2));
    681 
    682     // Modify the file added after initial hash
    683     try cwd.writeFile(temp_file2, "Hello world the second, updated\n");
    684 
    685     while (isProblematicTimestamp(std.time.nanoTimestamp())) {
    686         std.time.sleep(1);
    687     }
    688 
    689     {
    690         var ch = try CacheHash.init(testing.allocator, cwd, temp_manifest_dir);
    691         defer ch.release();
    692 
    693         ch.add("1234");
    694         _ = try ch.addFile(temp_file1, null);
    695 
    696         // A file that we depend on has been updated, so the cache should not contain an entry for it
    697         testing.expectEqual(@as(?[32]u8, null), try ch.hit());
    698 
    699         _ = try ch.addFilePost(temp_file2);
    700 
    701         digest3 = ch.final();
    702     }
    703 
    704     testing.expect(!mem.eql(u8, &digest1, &digest3));
    705 
    706     try cwd.deleteTree(temp_manifest_dir);
    707     try cwd.deleteFile(temp_file1);
    708     try cwd.deleteFile(temp_file2);
    709 }