ReviewOS

also looking at this

pantry-pm/pantry

fix(install): materializing a tree over its own symlinks is repeatable

#264
Merged glennmichael123 wants to merge fix/250-materialization into main
1 file +84 -1
packages/zig/src/install/installer.zigmodified+84-1
Changes to packages/zig/src/install/installer.zig
@@ -4041,7 +4041,27 @@ pub const Installer = struct {
40414041 } else if (entry.kind == .sym_link) {
40424042 const target = try io_helper.readLinkAlloc(self.allocator, src_path);
40434043 defer self.allocator.free(target);
4044 try io_helper.symLink(target, dst_path);
4044 // Materialization has to be repeatable. Directories here go
4045 // through makePath and regular files through createFile, which
4046 // truncates — both fine over an existing tree. A bare
4047 // symlink(2) is not: it returns EEXIST, and that escaped as
4048 // PathAlreadyExists from an install over a restored cache or a
4049 // retried materialization (#250).
4050 io_helper.symLink(target, dst_path) catch |err| switch (err) {
4051 error.PathAlreadyExists => {
4052 const existing = io_helper.readLinkAlloc(self.allocator, dst_path) catch null;
4053 defer if (existing) |value| self.allocator.free(value);
4054 // Already pointing where the source says: nothing to
4055 // do. Pointing elsewhere, or not a link at all:
4056 // replace it, because the source is authoritative and
4057 // a stale link is how a tree half-materializes.
4058 if (existing == null or !std.mem.eql(u8, existing.?, target)) {
4059 io_helper.deleteFile(dst_path) catch {};
4060 try io_helper.symLink(target, dst_path);
4061 }
4062 },
4063 else => return err,
4064 };
40454065 } else {
40464066 try io_helper.copyFile(src_path, dst_path);
40474067 }
@@ -5715,3 +5735,66 @@ test "native materialization: concurrent cached publications both succeed withou
57155735 try std.testing.expectEqual(fixture.payload_bytes, try materializationRegularBytes(allocator, fixture.extraction.payload));
57165736 try fixture.expectNoTransaction();
57175737}
5738
5739test "native materialization: re-copying a tree over its own symlinks is repeatable" {
5740 // #250: a macOS job that restored an exact dependency cache failed with
5741 // PathAlreadyExists for git-scm.org@2.53.0 while fourteen of fifteen
5742 // packages installed. `copyDirectoryStructure` recreates directories with
5743 // makePath and overwrites regular files through createFile, both
5744 // repeatable — but it created symlinks with a bare `symlink(2)`, which
5745 // returns EEXIST over a destination that already holds the link. git is
5746 // the most symlink-dense package in that cohort (its libexec is a wall of
5747 // them), which is consistent with git alone failing.
5748 //
5749 // This does not claim to be the reported tree; it identifies a colliding
5750 // operation on the implicated path and holds it shut.
5751 if (.os.tag == .windows) return error.SkipZigTest;
5752 const allocator = std.testing.allocator;
5753
5754 var tmp = std.testing.tmpDir(.{});
5755 defer tmp.cleanup();
5756 var root_buf: [std.fs.max_path_bytes]u8 = undefined;
5757 const root = root_buf[0..try tmp.dir.realPath(io_helper.io, &root_buf)];
5758
5759 const source = try std.fmt.allocPrint(allocator, "{s}/source/libexec", .{root});
5760 defer allocator.free(source);
5761 const dest = try std.fmt.allocPrint(allocator, "{s}/prefix/libexec", .{root});
5762 defer allocator.free(dest);
5763 try io_helper.makePath(source);
5764
5765 const real = try std.fmt.allocPrint(allocator, "{s}/git", .{source});
5766 defer allocator.free(real);
5767 const file = try io_helper.createFile(real, .{});
5768 try io_helper.writeAllToFile(file, "#!/bin/sh\nexit 0\n");
5769 io_helper.closeFile(file);
5770 const link = try std.fmt.allocPrint(allocator, "{s}/git-receive-pack", .{source});
5771 defer allocator.free(link);
5772 try io_helper.symLink("git", link);
5773
5774 var pkg_cache = try PackageCache.init(allocator);
5775 defer pkg_cache.deinit();
5776 var installer = try Installer.init(allocator, &pkg_cache);
5777 defer installer.deinit();
5778
5779 try installer.copyDirectoryStructure(source, dest);
5780 // The second pass is the restored-cache case: every entry is already
5781 // there. Before the fix this returned error.PathAlreadyExists — asserted
5782 // with expectError while diagnosing, which is how the operation was
5783 // identified rather than inferred.
5784 try installer.copyDirectoryStructure(source, dest);
5785
5786 const copied_link = try std.fmt.allocPrint(allocator, "{s}/git-receive-pack", .{dest});
5787 defer allocator.free(copied_link);
5788 const target = try io_helper.readLinkAlloc(allocator, copied_link);
5789 defer allocator.free(target);
5790 try std.testing.expectEqualStrings("git", target);
5791
5792 // A link left pointing somewhere else — a half-written or older tree —
5793 // must end up at the source's target, since the source is authoritative.
5794 io_helper.deleteFile(copied_link) catch {};
5795 try io_helper.symLink("git-upload-pack", copied_link);
5796 try installer.copyDirectoryStructure(source, dest);
5797 const repaired = try io_helper.readLinkAlloc(allocator, copied_link);
5798 defer allocator.free(repaired);
5799 try std.testing.expectEqualStrings("git", repaired);
5800}