| ... | ... | @@ -294,7 +294,7 @@ pub const Manifest = struct { |
| 294 | 294 | files: Files = .empty, |
| 295 | 295 | /// Indexes line up with `files`, but only up until `hit` is called. Uses |
| 296 | 296 | /// `Cache.gpa`. |
| 297 | | input_files: std.ArrayList(InputFile) = .empty, |
| 297 | input_paths: std.ArrayList(InputPath) = .empty, |
| 298 | 298 | diagnostic: Diagnostic = .none, |
| 299 | 299 | /// Keeps track of the last time we performed a file system write to observe |
| 300 | 300 | /// what time the file system thinks it is, according to its own granularity. |
| ... | ... | @@ -304,11 +304,11 @@ pub const Manifest = struct { |
| 304 | 304 | /// final terminating byte can be added without allocation. Uses |
| 305 | 305 | /// `Cache.gpa`. |
| 306 | 306 | contents: std.ArrayList(u8) = .empty, |
| 307 | | /// All contents from all `input_files` whose contents were requested, |
| 307 | /// All contents from all `input_paths` whose contents were requested, |
| 308 | 308 | /// concatenated. Total byte size will be less than `max_input_content_len` |
| 309 | 309 | /// otherwise an error is returned. |
| 310 | 310 | /// |
| 311 | | /// Data is invalidated when `addFilePost` is called. |
| 311 | /// Data is invalidated when `addPathPost` is called. |
| 312 | 312 | all_input_content: std.ArrayList(u8) = .empty, |
| 313 | 313 | max_input_content_len: usize = std.math.maxInt(u32), |
| 314 | 314 | |
| ... | ... | @@ -328,23 +328,24 @@ pub const Manifest = struct { |
| 328 | 328 | InvalidFormat, |
| 329 | 329 | } || Allocator.Error || Io.Cancelable; |
| 330 | 330 | |
| 331 | | fn fail(c: *Check, m: *Manifest, diagnostic: Diagnostic) void { |
| 332 | | if (!@atomicRmw(bool, &c.diagnostic_lock, .Xchg, true, .unordered)) { |
| 331 | fn fail(c: *Check, m: *Manifest, diagnostic: Diagnostic) error{CacheCheckFailed} { |
| 332 | if (!@atomicRmw(bool, &c.diagnostic_lock, .Xchg, true, .monotonic)) { |
| 333 | 333 | m.diagnostic = diagnostic; |
| 334 | 334 | } |
| 335 | return error.CacheCheckFailed; |
| 335 | 336 | } |
| 336 | 337 | }; |
| 337 | 338 | |
| 338 | 339 | pub const Files = std.array_hash_map.Custom(File.Offset, void, File.HashContext, false); |
| 339 | 340 | |
| 340 | | /// Source files whose prefix and relative path are included when computing |
| 341 | | /// the cache manifest digest. It's the information needed to lazily hash |
| 342 | | /// the input files only when a cache miss occurs. |
| 341 | /// Source files and directories whose prefix and relative path are |
| 342 | /// included when computing the cache manifest digest. It's the information |
| 343 | /// needed to lazily hash the input files only when a cache miss occurs. |
| 343 | 344 | /// |
| 344 | 345 | /// `File.prefix`, `File.path`, and `File.mode` will be always populated, |
| 345 | 346 | /// but the other fields of `File` will be populated depending on the |
| 346 | | /// fields of `InputFile`. |
| 347 | | pub const InputFile = struct { |
| 347 | /// fields of `InputPath`. |
| 348 | pub const InputPath = struct { |
| 348 | 349 | request_handle: bool, |
| 349 | 350 | have_handle: bool, |
| 350 | 351 | /// Determines whether `File.size`, `File.inode`, and `File.mtime` are populated. |
| ... | ... | @@ -360,7 +361,7 @@ pub const Manifest = struct { |
| 360 | 361 | /// `have_handle` determines whether this is populated. |
| 361 | 362 | handle: Io.File, |
| 362 | 363 | |
| 363 | | /// Index into `Manifest.input_files`. |
| 364 | /// Index into `Manifest.input_paths`. |
| 364 | 365 | pub const Index = enum(u32) { |
| 365 | 366 | _, |
| 366 | 367 | }; |
| ... | ... | @@ -406,13 +407,13 @@ pub const Manifest = struct { |
| 406 | 407 | pub const Offset = enum(u32) { |
| 407 | 408 | _, |
| 408 | 409 | |
| 409 | | pub fn get(offset: Offset, m: *const Manifest) *File { |
| 410 | | return @ptrCast(m.contents.items[@backingInt(offset)..][0..@sizeOf(File)]); |
| 410 | pub fn get(offset: Offset, contents: []u8) *File { |
| 411 | return @ptrCast(@alignCast(contents.items[@backingInt(offset)..][0..@sizeOf(File)])); |
| 411 | 412 | } |
| 412 | 413 | |
| 413 | | pub fn getFallible(offset: Offset, m: *const Manifest) error{EndOfStream}!*File { |
| 414 | | if (@backingInt(offset) + @sizeOf(File) >= m.contents.len) return error.EndOfStream; |
| 415 | | return get(offset, m); |
| 414 | pub fn getFallible(offset: Offset, contents: []u8) error{InvalidFormat}!*File { |
| 415 | if (@backingInt(offset) + @sizeOf(File) >= contents.items.len) return error.InvalidFormat; |
| 416 | return get(offset, contents); |
| 416 | 417 | } |
| 417 | 418 | }; |
| 418 | 419 | |
| ... | ... | @@ -432,25 +433,6 @@ pub const Manifest = struct { |
| 432 | 433 | } |
| 433 | 434 | }; |
| 434 | 435 | |
| 435 | | pub fn path(file: *const File) [:0]const u8 { |
| 436 | | return pathFallible(file) catch unreachable; |
| 437 | | } |
| 438 | | |
| 439 | | pub fn pathFallible(file: *const File) error{EndOfStream}![:0]const u8 { |
| 440 | | const ptr: [*]u8 = &file.path_start; |
| 441 | | const len = mem.findScalar(u8, ptr, 0) orelse return error.EndOfStream; |
| 442 | | return ptr[0..len :0]; |
| 443 | | } |
| 444 | | |
| 445 | | fn manifestDigestHash(file: *const File, hasher: *Hasher) void { |
| 446 | | const path_ptr: [*]u8 = &file.path_start; |
| 447 | | const path_len = mem.findScalar(u8, path_ptr, 0).?; |
| 448 | | comptime assert(@offsetOf(File, "path_start") - @offsetOf(File, "flags") == 1); |
| 449 | | // Includes flags and sentinel. |
| 450 | | const hash_string = (path_ptr - 1)[0 .. path_len + 2]; |
| 451 | | hasher.update(hash_string); |
| 452 | | } |
| 453 | | |
| 454 | 436 | fn setStat(file: *File, m: *Manifest, stat: Stat) Io.Cancelable!void { |
| 455 | 437 | file.size = stat.size; |
| 456 | 438 | file.inode = stat.inode; |
| ... | ... | @@ -490,6 +472,16 @@ pub const Manifest = struct { |
| 490 | 472 | pub const FileOp = struct { |
| 491 | 473 | file_offset: File.Offset, |
| 492 | 474 | err: anyerror, |
| 475 | |
| 476 | /// Returned `Path` references `Manifest.contents`. |
| 477 | pub fn path(fo: FileOp, manifest: *const Manifest) Path { |
| 478 | const contents = manifest.contents.items; |
| 479 | const prefix = fo.file_offset.get(contents).flags.prefix; |
| 480 | return .{ |
| 481 | .root_dir = manifest.cache.prefixes()[prefix], |
| 482 | .sub_path = filePath(contents, fo.file_offset), |
| 483 | }; |
| 484 | } |
| 493 | 485 | }; |
| 494 | 486 | }; |
| 495 | 487 | |
| ... | ... | @@ -499,42 +491,49 @@ pub const Manifest = struct { |
| 499 | 491 | mtime: Io.Timestamp, |
| 500 | 492 | }; |
| 501 | 493 | |
| 502 | | pub const AddInputFileOptions = struct { |
| 503 | | /// If `is_directory` is true, this handle must be opened with |
| 504 | | /// iteration capability. |
| 505 | | handle: ?Io.File = null, |
| 494 | pub const PathHandle = union(enum) { |
| 495 | file: ?Io.File, |
| 496 | /// If provided, this handle must be opened with iteration capability. |
| 497 | dir: ?Io.Dir, |
| 498 | }; |
| 499 | |
| 500 | pub const AddInputPathOptions = struct { |
| 501 | handle: PathHandle = .{ .file = null }, |
| 506 | 502 | stat: ?Stat = null, |
| 507 | 503 | request_handle: bool = false, |
| 508 | | /// Can request file or directory contents depending on `is_directory`. |
| 504 | /// Can request file or directory contents depending on `handle`. |
| 509 | 505 | 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. |
| 514 | | is_directory: bool = false, |
| 515 | 506 | /// Content hashing skipped; any difference in metadata implies cache |
| 516 | 507 | /// miss. |
| 517 | 508 | metadata_only: bool = false, |
| 518 | 509 | }; |
| 519 | 510 | |
| 520 | | pub const AddInputFileError = error{ |
| 511 | pub const AddInputPathError = error{ |
| 521 | 512 | /// The same file path has been added to the cache manifest both as a |
| 522 | 513 | /// directory and as a normal file, making the intended caching |
| 523 | 514 | /// behavior ambiguous. |
| 524 | 515 | IsDirectoryAmbiguous, |
| 525 | 516 | } || Allocator.Error; |
| 526 | 517 | |
| 527 | | /// Add a file as a dependency of process being cached. When `hit` is |
| 528 | | /// called, the file's contents will be checked to ensure that it matches |
| 529 | | /// the contents from previous times. |
| 518 | /// Add a file or directory path as a dependency of process being cached. |
| 519 | /// When `hit` is called, the contents will be checked to ensure |
| 520 | /// that it matches the contents from previous times. |
| 530 | 521 | /// |
| 531 | 522 | /// The contents of the input file may be requested and subsequently |
| 532 | | /// obtained via methods of the returned `InputFile.Index` after calling |
| 523 | /// obtained via methods of the returned `InputPath.Index` after calling |
| 533 | 524 | /// `hit`. |
| 534 | | pub fn addInputFile(m: *Manifest, path: Path, options: AddInputFileOptions) Allocator.Error!InputFile.Index { |
| 525 | /// |
| 526 | /// Contents of a directory are considered to be the sorted list of file |
| 527 | /// names of direct entries, separated by null byte. Each file name is |
| 528 | /// prefixed by `Io.File.Kind` byte, +1 so that the zero tag is not aliased |
| 529 | /// by the entry separator. |
| 530 | /// |
| 531 | /// See also: |
| 532 | /// * `addPathPost` |
| 533 | pub fn addInputPath(m: *Manifest, path: Path, options: AddInputPathOptions) AddInputPathError!InputPath.Index { |
| 535 | 534 | const gpa = m.cache.gpa; |
| 536 | 535 | try m.files.ensureUnusedCapacity(gpa, 1); |
| 537 | | try m.input_files.ensureUnusedCapacity(gpa, 1); |
| 536 | try m.input_paths.ensureUnusedCapacity(gpa, 1); |
| 538 | 537 | |
| 539 | 538 | const prev_contents_len = m.contents.items.len; |
| 540 | 539 | const header: *File = @ptrCast(try m.contents.addManyAsSlice(gpa, @sizeOf(File))); |
| ... | ... | @@ -558,7 +557,7 @@ pub const Manifest = struct { |
| 558 | 557 | }); |
| 559 | 558 | if (gop.found_existing) { |
| 560 | 559 | m.contents.shrinkRetainingCapacity(prev_contents_len); |
| 561 | | const existing_input_file = &m.input_files.items[gop.index]; |
| 560 | const existing_input_file = &m.input_paths.items[gop.index]; |
| 562 | 561 | if (options.handle) |handle| { |
| 563 | 562 | existing_input_file.handle = handle; |
| 564 | 563 | existing_input_file.have_handle = true; |
| ... | ... | @@ -579,7 +578,7 @@ pub const Manifest = struct { |
| 579 | 578 | if (!options.metadata_only) |
| 580 | 579 | existing_header.flags.metadata_only = false; |
| 581 | 580 | } else { |
| 582 | | m.input_files.appendAssumeCapacity(.{ |
| 581 | m.input_paths.appendAssumeCapacity(.{ |
| 583 | 582 | .request_handle = options.request_handle, |
| 584 | 583 | .have_handle = options.handle != null, |
| 585 | 584 | .handle = if (options.handle) |handle| handle else undefined, |
| ... | ... | @@ -587,7 +586,7 @@ pub const Manifest = struct { |
| 587 | 586 | .have_digest = false, |
| 588 | 587 | .have_stat = options.stat != null, |
| 589 | 588 | }); |
| 590 | | assert(m.input_files.items.len - 1 == gop.index); |
| 589 | assert(m.input_paths.items.len - 1 == gop.index); |
| 591 | 590 | if (options.stat) |stat| { |
| 592 | 591 | header.size = stat.size; |
| 593 | 592 | header.inode = stat.inode; |
| ... | ... | @@ -597,9 +596,9 @@ pub const Manifest = struct { |
| 597 | 596 | return @fromBackingInt(gop.index); |
| 598 | 597 | } |
| 599 | 598 | |
| 600 | | pub fn addInputFileOptional(m: *Manifest, opt_path: ?Path, options: AddInputFileOptions) Allocator.Error!void { |
| 599 | pub fn addInputFileOptional(m: *Manifest, opt_path: ?Path, options: AddInputPathOptions) Allocator.Error!void { |
| 601 | 600 | m.hash.add(opt_path != null); |
| 602 | | _ = try addInputFile(m, opt_path orelse return, options); |
| 601 | _ = try addInputPath(m, opt_path orelse return, options); |
| 603 | 602 | } |
| 604 | 603 | |
| 605 | 604 | /// Check the cache to see if the input exists in it. |
| ... | ... | @@ -623,8 +622,8 @@ pub const Manifest = struct { |
| 623 | 622 | pub fn checkProgressless(man: *Manifest) Check.Error!Check.Status { |
| 624 | 623 | assert(man.manifest_file == null); |
| 625 | 624 | |
| 626 | | for (man.files.keys()[0..man.input_files.items.len]) |file_off| { |
| 627 | | file_off.get(man).manifestDigestHash(&man.hash.hasher); |
| 625 | for (man.files.keys()[0..man.input_paths.items.len]) |file_off| { |
| 626 | man.digestHash(file_off, &man.hash.hasher); |
| 628 | 627 | } |
| 629 | 628 | |
| 630 | 629 | man.diagnostic = .none; |
| ... | ... | @@ -709,16 +708,16 @@ pub const Manifest = struct { |
| 709 | 708 | |
| 710 | 709 | // We're going to construct a second hash. Its input will begin with the digest we've |
| 711 | 710 | // already computed (`bin_digest`), and then it'll have the digests of each input file, |
| 712 | | // including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post" |
| 711 | // including "post" files (see `addPathPost`). If this is a hit, we learn the set of "post" |
| 713 | 712 | // files from the manifest on disk. If this is a miss, we'll learn those from future calls |
| 714 | | // to `addFilePost` etc. As such, the state of `man.hash.hasher` after this function |
| 713 | // to `addPathPost` etc. As such, the state of `man.hash.hasher` after this function |
| 715 | 714 | // depends on whether this is a hit or a miss. |
| 716 | 715 | // |
| 717 | 716 | // If we return `CacheStatus.hit`, then `man.hash.hasher` must already include |
| 718 | 717 | // the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache |
| 719 | 718 | // miss, `man.hash.hasher` will include the digests of all non-"post" files -- that is, |
| 720 | 719 | // the ones we've already been told about. The rest will be discovered through calls to |
| 721 | | // `addFilePost` etc, which will update the hasher. After all files are added, the user can |
| 720 | // `addPathPost` etc, which will update the hasher. After all files are added, the user can |
| 722 | 721 | // use `final`, and will at some point `writeManifest` the file list to disk. |
| 723 | 722 | |
| 724 | 723 | man.hash.hasher = hasher_init; |
| ... | ... | @@ -762,11 +761,11 @@ pub const Manifest = struct { |
| 762 | 761 | } |
| 763 | 762 | |
| 764 | 763 | fn shrinkFilesToInput(m: *Manifest) void { |
| 765 | | if (m.files.count() <= m.input_files.items.len) return; |
| 766 | | const off = m.files.keys()[m.input_files.items.len]; |
| 764 | if (m.files.count() <= m.input_paths.items.len) return; |
| 765 | const off = m.files.keys()[m.input_paths.items.len]; |
| 767 | 766 | m.contents.shrinkRetainingCapacity(@backingInt(off)); |
| 768 | | assert(m.contents.len % @alignOf(File) == 0); |
| 769 | | m.files.shrinkRetainingCapacity(m.input_files.items.len); |
| 767 | assert(m.contents.items.len % @alignOf(File) == 0); |
| 768 | m.files.shrinkRetainingCapacity(m.input_paths.items.len); |
| 770 | 769 | } |
| 771 | 770 | |
| 772 | 771 | /// Assumes that `self.hash.hasher` has been updated only with the original digest and that |
| ... | ... | @@ -784,59 +783,70 @@ pub const Manifest = struct { |
| 784 | 783 | return error.CacheCheckFailed; |
| 785 | 784 | }, |
| 786 | 785 | }; |
| 786 | const contents = m.contents.items; |
| 787 | 787 | |
| 788 | | // Guess number of files based on manifest contents len to reduce allocations. |
| 789 | | try m.files.ensureUnusedCapacity(gpa, m.contents.len / (@sizeOf(File) + 32)); |
| 790 | | |
| 791 | | var file_index: usize = 0; |
| 792 | | var off: usize = 0; |
| 788 | var off: u32 = 0; |
| 789 | var c: Check = .{}; |
| 793 | 790 | |
| 794 | | // This group we always want to compute the hash digests, even on a cache miss. |
| 791 | // This group we always want to compute the hash digests, even on a |
| 792 | // cache miss, because they will be used in the manifest digest. |
| 795 | 793 | var input_group: Io.Group = .init; |
| 796 | 794 | defer input_group.cancel(io); |
| 797 | 795 | |
| 796 | // First the input files section, which must match our input files, |
| 797 | // otherwise it's invalid format. |
| 798 | for (m.input_paths.items, m.files.keys()[0..m.input_paths.items.len]) |*input_path, input_file_off| { |
| 799 | if (off + 1 >= contents.len) return error.InvalidFormat; |
| 800 | const file_off: File.Offset = @fromBackingInt(off); |
| 801 | const file = try file_off.getFallible(contents); |
| 802 | if (file.flags.prefix >= m.cache.prefixes_len) return error.InvalidFormat; |
| 803 | const path = try filePathFallible(contents, file_off); |
| 804 | if (path.len == 0) return error.InvalidFormat; |
| 805 | if (input_file_off != file_off) return error.InvalidFormat; |
| 806 | |
| 807 | input_group.async(io, checkInputFile, .{ m, &c, file_off, path, input_path }); |
| 808 | |
| 809 | off = @intCast(@as(usize, off) + @sizeOf(File) + path.len + 1); |
| 810 | } |
| 811 | |
| 812 | // Guess number of files based on manifest contents len to reduce allocations. |
| 813 | // This is not an upper bound; subsequent insertions may potentially allocate. |
| 814 | try m.files.ensureUnusedCapacity(gpa, contents.len / (@sizeOf(File) + 32)); |
| 815 | |
| 798 | 816 | // This group we would like to cancel as soon as a cache miss is discovered. |
| 799 | 817 | const PostResult = union(enum) { |
| 800 | 818 | checkFile: Check.Status, |
| 801 | 819 | }; |
| 802 | 820 | var post_select_buffer: [10]PostResult = undefined; |
| 803 | | var post_select: Io.Select(PostResult) = .init(&post_select_buffer); |
| 821 | var post_select: Io.Select(PostResult) = .init(io, &post_select_buffer); |
| 804 | 822 | var post_select_remaining: usize = 0; |
| 805 | | var c: Check = .{}; |
| 806 | | defer post_select.cancel(io); |
| 823 | defer post_select.cancelDiscard(); |
| 807 | 824 | |
| 808 | | while (off + 1 < m.contents.len) { |
| 825 | while (off + 1 < contents.len) { |
| 809 | 826 | const file_off: File.Offset = @fromBackingInt(off); |
| 810 | | const file = try File.getFallible(file_off, m); |
| 827 | const file = try file_off.getFallible(m); |
| 811 | 828 | if (file.flags.prefix >= m.cache.prefixes_len) return error.InvalidFormat; |
| 812 | | const path = try file.pathFallible(); |
| 829 | const path = try filePathFallible(contents, file_off); |
| 813 | 830 | if (path.len == 0) return error.InvalidFormat; |
| 814 | 831 | |
| 815 | | if (file_index < m.input_files.items.len) { |
| 816 | | if (m.files.keys()[file_index] != file_off) return error.InvalidFormat; |
| 817 | | |
| 818 | | input_group.async(io, checkInputFile, .{ m, &c, file_off, path }); |
| 819 | | } else { |
| 820 | | try m.files.put(gpa, file_off); |
| 832 | try m.files.put(gpa, file_off, {}); |
| 821 | 833 | |
| 822 | | post_select.async(.checkFile, checkFile, .{ m, &c, file_off, path }); |
| 823 | | post_select_remaining += 1; |
| 824 | | } |
| 834 | post_select.async(.checkFile, checkFile, .{ m, &c, file_off, path }); |
| 835 | post_select_remaining += 1; |
| 825 | 836 | |
| 826 | | file_index += 1; |
| 827 | | off += @sizeOf(File) + path.len + 1; |
| 837 | off = @intCast(@as(usize, off) + @sizeOf(File) + path.len + 1); |
| 828 | 838 | } |
| 829 | 839 | |
| 830 | 840 | // Final terminating zero byte to distinguish empty manifest file from |
| 831 | 841 | // manifest with zero files. |
| 832 | | const file_valid = off + 1 == m.contents.len and m.contents[off] == 0; |
| 833 | | if (!file_valid or file_index < m.input_files.items.len) { |
| 842 | const file_valid = off + 1 == contents.len and contents[off] == 0; |
| 843 | if (!file_valid) { |
| 834 | 844 | try input_group.await(io); |
| 835 | 845 | return .miss; |
| 836 | 846 | } |
| 837 | 847 | |
| 838 | 848 | // Don't track the trailing zero byte in contents. |
| 839 | | m.contents.len -= 1; |
| 849 | m.contents.items.len -= 1; |
| 840 | 850 | |
| 841 | 851 | var post_await_buffer: [10]PostResult = undefined; |
| 842 | 852 | while (post_select_remaining > 0) { |
| ... | ... | @@ -880,11 +890,17 @@ pub const Manifest = struct { |
| 880 | 890 | return .hit; |
| 881 | 891 | } |
| 882 | 892 | |
| 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 |
| 893 | fn checkInputFile( |
| 894 | m: *Manifest, |
| 895 | c: *Check, |
| 896 | file_off: File.Offset, |
| 897 | file_path: [:0]const u8, |
| 898 | input_path: *InputPath, |
| 899 | ) Io.Cancelable!void { |
| 900 | if (input_path.have_handle) @panic("TODO"); |
| 901 | if (input_path.have_stat) @panic("TODO"); |
| 902 | if (input_path.contents != .not_requested) @panic("TODO"); |
| 903 | if (input_path.request_handle) @panic("TODO"); |
| 888 | 904 | switch (try checkFile(m, c, file_off, file_path)) { |
| 889 | 905 | .hit => return, |
| 890 | 906 | .miss => @atomicStore(Check.Status, &c.status, .miss, .unordered), |
| ... | ... | @@ -897,7 +913,7 @@ pub const Manifest = struct { |
| 897 | 913 | c: *Check, |
| 898 | 914 | file_off: File.Offset, |
| 899 | 915 | file_path: [:0]const u8, |
| 900 | | ) Io.Cancelable!Check.Status { |
| 916 | ) error{ Canceled, CacheCheckFailed }!Check.Status { |
| 901 | 917 | const file = file_off.get(m); |
| 902 | 918 | const cache = m.cache; |
| 903 | 919 | const gpa = cache.gpa; |
| ... | ... | @@ -905,7 +921,7 @@ pub const Manifest = struct { |
| 905 | 921 | const parent_dir = cache.prefixes()[file.flags.prefix].handle; |
| 906 | 922 | |
| 907 | 923 | if (file.flags.metadata_only) { |
| 908 | | const actual_stat = parent_dir.statFile() catch |err| switch (err) { |
| 924 | const actual_stat = parent_dir.statFile(io, file_path, .{}) catch |err| switch (err) { |
| 909 | 925 | error.FileNotFound => return .miss, |
| 910 | 926 | error.Canceled => |e| return e, |
| 911 | 927 | else => |e| return c.fail(m, .{ .file_stat = .{ |
| ... | ... | @@ -999,10 +1015,10 @@ pub const Manifest = struct { |
| 999 | 1015 | /// not including post files). |
| 1000 | 1016 | /// |
| 1001 | 1017 | /// Assumes that `bin_digest` is populated for all input files. |
| 1002 | | pub fn unhit(man: *Manifest, bin_digest: BinDigest) void { |
| 1018 | pub fn unhit(man: *Manifest, bin_digest: *const BinDigest) void { |
| 1003 | 1019 | // Reset the hash. |
| 1004 | 1020 | man.hash.hasher = hasher_init; |
| 1005 | | man.hash.hasher.update(&bin_digest); |
| 1021 | man.hash.hasher.update(bin_digest); |
| 1006 | 1022 | man.shrinkFilesToInput(); |
| 1007 | 1023 | for (man.files.keys()) |off| { |
| 1008 | 1024 | const file = off.get(man); |
| ... | ... | @@ -1053,11 +1069,8 @@ pub const Manifest = struct { |
| 1053 | 1069 | return timestamp.nanoseconds >= man.recent_problematic_timestamp.nanoseconds; |
| 1054 | 1070 | } |
| 1055 | 1071 | |
| 1056 | | pub const AddFilePostOptions = struct { |
| 1057 | | handle: union(enum) { |
| 1058 | | file: ?Io.File, |
| 1059 | | dir: ?Io.Dir, |
| 1060 | | } = .{ .file = null }, |
| 1072 | pub const AddPathPostOptions = struct { |
| 1073 | handle: PathHandle = .{ .file = null }, |
| 1061 | 1074 | stat: ?Stat = null, |
| 1062 | 1075 | /// If it is a directory, there is a special encoding required for contents, which |
| 1063 | 1076 | /// is null-separated sorted entries, each one prefixed with `File.Kind`. |
| ... | ... | @@ -1065,7 +1078,7 @@ pub const Manifest = struct { |
| 1065 | 1078 | metadata_only: bool = false, |
| 1066 | 1079 | }; |
| 1067 | 1080 | |
| 1068 | | pub const AddFilePostError = error{ |
| 1081 | pub const AddPathPostError = error{ |
| 1069 | 1082 | /// The same file path has been added to the cache manifest both as a |
| 1070 | 1083 | /// directory and as a normal file, making the intended caching |
| 1071 | 1084 | /// behavior ambiguous. |
| ... | ... | @@ -1074,7 +1087,10 @@ pub const Manifest = struct { |
| 1074 | 1087 | |
| 1075 | 1088 | /// Add a file as a dependency of process being cached, after cache miss |
| 1076 | 1089 | /// occurs. |
| 1077 | | pub fn addFilePost(m: *Manifest, path: Path, options: AddFilePostOptions) AddFilePostError!void { |
| 1090 | /// |
| 1091 | /// See also: |
| 1092 | /// * `addInputPath` |
| 1093 | pub fn addPathPost(m: *Manifest, path: Path, options: AddPathPostOptions) AddPathPostError!void { |
| 1078 | 1094 | assert(m.manifest_file != null); |
| 1079 | 1095 | const cache = m.cache; |
| 1080 | 1096 | const gpa = cache.gpa; |
| ... | ... | @@ -1236,14 +1252,14 @@ pub const Manifest = struct { |
| 1236 | 1252 | // Clang is invoked in single-source mode but other programs may not |
| 1237 | 1253 | .target, .target_must_resolve => {}, |
| 1238 | 1254 | .prereq => |file_path| if (self.manifest_file == null) { |
| 1239 | | _ = try self.addFilePath(.initCwd(file_path), null); |
| 1240 | | } else try self.addFilePost(file_path), |
| 1255 | _ = try self.addInputPath(.initCwd(file_path), .{}); |
| 1256 | } else try self.addPathPost(file_path), |
| 1241 | 1257 | .prereq_must_resolve => { |
| 1242 | 1258 | resolve_buf.clearRetainingCapacity(); |
| 1243 | 1259 | try token.resolve(gpa, &resolve_buf); |
| 1244 | 1260 | if (self.manifest_file == null) { |
| 1245 | | _ = try self.addFilePath(.initCwd(resolve_buf.items), null); |
| 1246 | | } else try self.addFilePost(resolve_buf.items); |
| 1261 | _ = try self.addInputPath(.initCwd(resolve_buf.items), .{}); |
| 1262 | } else try self.addPathPost(resolve_buf.items); |
| 1247 | 1263 | }, |
| 1248 | 1264 | else => |err| { |
| 1249 | 1265 | try err.printError(gpa, &error_buf); |
| ... | ... | @@ -1336,25 +1352,45 @@ pub const Manifest = struct { |
| 1336 | 1352 | return .{ .manifest_file = self.manifest_file.? }; |
| 1337 | 1353 | } |
| 1338 | 1354 | |
| 1339 | | pub fn takeFiles(man: *Manifest) Files { |
| 1340 | | defer man.files = .empty; |
| 1341 | | return man.files; |
| 1342 | | } |
| 1355 | pub const SelfContainedFiles = struct { |
| 1356 | /// References memory inside `contents`. |
| 1357 | files: Files, |
| 1358 | contents: std.ArrayList(u8), |
| 1359 | |
| 1360 | pub const empty: @This() = .{ |
| 1361 | .files = .empty, |
| 1362 | .contents = .empty, |
| 1363 | }; |
| 1364 | |
| 1365 | pub fn deinit(scf: *SelfContainedFiles, gpa: Allocator) void { |
| 1366 | scf.files.deinit(gpa); |
| 1367 | scf.contents.deinit(gpa); |
| 1368 | scf.* = undefined; |
| 1369 | } |
| 1370 | |
| 1371 | pub fn path(scf: *const SelfContainedFiles, file_offset: File.Offset) [:0]const u8 { |
| 1372 | return filePath(scf.contents.items, file_offset); |
| 1373 | } |
| 1374 | }; |
| 1343 | 1375 | |
| 1344 | | pub fn freeFiles(gpa: Allocator, files: *Files) void { |
| 1345 | | for (files.keys()) |*file| file.deinit(gpa); |
| 1346 | | files.deinit(gpa); |
| 1376 | pub fn takeFiles(m: *Manifest) SelfContainedFiles { |
| 1377 | defer m.files = .empty; |
| 1378 | defer m.contents = .empty; |
| 1379 | return .{ |
| 1380 | .files = m.files, |
| 1381 | .contents = m.contents, |
| 1382 | }; |
| 1347 | 1383 | } |
| 1348 | 1384 | |
| 1349 | 1385 | /// Releases the manifest file and frees any memory the Manifest was using. |
| 1350 | 1386 | /// `Manifest.hit` must be called first. |
| 1351 | 1387 | /// |
| 1352 | 1388 | /// Don't forget to call `writeManifest` before this! |
| 1353 | | pub fn deinit(man: *Manifest) void { |
| 1354 | | const io = man.cache.io; |
| 1355 | | const gpa = man.cache.gpa; |
| 1389 | pub fn deinit(m: *Manifest) void { |
| 1390 | const io = m.cache.io; |
| 1391 | const gpa = m.cache.gpa; |
| 1356 | 1392 | |
| 1357 | | if (man.manifest_file) |file| { |
| 1393 | if (m.manifest_file) |file| { |
| 1358 | 1394 | if (builtin.os.tag == .windows) { |
| 1359 | 1395 | // See Lock.release for why this is required on Windows |
| 1360 | 1396 | file.unlock(io); |
| ... | ... | @@ -1362,8 +1398,9 @@ pub const Manifest = struct { |
| 1362 | 1398 | |
| 1363 | 1399 | file.close(io); |
| 1364 | 1400 | } |
| 1365 | | freeFiles(gpa, &man.files); |
| 1366 | | man.* = undefined; |
| 1401 | m.files.deinit(gpa); |
| 1402 | m.contents.deinit(gpa); |
| 1403 | m.* = undefined; |
| 1367 | 1404 | } |
| 1368 | 1405 | |
| 1369 | 1406 | pub fn populateFileSystemInputs(man: *Manifest, buf: *std.ArrayList(u8)) Allocator.Error!void { |
| ... | ... | @@ -1497,6 +1534,25 @@ pub const Manifest = struct { |
| 1497 | 1534 | hasher.update(contents.items[contents_start..][0..contents_len]); |
| 1498 | 1535 | hasher.final(bin_digest); |
| 1499 | 1536 | } |
| 1537 | |
| 1538 | fn digestHash(m: *const Manifest, off: File.Offset, hasher: *Hasher) void { |
| 1539 | const contents = m.contents.items; |
| 1540 | const flags_off = @offsetOf(File, "flags"); |
| 1541 | comptime assert(@offsetOf(File, "path_start") - flags_off == 1); |
| 1542 | const hash_start = @backingInt(off) + flags_off; |
| 1543 | const hash_end = mem.findScalarPos(u8, contents, hash_start, 0).?; |
| 1544 | hasher.update(contents[hash_start..hash_end]); |
| 1545 | } |
| 1546 | |
| 1547 | fn filePathFallible(contents: []const u8, off: File.Offset) error{InvalidFormat}![:0]const u8 { |
| 1548 | const path_start = @backingInt(off) + @offsetOf(File, "path_start"); |
| 1549 | const path_end = mem.findScalarPos(u8, contents, path_start, 0) orelse return error.InvalidFormat; |
| 1550 | return contents[path_start..path_end :0]; |
| 1551 | } |
| 1552 | |
| 1553 | fn filePath(contents: []const u8, off: File.Offset) [:0]const u8 { |
| 1554 | return filePathFallible(contents, off) catch unreachable; |
| 1555 | } |
| 1500 | 1556 | }; |
| 1501 | 1557 | |
| 1502 | 1558 | /// Create/Write a file, close it, then grab its stat.mtime timestamp. |
| ... | ... | @@ -1555,7 +1611,7 @@ test "cache file and then recall it" { |
| 1555 | 1611 | ch.hash.add(true); |
| 1556 | 1612 | ch.hash.add(@as(u16, 1234)); |
| 1557 | 1613 | ch.hash.addBytes("1234"); |
| 1558 | | _ = try ch.addFilePath(.initCwd(temp_file), null); |
| 1614 | _ = try ch.addInputPath(.initCwd(temp_file), .{}); |
| 1559 | 1615 | |
| 1560 | 1616 | // There should be nothing in the cache |
| 1561 | 1617 | try testing.expectEqual(false, try ch.hit(.none)); |
| ... | ... | @@ -1570,7 +1626,7 @@ test "cache file and then recall it" { |
| 1570 | 1626 | ch.hash.add(true); |
| 1571 | 1627 | ch.hash.add(@as(u16, 1234)); |
| 1572 | 1628 | ch.hash.addBytes("1234"); |
| 1573 | | _ = try ch.addFilePath(.initCwd(temp_file), null); |
| 1629 | _ = try ch.addInputPath(.initCwd(temp_file), .{}); |
| 1574 | 1630 | |
| 1575 | 1631 | // Cache hit! We just "built" the same file |
| 1576 | 1632 | try testing.expect(try ch.hit(.none)); |
| ... | ... | @@ -1623,7 +1679,7 @@ test "check that changing a file makes cache fail" { |
| 1623 | 1679 | defer ch.deinit(); |
| 1624 | 1680 | |
| 1625 | 1681 | ch.hash.addBytes("1234"); |
| 1626 | | const temp_file_idx = try ch.addFilePath(.initCwd(temp_file), 100); |
| 1682 | const temp_file_idx = try ch.addInputPath(.initCwd(temp_file), .{ .request_contents = true }); |
| 1627 | 1683 | |
| 1628 | 1684 | // There should be nothing in the cache |
| 1629 | 1685 | try testing.expectEqual(false, try ch.hit(.none)); |
| ... | ... | @@ -1642,7 +1698,7 @@ test "check that changing a file makes cache fail" { |
| 1642 | 1698 | defer ch.deinit(); |
| 1643 | 1699 | |
| 1644 | 1700 | ch.hash.addBytes("1234"); |
| 1645 | | const temp_file_idx = try ch.addFilePath(.initCwd(temp_file), 100); |
| 1701 | const temp_file_idx = try ch.addInputPath(.initCwd(temp_file), .{ .request_contents = true }); |
| 1646 | 1702 | |
| 1647 | 1703 | // A file that we depend on has been updated, so the cache should not contain an entry for it |
| 1648 | 1704 | try testing.expectEqual(false, try ch.hit(.none)); |
| ... | ... | @@ -1689,7 +1745,7 @@ test "no file inputs" { |
| 1689 | 1745 | man.hash.addBytes("1234"); |
| 1690 | 1746 | |
| 1691 | 1747 | // There should be nothing in the cache |
| 1692 | | try testing.expectEqual(false, try man.hit(.none)); |
| 1748 | try testing.expectEqual(false, try man.check(.none)); |
| 1693 | 1749 | |
| 1694 | 1750 | digest1 = man.final(); |
| 1695 | 1751 | |
| ... | ... | @@ -1701,7 +1757,7 @@ test "no file inputs" { |
| 1701 | 1757 | |
| 1702 | 1758 | man.hash.addBytes("1234"); |
| 1703 | 1759 | |
| 1704 | | try testing.expect(try man.hit(.none)); |
| 1760 | try testing.expect(try man.check(.none)); |
| 1705 | 1761 | digest2 = man.final(); |
| 1706 | 1762 | try testing.expectEqual(false, man.have_exclusive_lock); |
| 1707 | 1763 | } |
| ... | ... | @@ -1750,12 +1806,12 @@ test "Manifest with files added after initial hash work" { |
| 1750 | 1806 | defer ch.deinit(); |
| 1751 | 1807 | |
| 1752 | 1808 | ch.hash.addBytes("1234"); |
| 1753 | | _ = try ch.addFilePath(.initCwd(temp_file1), null); |
| 1809 | _ = try ch.addInputPath(.initCwd(temp_file1), .{}); |
| 1754 | 1810 | |
| 1755 | 1811 | // There should be nothing in the cache |
| 1756 | 1812 | try testing.expectEqual(false, try ch.hit(.none)); |
| 1757 | 1813 | |
| 1758 | | _ = try ch.addFilePost(temp_file2); |
| 1814 | _ = try ch.addPathPost(temp_file2); |
| 1759 | 1815 | |
| 1760 | 1816 | digest1 = ch.final(); |
| 1761 | 1817 | try ch.writeManifest(); |
| ... | ... | @@ -1765,7 +1821,7 @@ test "Manifest with files added after initial hash work" { |
| 1765 | 1821 | defer ch.deinit(); |
| 1766 | 1822 | |
| 1767 | 1823 | ch.hash.addBytes("1234"); |
| 1768 | | _ = try ch.addFilePath(.initCwd(temp_file1), null); |
| 1824 | _ = try ch.addInputPath(.initCwd(temp_file1), .{}); |
| 1769 | 1825 | |
| 1770 | 1826 | try testing.expect(try ch.hit(.none)); |
| 1771 | 1827 | digest2 = ch.final(); |
| ... | ... | @@ -1788,12 +1844,12 @@ test "Manifest with files added after initial hash work" { |
| 1788 | 1844 | defer ch.deinit(); |
| 1789 | 1845 | |
| 1790 | 1846 | ch.hash.addBytes("1234"); |
| 1791 | | _ = try ch.addFilePath(.initCwd(temp_file1), null); |
| 1847 | _ = try ch.addInputPath(.initCwd(temp_file1), .{}); |
| 1792 | 1848 | |
| 1793 | 1849 | // A file that we depend on has been updated, so the cache should not contain an entry for it |
| 1794 | 1850 | try testing.expectEqual(false, try ch.hit(.none)); |
| 1795 | 1851 | |
| 1796 | | _ = try ch.addFilePost(temp_file2); |
| 1852 | _ = try ch.addPathPost(temp_file2); |
| 1797 | 1853 | |
| 1798 | 1854 | digest3 = ch.final(); |
| 1799 | 1855 | |