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 {...@@ -48,7 +48,7 @@ pub fn obtain(cache: *Cache) Manifest {
48 .cache = cache,48 .cache = cache,
49 .hash = cache.hash,49 .hash = cache.hash,
50 .manifest_file = null,50 .manifest_file = null,
51 .dirty = false,51 .state = .input,
52 };52 };
53}53}
5454
...@@ -274,8 +274,7 @@ pub const Manifest = struct {...@@ -274,8 +274,7 @@ pub const Manifest = struct {
274 hash: HashHelper,274 hash: HashHelper,
275 /// When this is null, `Manifest` is in "pre-check" phase. Otherwise it is in "post-check" phase.275 /// When this is null, `Manifest` is in "pre-check" phase. Otherwise it is in "post-check" phase.
276 manifest_file: ?Io.File,276 manifest_file: ?Io.File,
277 /// `finalize` has not been called yet.277 state: State,
278 dirty: bool,
279 /// Set this flag to true before calling `check` in order to indicate that upon a cache hit, the code278 /// Set this flag to true before calling `check` in order to indicate that upon a cache hit, the code
280 /// using the cache will not modify the files within the cache directory. This allows multiple processes279 /// using the cache will not modify the files within the cache directory. This allows multiple processes
281 /// to utilize the same cache directory at the same time.280 /// to utilize the same cache directory at the same time.
...@@ -309,6 +308,27 @@ pub const Manifest = struct {...@@ -309,6 +308,27 @@ pub const Manifest = struct {
309308
310 pub const Files = std.array_hash_map.Custom(File.Offset, void, File.HashContext, false);309 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
312 /// Source files and directories whose prefix and relative path are332 /// Source files and directories whose prefix and relative path are
313 /// included when computing the cache manifest digest. It's the information333 /// included when computing the cache manifest digest. It's the information
314 /// needed to lazily hash the input files only when a cache miss occurs.334 /// needed to lazily hash the input files only when a cache miss occurs.
...@@ -656,6 +676,7 @@ pub const Manifest = struct {...@@ -656,6 +676,7 @@ pub const Manifest = struct {
656 /// See also:676 /// See also:
657 /// * `addDiscoveredPath`677 /// * `addDiscoveredPath`
658 pub fn addInputPath(m: *Manifest, path: Path, options: AddInputPathOptions) Allocator.Error!InputPath.Index {678 pub fn addInputPath(m: *Manifest, path: Path, options: AddInputPathOptions) Allocator.Error!InputPath.Index {
679 assert(m.state == .input);
659 const cache = m.cache;680 const cache = m.cache;
660 const gpa = cache.gpa;681 const gpa = cache.gpa;
661 const is_directory = options.handle.isDirectory();682 const is_directory = options.handle.isDirectory();
...@@ -733,6 +754,7 @@ pub const Manifest = struct {...@@ -733,6 +754,7 @@ pub const Manifest = struct {
733 }754 }
734755
735 pub fn addInputPathOptional(m: *Manifest, opt_path: ?Path, options: AddInputPathOptions) Allocator.Error!void {756 pub fn addInputPathOptional(m: *Manifest, opt_path: ?Path, options: AddInputPathOptions) Allocator.Error!void {
757 assert(m.state == .input);
736 m.hash.add(opt_path != null);758 m.hash.add(opt_path != null);
737 _ = try addInputPath(m, opt_path orelse return, options);759 _ = try addInputPath(m, opt_path orelse return, options);
738 }760 }
...@@ -772,6 +794,7 @@ pub const Manifest = struct {...@@ -772,6 +794,7 @@ pub const Manifest = struct {
772 }794 }
773795
774 pub fn checkProgressless(man: *Manifest) CheckError!CheckResult {796 pub fn checkProgressless(man: *Manifest) CheckError!CheckResult {
797 assert(man.state == .input);
775 assert(man.manifest_file == null);798 assert(man.manifest_file == null);
776799
777 // This is *not* hashing the contents of the input files. It is the800 // This is *not* hashing the contents of the input files. It is the
...@@ -872,6 +895,8 @@ pub const Manifest = struct {...@@ -872,6 +895,8 @@ pub const Manifest = struct {
872 // of all non-discovered files -- that is, the ones we've already been told about. The rest will be discovered895 // of all non-discovered files -- that is, the ones we've already been told about. The rest will be discovered
873 // through calls to `addDiscoveredPath` etc, which will update the hasher. After all files are added, the user896 // through calls to `addDiscoveredPath` etc, which will update the hasher. After all files are added, the user
874 // can use `final`, and will at some point `finalize` the file list to disk.897 // 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
876 hit: {901 hit: {
877 const miss_result = miss: {902 const miss_result = miss: {
...@@ -891,9 +916,7 @@ pub const Manifest = struct {...@@ -891,9 +916,7 @@ pub const Manifest = struct {
891916
892 // Cache miss, but `checkLocked` guarantees that all input files have their digests computed, even on a917 // Cache miss, but `checkLocked` guarantees that all input files have their digests computed, even on a
893 // cache miss, which is needed because they will be used in the manifest digest.918 // cache miss, which is needed because they will be used in the manifest digest.
894 man.dirty = true;919 man.state = .miss;
895 man.shrinkFilesToInput();
896 hashFiles(man, &input_digest);
897 return miss_result;920 return miss_result;
898 }921 }
899922
...@@ -904,19 +927,22 @@ pub const Manifest = struct {...@@ -904,19 +927,22 @@ pub const Manifest = struct {
904 };927 };
905 }928 }
906929
907 hashFiles(man, &input_digest);930 man.state = .hit;
908 return .hit;931 return .hit;
909 }932 }
910933
911 fn hashFiles(m: *Manifest, input_digest: *BinDigest) void {934 fn transitionToMissDiscovered(m: *Manifest) void {
912 const contents = m.contents.items;935 switch (m.state) {
913936 .input => unreachable,
914 m.hash.hasher = hasher_init;937 .hit => unreachable,
915 m.hash.hasher.update(input_digest);938 .hit_digested => unreachable,
916939 .miss => {
917 for (m.files.keys()) |off| {940 m.shrinkFilesToInput();
918 const file = off.get(contents);941 m.state = .miss_discovered;
919 m.hash.hasher.update(&file.digest);942 },
943 .miss_discovered => {},
944 .miss_digested => unreachable,
945 .miss_finalized => unreachable,
920 }946 }
921 }947 }
922948
...@@ -1611,6 +1637,7 @@ pub const Manifest = struct {...@@ -1611,6 +1637,7 @@ pub const Manifest = struct {
1611 /// * `addInputPath`1637 /// * `addInputPath`
1612 pub fn addDiscoveredPath(m: *Manifest, options: AddDiscoveredPathOptions) AddDiscoveredPathError!void {1638 pub fn addDiscoveredPath(m: *Manifest, options: AddDiscoveredPathOptions) AddDiscoveredPathError!void {
1613 assert(m.manifest_file != null);1639 assert(m.manifest_file != null);
1640 transitionToMissDiscovered(m);
1614 const cache = m.cache;1641 const cache = m.cache;
1615 const gpa = cache.gpa;1642 const gpa = cache.gpa;
1616 const io = cache.io;1643 const io = cache.io;
...@@ -1705,8 +1732,6 @@ pub const Manifest = struct {...@@ -1705,8 +1732,6 @@ pub const Manifest = struct {
1705 try populateFile(m, header, need_stat, handle, options.contents, metadata_only, options.diagnostic);1732 try populateFile(m, header, need_stat, handle, options.contents, metadata_only, options.diagnostic);
1706 },1733 },
1707 }1734 }
1708
1709 m.hash.hasher.update(&header.digest);
1710 }1735 }
17111736
1712 fn populateFile(1737 fn populateFile(
...@@ -1840,39 +1865,63 @@ pub const Manifest = struct {...@@ -1840,39 +1865,63 @@ pub const Manifest = struct {
1840 };1865 };
1841 }1866 }
18421867
1843 /// Returns a binary hash of the inputs, input file contents, and discovered file contents.1868 /// See also `hitDigestHex`.
1844 pub fn finalBin(self: *Manifest) BinDigest {1869 pub fn hitDigest(m: *Manifest) BinDigest {
1845 assert(self.manifest_file != null);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 }
1847 // We don't close the manifest file yet, because we want to keep it locked until the API user is done using it.1900 // We don't close the manifest file yet, because we want to keep it locked until the API user is done using it.
1848 // We also don't write out the manifest yet, because until `finalize` is called we still might be working on1901 // We also don't write out the manifest yet, because until `finalize` is called we still might be working on
1849 // creating the artifacts to cache.1902 // creating the artifacts to cache.
1850
1851 var bin_digest: BinDigest = undefined;1903 var bin_digest: BinDigest = undefined;
1852 self.hash.hasher.final(&bin_digest);1904 hasher.final(&bin_digest);
1853 return bin_digest;1905 return bin_digest;
1854 }1906 }
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
1862 /// If `want_shared_lock` is true, this function automatically downgrades the1908 /// If `want_shared_lock` is true, this function automatically downgrades the
1863 /// lock from exclusive to shared.1909 /// lock from exclusive to shared.
1864 pub fn finalize(m: *Manifest) !void {1910 pub fn finalize(m: *Manifest) !void {
1865 assert(m.have_exclusive_lock);
1866 const io = m.cache.io;1911 const io = m.cache.io;
1867 const manifest_file = m.manifest_file.?;1912 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 {
1869 m.contents.appendAssumeCapacity(0);1918 m.contents.appendAssumeCapacity(0);
1870 defer _ = m.contents.pop().?;1919 defer _ = m.contents.pop().?;
18711920
1872 try manifest_file.setLength(io, m.contents.items.len);1921 try manifest_file.setLength(io, m.contents.items.len);
1873 try manifest_file.writePositionalAll(io, m.contents.items, 0);1922 try manifest_file.writePositionalAll(io, m.contents.items, 0);
18741923
1875 m.dirty = false;1924 m.state = .miss_finalized;
1876 }1925 }
18771926
1878 if (m.want_shared_lock) {1927 if (m.want_shared_lock) {
...@@ -2197,39 +2246,39 @@ test "cache file and then recall it" {...@@ -2197,39 +2246,39 @@ test "cache file and then recall it" {
2197 defer cache.manifest_dir.close(io);2246 defer cache.manifest_dir.close(io);
21982247
2199 {2248 {
2200 var ch = cache.obtain();2249 var man = cache.obtain();
2201 defer ch.deinit();2250 defer man.deinit();
22022251
2203 ch.hash.add(true);2252 man.hash.add(true);
2204 ch.hash.add(@as(u16, 1234));2253 man.hash.add(@as(u16, 1234));
2205 ch.hash.addBytes("1234");2254 man.hash.addBytes("1234");
2206 _ = try ch.addInputPath(.{2255 _ = try man.addInputPath(.{
2207 .root_dir = tmp_directory,2256 .root_dir = tmp_directory,
2208 .sub_path = temp_file,2257 .sub_path = temp_file,
2209 }, .{});2258 }, .{});
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();2262 digest1 = man.missDigestHex();
2214 try ch.finalize();2263 try man.finalize();
2215 }2264 }
2216 {2265 {
2217 var ch = cache.obtain();2266 var man = cache.obtain();
2218 defer ch.deinit();2267 defer man.deinit();
22192268
2220 ch.hash.add(true);2269 man.hash.add(true);
2221 ch.hash.add(@as(u16, 1234));2270 man.hash.add(@as(u16, 1234));
2222 ch.hash.addBytes("1234");2271 man.hash.addBytes("1234");
2223 _ = try ch.addInputPath(.{2272 _ = try man.addInputPath(.{
2224 .root_dir = tmp_directory,2273 .root_dir = tmp_directory,
2225 .sub_path = temp_file,2274 .sub_path = temp_file,
2226 }, .{});2275 }, .{});
22272276
2228 // Cache hit! We just "built" the same file2277 // Cache hit! We just "built" the same file
2229 try testing.expectEqual(.hit, try ch.check(.none));2278 try testing.expectEqual(.hit, try man.check(.none));
2230 digest2 = ch.final();2279 digest2 = man.hitDigestHex();
22312280
2232 try testing.expectEqual(false, ch.have_exclusive_lock);2281 try testing.expectEqual(false, man.have_exclusive_lock);
2233 }2282 }
22342283
2235 try testing.expectEqual(digest1, digest2);2284 try testing.expectEqual(digest1, digest2);
...@@ -2291,7 +2340,7 @@ test "check that changing a file causes cache miss" {...@@ -2291,7 +2340,7 @@ test "check that changing a file causes cache miss" {
22912340
2292 try testing.expectEqualStrings(original_temp_file_contents, temp_file_idx.contents(&man));2341 try testing.expectEqualStrings(original_temp_file_contents, temp_file_idx.contents(&man));
22932342
2294 digest1 = man.final();2343 digest1 = man.missDigestHex();
22952344
2296 try man.finalize();2345 try man.finalize();
2297 }2346 }
...@@ -2316,7 +2365,7 @@ test "check that changing a file causes cache miss" {...@@ -2316,7 +2365,7 @@ test "check that changing a file causes cache miss" {
23162365
2317 try testing.expectEqualStrings(updated_temp_file_contents, temp_file_idx.contents(&man));2366 try testing.expectEqualStrings(updated_temp_file_contents, temp_file_idx.contents(&man));
23182367
2319 digest2 = man.final();2368 digest2 = man.missDigestHex();
23202369
2321 try man.finalize();2370 try man.finalize();
2322 }2371 }
...@@ -2356,7 +2405,7 @@ test "no file inputs" {...@@ -2356,7 +2405,7 @@ test "no file inputs" {
23562405
2357 try testing.expectEqual(.incomplete_manifest, try man.check(.none));2406 try testing.expectEqual(.incomplete_manifest, try man.check(.none));
23582407
2359 digest1 = man.final();2408 digest1 = man.missDigestHex();
23602409
2361 try man.finalize();2410 try man.finalize();
2362 }2411 }
...@@ -2367,14 +2416,14 @@ test "no file inputs" {...@@ -2367,14 +2416,14 @@ test "no file inputs" {
2367 man.hash.addBytes("1234");2416 man.hash.addBytes("1234");
23682417
2369 try testing.expectEqual(.hit, try man.check(.none));2418 try testing.expectEqual(.hit, try man.check(.none));
2370 digest2 = man.final();2419 digest2 = man.hitDigestHex();
2371 try testing.expectEqual(false, man.have_exclusive_lock);2420 try testing.expectEqual(false, man.have_exclusive_lock);
2372 }2421 }
23732422
2374 try testing.expectEqual(digest1, digest2);2423 try testing.expectEqual(digest1, digest2);
2375}2424}
23762425
2377test "Manifest with files added after initial hash work" {2426test "Manifest with files added after initial hash" {
2378 const io = testing.io;2427 const io = testing.io;
23792428
2380 var tmp = testing.tmpDir(.{});2429 var tmp = testing.tmpDir(.{});
...@@ -2433,7 +2482,7 @@ test "Manifest with files added after initial hash work" {...@@ -2433,7 +2482,7 @@ test "Manifest with files added after initial hash work" {
2433 .sub_path = temp_file2,2482 .sub_path = temp_file2,
2434 } } });2483 } } });
24352484
2436 digest1 = man.final();2485 digest1 = man.missDigestHex();
2437 try man.finalize();2486 try man.finalize();
2438 }2487 }
2439 {2488 {
...@@ -2447,7 +2496,7 @@ test "Manifest with files added after initial hash work" {...@@ -2447,7 +2496,7 @@ test "Manifest with files added after initial hash work" {
2447 }, .{});2496 }, .{});
24482497
2449 try testing.expect(.hit == try man.check(.none));2498 try testing.expect(.hit == try man.check(.none));
2450 digest2 = man.final();2499 digest2 = man.hitDigestHex();
24512500
2452 try testing.expectEqual(false, man.have_exclusive_lock);2501 try testing.expectEqual(false, man.have_exclusive_lock);
2453 }2502 }
...@@ -2484,7 +2533,7 @@ test "Manifest with files added after initial hash work" {...@@ -2484,7 +2533,7 @@ test "Manifest with files added after initial hash work" {
2484 .sub_path = temp_file2,2533 .sub_path = temp_file2,
2485 } } });2534 } } });
24862535
2487 digest3 = man.final();2536 digest3 = man.missDigestHex();
24882537
2489 try man.finalize();2538 try man.finalize();
2490 }2539 }