| ... | ... | @@ -49,7 +49,6 @@ pub fn obtain(cache: *Cache) Manifest { |
| 49 | 49 | .hash = cache.hash, |
| 50 | 50 | .manifest_file = null, |
| 51 | 51 | .manifest_dirty = false, |
| 52 | | .hex_digest = undefined, |
| 53 | 52 | }; |
| 54 | 53 | } |
| 55 | 54 | |
| ... | ... | @@ -251,7 +250,7 @@ pub const HashHelper = struct { |
| 251 | 250 | |
| 252 | 251 | pub fn binToHex(bin_digest: BinDigest) HexDigest { |
| 253 | 252 | var out_digest: HexDigest = undefined; |
| 254 | | var w: std.Io.Writer = .fixed(&out_digest); |
| 253 | var w: Io.Writer = .fixed(&out_digest); |
| 255 | 254 | w.printHex(&bin_digest, .lower) catch unreachable; |
| 256 | 255 | return out_digest; |
| 257 | 256 | } |
| ... | ... | @@ -278,7 +277,6 @@ pub const Manifest = struct { |
| 278 | 277 | cache: *Cache, |
| 279 | 278 | /// Current state for incremental hashing. |
| 280 | 279 | hash: HashHelper, |
| 281 | | hex_digest: HexDigest, |
| 282 | 280 | /// When this is null, `Manifest` is in "pre-check" phase. Otherwise it is in "post-check" phase. |
| 283 | 281 | manifest_file: ?Io.File, |
| 284 | 282 | manifest_dirty: bool, |
| ... | ... | @@ -309,9 +307,34 @@ pub const Manifest = struct { |
| 309 | 307 | /// All contents from all `input_files` whose contents were requested, |
| 310 | 308 | /// concatenated. Total byte size will be less than `max_input_content_len` |
| 311 | 309 | /// otherwise an error is returned. |
| 310 | /// |
| 311 | /// Data is invalidated when `addFilePost` is called. |
| 312 | 312 | all_input_content: std.ArrayList(u8) = .empty, |
| 313 | 313 | max_input_content_len: usize = std.math.maxInt(u32), |
| 314 | 314 | |
| 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 | |
| 315 | 338 | pub const Files = std.array_hash_map.Custom(File.Offset, void, File.HashContext, false); |
| 316 | 339 | |
| 317 | 340 | /// Source files whose prefix and relative path are included when computing |
| ... | ... | @@ -328,7 +351,7 @@ pub const Manifest = struct { |
| 328 | 351 | have_stat: bool, |
| 329 | 352 | /// Determines whether `File.digest` is populated. |
| 330 | 353 | have_digest: bool, |
| 331 | | contents: enum (usize) { |
| 354 | contents: enum(usize) { |
| 332 | 355 | requested = std.math.maxInt(u32) - 1, |
| 333 | 356 | not_requested = std.math.maxInt(u32), |
| 334 | 357 | /// Byte offset index into `Manifest.all_input_content`. |
| ... | ... | @@ -356,12 +379,29 @@ pub const Manifest = struct { |
| 356 | 379 | /// Terminated by zero byte, then followed by padding until 8-byte aligned. |
| 357 | 380 | path_start: [0]u8, |
| 358 | 381 | |
| 359 | | pub const Flags = packed struct (u8) { |
| 382 | pub const Flags = packed struct(u8) { |
| 360 | 383 | is_directory: bool, |
| 361 | 384 | metadata_only: bool, |
| 362 | 385 | prefix: u6, |
| 363 | 386 | }; |
| 364 | 387 | |
| 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 | |
| 365 | 405 | /// Byte index within `Manifest.contents` where the entry starts. |
| 366 | 406 | pub const Offset = enum(u32) { |
| 367 | 407 | _, |
| ... | ... | @@ -392,7 +432,6 @@ pub const Manifest = struct { |
| 392 | 432 | } |
| 393 | 433 | }; |
| 394 | 434 | |
| 395 | | |
| 396 | 435 | pub fn path(file: *const File) [:0]const u8 { |
| 397 | 436 | return pathFallible(file) catch unreachable; |
| 398 | 437 | } |
| ... | ... | @@ -408,7 +447,7 @@ pub const Manifest = struct { |
| 408 | 447 | const path_len = mem.findScalar(u8, path_ptr, 0).?; |
| 409 | 448 | comptime assert(@offsetOf(File, "path_start") - @offsetOf(File, "flags") == 1); |
| 410 | 449 | // 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]; |
| 412 | 451 | hasher.update(hash_string); |
| 413 | 452 | } |
| 414 | 453 | |
| ... | ... | @@ -424,9 +463,20 @@ pub const Manifest = struct { |
| 424 | 463 | } |
| 425 | 464 | } |
| 426 | 465 | |
| 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 | } |
| 427 | 478 | }; |
| 428 | 479 | |
| 429 | | |
| 430 | 480 | pub const Diagnostic = union(enum) { |
| 431 | 481 | none, |
| 432 | 482 | manifest_create: Io.File.OpenError, |
| ... | ... | @@ -438,7 +488,7 @@ pub const Manifest = struct { |
| 438 | 488 | file_hash: FileOp, |
| 439 | 489 | |
| 440 | 490 | pub const FileOp = struct { |
| 441 | | file_index: usize, |
| 491 | file_offset: File.Offset, |
| 442 | 492 | err: anyerror, |
| 443 | 493 | }; |
| 444 | 494 | }; |
| ... | ... | @@ -450,16 +500,24 @@ pub const Manifest = struct { |
| 450 | 500 | }; |
| 451 | 501 | |
| 452 | 502 | pub const AddInputFileOptions = struct { |
| 503 | /// If `is_directory` is true, this handle must be opened with |
| 504 | /// iteration capability. |
| 453 | 505 | handle: ?Io.File = null, |
| 454 | 506 | stat: ?Stat = null, |
| 455 | 507 | request_handle: bool = false, |
| 508 | /// Can request file or directory contents depending on `is_directory`. |
| 456 | 509 | 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. |
| 457 | 514 | is_directory: bool = false, |
| 515 | /// Content hashing skipped; any difference in metadata implies cache |
| 516 | /// miss. |
| 458 | 517 | metadata_only: bool = false, |
| 459 | | |
| 460 | 518 | }; |
| 461 | 519 | |
| 462 | | pub const AddInputFileError = error { |
| 520 | pub const AddInputFileError = error{ |
| 463 | 521 | /// The same file path has been added to the cache manifest both as a |
| 464 | 522 | /// directory and as a normal file, making the intended caching |
| 465 | 523 | /// behavior ambiguous. |
| ... | ... | @@ -544,16 +602,6 @@ pub const Manifest = struct { |
| 544 | 602 | _ = try addInputFile(m, opt_path orelse return, options); |
| 545 | 603 | } |
| 546 | 604 | |
| 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 | | |
| 557 | 605 | /// Check the cache to see if the input exists in it. |
| 558 | 606 | /// A hex encoding of its hash is available by calling `final`. |
| 559 | 607 | /// |
| ... | ... | @@ -566,13 +614,13 @@ pub const Manifest = struct { |
| 566 | 614 | /// The lock on the manifest file is released when `deinit` is called. As another |
| 567 | 615 | /// option, one may call `toOwnedLock` to obtain a smaller object which can represent |
| 568 | 616 | /// 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 { |
| 570 | 618 | const node = parent_progress_node.start("Reusing Cache Artifacts", 0); |
| 571 | 619 | defer node.end(); |
| 572 | 620 | return checkProgressless(man); |
| 573 | 621 | } |
| 574 | 622 | |
| 575 | | pub fn checkProgressless(man: *Manifest) CheckError!CheckStatus { |
| 623 | pub fn checkProgressless(man: *Manifest) Check.Error!Check.Status { |
| 576 | 624 | assert(man.manifest_file == null); |
| 577 | 625 | |
| 578 | 626 | for (man.files.keys()[0..man.input_files.items.len]) |file_off| { |
| ... | ... | @@ -583,9 +631,8 @@ pub const Manifest = struct { |
| 583 | 631 | |
| 584 | 632 | var bin_digest: BinDigest = undefined; |
| 585 | 633 | 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; |
| 589 | 636 | const io = man.cache.io; |
| 590 | 637 | |
| 591 | 638 | // We'll try to open the cache with an exclusive lock, but if that would block |
| ... | ... | @@ -724,7 +771,7 @@ pub const Manifest = struct { |
| 724 | 771 | |
| 725 | 772 | /// Assumes that `self.hash.hasher` has been updated only with the original digest and that |
| 726 | 773 | /// `self.files` contains only the original input files. |
| 727 | | fn checkLocked(m: *Manifest) CheckError!CheckStatus { |
| 774 | fn checkLocked(m: *Manifest) Check.Error!Check.Status { |
| 728 | 775 | const gpa = m.cache.gpa; |
| 729 | 776 | const io = m.cache.io; |
| 730 | 777 | |
| ... | ... | @@ -750,11 +797,12 @@ pub const Manifest = struct { |
| 750 | 797 | |
| 751 | 798 | // This group we would like to cancel as soon as a cache miss is discovered. |
| 752 | 799 | const PostResult = union(enum) { |
| 753 | | checkFile: CheckFileResult, |
| 800 | checkFile: Check.Status, |
| 754 | 801 | }; |
| 755 | 802 | var post_select_buffer: [10]PostResult = undefined; |
| 756 | 803 | var post_select: Io.Select(PostResult) = .init(&post_select_buffer); |
| 757 | 804 | var post_select_remaining: usize = 0; |
| 805 | var c: Check = .{}; |
| 758 | 806 | defer post_select.cancel(io); |
| 759 | 807 | |
| 760 | 808 | while (off + 1 < m.contents.len) { |
| ... | ... | @@ -767,11 +815,11 @@ pub const Manifest = struct { |
| 767 | 815 | if (file_index < m.input_files.items.len) { |
| 768 | 816 | if (m.files.keys()[file_index] != file_off) return error.InvalidFormat; |
| 769 | 817 | |
| 770 | | input_group.async(io, checkFile, .{m.cache, file, path}); |
| 818 | input_group.async(io, checkInputFile, .{ m, &c, file_off, path }); |
| 771 | 819 | } else { |
| 772 | 820 | try m.files.put(gpa, file_off); |
| 773 | 821 | |
| 774 | | post_select.async(.checkFile, checkFile, .{m.cache, file, path}); |
| 822 | post_select.async(.checkFile, checkFile, .{ m, &c, file_off, path }); |
| 775 | 823 | post_select_remaining += 1; |
| 776 | 824 | } |
| 777 | 825 | |
| ... | ... | @@ -794,6 +842,18 @@ pub const Manifest = struct { |
| 794 | 842 | while (post_select_remaining > 0) { |
| 795 | 843 | const n = try post_select.awaitMany(&post_await_buffer, 1); |
| 796 | 844 | 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 | |
| 797 | 857 | for (post_await_buffer[0..n]) |u| switch (u) { |
| 798 | 858 | .checkFile => |result| switch (result) { |
| 799 | 859 | .hit => continue, |
| ... | ... | @@ -811,6 +871,7 @@ pub const Manifest = struct { |
| 811 | 871 | } |
| 812 | 872 | |
| 813 | 873 | try input_group.await(io); |
| 874 | if (c.status == .miss) return .miss; |
| 814 | 875 | |
| 815 | 876 | for (m.files.keys()) |file_off| { |
| 816 | 877 | m.hash.hasher.update(&file_off.get(m).digest); |
| ... | ... | @@ -819,54 +880,121 @@ pub const Manifest = struct { |
| 819 | 880 | return .hit; |
| 820 | 881 | } |
| 821 | 882 | |
| 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 | } |
| 827 | 893 | |
| 828 | 894 | /// 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; |
| 830 | 904 | 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 | } |
| 832 | 924 | |
| 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, |
| 835 | 965 | 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, |
| 838 | 968 | .err = e, |
| 839 | | } }}, |
| 969 | } }), |
| 840 | 970 | }; |
| 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; |
| 864 | 992 | } |
| 865 | 993 | |
| 866 | 994 | return .hit; |
| 867 | 995 | } |
| 868 | 996 | |
| 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`. |
| 870 | 998 | /// The hasher contains the original input digest, and all original input file digests (i.e. |
| 871 | 999 | /// not including post files). |
| 872 | 1000 | /// |
| ... | ... | @@ -931,16 +1059,18 @@ pub const Manifest = struct { |
| 931 | 1059 | dir: ?Io.Dir, |
| 932 | 1060 | } = .{ .file = null }, |
| 933 | 1061 | 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`. |
| 934 | 1064 | contents: ?[]const u8 = null, |
| 935 | 1065 | metadata_only: bool = false, |
| 936 | 1066 | }; |
| 937 | 1067 | |
| 938 | | pub const AddFilePostError = error { |
| 1068 | pub const AddFilePostError = error{ |
| 939 | 1069 | /// The same file path has been added to the cache manifest both as a |
| 940 | 1070 | /// directory and as a normal file, making the intended caching |
| 941 | 1071 | /// behavior ambiguous. |
| 942 | 1072 | IsDirectoryAmbiguous, |
| 943 | | } || Allocator.Error; |
| 1073 | } || Io.Cancelable || Allocator.Error; |
| 944 | 1074 | |
| 945 | 1075 | /// Add a file as a dependency of process being cached, after cache miss |
| 946 | 1076 | /// occurs. |
| ... | ... | @@ -1006,7 +1136,10 @@ pub const Manifest = struct { |
| 1006 | 1136 | try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only); |
| 1007 | 1137 | } else { |
| 1008 | 1138 | 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 | }); |
| 1010 | 1143 | defer handle.close(io); |
| 1011 | 1144 | try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only); |
| 1012 | 1145 | }, |
| ... | ... | @@ -1022,7 +1155,14 @@ pub const Manifest = struct { |
| 1022 | 1155 | } |
| 1023 | 1156 | } |
| 1024 | 1157 | |
| 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 { |
| 1026 | 1166 | const io = m.cache.io; |
| 1027 | 1167 | |
| 1028 | 1168 | if (need_stat) { |
| ... | ... | @@ -1039,14 +1179,32 @@ pub const Manifest = struct { |
| 1039 | 1179 | } |
| 1040 | 1180 | } |
| 1041 | 1181 | |
| 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 | } |
| 1050 | 1208 | } |
| 1051 | 1209 | |
| 1052 | 1210 | pub fn addDepFile(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { |
| ... | ... | @@ -1062,7 +1220,7 @@ pub const Manifest = struct { |
| 1062 | 1220 | fn addDepFileMaybePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { |
| 1063 | 1221 | const gpa = self.cache.gpa; |
| 1064 | 1222 | 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); |
| 1066 | 1224 | defer gpa.free(dep_file_contents); |
| 1067 | 1225 | |
| 1068 | 1226 | var error_buf: std.ArrayList(u8) = .empty; |
| ... | ... | @@ -1124,7 +1282,6 @@ pub const Manifest = struct { |
| 1124 | 1282 | const io = m.cache.io; |
| 1125 | 1283 | const manifest_file = m.manifest_file.?; |
| 1126 | 1284 | if (m.manifest_dirty) { |
| 1127 | | |
| 1128 | 1285 | m.contents.appendAssumeCapacity(0); |
| 1129 | 1286 | defer _ = m.contents.pop().?; |
| 1130 | 1287 | |
| ... | ... | @@ -1264,22 +1421,85 @@ pub const Manifest = struct { |
| 1264 | 1421 | other_file.prefix = prefix_map[other_file.prefix]; |
| 1265 | 1422 | } |
| 1266 | 1423 | } |
| 1267 | | }; |
| 1268 | 1424 | |
| 1269 | | fn 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); |
| 1278 | 1436 | } |
| 1279 | | hasher.final(bin_digest); |
| 1280 | | } |
| 1281 | 1437 | |
| 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. |
| 1283 | 1503 | fn testGetCurrentFileTimestamp(io: Io, dir: Io.Dir) !Io.Timestamp { |
| 1284 | 1504 | const test_out_file = "test-filetimestamp.tmp"; |
| 1285 | 1505 | |
| ... | ... | @@ -1312,7 +1532,7 @@ test "cache file and then recall it" { |
| 1312 | 1532 | // Wait for file timestamps to tick |
| 1313 | 1533 | const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir); |
| 1314 | 1534 | 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); |
| 1316 | 1536 | } |
| 1317 | 1537 | |
| 1318 | 1538 | var digest1: HexDigest = undefined; |
| ... | ... | @@ -1382,7 +1602,7 @@ test "check that changing a file makes cache fail" { |
| 1382 | 1602 | // Wait for file timestamps to tick |
| 1383 | 1603 | const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir); |
| 1384 | 1604 | 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); |
| 1386 | 1606 | } |
| 1387 | 1607 | |
| 1388 | 1608 | var digest1: HexDigest = undefined; |
| ... | ... | @@ -1508,7 +1728,7 @@ test "Manifest with files added after initial hash work" { |
| 1508 | 1728 | // Wait for file timestamps to tick |
| 1509 | 1729 | const initial_time = try testGetCurrentFileTimestamp(io, tmp.dir); |
| 1510 | 1730 | 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); |
| 1512 | 1732 | } |
| 1513 | 1733 | |
| 1514 | 1734 | var digest1: HexDigest = undefined; |
| ... | ... | @@ -1560,7 +1780,7 @@ test "Manifest with files added after initial hash work" { |
| 1560 | 1780 | // Wait for file timestamps to tick |
| 1561 | 1781 | const initial_time2 = try testGetCurrentFileTimestamp(io, tmp.dir); |
| 1562 | 1782 | 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); |
| 1564 | 1784 | } |
| 1565 | 1785 | |
| 1566 | 1786 | { |