| ... | @@ -62,7 +62,7 @@ pub const PrefixedPath = struct { | ... | @@ -62,7 +62,7 @@ pub const PrefixedPath = struct { |
| 62 | sub_path: []const u8, | 62 | sub_path: []const u8, |
| 63 | | 63 | |
| 64 | fn eql(a: PrefixedPath, b: PrefixedPath) bool { | 64 | fn eql(a: PrefixedPath, b: PrefixedPath) bool { |
| 65 | return a.prefix == b.prefix and std.mem.eql(u8, a.sub_path, b.sub_path); | 65 | return a.prefix == b.prefix and mem.eql(u8, a.sub_path, b.sub_path); |
| 66 | } | 66 | } |
| 67 | | 67 | |
| 68 | fn hash(pp: PrefixedPath) u32 { | 68 | fn hash(pp: PrefixedPath) u32 { |
| ... | @@ -118,7 +118,7 @@ fn getPrefixSubpath(gpa: Allocator, cwd: []const u8, prefix: []const u8, path: [ | ... | @@ -118,7 +118,7 @@ fn getPrefixSubpath(gpa: Allocator, cwd: []const u8, prefix: []const u8, path: [ |
| 118 | return error.NotASubPath; | 118 | return error.NotASubPath; |
| 119 | } | 119 | } |
| 120 | const first_component = component_iterator.first(); | 120 | const first_component = component_iterator.first(); |
| 121 | if (first_component != null and std.mem.eql(u8, first_component.?.name, "..")) { | 121 | if (first_component != null and mem.eql(u8, first_component.?.name, "..")) { |
| 122 | return error.NotASubPath; | 122 | return error.NotASubPath; |
| 123 | } | 123 | } |
| 124 | return relative; | 124 | return relative; |
| ... | @@ -130,10 +130,6 @@ pub const hex_digest_len = bin_digest_len * 2; | ... | @@ -130,10 +130,6 @@ pub const hex_digest_len = bin_digest_len * 2; |
| 130 | pub const BinDigest = [bin_digest_len]u8; | 130 | pub const BinDigest = [bin_digest_len]u8; |
| 131 | pub const HexDigest = [hex_digest_len]u8; | 131 | pub const HexDigest = [hex_digest_len]u8; |
| 132 | | 132 | |
| 133 | /// This is currently just an arbitrary non-empty string that can't match another manifest line. | | |
| 134 | const manifest_header = "0"; | | |
| 135 | pub const manifest_file_size_max = 100 * 1024 * 1024; | | |
| 136 | | | |
| 137 | /// The type used for hashing file contents. Currently, this is SipHash128(1, 3), because it | 133 | /// The type used for hashing file contents. Currently, this is SipHash128(1, 3), because it |
| 138 | /// provides enough collision resistance for the Manifest use cases, while being one of our | 134 | /// provides enough collision resistance for the Manifest use cases, while being one of our |
| 139 | /// fastest options right now. | 135 | /// fastest options right now. |
| ... | @@ -149,50 +145,6 @@ pub const hasher_init: Hasher = Hasher.init(&.{ | ... | @@ -149,50 +145,6 @@ pub const hasher_init: Hasher = Hasher.init(&.{ |
| 149 | 0x77, 0xd6, 0xf0, 0x60, | 145 | 0x77, 0xd6, 0xf0, 0x60, |
| 150 | }); | 146 | }); |
| 151 | | 147 | |
| 152 | pub const File = struct { | | |
| 153 | prefixed_path: PrefixedPath, | | |
| 154 | max_file_size: ?usize, | | |
| 155 | /// Populated if the user calls `addOpenedFile`. | | |
| 156 | /// The handle is not owned here. | | |
| 157 | handle: ?Io.File, | | |
| 158 | stat: Stat, | | |
| 159 | bin_digest: BinDigest, | | |
| 160 | contents: ?[]const u8, | | |
| 161 | | | |
| 162 | pub const Stat = struct { | | |
| 163 | inode: Io.File.INode, | | |
| 164 | size: u64, | | |
| 165 | mtime: Io.Timestamp, | | |
| 166 | | | |
| 167 | pub fn fromFs(fs_stat: Io.File.Stat) Stat { | | |
| 168 | return .{ | | |
| 169 | .inode = fs_stat.inode, | | |
| 170 | .size = fs_stat.size, | | |
| 171 | .mtime = fs_stat.mtime, | | |
| 172 | }; | | |
| 173 | } | | |
| 174 | }; | | |
| 175 | | | |
| 176 | pub fn deinit(self: *File, gpa: Allocator) void { | | |
| 177 | gpa.free(self.prefixed_path.sub_path); | | |
| 178 | if (self.contents) |contents| { | | |
| 179 | gpa.free(contents); | | |
| 180 | self.contents = null; | | |
| 181 | } | | |
| 182 | self.* = undefined; | | |
| 183 | } | | |
| 184 | | | |
| 185 | pub fn updateMaxSize(file: *File, new_max_size: ?usize) void { | | |
| 186 | const new = new_max_size orelse return; | | |
| 187 | file.max_file_size = if (file.max_file_size) |old| @max(old, new) else new; | | |
| 188 | } | | |
| 189 | | | |
| 190 | pub fn updateHandle(file: *File, new_handle: ?Io.File) void { | | |
| 191 | const handle = new_handle orelse return; | | |
| 192 | file.handle = handle; | | |
| 193 | } | | |
| 194 | }; | | |
| 195 | | | |
| 196 | pub const HashHelper = struct { | 148 | pub const HashHelper = struct { |
| 197 | hasher: Hasher = hasher_init, | 149 | hasher: Hasher = hasher_init, |
| 198 | | 150 | |
| ... | @@ -319,10 +271,15 @@ pub const Lock = struct { | ... | @@ -319,10 +271,15 @@ pub const Lock = struct { |
| 319 | } | 271 | } |
| 320 | }; | 272 | }; |
| 321 | | 273 | |
| | 274 | /// Format: a series of consecutive `Manifest.File`, followed by a final |
| | 275 | /// terminating zero byte to distinguish empty manifest file from manifest with |
| | 276 | /// zero files. |
| 322 | pub const Manifest = struct { | 277 | pub const Manifest = struct { |
| 323 | cache: *Cache, | 278 | cache: *Cache, |
| 324 | /// Current state for incremental hashing. | 279 | /// Current state for incremental hashing. |
| 325 | hash: HashHelper, | 280 | hash: HashHelper, |
| | 281 | hex_digest: HexDigest, |
| | 282 | /// When this is null, `Manifest` is in "pre-check" phase. Otherwise it is in "post-check" phase. |
| 326 | manifest_file: ?Io.File, | 283 | manifest_file: ?Io.File, |
| 327 | manifest_dirty: bool, | 284 | manifest_dirty: bool, |
| 328 | /// Set this flag to true before calling hit() in order to indicate that | 285 | /// Set this flag to true before calling hit() in order to indicate that |
| ... | @@ -335,12 +292,140 @@ pub const Manifest = struct { | ... | @@ -335,12 +292,140 @@ pub const Manifest = struct { |
| 335 | // order to obtain a problematic timestamp for the next call. Calls after that | 292 | // order to obtain a problematic timestamp for the next call. Calls after that |
| 336 | // will then use the same timestamp, to avoid unnecessary filesystem writes. | 293 | // will then use the same timestamp, to avoid unnecessary filesystem writes. |
| 337 | want_refresh_timestamp: bool = true, | 294 | want_refresh_timestamp: bool = true, |
| 338 | files: Files = .{}, | 295 | /// Uses `Cache.gpa`. |
| 339 | hex_digest: HexDigest, | 296 | files: Files = .empty, |
| | 297 | /// Indexes line up with `files`, but only up until `hit` is called. Uses |
| | 298 | /// `Cache.gpa`. |
| | 299 | input_files: std.ArrayList(InputFile) = .empty, |
| 340 | diagnostic: Diagnostic = .none, | 300 | diagnostic: Diagnostic = .none, |
| 341 | /// Keeps track of the last time we performed a file system write to observe | 301 | /// Keeps track of the last time we performed a file system write to observe |
| 342 | /// what time the file system thinks it is, according to its own granularity. | 302 | /// what time the file system thinks it is, according to its own granularity. |
| 343 | recent_problematic_timestamp: Io.Timestamp = .zero, | 303 | recent_problematic_timestamp: Io.Timestamp = .zero, |
| | 304 | /// The entire manifest file contents, except for the final terminating |
| | 305 | /// zero byte. However maintains always at least 1 unused capacity so the |
| | 306 | /// final terminating byte can be added without allocation. Uses |
| | 307 | /// `Cache.gpa`. |
| | 308 | contents: std.ArrayList(u8) = .empty, |
| | 309 | /// All contents from all `input_files` whose contents were requested, |
| | 310 | /// concatenated. Total byte size will be less than `max_input_content_len` |
| | 311 | /// otherwise an error is returned. |
| | 312 | all_input_content: std.ArrayList(u8) = .empty, |
| | 313 | max_input_content_len: usize = std.math.maxInt(u32), |
| | 314 | |
| | 315 | pub const Files = std.array_hash_map.Custom(File.Offset, void, File.HashContext, false); |
| | 316 | |
| | 317 | /// Source files whose prefix and relative path are included when computing |
| | 318 | /// the cache manifest digest. It's the information needed to lazily hash |
| | 319 | /// the input files only when a cache miss occurs. |
| | 320 | /// |
| | 321 | /// `File.prefix`, `File.path`, and `File.mode` will be always populated, |
| | 322 | /// but the other fields of `File` will be populated depending on the |
| | 323 | /// fields of `InputFile`. |
| | 324 | pub const InputFile = struct { |
| | 325 | request_handle: bool, |
| | 326 | have_handle: bool, |
| | 327 | /// Determines whether `File.size`, `File.inode`, and `File.mtime` are populated. |
| | 328 | have_stat: bool, |
| | 329 | /// Determines whether `File.digest` is populated. |
| | 330 | have_digest: bool, |
| | 331 | contents: enum (usize) { |
| | 332 | requested = std.math.maxInt(u32) - 1, |
| | 333 | not_requested = std.math.maxInt(u32), |
| | 334 | /// Byte offset index into `Manifest.all_input_content`. |
| | 335 | _, |
| | 336 | }, |
| | 337 | /// `have_handle` determines whether this is populated. |
| | 338 | handle: Io.File, |
| | 339 | |
| | 340 | /// Index into `Manifest.input_files`. |
| | 341 | pub const Index = enum(u32) { |
| | 342 | _, |
| | 343 | }; |
| | 344 | }; |
| | 345 | |
| | 346 | /// The data per tracked input file that is stored in the manifest file. |
| | 347 | pub const File = extern struct { |
| | 348 | size: u64, |
| | 349 | inode: u64, |
| | 350 | digest: BinDigest, |
| | 351 | /// Nanoseconds. |
| | 352 | mtime: i64, |
| | 353 | /// Starting with this field and continuing into the path, excluding the null byte, |
| | 354 | /// is the string that is hashed for the manifest digest. |
| | 355 | flags: Flags, |
| | 356 | /// Terminated by zero byte, then followed by padding until 8-byte aligned. |
| | 357 | path_start: [0]u8, |
| | 358 | |
| | 359 | pub const Flags = packed struct (u8) { |
| | 360 | is_directory: bool, |
| | 361 | metadata_only: bool, |
| | 362 | prefix: u6, |
| | 363 | }; |
| | 364 | |
| | 365 | /// Byte index within `Manifest.contents` where the entry starts. |
| | 366 | pub const Offset = enum(u32) { |
| | 367 | _, |
| | 368 | |
| | 369 | pub fn get(offset: Offset, m: *const Manifest) *File { |
| | 370 | return @ptrCast(m.contents.items[@backingInt(offset)..][0..@sizeOf(File)]); |
| | 371 | } |
| | 372 | |
| | 373 | pub fn getFallible(offset: Offset, m: *const Manifest) error{EndOfStream}!*File { |
| | 374 | if (@backingInt(offset) + @sizeOf(File) >= m.contents.len) return error.EndOfStream; |
| | 375 | return get(offset, m); |
| | 376 | } |
| | 377 | }; |
| | 378 | |
| | 379 | pub const HashContext = struct { |
| | 380 | manifest: *const Manifest, |
| | 381 | |
| | 382 | pub fn hash(this: @This(), off: Offset) u32 { |
| | 383 | const file = off.get(this.manifest); |
| | 384 | return @truncate(std.hash.Wyhash.hash(file.prefix, file.path())); |
| | 385 | } |
| | 386 | |
| | 387 | pub fn eql(this: @This(), a_off: Offset, b_off: Offset, b_index: usize) bool { |
| | 388 | _ = b_index; |
| | 389 | const a = a_off.get(this.manifest); |
| | 390 | const b = b_off.get(this.manifest); |
| | 391 | return a.prefix == b.prefix and mem.eql(u8, a.path(), b.path()); |
| | 392 | } |
| | 393 | }; |
| | 394 | |
| | 395 | |
| | 396 | pub fn path(file: *const File) [:0]const u8 { |
| | 397 | return pathFallible(file) catch unreachable; |
| | 398 | } |
| | 399 | |
| | 400 | pub fn pathFallible(file: *const File) error{EndOfStream}![:0]const u8 { |
| | 401 | const ptr: [*]u8 = &file.path_start; |
| | 402 | const len = mem.findScalar(u8, ptr, 0) orelse return error.EndOfStream; |
| | 403 | return ptr[0..len :0]; |
| | 404 | } |
| | 405 | |
| | 406 | fn manifestDigestHash(file: *const File, hasher: *Hasher) void { |
| | 407 | const path_ptr: [*]u8 = &file.path_start; |
| | 408 | const path_len = mem.findScalar(u8, path_ptr, 0).?; |
| | 409 | comptime assert(@offsetOf(File, "path_start") - @offsetOf(File, "flags") == 1); |
| | 410 | // Includes flags and sentinel. |
| | 411 | const hash_string = (path_ptr - 1)[0..path_len + 2]; |
| | 412 | hasher.update(hash_string); |
| | 413 | } |
| | 414 | |
| | 415 | fn setStat(file: *File, m: *Manifest, stat: Stat) Io.Cancelable!void { |
| | 416 | file.size = stat.size; |
| | 417 | file.inode = stat.inode; |
| | 418 | file.mtime = stat.mtime; |
| | 419 | |
| | 420 | if (try m.isProblematicTimestamp(stat.mtime)) { |
| | 421 | // The actual file has an unreliable timestamp; force it to be hashed. |
| | 422 | file.stat.mtime = 0; |
| | 423 | file.stat.inode = 0; |
| | 424 | } |
| | 425 | } |
| | 426 | |
| | 427 | }; |
| | 428 | |
| 344 | | 429 | |
| 345 | pub const Diagnostic = union(enum) { | 430 | pub const Diagnostic = union(enum) { |
| 346 | none, | 431 | none, |
| ... | @@ -358,110 +443,118 @@ pub const Manifest = struct { | ... | @@ -358,110 +443,118 @@ pub const Manifest = struct { |
| 358 | }; | 443 | }; |
| 359 | }; | 444 | }; |
| 360 | | 445 | |
| 361 | pub const Files = std.array_hash_map.Custom(File, void, FilesContext, false); | 446 | pub const Stat = struct { |
| 362 | | 447 | size: u64, |
| 363 | pub const FilesContext = struct { | 448 | inode: Io.File.INode, |
| 364 | pub fn hash(fc: FilesContext, file: File) u32 { | 449 | mtime: Io.Timestamp, |
| 365 | _ = fc; | | |
| 366 | return file.prefixed_path.hash(); | | |
| 367 | } | | |
| 368 | | | |
| 369 | pub fn eql(fc: FilesContext, a: File, b: File, b_index: usize) bool { | | |
| 370 | _ = fc; | | |
| 371 | _ = b_index; | | |
| 372 | return a.prefixed_path.eql(b.prefixed_path); | | |
| 373 | } | | |
| 374 | }; | 450 | }; |
| 375 | | 451 | |
| 376 | const FilesAdapter = struct { | 452 | pub const AddInputFileOptions = struct { |
| 377 | pub fn eql(context: @This(), a: PrefixedPath, b: File, b_index: usize) bool { | 453 | handle: ?Io.File = null, |
| 378 | _ = context; | 454 | stat: ?Stat = null, |
| 379 | _ = b_index; | 455 | request_handle: bool = false, |
| 380 | return a.eql(b.prefixed_path); | 456 | request_contents: bool = false, |
| 381 | } | 457 | is_directory: bool = false, |
| | 458 | metadata_only: bool = false, |
| 382 | | 459 | |
| 383 | pub fn hash(context: @This(), key: PrefixedPath) u32 { | | |
| 384 | _ = context; | | |
| 385 | return key.hash(); | | |
| 386 | } | | |
| 387 | }; | 460 | }; |
| 388 | | 461 | |
| | 462 | pub const AddInputFileError = error { |
| | 463 | /// The same file path has been added to the cache manifest both as a |
| | 464 | /// directory and as a normal file, making the intended caching |
| | 465 | /// behavior ambiguous. |
| | 466 | IsDirectoryAmbiguous, |
| | 467 | } || Allocator.Error; |
| | 468 | |
| 389 | /// Add a file as a dependency of process being cached. When `hit` is | 469 | /// Add a file as a dependency of process being cached. When `hit` is |
| 390 | /// called, the file's contents will be checked to ensure that it matches | 470 | /// called, the file's contents will be checked to ensure that it matches |
| 391 | /// the contents from previous times. | 471 | /// the contents from previous times. |
| 392 | /// | 472 | /// |
| 393 | /// Max file size will be used to determine the amount of space the file contents | 473 | /// The contents of the input file may be requested and subsequently |
| 394 | /// are allowed to take up in memory. If max_file_size is null, then the contents | 474 | /// obtained via methods of the returned `InputFile.Index` after calling |
| 395 | /// will not be loaded into memory. | 475 | /// `hit`. |
| 396 | /// | 476 | pub fn addInputFile(m: *Manifest, path: Path, options: AddInputFileOptions) Allocator.Error!InputFile.Index { |
| 397 | /// Returns the index of the entry in the `files` array list. You can use it | | |
| 398 | /// to access the contents of the file after calling `hit()` like so: | | |
| 399 | /// | | |
| 400 | /// ``` | | |
| 401 | /// var file_contents = cache_hash.files.keys()[file_index].contents.?; | | |
| 402 | /// ``` | | |
| 403 | pub fn addFilePath(m: *Manifest, file_path: Path, max_file_size: ?usize) !usize { | | |
| 404 | return addOpenedFile(m, file_path, null, max_file_size); | | |
| 405 | } | | |
| 406 | | | |
| 407 | /// Same as `addFilePath` except the file has already been opened. | | |
| 408 | pub fn addOpenedFile(m: *Manifest, path: Path, handle: ?Io.File, max_file_size: ?usize) !usize { | | |
| 409 | const gpa = m.cache.gpa; | 477 | const gpa = m.cache.gpa; |
| 410 | try m.files.ensureUnusedCapacity(gpa, 1); | 478 | try m.files.ensureUnusedCapacity(gpa, 1); |
| 411 | const resolved_path = try std.fs.path.resolve(gpa, &.{ | 479 | try m.input_files.ensureUnusedCapacity(gpa, 1); |
| 412 | path.root_dir.path orelse ".", | | |
| 413 | path.subPathOrDot(), | | |
| 414 | }); | | |
| 415 | errdefer gpa.free(resolved_path); | | |
| 416 | const prefixed_path = try m.cache.findPrefixResolved(resolved_path); | | |
| 417 | return addFileInner(m, prefixed_path, handle, max_file_size); | | |
| 418 | } | | |
| 419 | | | |
| 420 | fn addFileInner(self: *Manifest, prefixed_path: PrefixedPath, handle: ?Io.File, max_file_size: ?usize) usize { | | |
| 421 | const gop = self.files.getOrPutAssumeCapacityAdapted(prefixed_path, FilesAdapter{}); | | |
| 422 | if (gop.found_existing) { | | |
| 423 | self.cache.gpa.free(prefixed_path.sub_path); | | |
| 424 | gop.key_ptr.updateMaxSize(max_file_size); | | |
| 425 | gop.key_ptr.updateHandle(handle); | | |
| 426 | return gop.index; | | |
| 427 | } | | |
| 428 | gop.key_ptr.* = .{ | | |
| 429 | .prefixed_path = prefixed_path, | | |
| 430 | .contents = null, | | |
| 431 | .max_file_size = max_file_size, | | |
| 432 | .stat = undefined, | | |
| 433 | .bin_digest = undefined, | | |
| 434 | .handle = handle, | | |
| 435 | }; | | |
| 436 | | 480 | |
| 437 | self.hash.add(prefixed_path.prefix); | 481 | const prev_contents_len = m.contents.items.len; |
| 438 | self.hash.addBytes(prefixed_path.sub_path); | 482 | const header: *File = @ptrCast(try m.contents.addManyAsSlice(gpa, @sizeOf(File))); |
| | 483 | errdefer m.contents.shrinkRetainingCapacity(prev_contents_len); |
| 439 | | 484 | |
| 440 | return gop.index; | 485 | header.* = .{ |
| 441 | } | 486 | .flags = .{ |
| | 487 | .prefix = try m.cache.findAppendPrefixedPath(&m.contents, path), |
| | 488 | .is_directory = options.is_directory, |
| | 489 | .metadata_only = options.metadata_only, |
| | 490 | }, |
| | 491 | .size = undefined, |
| | 492 | .inode = undefined, |
| | 493 | .mtime = undefined, |
| | 494 | .digest = undefined, |
| | 495 | }; |
| | 496 | assert(m.contents.items.len % @alignOf(File) == 0); |
| 442 | | 497 | |
| 443 | pub fn addOptionalFilePath(self: *Manifest, optional_file_path: ?Path) !void { | 498 | const gop = try m.files.getOrPutAssumeCapacityContext(@fromBackingInt(prev_contents_len), .{ |
| 444 | self.hash.add(optional_file_path != null); | 499 | .manifest = m, |
| 445 | const file_path = optional_file_path orelse return; | 500 | }); |
| 446 | _ = try self.addFilePath(file_path, null); | 501 | if (gop.found_existing) { |
| | 502 | m.contents.shrinkRetainingCapacity(prev_contents_len); |
| | 503 | const existing_input_file = &m.input_files.items[gop.index]; |
| | 504 | if (options.handle) |handle| { |
| | 505 | existing_input_file.handle = handle; |
| | 506 | existing_input_file.have_handle = true; |
| | 507 | } |
| | 508 | if (options.request_contents) switch (existing_input_file.contents) { |
| | 509 | .requested, .not_requested => existing_input_file.contents = .requested, |
| | 510 | _ => {}, |
| | 511 | }; |
| | 512 | const existing_header = &m.files.keys()[gop.index]; |
| | 513 | if (options.stat) |stat| { |
| | 514 | existing_input_file.have_stat = true; |
| | 515 | existing_header.size = stat.size; |
| | 516 | existing_header.inode = stat.inode; |
| | 517 | existing_header.mtime = stat.mtime; |
| | 518 | } |
| | 519 | if (existing_header.flags.is_directory != options.is_directory) |
| | 520 | return error.IsDirectoryAmbiguous; |
| | 521 | if (!options.metadata_only) |
| | 522 | existing_header.flags.metadata_only = false; |
| | 523 | } else { |
| | 524 | m.input_files.appendAssumeCapacity(.{ |
| | 525 | .request_handle = options.request_handle, |
| | 526 | .have_handle = options.handle != null, |
| | 527 | .handle = if (options.handle) |handle| handle else undefined, |
| | 528 | .contents = if (options.request_contents) .requested else .not_requested, |
| | 529 | .have_digest = false, |
| | 530 | .have_stat = options.stat != null, |
| | 531 | }); |
| | 532 | assert(m.input_files.items.len - 1 == gop.index); |
| | 533 | if (options.stat) |stat| { |
| | 534 | header.size = stat.size; |
| | 535 | header.inode = stat.inode; |
| | 536 | header.mtime = stat.mtime; |
| | 537 | } |
| | 538 | } |
| | 539 | return @fromBackingInt(gop.index); |
| 447 | } | 540 | } |
| 448 | | 541 | |
| 449 | pub fn addDepFile(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { | 542 | pub fn addInputFileOptional(m: *Manifest, opt_path: ?Path, options: AddInputFileOptions) Allocator.Error!void { |
| 450 | assert(self.manifest_file == null); | 543 | m.hash.add(opt_path != null); |
| 451 | return self.addDepFileMaybePost(dir, dep_file_sub_path); | 544 | _ = try addInputFile(m, opt_path orelse return, options); |
| 452 | } | 545 | } |
| 453 | | 546 | |
| 454 | pub const HitError = error{ | 547 | pub const CheckError = error{ |
| 455 | /// Unable to check the cache for a reason that has been recorded into | 548 | /// Unable to check the cache for a reason that has been recorded into |
| 456 | /// the `diagnostic` field. | 549 | /// the `diagnostic` field. |
| 457 | CacheCheckFailed, | 550 | CacheCheckFailed, |
| 458 | /// A cache manifest file exists however it could not be parsed. | 551 | /// A cache manifest file exists however it could not be parsed. |
| 459 | InvalidFormat, | 552 | InvalidFormat, |
| 460 | OutOfMemory, | 553 | } || Allocator.Error || Io.Cancelable; |
| 461 | Canceled, | 554 | |
| 462 | }; | 555 | pub const CheckStatus = enum { hit, miss }; |
| 463 | | 556 | |
| 464 | /// Check the cache to see if the input exists in it. If it exists, returns `true`. | 557 | /// Check the cache to see if the input exists in it. |
| 465 | /// A hex encoding of its hash is available by calling `final`. | 558 | /// A hex encoding of its hash is available by calling `final`. |
| 466 | /// | 559 | /// |
| 467 | /// This function will also acquire an exclusive lock to the manifest file. This means | 560 | /// This function will also acquire an exclusive lock to the manifest file. This means |
| ... | @@ -473,50 +566,48 @@ pub const Manifest = struct { | ... | @@ -473,50 +566,48 @@ pub const Manifest = struct { |
| 473 | /// The lock on the manifest file is released when `deinit` is called. As another | 566 | /// The lock on the manifest file is released when `deinit` is called. As another |
| 474 | /// option, one may call `toOwnedLock` to obtain a smaller object which can represent | 567 | /// option, one may call `toOwnedLock` to obtain a smaller object which can represent |
| 475 | /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called. | 568 | /// the lock. `deinit` is safe to call whether or not `toOwnedLock` has been called. |
| 476 | pub fn hit(man: *Manifest, parent_progress_node: std.Progress.Node) HitError!bool { | 569 | pub fn check(man: *Manifest, parent_progress_node: std.Progress.Node) CheckError!CheckStatus { |
| 477 | const node = parent_progress_node.start("Reusing Cache Artifacts", 0); | 570 | const node = parent_progress_node.start("Reusing Cache Artifacts", 0); |
| 478 | defer node.end(); | 571 | defer node.end(); |
| 479 | return hitInner(man); | 572 | return checkProgressless(man); |
| 480 | } | 573 | } |
| 481 | | 574 | |
| 482 | pub fn hitInner(self: *Manifest) HitError!bool { | 575 | pub fn checkProgressless(man: *Manifest) CheckError!CheckStatus { |
| 483 | assert(self.manifest_file == null); | 576 | assert(man.manifest_file == null); |
| 484 | | 577 | |
| 485 | self.diagnostic = .none; | 578 | for (man.files.keys()[0..man.input_files.items.len]) |file_off| { |
| | 579 | file_off.get(man).manifestDigestHash(&man.hash.hasher); |
| | 580 | } |
| 486 | | 581 | |
| 487 | const ext = ".txt"; | 582 | man.diagnostic = .none; |
| 488 | var manifest_file_path: [hex_digest_len + ext.len]u8 = undefined; | | |
| 489 | | 583 | |
| 490 | var bin_digest: BinDigest = undefined; | 584 | var bin_digest: BinDigest = undefined; |
| 491 | self.hash.hasher.final(&bin_digest); | 585 | man.hash.hasher.final(&bin_digest); |
| 492 | | 586 | man.hex_digest = binToHex(bin_digest); |
| 493 | self.hex_digest = binToHex(bin_digest); | | |
| 494 | | 587 | |
| 495 | @memcpy(manifest_file_path[0..self.hex_digest.len], &self.hex_digest); | 588 | const manifest_file_path = &man.hex_digest; |
| 496 | manifest_file_path[hex_digest_len..][0..ext.len].* = ext.*; | 589 | const io = man.cache.io; |
| 497 | | | |
| 498 | const io = self.cache.io; | | |
| 499 | | 590 | |
| 500 | // We'll try to open the cache with an exclusive lock, but if that would block | 591 | // We'll try to open the cache with an exclusive lock, but if that would block |
| 501 | // and `want_shared_lock` is set, a shared lock might be sufficient, so we'll | 592 | // and `want_shared_lock` is set, a shared lock might be sufficient, so we'll |
| 502 | // open with a shared lock instead. | 593 | // open with a shared lock instead. |
| 503 | while (true) { | 594 | while (true) { |
| 504 | if (self.cache.manifest_dir.createFile(io, &manifest_file_path, .{ | 595 | if (man.cache.manifest_dir.createFile(io, manifest_file_path, .{ |
| 505 | .read = true, | 596 | .read = true, |
| 506 | .truncate = false, | 597 | .truncate = false, |
| 507 | .lock = .exclusive, | 598 | .lock = .exclusive, |
| 508 | .lock_nonblocking = self.want_shared_lock, | 599 | .lock_nonblocking = man.want_shared_lock, |
| 509 | })) |manifest_file| { | 600 | })) |manifest_file| { |
| 510 | self.manifest_file = manifest_file; | 601 | man.manifest_file = manifest_file; |
| 511 | self.have_exclusive_lock = true; | 602 | man.have_exclusive_lock = true; |
| 512 | break; | 603 | break; |
| 513 | } else |err| switch (err) { | 604 | } else |err| switch (err) { |
| 514 | error.WouldBlock => { | 605 | error.WouldBlock => { |
| 515 | self.manifest_file = self.cache.manifest_dir.openFile(io, &manifest_file_path, .{ | 606 | man.manifest_file = man.cache.manifest_dir.openFile(io, manifest_file_path, .{ |
| 516 | .mode = .read_write, | 607 | .mode = .read_write, |
| 517 | .lock = .shared, | 608 | .lock = .shared, |
| 518 | }) catch |e| { | 609 | }) catch |e| { |
| 519 | self.diagnostic = .{ .manifest_create = e }; | 610 | man.diagnostic = .{ .manifest_create = e }; |
| 520 | return error.CacheCheckFailed; | 611 | return error.CacheCheckFailed; |
| 521 | }; | 612 | }; |
| 522 | break; | 613 | break; |
| ... | @@ -532,315 +623,262 @@ pub const Manifest = struct { | ... | @@ -532,315 +623,262 @@ pub const Manifest = struct { |
| 532 | // failure was a race, or ENOENT, indicating deletion of | 623 | // failure was a race, or ENOENT, indicating deletion of |
| 533 | // the directory of our open handle. | 624 | // the directory of our open handle. |
| 534 | if (!builtin.os.tag.isDarwin()) { | 625 | if (!builtin.os.tag.isDarwin()) { |
| 535 | self.diagnostic = .{ .manifest_create = error.FileNotFound }; | 626 | man.diagnostic = .{ .manifest_create = error.FileNotFound }; |
| 536 | return error.CacheCheckFailed; | 627 | return error.CacheCheckFailed; |
| 537 | } | 628 | } |
| 538 | | 629 | |
| 539 | if (self.cache.manifest_dir.createFile(io, &manifest_file_path, .{ | 630 | if (man.cache.manifest_dir.createFile(io, manifest_file_path, .{ |
| 540 | .read = true, | 631 | .read = true, |
| 541 | .truncate = false, | 632 | .truncate = false, |
| 542 | .lock = .exclusive, | 633 | .lock = .exclusive, |
| 543 | .lock_nonblocking = self.want_shared_lock, | 634 | .lock_nonblocking = man.want_shared_lock, |
| 544 | .exclusive = true, | 635 | .exclusive = true, |
| 545 | })) |manifest_file| { | 636 | })) |manifest_file| { |
| 546 | self.manifest_file = manifest_file; | 637 | man.manifest_file = manifest_file; |
| 547 | self.have_exclusive_lock = true; | 638 | man.have_exclusive_lock = true; |
| 548 | break; | 639 | break; |
| 549 | } else |excl_err| switch (excl_err) { | 640 | } else |excl_err| switch (excl_err) { |
| 550 | error.WouldBlock, error.PathAlreadyExists => continue, | 641 | error.WouldBlock, error.PathAlreadyExists => continue, |
| 551 | error.FileNotFound => { | 642 | error.FileNotFound => { |
| 552 | self.diagnostic = .{ .manifest_create = error.FileNotFound }; | 643 | man.diagnostic = .{ .manifest_create = error.FileNotFound }; |
| 553 | return error.CacheCheckFailed; | 644 | return error.CacheCheckFailed; |
| 554 | }, | 645 | }, |
| 555 | error.Canceled => |e| return e, | 646 | error.Canceled => |e| return e, |
| 556 | else => |e| { | 647 | else => |e| { |
| 557 | self.diagnostic = .{ .manifest_create = e }; | 648 | man.diagnostic = .{ .manifest_create = e }; |
| 558 | return error.CacheCheckFailed; | 649 | return error.CacheCheckFailed; |
| 559 | }, | 650 | }, |
| 560 | } | 651 | } |
| 561 | }, | 652 | }, |
| 562 | error.Canceled => |e| return e, | 653 | error.Canceled => |e| return e, |
| 563 | else => |e| { | 654 | else => |e| { |
| 564 | self.diagnostic = .{ .manifest_create = e }; | 655 | man.diagnostic = .{ .manifest_create = e }; |
| 565 | return error.CacheCheckFailed; | 656 | return error.CacheCheckFailed; |
| 566 | }, | 657 | }, |
| 567 | } | 658 | } |
| 568 | } | 659 | } |
| 569 | | 660 | |
| 570 | self.want_refresh_timestamp = true; | 661 | man.want_refresh_timestamp = true; |
| 571 | | | |
| 572 | const input_file_count = self.files.entries.len; | | |
| 573 | | 662 | |
| 574 | // We're going to construct a second hash. Its input will begin with the digest we've | 663 | // We're going to construct a second hash. Its input will begin with the digest we've |
| 575 | // already computed (`bin_digest`), and then it'll have the digests of each input file, | 664 | // already computed (`bin_digest`), and then it'll have the digests of each input file, |
| 576 | // including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post" | 665 | // including "post" files (see `addFilePost`). If this is a hit, we learn the set of "post" |
| 577 | // files from the manifest on disk. If this is a miss, we'll learn those from future calls | 666 | // files from the manifest on disk. If this is a miss, we'll learn those from future calls |
| 578 | // to `addFilePost` etc. As such, the state of `self.hash.hasher` after this function | 667 | // to `addFilePost` etc. As such, the state of `man.hash.hasher` after this function |
| 579 | // depends on whether this is a hit or a miss. | 668 | // depends on whether this is a hit or a miss. |
| 580 | // | 669 | // |
| 581 | // If we return `true` indicating a cache hit, then `self.hash.hasher` must already include | 670 | // If we return `CacheStatus.hit`, then `man.hash.hasher` must already include |
| 582 | // the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache | 671 | // the digests of the "post" files, so the caller can call `final`. Otherwise, on a cache |
| 583 | // miss, `self.hash.hasher` will include the digests of all non-"post" files -- that is, | 672 | // miss, `man.hash.hasher` will include the digests of all non-"post" files -- that is, |
| 584 | // the ones we've already been told about. The rest will be discovered through calls to | 673 | // the ones we've already been told about. The rest will be discovered through calls to |
| 585 | // `addFilePost` etc, which will update the hasher. After all files are added, the user can | 674 | // `addFilePost` etc, which will update the hasher. After all files are added, the user can |
| 586 | // use `final`, and will at some point `writeManifest` the file list to disk. | 675 | // use `final`, and will at some point `writeManifest` the file list to disk. |
| 587 | | 676 | |
| 588 | self.hash.hasher = hasher_init; | 677 | man.hash.hasher = hasher_init; |
| 589 | self.hash.hasher.update(&bin_digest); | 678 | man.hash.hasher.update(&bin_digest); |
| 590 | | 679 | |
| 591 | hit: { | 680 | hit: { |
| 592 | const file_digests_populated: usize = digests: { | 681 | digests: { |
| 593 | switch (try self.hitWithCurrentLock()) { | 682 | switch (try man.checkLocked()) { |
| 594 | .hit => break :hit, | 683 | .hit => break :hit, |
| 595 | .miss => |m| if (!try self.upgradeToExclusiveLock()) { | 684 | .miss => if (!try man.upgradeToExclusiveLock()) break :digests, |
| 596 | break :digests m.file_digests_populated; | | |
| 597 | }, | | |
| 598 | } | 685 | } |
| 599 | // We've just had a miss with the shared lock, and upgraded to an exclusive lock. Someone | 686 | // We've just had a miss with the shared lock, and upgraded to an exclusive lock. Someone |
| 600 | // else might have modified the digest, so we need to check again before deciding to miss. | 687 | // else might have modified the digest, so we need to check again before deciding to miss. |
| 601 | // Before trying again, we must reset `self.hash.hasher` and `self.files`. | 688 | // Before trying again, we must reset `man.hash.hasher` and `man.files`. |
| 602 | // This is basically just the first half of `unhit`. | 689 | // This is basically just the first half of `unhit`. |
| 603 | self.hash.hasher = hasher_init; | 690 | man.hash.hasher = hasher_init; |
| 604 | self.hash.hasher.update(&bin_digest); | 691 | man.hash.hasher.update(&bin_digest); |
| 605 | while (self.files.count() != input_file_count) { | 692 | man.shrinkFilesToInput(); |
| 606 | var file = self.files.pop().?; | 693 | switch (try man.checkLocked()) { |
| 607 | file.key.deinit(self.cache.gpa); | | |
| 608 | } | | |
| 609 | switch (try self.hitWithCurrentLock()) { | | |
| 610 | .hit => break :hit, | 694 | .hit => break :hit, |
| 611 | .miss => |m| break :digests m.file_digests_populated, | 695 | .miss => break :digests, |
| 612 | } | | |
| 613 | }; | | |
| 614 | | | |
| 615 | // This is a guaranteed cache miss. We're almost ready to return `false`, but there's a | | |
| 616 | // little bookkeeping to do first. The first `file_digests_populated` entries in `files` | | |
| 617 | // have their `bin_digest` populated; there may be some left in `input_file_count` which | | |
| 618 | // we'll need to populate ourselves. Other than that, this is basically `unhit`. | | |
| 619 | self.manifest_dirty = true; | | |
| 620 | self.hash.hasher = hasher_init; | | |
| 621 | self.hash.hasher.update(&bin_digest); | | |
| 622 | while (self.files.count() != input_file_count) { | | |
| 623 | var file = self.files.pop().?; | | |
| 624 | file.key.deinit(self.cache.gpa); | | |
| 625 | } | | |
| 626 | for (self.files.keys(), 0..) |*file, idx| { | | |
| 627 | if (idx < file_digests_populated) { | | |
| 628 | // `bin_digest` is already populated by `hitWithCurrentLock`, so we can use it directly. | | |
| 629 | self.hash.hasher.update(&file.bin_digest); | | |
| 630 | } else { | | |
| 631 | self.populateFileHash(file) catch |err| { | | |
| 632 | self.diagnostic = .{ .file_hash = .{ | | |
| 633 | .file_index = idx, | | |
| 634 | .err = err, | | |
| 635 | } }; | | |
| 636 | return error.CacheCheckFailed; | | |
| 637 | }; | | |
| 638 | } | 696 | } |
| 639 | } | 697 | } |
| 640 | return false; | 698 | |
| | 699 | // Cache miss. `checkLocked` guarantees that all input files have their digests populated |
| | 700 | // unless it returns an error. |
| | 701 | man.manifest_dirty = true; |
| | 702 | // All input file digests are already populated by `checkLocked`, so we can call `unhit` directly. |
| | 703 | unhit(man, &bin_digest); |
| | 704 | return .miss; |
| 641 | } | 705 | } |
| 642 | | 706 | |
| 643 | if (self.want_shared_lock) { | 707 | if (man.want_shared_lock) { |
| 644 | self.downgradeToSharedLock() catch |err| { | 708 | man.downgradeToSharedLock() catch |err| { |
| 645 | self.diagnostic = .{ .manifest_lock = err }; | 709 | man.diagnostic = .{ .manifest_lock = err }; |
| 646 | return error.CacheCheckFailed; | 710 | return error.CacheCheckFailed; |
| 647 | }; | 711 | }; |
| 648 | } | 712 | } |
| 649 | | 713 | |
| 650 | return true; | 714 | return .hit; |
| | 715 | } |
| | 716 | |
| | 717 | fn shrinkFilesToInput(m: *Manifest) void { |
| | 718 | if (m.files.count() <= m.input_files.items.len) return; |
| | 719 | const off = m.files.keys()[m.input_files.items.len]; |
| | 720 | m.contents.shrinkRetainingCapacity(@backingInt(off)); |
| | 721 | assert(m.contents.len % @alignOf(File) == 0); |
| | 722 | m.files.shrinkRetainingCapacity(m.input_files.items.len); |
| 651 | } | 723 | } |
| 652 | | 724 | |
| 653 | /// Assumes that `self.hash.hasher` has been updated only with the original digest and that | 725 | /// Assumes that `self.hash.hasher` has been updated only with the original digest and that |
| 654 | /// `self.files` contains only the original input files. | 726 | /// `self.files` contains only the original input files. |
| 655 | fn hitWithCurrentLock(self: *Manifest) HitError!union(enum) { | 727 | fn checkLocked(m: *Manifest) CheckError!CheckStatus { |
| 656 | hit, | 728 | const gpa = m.cache.gpa; |
| 657 | miss: struct { | 729 | const io = m.cache.io; |
| 658 | file_digests_populated: usize, | 730 | |
| 659 | }, | 731 | var manifest_reader = m.manifest_file.?.reader(io, &.{}); // Reads positionally from zero. |
| 660 | } { | 732 | m.contents.clearRetainingCapacity(); |
| 661 | const gpa = self.cache.gpa; | 733 | manifest_reader.interface.appendRemainingUnlimited(gpa, &m.contents) catch |err| switch (err) { |
| 662 | const io = self.cache.io; | | |
| 663 | const input_file_count = self.files.entries.len; | | |
| 664 | var tiny_buffer: [1]u8 = undefined; // allows allocRemaining to detect limit exceeded | | |
| 665 | var manifest_reader = self.manifest_file.?.reader(io, &tiny_buffer); // Reads positionally from zero. | | |
| 666 | const limit: std.Io.Limit = .limited(manifest_file_size_max); | | |
| 667 | const file_contents = manifest_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) { | | |
| 668 | error.OutOfMemory => |e| return e, | 734 | error.OutOfMemory => |e| return e, |
| 669 | error.StreamTooLong => return error.OutOfMemory, | | |
| 670 | error.ReadFailed => { | 735 | error.ReadFailed => { |
| 671 | self.diagnostic = .{ .manifest_read = manifest_reader.err.? }; | 736 | m.diagnostic = .{ .manifest_read = manifest_reader.err.? }; |
| 672 | return error.CacheCheckFailed; | 737 | return error.CacheCheckFailed; |
| 673 | }, | 738 | }, |
| 674 | }; | 739 | }; |
| 675 | defer gpa.free(file_contents); | 740 | |
| 676 | | 741 | // Guess number of files based on manifest contents len to reduce allocations. |
| 677 | var any_file_changed = false; | 742 | try m.files.ensureUnusedCapacity(gpa, m.contents.len / (@sizeOf(File) + 32)); |
| 678 | var line_iter = mem.tokenizeScalar(u8, file_contents, '\n'); | 743 | |
| 679 | var idx: usize = 0; | 744 | var file_index: usize = 0; |
| 680 | const header_valid = valid: { | 745 | var off: usize = 0; |
| 681 | const line = line_iter.next() orelse break :valid false; | 746 | |
| 682 | break :valid std.mem.eql(u8, line, manifest_header); | 747 | // This group we always want to compute the hash digests, even on a cache miss. |
| | 748 | var input_group: Io.Group = .init; |
| | 749 | defer input_group.cancel(io); |
| | 750 | |
| | 751 | // This group we would like to cancel as soon as a cache miss is discovered. |
| | 752 | const PostResult = union(enum) { |
| | 753 | checkFile: CheckFileResult, |
| 683 | }; | 754 | }; |
| 684 | if (!header_valid) { | 755 | var post_select_buffer: [10]PostResult = undefined; |
| 685 | return .{ .miss = .{ .file_digests_populated = 0 } }; | 756 | var post_select: Io.Select(PostResult) = .init(&post_select_buffer); |
| | 757 | var post_select_remaining: usize = 0; |
| | 758 | defer post_select.cancel(io); |
| | 759 | |
| | 760 | while (off + 1 < m.contents.len) { |
| | 761 | const file_off: File.Offset = @fromBackingInt(off); |
| | 762 | const file = try File.getFallible(file_off, m); |
| | 763 | if (file.flags.prefix >= m.cache.prefixes_len) return error.InvalidFormat; |
| | 764 | const path = try file.pathFallible(); |
| | 765 | if (path.len == 0) return error.InvalidFormat; |
| | 766 | |
| | 767 | if (file_index < m.input_files.items.len) { |
| | 768 | if (m.files.keys()[file_index] != file_off) return error.InvalidFormat; |
| | 769 | |
| | 770 | input_group.async(io, checkFile, .{m.cache, file, path}); |
| | 771 | } else { |
| | 772 | try m.files.put(gpa, file_off); |
| | 773 | |
| | 774 | post_select.async(.checkFile, checkFile, .{m.cache, file, path}); |
| | 775 | post_select_remaining += 1; |
| | 776 | } |
| | 777 | |
| | 778 | file_index += 1; |
| | 779 | off += @sizeOf(File) + path.len + 1; |
| 686 | } | 780 | } |
| 687 | while (line_iter.next()) |line| { | | |
| 688 | defer idx += 1; | | |
| 689 | | | |
| 690 | var iter = mem.tokenizeScalar(u8, line, ' '); | | |
| 691 | const size = iter.next() orelse return error.InvalidFormat; | | |
| 692 | const inode = iter.next() orelse return error.InvalidFormat; | | |
| 693 | const mtime_nsec_str = iter.next() orelse return error.InvalidFormat; | | |
| 694 | const digest_str = iter.next() orelse return error.InvalidFormat; | | |
| 695 | const prefix_str = iter.next() orelse return error.InvalidFormat; | | |
| 696 | const file_path = iter.rest(); | | |
| 697 | | | |
| 698 | const stat_size = fmt.parseInt(u64, size, 10) catch return error.InvalidFormat; | | |
| 699 | const stat_inode = fmt.parseInt(Io.File.INode, inode, 10) catch return error.InvalidFormat; | | |
| 700 | const stat_mtime = fmt.parseInt(i64, mtime_nsec_str, 10) catch return error.InvalidFormat; | | |
| 701 | const file_bin_digest = b: { | | |
| 702 | if (digest_str.len != hex_digest_len) return error.InvalidFormat; | | |
| 703 | var bd: BinDigest = undefined; | | |
| 704 | _ = fmt.hexToBytes(&bd, digest_str) catch return error.InvalidFormat; | | |
| 705 | break :b bd; | | |
| 706 | }; | | |
| 707 | | 781 | |
| 708 | const prefix = fmt.parseInt(u8, prefix_str, 10) catch return error.InvalidFormat; | 782 | // Final terminating zero byte to distinguish empty manifest file from |
| 709 | if (prefix >= self.cache.prefixes_len) return error.InvalidFormat; | 783 | // manifest with zero files. |
| 710 | | 784 | const file_valid = off + 1 == m.contents.len and m.contents[off] == 0; |
| 711 | if (file_path.len == 0) return error.InvalidFormat; | 785 | if (!file_valid or file_index < m.input_files.items.len) { |
| 712 | | 786 | try input_group.await(io); |
| 713 | const cache_hash_file = f: { | 787 | return .miss; |
| 714 | const prefixed_path: PrefixedPath = .{ | 788 | } |
| 715 | .prefix = prefix, | | |
| 716 | .sub_path = file_path, // expires with file_contents | | |
| 717 | }; | | |
| 718 | if (idx < input_file_count) { | | |
| 719 | const file = &self.files.keys()[idx]; | | |
| 720 | if (!file.prefixed_path.eql(prefixed_path)) | | |
| 721 | return error.InvalidFormat; | | |
| 722 | | | |
| 723 | file.stat = .{ | | |
| 724 | .size = stat_size, | | |
| 725 | .inode = stat_inode, | | |
| 726 | .mtime = .{ .nanoseconds = stat_mtime }, | | |
| 727 | }; | | |
| 728 | file.bin_digest = file_bin_digest; | | |
| 729 | break :f file; | | |
| 730 | } | | |
| 731 | const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{}); | | |
| 732 | errdefer _ = self.files.pop(); | | |
| 733 | if (!gop.found_existing) { | | |
| 734 | gop.key_ptr.* = .{ | | |
| 735 | .prefixed_path = .{ | | |
| 736 | .prefix = prefix, | | |
| 737 | .sub_path = try gpa.dupe(u8, file_path), | | |
| 738 | }, | | |
| 739 | .contents = null, | | |
| 740 | .max_file_size = null, | | |
| 741 | .handle = null, | | |
| 742 | .stat = .{ | | |
| 743 | .size = stat_size, | | |
| 744 | .inode = stat_inode, | | |
| 745 | .mtime = .{ .nanoseconds = stat_mtime }, | | |
| 746 | }, | | |
| 747 | .bin_digest = file_bin_digest, | | |
| 748 | }; | | |
| 749 | } | | |
| 750 | break :f gop.key_ptr; | | |
| 751 | }; | | |
| 752 | | 789 | |
| 753 | const pp = cache_hash_file.prefixed_path; | 790 | // Don't track the trailing zero byte in contents. |
| 754 | const dir = self.cache.prefixes()[pp.prefix].handle; | 791 | m.contents.len -= 1; |
| 755 | const this_file = dir.openFile(io, pp.sub_path, .{ .mode = .read_only }) catch |err| switch (err) { | 792 | |
| 756 | error.FileNotFound => { | 793 | var post_await_buffer: [10]PostResult = undefined; |
| 757 | // Every digest before this one has been populated successfully. | 794 | while (post_select_remaining > 0) { |
| 758 | return .{ .miss = .{ .file_digests_populated = idx } }; | 795 | const n = try post_select.awaitMany(&post_await_buffer, 1); |
| 759 | }, | 796 | post_select_remaining -= n; |
| 760 | error.Canceled => |e| return e, | 797 | for (post_await_buffer[0..n]) |u| switch (u) { |
| 761 | else => |e| { | 798 | .checkFile => |result| switch (result) { |
| 762 | self.diagnostic = .{ .file_open = .{ | 799 | .hit => continue, |
| 763 | .file_index = idx, | 800 | .miss => { |
| 764 | .err = e, | 801 | post_select.cancelDiscard(); |
| 765 | } }; | 802 | try input_group.await(io); |
| 766 | return error.CacheCheckFailed; | 803 | return .miss; |
| | 804 | }, |
| | 805 | .fail => |diagnostic| { |
| | 806 | m.diagnostic = diagnostic; |
| | 807 | return error.CacheCheckFailed; |
| | 808 | }, |
| 767 | }, | 809 | }, |
| 768 | }; | 810 | }; |
| 769 | defer this_file.close(io); | 811 | } |
| 770 | | 812 | |
| 771 | const actual_stat = this_file.stat(io) catch |err| { | 813 | try input_group.await(io); |
| 772 | self.diagnostic = .{ .file_stat = .{ | | |
| 773 | .file_index = idx, | | |
| 774 | .err = err, | | |
| 775 | } }; | | |
| 776 | return error.CacheCheckFailed; | | |
| 777 | }; | | |
| 778 | const size_match = actual_stat.size == cache_hash_file.stat.size; | | |
| 779 | const mtime_match = actual_stat.mtime.nanoseconds == cache_hash_file.stat.mtime.nanoseconds; | | |
| 780 | const inode_match = actual_stat.inode == cache_hash_file.stat.inode; | | |
| 781 | | | |
| 782 | if (!size_match or !mtime_match or !inode_match) { | | |
| 783 | cache_hash_file.stat = .{ | | |
| 784 | .size = actual_stat.size, | | |
| 785 | .mtime = actual_stat.mtime, | | |
| 786 | .inode = actual_stat.inode, | | |
| 787 | }; | | |
| 788 | | | |
| 789 | if (try self.isProblematicTimestamp(cache_hash_file.stat.mtime)) { | | |
| 790 | // The actual file has an unreliable timestamp, force it to be hashed | | |
| 791 | cache_hash_file.stat.mtime = .zero; | | |
| 792 | cache_hash_file.stat.inode = 0; | | |
| 793 | } | | |
| 794 | | 814 | |
| 795 | var actual_digest: BinDigest = undefined; | 815 | for (m.files.keys()) |file_off| { |
| 796 | hashFile(io, this_file, &actual_digest) catch |err| { | 816 | m.hash.hasher.update(&file_off.get(m).digest); |
| 797 | self.diagnostic = .{ .file_read = .{ | 817 | } |
| 798 | .file_index = idx, | | |
| 799 | .err = err, | | |
| 800 | } }; | | |
| 801 | return error.CacheCheckFailed; | | |
| 802 | }; | | |
| 803 | | 818 | |
| 804 | if (!mem.eql(u8, &cache_hash_file.bin_digest, &actual_digest)) { | 819 | return .hit; |
| 805 | cache_hash_file.bin_digest = actual_digest; | 820 | } |
| 806 | // keep going until we have the input file digests | | |
| 807 | any_file_changed = true; | | |
| 808 | } | | |
| 809 | } | | |
| 810 | | 821 | |
| 811 | if (!any_file_changed) { | 822 | const CheckFileResult = union(enum) { |
| 812 | self.hash.hasher.update(&cache_hash_file.bin_digest); | 823 | hit, |
| 813 | } | 824 | miss, |
| 814 | } | 825 | fail: Diagnostic, |
| | 826 | }; |
| 815 | | 827 | |
| 816 | // If the manifest was somehow missing one of our input files, or if any file hash has changed, | 828 | /// Runs concurrently with other `checkFile`. |
| 817 | // then this is a cache miss. However, we have successfully populated some or all of the file | 829 | fn checkFile(cache: *const Cache, file: *File, file_path: [:0]const u8) Io.Cancelable!CheckFileResult { |
| 818 | // digests. | 830 | const io = cache.io; |
| 819 | if (any_file_changed or idx < input_file_count) { | 831 | const dir = cache.prefixes()[file.flags.prefix].handle; |
| 820 | return .{ .miss = .{ .file_digests_populated = idx } }; | 832 | |
| | 833 | const this_file = dir.openFile(io, file_path, .{ .mode = .read_only }) catch |err| switch (err) { |
| | 834 | error.FileNotFound => return .miss, |
| | 835 | error.Canceled => |e| return e, |
| | 836 | else => |e| return .{ .fail = .{ .file_open = .{ |
| | 837 | .file_index = file_index, |
| | 838 | .err = e, |
| | 839 | } }}, |
| | 840 | }; |
| | 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 | } |
| 821 | } | 864 | } |
| 822 | | 865 | |
| 823 | return .hit; | 866 | return .hit; |
| 824 | } | 867 | } |
| 825 | | 868 | |
| 826 | /// Reset `self.hash.hasher` to the state it should be in after `hit` returns `false`. | 869 | /// Reset `man.hash.hasher` to the state it should be in after `hit` returns `CheckStatus.miss`. |
| 827 | /// The hasher contains the original input digest, and all original input file digests (i.e. | 870 | /// The hasher contains the original input digest, and all original input file digests (i.e. |
| 828 | /// not including post files). | 871 | /// not including post files). |
| 829 | /// Assumes that `bin_digest` is populated for all files up to `input_file_count`. As such, | 872 | /// |
| 830 | /// this is not necessarily safe to call within `hit`. | 873 | /// Assumes that `bin_digest` is populated for all input files. |
| 831 | pub fn unhit(self: *Manifest, bin_digest: BinDigest, input_file_count: usize) void { | 874 | pub fn unhit(man: *Manifest, bin_digest: BinDigest) void { |
| 832 | // Reset the hash. | 875 | // Reset the hash. |
| 833 | self.hash.hasher = hasher_init; | 876 | man.hash.hasher = hasher_init; |
| 834 | self.hash.hasher.update(&bin_digest); | 877 | man.hash.hasher.update(&bin_digest); |
| 835 | | 878 | man.shrinkFilesToInput(); |
| 836 | // Remove files not in the initial hash. | 879 | for (man.files.keys()) |off| { |
| 837 | while (self.files.count() != input_file_count) { | 880 | const file = off.get(man); |
| 838 | var file = self.files.pop().?; | 881 | man.hash.hasher.update(&file.digest); |
| 839 | file.key.deinit(self.cache.gpa); | | |
| 840 | } | | |
| 841 | | | |
| 842 | for (self.files.keys()) |file| { | | |
| 843 | self.hash.hasher.update(&file.bin_digest); | | |
| 844 | } | 882 | } |
| 845 | } | 883 | } |
| 846 | | 884 | |
| ... | @@ -887,203 +925,133 @@ pub const Manifest = struct { | ... | @@ -887,203 +925,133 @@ pub const Manifest = struct { |
| 887 | return timestamp.nanoseconds >= man.recent_problematic_timestamp.nanoseconds; | 925 | return timestamp.nanoseconds >= man.recent_problematic_timestamp.nanoseconds; |
| 888 | } | 926 | } |
| 889 | | 927 | |
| 890 | fn populateFileHash(self: *Manifest, ch_file: *File) !void { | 928 | pub const AddFilePostOptions = struct { |
| 891 | const io = self.cache.io; | 929 | handle: union(enum) { |
| 892 | | 930 | file: ?Io.File, |
| 893 | if (ch_file.handle) |handle| { | 931 | dir: ?Io.Dir, |
| 894 | return populateFileHashHandle(self, ch_file, handle); | 932 | } = .{ .file = null }, |
| 895 | } else { | 933 | stat: ?Stat = null, |
| 896 | const pp = ch_file.prefixed_path; | 934 | contents: ?[]const u8 = null, |
| 897 | const dir = self.cache.prefixes()[pp.prefix].handle; | 935 | metadata_only: bool = false, |
| 898 | const handle = try dir.openFile(io, pp.sub_path, .{}); | 936 | }; |
| 899 | defer handle.close(io); | | |
| 900 | return populateFileHashHandle(self, ch_file, handle); | | |
| 901 | } | | |
| 902 | } | | |
| 903 | | | |
| 904 | fn populateFileHashHandle(self: *Manifest, ch_file: *File, io_file: Io.File) !void { | | |
| 905 | const io = self.cache.io; | | |
| 906 | const gpa = self.cache.gpa; | | |
| 907 | | 937 | |
| 908 | const actual_stat = try io_file.stat(io); | 938 | pub const AddFilePostError = error { |
| 909 | ch_file.stat = .{ | 939 | /// The same file path has been added to the cache manifest both as a |
| 910 | .size = actual_stat.size, | 940 | /// directory and as a normal file, making the intended caching |
| 911 | .mtime = actual_stat.mtime, | 941 | /// behavior ambiguous. |
| 912 | .inode = actual_stat.inode, | 942 | IsDirectoryAmbiguous, |
| 913 | }; | 943 | } || Allocator.Error; |
| | 944 | |
| | 945 | /// Add a file as a dependency of process being cached, after cache miss |
| | 946 | /// occurs. |
| | 947 | pub fn addFilePost(m: *Manifest, path: Path, options: AddFilePostOptions) AddFilePostError!void { |
| | 948 | assert(m.manifest_file != null); |
| | 949 | const cache = m.cache; |
| | 950 | const gpa = cache.gpa; |
| | 951 | const io = cache.io; |
| | 952 | const is_directory = options.handle == .dir; |
| 914 | | 953 | |
| 915 | if (try self.isProblematicTimestamp(ch_file.stat.mtime)) { | 954 | try m.files.ensureUnusedCapacity(gpa, 1); |
| 916 | // The actual file has an unreliable timestamp, force it to be hashed | | |
| 917 | ch_file.stat.mtime = .zero; | | |
| 918 | ch_file.stat.inode = 0; | | |
| 919 | } | | |
| 920 | | 955 | |
| 921 | if (ch_file.max_file_size) |max_file_size| { | 956 | const prev_contents_len = m.contents.items.len; |
| 922 | if (ch_file.stat.size > max_file_size) return error.FileTooBig; | 957 | const new_header: *File = @ptrCast(try m.contents.addManyAsSlice(gpa, @sizeOf(File))); |
| | 958 | errdefer m.contents.shrinkRetainingCapacity(prev_contents_len); |
| 923 | | 959 | |
| 924 | // Hash while reading from disk, to keep the contents in the cpu | 960 | new_header.* = .{ |
| 925 | // cache while doing hashing. | 961 | .flags = .{ |
| 926 | const contents = try gpa.alloc(u8, @intCast(ch_file.stat.size)); | 962 | .prefix = try cache.findAppendPrefixedPath(&m.contents, path), |
| 927 | errdefer gpa.free(contents); | 963 | .is_directory = is_directory, |
| | 964 | .metadata_only = options.metadata_only, |
| | 965 | }, |
| | 966 | .size = undefined, |
| | 967 | .inode = undefined, |
| | 968 | .mtime = undefined, |
| | 969 | .digest = @splat(0), |
| | 970 | }; |
| | 971 | assert(m.contents.items.len % @alignOf(File) == 0); |
| 928 | | 972 | |
| 929 | var hasher = hasher_init; | 973 | const gop = m.files.getOrPutAssumeCapacity(@fromBackingInt(prev_contents_len), .{ |
| 930 | var off: usize = 0; | 974 | .manifest = m, |
| 931 | while (true) { | 975 | }); |
| 932 | const bytes_read = try io_file.readPositional(io, &.{contents[off..]}, off); | 976 | m.files.lockPointers(); |
| 933 | if (bytes_read == 0) break; | 977 | defer m.files.unlockPointers(); |
| 934 | hasher.update(contents[off..][0..bytes_read]); | 978 | |
| 935 | off += bytes_read; | 979 | const header = if (gop.found_existing) h: { |
| | 980 | m.contents.shrinkRetainingCapacity(prev_contents_len); |
| | 981 | const existing_off = gop.key_ptr.*; |
| | 982 | const header = existing_off.get(m); |
| | 983 | if (header.flags.is_directory != is_directory) |
| | 984 | return error.IsDirectoryAmbiguous; |
| | 985 | if (!options.metadata_only) |
| | 986 | header.flags.metadata_only = false; |
| | 987 | break :h header; |
| | 988 | } else new_header; |
| | 989 | |
| | 990 | if (options.stat) |stat| { |
| | 991 | try header.setStat(m, stat); |
| | 992 | if (header.metadata_only) { |
| | 993 | return; |
| | 994 | } else if (options.contents) |contents| { |
| | 995 | var hasher = hasher_init; |
| | 996 | hasher.update(contents); |
| | 997 | hasher.final(&header.digest); |
| | 998 | return; |
| 936 | } | 999 | } |
| 937 | hasher.final(&ch_file.bin_digest); | | |
| 938 | | | |
| 939 | ch_file.contents = contents; | | |
| 940 | } else { | | |
| 941 | try hashFile(io, io_file, &ch_file.bin_digest); | | |
| 942 | } | 1000 | } |
| 943 | | 1001 | |
| 944 | self.hash.hasher.update(&ch_file.bin_digest); | 1002 | const need_stat = options.stat == null; |
| 945 | } | | |
| 946 | | 1003 | |
| 947 | /// Add a file as a dependency of process being cached, after the initial hash has been | 1004 | switch (options.handle) { |
| 948 | /// calculated. This is useful for processes that don't know all the files that | 1005 | .dir => |opt_handle| if (opt_handle) |handle| { |
| 949 | /// are depended on ahead of time. For example, a source file that can import other files | 1006 | try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only); |
| 950 | /// will need to be recompiled if the imported file is changed. | 1007 | } else { |
| 951 | pub fn addFilePostFetch(self: *Manifest, file_path: []const u8, max_file_size: usize) ![]const u8 { | 1008 | const dir = cache.prefixes()[header.flags.prefix].handle; |
| 952 | assert(self.manifest_file != null); | 1009 | const handle = try dir.openDir(io, header.path(), .{ .access_sub_paths = false, .iterate = true, }); |
| 953 | | 1010 | defer handle.close(io); |
| 954 | const gpa = self.cache.gpa; | 1011 | try populateDirectory(m, header, need_stat, handle, options.contents, header.metadata_only); |
| 955 | const prefixed_path = try self.cache.findPrefix(file_path); | 1012 | }, |
| 956 | errdefer gpa.free(prefixed_path.sub_path); | | |
| 957 | | | |
| 958 | const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{}); | | |
| 959 | errdefer _ = self.files.pop(); | | |
| 960 | | 1013 | |
| 961 | if (gop.found_existing) { | 1014 | .file => |opt_handle| if (opt_handle) |handle| { |
| 962 | gpa.free(prefixed_path.sub_path); | 1015 | try populateFile(m, header, need_stat, handle, options.contents, header.metadata_only); |
| 963 | return gop.key_ptr.contents.?; | 1016 | } else { |
| | 1017 | const dir = cache.prefixes()[header.flags.prefix].handle; |
| | 1018 | const handle = try dir.openFile(io, header.path(), .{ .mode = .read_only }); |
| | 1019 | defer handle.close(io); |
| | 1020 | try populateFile(m, header, need_stat, handle, options.contents, header.metadata_only); |
| | 1021 | }, |
| 964 | } | 1022 | } |
| 965 | | | |
| 966 | gop.key_ptr.* = .{ | | |
| 967 | .prefixed_path = prefixed_path, | | |
| 968 | .max_file_size = max_file_size, | | |
| 969 | .stat = undefined, | | |
| 970 | .bin_digest = undefined, | | |
| 971 | .contents = null, | | |
| 972 | .handle = null, | | |
| 973 | }; | | |
| 974 | | | |
| 975 | self.files.lockPointers(); | | |
| 976 | defer self.files.unlockPointers(); | | |
| 977 | | | |
| 978 | try self.populateFileHash(gop.key_ptr); | | |
| 979 | return gop.key_ptr.contents.?; | | |
| 980 | } | | |
| 981 | | | |
| 982 | /// Add a file as a dependency of process being cached, after the initial hash has been | | |
| 983 | /// calculated. | | |
| 984 | /// | | |
| 985 | /// This is useful for processes that don't know the all the files that are | | |
| 986 | /// depended on ahead of time. For example, a source file that can import | | |
| 987 | /// other files will need to be recompiled if the imported file is changed. | | |
| 988 | pub fn addFilePost(man: *Manifest, file_path: []const u8) !void { | | |
| 989 | assert(man.manifest_file != null); | | |
| 990 | const gpa = man.cache.gpa; | | |
| 991 | const prefixed_path = try man.cache.findPrefix(file_path); | | |
| 992 | var keep = false; | | |
| 993 | defer if (!keep) gpa.free(prefixed_path.sub_path); | | |
| 994 | keep = try addPrefixedPathPost(man, prefixed_path); | | |
| 995 | } | 1023 | } |
| 996 | | 1024 | |
| 997 | pub fn addPathPost(man: *Manifest, path: Path) !void { | 1025 | fn populateFile(m: *Manifest, file: *File, need_stat: bool, handle: Io.File, contents: ?[]const u8, metadata_only: bool,) !void { |
| 998 | assert(man.manifest_file != null); | 1026 | const io = m.cache.io; |
| 999 | const gpa = man.cache.gpa; | | |
| 1000 | const prefixed_path: PrefixedPath = try man.cache.findPrefixPath(path); | | |
| 1001 | var keep = false; | | |
| 1002 | defer if (!keep) gpa.free(prefixed_path.sub_path); | | |
| 1003 | keep = try addPrefixedPathPost(man, prefixed_path); | | |
| 1004 | } | | |
| 1005 | | | |
| 1006 | /// Low level function. `prefixed_path` references cloned memory. Returns | | |
| 1007 | /// whether or not `prefixed_path.sub_path` should be kept. | | |
| 1008 | pub fn addPrefixedPathPost(man: *Manifest, prefixed_path: PrefixedPath) !bool { | | |
| 1009 | assert(man.manifest_file != null); | | |
| 1010 | const gpa = man.cache.gpa; | | |
| 1011 | | | |
| 1012 | const gop = try man.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{}); | | |
| 1013 | errdefer _ = man.files.pop(); | | |
| 1014 | | | |
| 1015 | if (gop.found_existing) return false; | | |
| 1016 | | | |
| 1017 | gop.key_ptr.* = .{ | | |
| 1018 | .prefixed_path = prefixed_path, | | |
| 1019 | .max_file_size = null, | | |
| 1020 | .handle = null, | | |
| 1021 | .stat = undefined, | | |
| 1022 | .bin_digest = undefined, | | |
| 1023 | .contents = null, | | |
| 1024 | }; | | |
| 1025 | | | |
| 1026 | man.files.lockPointers(); | | |
| 1027 | defer man.files.unlockPointers(); | | |
| 1028 | | | |
| 1029 | try man.populateFileHash(gop.key_ptr); | | |
| 1030 | return true; | | |
| 1031 | } | | |
| 1032 | | | |
| 1033 | /// Like `addFilePost` but when the file contents have already been loaded from disk. | | |
| 1034 | pub fn addFilePostContents( | | |
| 1035 | man: *Manifest, | | |
| 1036 | file_path: []const u8, | | |
| 1037 | bytes: []const u8, | | |
| 1038 | stat: File.Stat, | | |
| 1039 | ) !void { | | |
| 1040 | assert(man.manifest_file != null); | | |
| 1041 | const gpa = man.cache.gpa; | | |
| 1042 | const prefixed_path = try man.cache.findPrefix(file_path); | | |
| 1043 | var keep = false; | | |
| 1044 | defer if (!keep) gpa.free(prefixed_path.sub_path); | | |
| 1045 | keep = try addPrefixedPathPostContents(man, prefixed_path, bytes, stat); | | |
| 1046 | } | | |
| 1047 | | | |
| 1048 | /// Low level function. `prefixed_path` references cloned memory. Returns | | |
| 1049 | /// whether or not `prefixed_path.sub_path` should be kept. | | |
| 1050 | pub fn addPrefixedPathPostContents( | | |
| 1051 | man: *Manifest, | | |
| 1052 | prefixed_path: PrefixedPath, | | |
| 1053 | bytes: []const u8, | | |
| 1054 | stat: File.Stat, | | |
| 1055 | ) !bool { | | |
| 1056 | const gpa = man.cache.gpa; | | |
| 1057 | const gop = try man.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{}); | | |
| 1058 | errdefer _ = man.files.pop(); | | |
| 1059 | | 1027 | |
| 1060 | if (gop.found_existing) return false; | 1028 | if (need_stat) { |
| 1061 | | 1029 | const stat = try handle.stat(io); |
| 1062 | const new_file = gop.key_ptr; | 1030 | try file.setStat(m, stat); |
| 1063 | | | |
| 1064 | new_file.* = .{ | | |
| 1065 | .prefixed_path = prefixed_path, | | |
| 1066 | .max_file_size = null, | | |
| 1067 | .handle = null, | | |
| 1068 | .stat = stat, | | |
| 1069 | .bin_digest = undefined, | | |
| 1070 | .contents = null, | | |
| 1071 | }; | | |
| 1072 | | | |
| 1073 | if (try man.isProblematicTimestamp(new_file.stat.mtime)) { | | |
| 1074 | // The actual file has an unreliable timestamp, force it to be hashed | | |
| 1075 | new_file.stat.mtime = .zero; | | |
| 1076 | new_file.stat.inode = 0; | | |
| 1077 | } | 1031 | } |
| 1078 | | 1032 | if (metadata_only) return; |
| 1079 | { | 1033 | if (contents) |bytes| { |
| 1080 | var hasher = hasher_init; | 1034 | var hasher = hasher_init; |
| 1081 | hasher.update(bytes); | 1035 | hasher.update(bytes); |
| 1082 | hasher.final(&new_file.bin_digest); | 1036 | hasher.final(&file.digest); |
| | 1037 | } else { |
| | 1038 | try hashFile(io, handle, &file.digest); |
| 1083 | } | 1039 | } |
| | 1040 | } |
| 1084 | | 1041 | |
| 1085 | man.hash.hasher.update(&new_file.bin_digest); | 1042 | fn populateDirectory(m: *Manifest, file: *File, need_stat: bool, handle: Io.File, contents: ?[]const u8, metadata_only: bool,) !void { |
| 1086 | return true; | 1043 | _ = m; |
| | 1044 | _ = file; |
| | 1045 | _ = need_stat; |
| | 1046 | _ = handle; |
| | 1047 | _ = contents; |
| | 1048 | _ = metadata_only; |
| | 1049 | @panic("TODO"); |
| | 1050 | } |
| | 1051 | |
| | 1052 | pub fn addDepFile(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { |
| | 1053 | assert(self.manifest_file == null); |
| | 1054 | return self.addDepFileMaybePost(dir, dep_file_sub_path); |
| 1087 | } | 1055 | } |
| 1088 | | 1056 | |
| 1089 | pub fn addDepFilePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { | 1057 | pub fn addDepFilePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { |
| ... | @@ -1094,7 +1062,7 @@ pub const Manifest = struct { | ... | @@ -1094,7 +1062,7 @@ pub const Manifest = struct { |
| 1094 | fn addDepFileMaybePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { | 1062 | fn addDepFileMaybePost(self: *Manifest, dir: Io.Dir, dep_file_sub_path: []const u8) !void { |
| 1095 | const gpa = self.cache.gpa; | 1063 | const gpa = self.cache.gpa; |
| 1096 | const io = self.cache.io; | 1064 | const io = self.cache.io; |
| 1097 | const dep_file_contents = try dir.readFileAlloc(io, dep_file_sub_path, gpa, .limited(manifest_file_size_max)); | 1065 | const dep_file_contents = try dir.readFileAlloc(io, dep_file_sub_path, gpa, .limited(file_size_max)); |
| 1098 | defer gpa.free(dep_file_contents); | 1066 | defer gpa.free(dep_file_contents); |
| 1099 | | 1067 | |
| 1100 | var error_buf: std.ArrayList(u8) = .empty; | 1068 | var error_buf: std.ArrayList(u8) = .empty; |
| ... | @@ -1151,39 +1119,24 @@ pub const Manifest = struct { | ... | @@ -1151,39 +1119,24 @@ pub const Manifest = struct { |
| 1151 | | 1119 | |
| 1152 | /// If `want_shared_lock` is true, this function automatically downgrades the | 1120 | /// If `want_shared_lock` is true, this function automatically downgrades the |
| 1153 | /// lock from exclusive to shared. | 1121 | /// lock from exclusive to shared. |
| 1154 | pub fn writeManifest(self: *Manifest) !void { | 1122 | pub fn writeManifest(m: *Manifest) !void { |
| 1155 | assert(self.have_exclusive_lock); | 1123 | assert(m.have_exclusive_lock); |
| 1156 | const io = self.cache.io; | 1124 | const io = m.cache.io; |
| 1157 | const manifest_file = self.manifest_file.?; | 1125 | const manifest_file = m.manifest_file.?; |
| 1158 | if (self.manifest_dirty) { | 1126 | if (m.manifest_dirty) { |
| 1159 | self.manifest_dirty = false; | 1127 | |
| 1160 | | 1128 | m.contents.appendAssumeCapacity(0); |
| 1161 | var buffer: [4000]u8 = undefined; | 1129 | defer _ = m.contents.pop().?; |
| 1162 | var fw = manifest_file.writer(io, &buffer); | 1130 | |
| 1163 | writeDirtyManifestToStream(self, &fw) catch |err| switch (err) { | 1131 | try manifest_file.setLength(io, m.contents.items.len); |
| 1164 | error.WriteFailed => return fw.err.?, | 1132 | try manifest_file.writePositionalAll(io, m.contents.items, 0); |
| 1165 | else => |e| return e, | | |
| 1166 | }; | | |
| 1167 | } | | |
| 1168 | | 1133 | |
| 1169 | if (self.want_shared_lock) { | 1134 | m.manifest_dirty = false; |
| 1170 | try self.downgradeToSharedLock(); | | |
| 1171 | } | 1135 | } |
| 1172 | } | | |
| 1173 | | 1136 | |
| 1174 | fn writeDirtyManifestToStream(self: *Manifest, fw: *Io.File.Writer) !void { | 1137 | if (m.want_shared_lock) { |
| 1175 | try fw.interface.writeAll(manifest_header ++ "\n"); | 1138 | try m.downgradeToSharedLock(); |
| 1176 | for (self.files.keys()) |file| { | | |
| 1177 | try fw.interface.print("{d} {d} {d} {x} {d} {s}\n", .{ | | |
| 1178 | file.stat.size, | | |
| 1179 | file.stat.inode, | | |
| 1180 | file.stat.mtime, | | |
| 1181 | &file.bin_digest, | | |
| 1182 | file.prefixed_path.prefix, | | |
| 1183 | file.prefixed_path.sub_path, | | |
| 1184 | }); | | |
| 1185 | } | 1139 | } |
| 1186 | try fw.end(); | | |
| 1187 | } | 1140 | } |
| 1188 | | 1141 | |
| 1189 | fn downgradeToSharedLock(self: *Manifest) !void { | 1142 | fn downgradeToSharedLock(self: *Manifest) !void { |
| ... | @@ -1275,33 +1228,40 @@ pub const Manifest = struct { | ... | @@ -1275,33 +1228,40 @@ pub const Manifest = struct { |
| 1275 | | 1228 | |
| 1276 | pub fn populateOtherManifest(man: *Manifest, other: *Manifest, prefix_map: [5]u8) Allocator.Error!void { | 1229 | pub fn populateOtherManifest(man: *Manifest, other: *Manifest, prefix_map: [5]u8) Allocator.Error!void { |
| 1277 | const gpa = other.cache.gpa; | 1230 | const gpa = other.cache.gpa; |
| | 1231 | assert(other.manifest_file != null); |
| 1278 | assert(@typeInfo(std.zig.Server.Message.PathPrefix).@"enum".field_names.len == man.cache.prefixes_len); | 1232 | assert(@typeInfo(std.zig.Server.Message.PathPrefix).@"enum".field_names.len == man.cache.prefixes_len); |
| 1279 | assert(man.cache.prefixes_len == 5); | 1233 | assert(man.cache.prefixes_len == 5); |
| 1280 | for (man.files.keys()) |file| { | | |
| 1281 | const prefixed_path: PrefixedPath = .{ | | |
| 1282 | .prefix = prefix_map[file.prefixed_path.prefix], | | |
| 1283 | .sub_path = try gpa.dupe(u8, file.prefixed_path.sub_path), | | |
| 1284 | }; | | |
| 1285 | errdefer gpa.free(prefixed_path.sub_path); | | |
| 1286 | | 1234 | |
| 1287 | const gop = try other.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{}); | 1235 | const orig_files_len = other.files.count(); |
| 1288 | errdefer _ = other.files.pop(); | 1236 | const orig_contents_len = other.contents.items.len; |
| | 1237 | errdefer { |
| | 1238 | other.files.shrinkRetainingCapacity(orig_files_len); |
| | 1239 | other.contents.shrinkRetainingCapacity(orig_contents_len); |
| | 1240 | } |
| | 1241 | |
| | 1242 | for (man.files.keys(), 0..) |off, file_index| { |
| | 1243 | try other.files.ensureUnusedCapacity(gpa, 1); |
| | 1244 | |
| | 1245 | const next_off = if (file_index < man.files.count()) |
| | 1246 | @backingInt(man.files.keys()[file_index + 1]) |
| | 1247 | else |
| | 1248 | man.contents.items.len; |
| | 1249 | |
| | 1250 | const copy_bytes = man.contents.items[@backingInt(off)..next_off]; |
| | 1251 | const prev_contents_len = other.contents.items.len; |
| | 1252 | try other.contents.appendSlice(gpa, copy_bytes); |
| | 1253 | |
| | 1254 | const gop = other.files.getOrPutAssumeCapacity(@fromBackingInt(prev_contents_len), .{ |
| | 1255 | .manifest = other, |
| | 1256 | }); |
| 1289 | | 1257 | |
| 1290 | if (gop.found_existing) { | 1258 | if (gop.found_existing) { |
| 1291 | gpa.free(prefixed_path.sub_path); | 1259 | other.contents.shrinkRetainingCapacity(prev_contents_len); |
| 1292 | continue; | 1260 | continue; |
| 1293 | } | 1261 | } |
| 1294 | | 1262 | |
| 1295 | gop.key_ptr.* = .{ | 1263 | const other_file = File.get(@fromBackingInt(prev_contents_len)); |
| 1296 | .prefixed_path = prefixed_path, | 1264 | other_file.prefix = prefix_map[other_file.prefix]; |
| 1297 | .max_file_size = file.max_file_size, | | |
| 1298 | .handle = file.handle, | | |
| 1299 | .stat = file.stat, | | |
| 1300 | .bin_digest = file.bin_digest, | | |
| 1301 | .contents = null, | | |
| 1302 | }; | | |
| 1303 | | | |
| 1304 | other.hash.hasher.update(&gop.key_ptr.bin_digest); | | |
| 1305 | } | 1265 | } |
| 1306 | } | 1266 | } |
| 1307 | }; | 1267 | }; |