| ... | @@ -104,6 +104,12 @@ pub const LazyStatus = enum { | ... | @@ -104,6 +104,12 @@ pub const LazyStatus = enum { |
| 104 | unavailable, | 104 | unavailable, |
| 105 | }; | 105 | }; |
| 106 | | 106 | |
| | 107 | pub const LocalStorage = struct { |
| | 108 | cache_root: Cache.Path, |
| | 109 | /// Path to "zig-pkg" inside the package in which the user ran `zig build`. |
| | 110 | pkg_root: Cache.Path, |
| | 111 | }; |
| | 112 | |
| 107 | /// Contains shared state among all `Fetch` tasks. | 113 | /// Contains shared state among all `Fetch` tasks. |
| 108 | pub const JobQueue = struct { | 114 | pub const JobQueue = struct { |
| 109 | io: Io, | 115 | io: Io, |
| ... | @@ -122,9 +128,8 @@ pub const JobQueue = struct { | ... | @@ -122,9 +128,8 @@ pub const JobQueue = struct { |
| 122 | /// This tracks `Fetch` tasks as well as recompression tasks. | 128 | /// This tracks `Fetch` tasks as well as recompression tasks. |
| 123 | group: Io.Group = .init, | 129 | group: Io.Group = .init, |
| 124 | global_cache: Cache.Directory, | 130 | global_cache: Cache.Directory, |
| 125 | local_cache: Cache.Path, | 131 | /// If `null`, indicates fetch globally only. |
| 126 | /// Path to "zig-pkg" inside the package in which the user ran `zig build`. | 132 | local_storage: ?*const LocalStorage, |
| 127 | root_pkg_path: Cache.Path, | | |
| 128 | /// If true then, no fetching occurs, and: | 133 | /// If true then, no fetching occurs, and: |
| 129 | /// * The `global_cache` directory is assumed to be the direct parent | 134 | /// * The `global_cache` directory is assumed to be the direct parent |
| 130 | /// directory of on-disk packages rather than having the "p/" directory | 135 | /// directory of on-disk packages rather than having the "p/" directory |
| ... | @@ -341,7 +346,7 @@ pub const JobQueue = struct { | ... | @@ -341,7 +346,7 @@ pub const JobQueue = struct { |
| 341 | ); | 346 | ); |
| 342 | } | 347 | } |
| 343 | | 348 | |
| 344 | fn recompress(jq: *JobQueue, package_hash: Package.Hash) Io.Cancelable!void { | 349 | fn recompress(jq: *JobQueue, package_hash: Package.Hash, package_root: Cache.Path) Io.Cancelable!void { |
| 345 | const pkg_hash_slice = package_hash.toSlice(); | 350 | const pkg_hash_slice = package_hash.toSlice(); |
| 346 | | 351 | |
| 347 | const prog_node = jq.prog_node.startFmt(0, "recompress {s}", .{pkg_hash_slice}); | 352 | const prog_node = jq.prog_node.startFmt(0, "recompress {s}", .{pkg_hash_slice}); |
| ... | @@ -359,7 +364,7 @@ pub const JobQueue = struct { | ... | @@ -359,7 +364,7 @@ pub const JobQueue = struct { |
| 359 | defer arena_instance.deinit(); | 364 | defer arena_instance.deinit(); |
| 360 | const arena = arena_instance.allocator(); | 365 | const arena = arena_instance.allocator(); |
| 361 | | 366 | |
| 362 | recompressFallible(jq, arena, dest_path, pkg_hash_slice, prog_node) catch |err| switch (err) { | 367 | recompressFallible(jq, arena, dest_path, pkg_hash_slice, package_root, prog_node) catch |err| switch (err) { |
| 363 | error.Canceled => |e| return e, | 368 | error.Canceled => |e| return e, |
| 364 | error.ReadFailed => comptime unreachable, | 369 | error.ReadFailed => comptime unreachable, |
| 365 | error.WriteFailed => comptime unreachable, | 370 | error.WriteFailed => comptime unreachable, |
| ... | @@ -372,6 +377,7 @@ pub const JobQueue = struct { | ... | @@ -372,6 +377,7 @@ pub const JobQueue = struct { |
| 372 | arena: Allocator, | 377 | arena: Allocator, |
| 373 | dest_path: Cache.Path, | 378 | dest_path: Cache.Path, |
| 374 | pkg_hash_slice: []const u8, | 379 | pkg_hash_slice: []const u8, |
| | 380 | package_root: Cache.Path, |
| 375 | prog_node: std.Progress.Node, | 381 | prog_node: std.Progress.Node, |
| 376 | ) !void { | 382 | ) !void { |
| 377 | const gpa = jq.http_client.allocator; | 383 | const gpa = jq.http_client.allocator; |
| ... | @@ -386,7 +392,7 @@ pub const JobQueue = struct { | ... | @@ -386,7 +392,7 @@ pub const JobQueue = struct { |
| 386 | var scanned_files: std.ArrayList(ScannedFile) = .empty; | 392 | var scanned_files: std.ArrayList(ScannedFile) = .empty; |
| 387 | defer scanned_files.deinit(gpa); | 393 | defer scanned_files.deinit(gpa); |
| 388 | | 394 | |
| 389 | var pkg_dir = try jq.root_pkg_path.openDir(io, pkg_hash_slice, .{ .iterate = true }); | 395 | var pkg_dir = try package_root.root_dir.handle.openDir(io, package_root.sub_path, .{ .iterate = true }); |
| 390 | defer pkg_dir.close(io); | 396 | defer pkg_dir.close(io); |
| 391 | | 397 | |
| 392 | { | 398 | { |
| ... | @@ -513,7 +519,6 @@ pub fn run(f: *Fetch) RunError!void { | ... | @@ -513,7 +519,6 @@ pub fn run(f: *Fetch) RunError!void { |
| 513 | const eb = &f.error_bundle; | 519 | const eb = &f.error_bundle; |
| 514 | const arena = f.arena.allocator(); | 520 | const arena = f.arena.allocator(); |
| 515 | const gpa = f.arena.child_allocator; | 521 | const gpa = f.arena.child_allocator; |
| 516 | const local_cache_root = job_queue.local_cache; | | |
| 517 | | 522 | |
| 518 | try eb.init(gpa); | 523 | try eb.init(gpa); |
| 519 | | 524 | |
| ... | @@ -534,32 +539,16 @@ pub fn run(f: *Fetch) RunError!void { | ... | @@ -534,32 +539,16 @@ pub fn run(f: *Fetch) RunError!void { |
| 534 | ); | 539 | ); |
| 535 | // Packages fetched by URL may not use relative paths to escape outside the | 540 | // Packages fetched by URL may not use relative paths to escape outside the |
| 536 | // fetched package directory from within the package cache. | 541 | // fetched package directory from within the package cache. |
| 537 | if (pkg_root.root_dir.eql(local_cache_root.root_dir)) { | 542 | |
| 538 | // `parent_package_root.sub_path` contains a path like this: | 543 | // This code path is only reachable recursively and the sub_path |
| 539 | // "p/$hash", or | 544 | // will already have been resolved to no longer have extra ".." or |
| 540 | // "p/$hash/foo", with possibly more directories after "foo". | 545 | // "." components. |
| 541 | // We want to fail unless the resolved relative path has a | 546 | assert(job_queue.local_storage != null); |
| 542 | // prefix of "p/$hash/". | 547 | assert(pkg_root.root_dir.eql(f.parent_package_root.root_dir)); |
| 543 | const prefix_len: usize = if (job_queue.read_only) 0 else "p/".len; | 548 | if (!std.mem.startsWith(u8, pkg_root.sub_path, f.parent_package_root.sub_path)) return f.fail( |
| 544 | const parent_sub_path = f.parent_package_root.sub_path; | 549 | f.location_tok, |
| 545 | const end = find_end: { | 550 | try eb.printString("dependency path outside project: '{f}'", .{pkg_root}), |
| 546 | if (parent_sub_path.len > prefix_len) { | 551 | ); |
| 547 | // Use `isSep` instead of `indexOfScalarPos` to account for | | |
| 548 | // Windows accepting both `\` and `/` as path separators. | | |
| 549 | for (parent_sub_path[prefix_len..], prefix_len..) |c, i| { | | |
| 550 | if (std.fs.path.isSep(c)) break :find_end i; | | |
| 551 | } | | |
| 552 | } | | |
| 553 | break :find_end parent_sub_path.len; | | |
| 554 | }; | | |
| 555 | const expected_prefix = parent_sub_path[0..end]; | | |
| 556 | if (!std.mem.startsWith(u8, pkg_root.sub_path, expected_prefix)) { | | |
| 557 | return f.fail( | | |
| 558 | f.location_tok, | | |
| 559 | try eb.printString("dependency path outside project: '{f}'", .{pkg_root}), | | |
| 560 | ); | | |
| 561 | } | | |
| 562 | } | | |
| 563 | f.package_root = pkg_root; | 552 | f.package_root = pkg_root; |
| 564 | try loadManifest(f, pkg_root); | 553 | try loadManifest(f, pkg_root); |
| 565 | if (!f.has_build_zig) try checkBuildFileExistence(f); | 554 | if (!f.has_build_zig) try checkBuildFileExistence(f); |
| ... | @@ -610,31 +599,33 @@ pub fn run(f: *Fetch) RunError!void { | ... | @@ -610,31 +599,33 @@ pub fn run(f: *Fetch) RunError!void { |
| 610 | return queueJobsForDeps(f); | 599 | return queueJobsForDeps(f); |
| 611 | } | 600 | } |
| 612 | | 601 | |
| 613 | const package_root = try job_queue.root_pkg_path.join(arena, expected_hash.toSlice()); | 602 | if (job_queue.local_storage) |ls| { |
| 614 | if (package_root.root_dir.handle.access(io, package_root.sub_path, .{})) |_| { | 603 | const package_root = try ls.pkg_root.join(arena, expected_hash.toSlice()); |
| 615 | assert(f.lazy_status != .unavailable); | 604 | if (package_root.root_dir.handle.access(io, package_root.sub_path, .{})) |_| { |
| 616 | f.package_root = package_root; | 605 | assert(f.lazy_status != .unavailable); |
| 617 | try loadManifest(f, f.package_root); | 606 | f.package_root = package_root; |
| 618 | try checkBuildFileExistence(f); | 607 | try loadManifest(f, f.package_root); |
| 619 | if (!job_queue.recursive) return; | 608 | try checkBuildFileExistence(f); |
| 620 | return queueJobsForDeps(f); | 609 | if (!job_queue.recursive) return; |
| 621 | } else |err| switch (err) { | 610 | return queueJobsForDeps(f); |
| 622 | error.FileNotFound => { | 611 | } else |err| switch (err) { |
| 623 | log.debug("FileNotFound: {f}", .{package_root}); | 612 | error.FileNotFound => { |
| 624 | if (job_queue.read_only and f.lazy_status == .eager) return f.fail( | 613 | log.debug("FileNotFound: {f}", .{package_root}); |
| 625 | f.name_tok, | 614 | if (job_queue.read_only and f.lazy_status == .eager) return f.fail( |
| 626 | try eb.printString("package not found at '{f}'", .{package_root}), | 615 | f.name_tok, |
| 627 | ); | 616 | try eb.printString("package not found at '{f}'", .{package_root}), |
| 628 | }, | 617 | ); |
| 629 | error.Canceled => |e| return e, | 618 | }, |
| 630 | else => |e| { | 619 | error.Canceled => |e| return e, |
| 631 | try eb.addRootErrorMessage(.{ | 620 | else => |e| { |
| 632 | .msg = try eb.printString("unable to open package cache directory {f}: {t}", .{ | 621 | try eb.addRootErrorMessage(.{ |
| 633 | package_root, e, | 622 | .msg = try eb.printString("unable to open package cache directory {f}: {t}", .{ |
| 634 | }), | 623 | package_root, e, |
| 635 | }); | 624 | }), |
| 636 | return error.FetchFailed; | 625 | }); |
| 637 | }, | 626 | return error.FetchFailed; |
| | 627 | }, |
| | 628 | } |
| 638 | } | 629 | } |
| 639 | | 630 | |
| 640 | // Check global cache before remote fetch. | 631 | // Check global cache before remote fetch. |
| ... | @@ -713,7 +704,14 @@ fn runResource( | ... | @@ -713,7 +704,14 @@ fn runResource( |
| 713 | break :r x; | 704 | break :r x; |
| 714 | }; | 705 | }; |
| 715 | const tmp_dir_sub_path = ".tmp-" ++ std.fmt.hex(rand_int); | 706 | const tmp_dir_sub_path = ".tmp-" ++ std.fmt.hex(rand_int); |
| 716 | const tmp_directory_path = try job_queue.root_pkg_path.join(arena, tmp_dir_sub_path); | 707 | const tmp_tmp_dir_sub_path = "tmp/" ++ tmp_dir_sub_path; |
| | 708 | const tmp_directory_path: Cache.Path = if (job_queue.local_storage) |ls| |
| | 709 | try ls.pkg_root.join(arena, tmp_dir_sub_path) |
| | 710 | else |
| | 711 | .{ |
| | 712 | .root_dir = job_queue.global_cache, |
| | 713 | .sub_path = tmp_tmp_dir_sub_path, |
| | 714 | }; |
| 717 | | 715 | |
| 718 | const package_sub_path = blk: { | 716 | const package_sub_path = blk: { |
| 719 | var tmp_directory: Cache.Directory = .{ | 717 | var tmp_directory: Cache.Directory = .{ |
| ... | @@ -772,19 +770,23 @@ fn runResource( | ... | @@ -772,19 +770,23 @@ fn runResource( |
| 772 | // zig package directory untouched as it may be in use. This is done even | 770 | // zig package directory untouched as it may be in use. This is done even |
| 773 | // if the hash is invalid, in case the package with the different hash is | 771 | // if the hash is invalid, in case the package with the different hash is |
| 774 | // used in the future. | 772 | // used in the future. |
| 775 | f.package_root = try job_queue.root_pkg_path.join(arena, computed_package_hash.toSlice()); | 773 | if (job_queue.local_storage) |ls| { |
| 776 | renameTmpIntoCache(io, package_sub_path, f.package_root) catch |err| { | 774 | f.package_root = try ls.pkg_root.join(arena, computed_package_hash.toSlice()); |
| 777 | try eb.addRootErrorMessage(.{ .msg = try eb.printString( | 775 | renameTmpIntoCache(io, package_sub_path, f.package_root) catch |err| { |
| 778 | "unable to rename temporary directory {f} into package cache directory {f}: {t}", | 776 | try eb.addRootErrorMessage(.{ .msg = try eb.printString( |
| 779 | .{ package_sub_path, f.package_root, err }, | 777 | "unable to rename temporary directory {f} into package cache directory {f}: {t}", |
| 780 | ) }); | 778 | .{ package_sub_path, f.package_root, err }, |
| 781 | return error.FetchFailed; | 779 | ) }); |
| 782 | }; | 780 | return error.FetchFailed; |
| | 781 | }; |
| | 782 | } else { |
| | 783 | f.package_root = tmp_directory_path; |
| | 784 | } |
| 783 | | 785 | |
| 784 | if (!disable_recompress) { | 786 | if (!disable_recompress) { |
| 785 | // Spin off a task to recompress the tarball, with filtered files deleted, into | 787 | // Spin off a task to recompress the tarball, with filtered files deleted, into |
| 786 | // the global cache. | 788 | // the global cache. |
| 787 | job_queue.group.async(io, JobQueue.recompress, .{ job_queue, computed_package_hash }); | 789 | job_queue.group.async(io, JobQueue.recompress, .{ job_queue, computed_package_hash, f.package_root }); |
| 788 | } | 790 | } |
| 789 | | 791 | |
| 790 | // Remove temporary directory root if not already renamed to global cache. | 792 | // Remove temporary directory root if not already renamed to global cache. |