authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-09-13 19:26:12-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-09-16 13:12:47-07:00
logd1b961c9ebb875b8e8c6fc082a1f9d5e6f175daa
treed2f6ca651c72027834db0700e9991dc69c1b03bc
parente38052663ca729764c1de7d5f28794d4509d8b79

std.Build.Cache: introduce State enum


1 files changed, 107 insertions(+), 58 deletions(-)

lib/std/Build/Cache.zig+107-58
......@@ -48,7 +48,7 @@ pub fn obtain(cache: *Cache) Manifest {
4848 .cache = cache,
4949 .hash = cache.hash,
5050 .manifest_file = null,
51 .dirty = false,
51 .state = .input,
5252 };
5353}
5454
......@@ -274,8 +274,7 @@ pub const Manifest = struct {
274274 hash: HashHelper,
275275 /// When this is null, `Manifest` is in "pre-check" phase. Otherwise it is in "post-check" phase.
276276 manifest_file: ?Io.File,
277 /// `finalize` has not been called yet.
278 dirty: bool,
277 state: State,
279278 /// Set this flag to true before calling `check` in order to indicate that upon a cache hit, the code
280279 /// using the cache will not modify the files within the cache directory. This allows multiple processes
281280 /// to utilize the same cache directory at the same time.
......@@ -309,6 +308,27 @@ pub const Manifest = struct {
309308
310309 pub const Files = std.array_hash_map.Custom(File.Offset, void, File.HashContext, false);
311310
311 pub const State = enum {
312 /// Call `addInputPath`, `addInputPathOptional`, and methods of `Manifest.hash`. Once all input files and state
313 /// have been added, call `check`.
314 input,
315 /// `check` has already been called and a hit occurred. Call `hitDigest` to find cache artifact directory.
316 hit,
317 /// `hitDigest` has already been called. No further operations are allowed.
318 hit_digested,
319 /// `check` has already been called and a miss occurred. `missDigest` has not been called yet. Miss diagnostic
320 /// data may still be accessed. Call `addDiscoveredPath` to add discovered files and directories.
321 miss,
322 /// A miss occurred, `missDigest` has not been called yet. Call `addDiscoveredPath` to add more discovered files
323 /// and directories. Miss diagnostic data may no longer be accessed.
324 miss_discovered,
325 /// `missDigest` has been called, but `finalize` has not been called yet.
326 miss_digested,
327 /// The cache manifest has been written to disk such that the next check for the same inputs will yield a hit.
328 /// No further operations are allowed.
329 miss_finalized,
330 };
331
312332 /// Source files and directories whose prefix and relative path are
313333 /// included when computing the cache manifest digest. It's the information
314334 /// needed to lazily hash the input files only when a cache miss occurs.
......@@ -656,6 +676,7 @@ pub const Manifest = struct {
656676 /// See also:
657677 /// * `addDiscoveredPath`
658678 pub fn addInputPath(m: *Manifest, path: Path, options: AddInputPathOptions) Allocator.Error!InputPath.Index {
679 assert(m.state == .input);
659680 const cache = m.cache;
660681 const gpa = cache.gpa;
661682 const is_directory = options.handle.isDirectory();
......@@ -733,6 +754,7 @@ pub const Manifest = struct {
733754 }
734755
735756 pub fn addInputPathOptional(m: *Manifest, opt_path: ?Path, options: AddInputPathOptions) Allocator.Error!void {
757 assert(m.state == .input);
736758 m.hash.add(opt_path != null);
737759 _ = try addInputPath(m, opt_path orelse return, options);
738760 }
......@@ -772,6 +794,7 @@ pub const Manifest = struct {
772794 }
773795
774796 pub fn checkProgressless(man: *Manifest) CheckError!CheckResult {
797 assert(man.state == .input);
775798 assert(man.manifest_file == null);
776799
777800 // This is *not* hashing the contents of the input files. It is the
......@@ -872,6 +895,8 @@ pub const Manifest = struct {
872895 // of all non-discovered files -- that is, the ones we've already been told about. The rest will be discovered
873896 // through calls to `addDiscoveredPath` etc, which will update the hasher. After all files are added, the user
874897 // can use `final`, and will at some point `finalize` the file list to disk.
898 man.hash.hasher = hasher_init;
899 man.hash.hasher.update(&input_digest);
875900
876901 hit: {
877902 const miss_result = miss: {
......@@ -891,9 +916,7 @@ pub const Manifest = struct {
891916
892917 // Cache miss, but `checkLocked` guarantees that all input files have their digests computed, even on a
893918 // cache miss, which is needed because they will be used in the manifest digest.
894 man.dirty = true;
895 man.shrinkFilesToInput();
896 hashFiles(man, &input_digest);
919 man.state = .miss;
897920 return miss_result;
898921 }
899922
......@@ -904,19 +927,22 @@ pub const Manifest = struct {
904927 };
905928 }
906929
907 hashFiles(man, &input_digest);
930 man.state = .hit;
908931 return .hit;
909932 }
910933
911 fn hashFiles(m: *Manifest, input_digest: *BinDigest) void {
912 const contents = m.contents.items;
913
914 m.hash.hasher = hasher_init;
915 m.hash.hasher.update(input_digest);
916
917 for (m.files.keys()) |off| {
918 const file = off.get(contents);
919 m.hash.hasher.update(&file.digest);
934 fn transitionToMissDiscovered(m: *Manifest) void {
935 switch (m.state) {
936 .input => unreachable,
937 .hit => unreachable,
938 .hit_digested => unreachable,
939 .miss => {
940 m.shrinkFilesToInput();
941 m.state = .miss_discovered;
942 },
943 .miss_discovered => {},
944 .miss_digested => unreachable,
945 .miss_finalized => unreachable,
920946 }
921947 }
922948
......@@ -1611,6 +1637,7 @@ pub const Manifest = struct {
16111637 /// * `addInputPath`
16121638 pub fn addDiscoveredPath(m: *Manifest, options: AddDiscoveredPathOptions) AddDiscoveredPathError!void {
16131639 assert(m.manifest_file != null);
1640 transitionToMissDiscovered(m);
16141641 const cache = m.cache;
16151642 const gpa = cache.gpa;
16161643 const io = cache.io;
......@@ -1705,8 +1732,6 @@ pub const Manifest = struct {
17051732 try populateFile(m, header, need_stat, handle, options.contents, metadata_only, options.diagnostic);
17061733 },
17071734 }
1708
1709 m.hash.hasher.update(&header.digest);
17101735 }
17111736
17121737 fn populateFile(
......@@ -1840,39 +1865,63 @@ pub const Manifest = struct {
18401865 };
18411866 }
18421867
1843 /// Returns a binary hash of the inputs, input file contents, and discovered file contents.
1844 pub fn finalBin(self: *Manifest) BinDigest {
1845 assert(self.manifest_file != null);
1868 /// See also `hitDigestHex`.
1869 pub fn hitDigest(m: *Manifest) BinDigest {
1870 assert(m.manifest_file != null);
1871 assert(m.state == .hit);
1872 m.state = .hit_digested;
1873 return finalDigest(m);
1874 }
1875
1876 pub fn hitDigestHex(m: *Manifest) HexDigest {
1877 return binToHex(m.hitDigest());
1878 }
1879
1880 /// See also `missDigestHex`.
1881 pub fn missDigest(m: *Manifest) BinDigest {
1882 assert(m.manifest_file != null);
1883 transitionToMissDiscovered(m);
1884 m.state = .miss_digested;
1885 return finalDigest(m);
1886 }
18461887
1888 pub fn missDigestHex(m: *Manifest) HexDigest {
1889 return binToHex(m.missDigest());
1890 }
1891
1892 fn finalDigest(m: *Manifest) BinDigest {
1893 const contents = m.contents.items;
1894 const hasher = &m.hash.hasher;
1895
1896 for (m.files.keys()) |off| {
1897 const file = off.get(contents);
1898 hasher.update(&file.digest);
1899 }
18471900 // We don't close the manifest file yet, because we want to keep it locked until the API user is done using it.
18481901 // We also don't write out the manifest yet, because until `finalize` is called we still might be working on
18491902 // creating the artifacts to cache.
1850
18511903 var bin_digest: BinDigest = undefined;
1852 self.hash.hasher.final(&bin_digest);
1904 hasher.final(&bin_digest);
18531905 return bin_digest;
18541906 }
18551907
1856 /// Returns a hex encoded hash of the inputs, input file contents, and discovered file contents.
1857 pub fn final(self: *Manifest) HexDigest {
1858 const bin_digest = self.finalBin();
1859 return binToHex(bin_digest);
1860 }
1861
18621908 /// If `want_shared_lock` is true, this function automatically downgrades the
18631909 /// lock from exclusive to shared.
18641910 pub fn finalize(m: *Manifest) !void {
1865 assert(m.have_exclusive_lock);
18661911 const io = m.cache.io;
18671912 const manifest_file = m.manifest_file.?;
1868 if (m.dirty) {
1913
1914 assert(m.state == .miss_digested);
1915 assert(m.have_exclusive_lock);
1916
1917 {
18691918 m.contents.appendAssumeCapacity(0);
18701919 defer _ = m.contents.pop().?;
18711920
18721921 try manifest_file.setLength(io, m.contents.items.len);
18731922 try manifest_file.writePositionalAll(io, m.contents.items, 0);
18741923
1875 m.dirty = false;
1924 m.state = .miss_finalized;
18761925 }
18771926
18781927 if (m.want_shared_lock) {
......@@ -2197,39 +2246,39 @@ test "cache file and then recall it" {
21972246 defer cache.manifest_dir.close(io);
21982247
21992248 {
2200 var ch = cache.obtain();
2201 defer ch.deinit();
2249 var man = cache.obtain();
2250 defer man.deinit();
22022251
2203 ch.hash.add(true);
2204 ch.hash.add(@as(u16, 1234));
2205 ch.hash.addBytes("1234");
2206 _ = try ch.addInputPath(.{
2252 man.hash.add(true);
2253 man.hash.add(@as(u16, 1234));
2254 man.hash.addBytes("1234");
2255 _ = try man.addInputPath(.{
22072256 .root_dir = tmp_directory,
22082257 .sub_path = temp_file,
22092258 }, .{});
22102259
2211 try testing.expectEqual(.incomplete_manifest, try ch.check(.none));
2260 try testing.expectEqual(.incomplete_manifest, try man.check(.none));
22122261
2213 digest1 = ch.final();
2214 try ch.finalize();
2262 digest1 = man.missDigestHex();
2263 try man.finalize();
22152264 }
22162265 {
2217 var ch = cache.obtain();
2218 defer ch.deinit();
2266 var man = cache.obtain();
2267 defer man.deinit();
22192268
2220 ch.hash.add(true);
2221 ch.hash.add(@as(u16, 1234));
2222 ch.hash.addBytes("1234");
2223 _ = try ch.addInputPath(.{
2269 man.hash.add(true);
2270 man.hash.add(@as(u16, 1234));
2271 man.hash.addBytes("1234");
2272 _ = try man.addInputPath(.{
22242273 .root_dir = tmp_directory,
22252274 .sub_path = temp_file,
22262275 }, .{});
22272276
22282277 // Cache hit! We just "built" the same file
2229 try testing.expectEqual(.hit, try ch.check(.none));
2230 digest2 = ch.final();
2278 try testing.expectEqual(.hit, try man.check(.none));
2279 digest2 = man.hitDigestHex();
22312280
2232 try testing.expectEqual(false, ch.have_exclusive_lock);
2281 try testing.expectEqual(false, man.have_exclusive_lock);
22332282 }
22342283
22352284 try testing.expectEqual(digest1, digest2);
......@@ -2291,7 +2340,7 @@ test "check that changing a file causes cache miss" {
22912340
22922341 try testing.expectEqualStrings(original_temp_file_contents, temp_file_idx.contents(&man));
22932342
2294 digest1 = man.final();
2343 digest1 = man.missDigestHex();
22952344
22962345 try man.finalize();
22972346 }
......@@ -2316,7 +2365,7 @@ test "check that changing a file causes cache miss" {
23162365
23172366 try testing.expectEqualStrings(updated_temp_file_contents, temp_file_idx.contents(&man));
23182367
2319 digest2 = man.final();
2368 digest2 = man.missDigestHex();
23202369
23212370 try man.finalize();
23222371 }
......@@ -2356,7 +2405,7 @@ test "no file inputs" {
23562405
23572406 try testing.expectEqual(.incomplete_manifest, try man.check(.none));
23582407
2359 digest1 = man.final();
2408 digest1 = man.missDigestHex();
23602409
23612410 try man.finalize();
23622411 }
......@@ -2367,14 +2416,14 @@ test "no file inputs" {
23672416 man.hash.addBytes("1234");
23682417
23692418 try testing.expectEqual(.hit, try man.check(.none));
2370 digest2 = man.final();
2419 digest2 = man.hitDigestHex();
23712420 try testing.expectEqual(false, man.have_exclusive_lock);
23722421 }
23732422
23742423 try testing.expectEqual(digest1, digest2);
23752424}
23762425
2377test "Manifest with files added after initial hash work" {
2426test "Manifest with files added after initial hash" {
23782427 const io = testing.io;
23792428
23802429 var tmp = testing.tmpDir(.{});
......@@ -2433,7 +2482,7 @@ test "Manifest with files added after initial hash work" {
24332482 .sub_path = temp_file2,
24342483 } } });
24352484
2436 digest1 = man.final();
2485 digest1 = man.missDigestHex();
24372486 try man.finalize();
24382487 }
24392488 {
......@@ -2447,7 +2496,7 @@ test "Manifest with files added after initial hash work" {
24472496 }, .{});
24482497
24492498 try testing.expect(.hit == try man.check(.none));
2450 digest2 = man.final();
2499 digest2 = man.hitDigestHex();
24512500
24522501 try testing.expectEqual(false, man.have_exclusive_lock);
24532502 }
......@@ -2484,7 +2533,7 @@ test "Manifest with files added after initial hash work" {
24842533 .sub_path = temp_file2,
24852534 } } });
24862535
2487 digest3 = man.final();
2536 digest3 = man.missDigestHex();
24882537
24892538 try man.finalize();
24902539 }