ReviewOS

also looking at this

pantry-pm/pantry

fix: preserve sources when removing linked workspace directories

#247
Merged chris wants to merge codex/preserve-linked-workspace-sources into main
4 files +98 -46
packages/zig/src/io_helper.zigmodified+48-40
Changes to packages/zig/src/io_helper.zig
@@ -375,48 +375,12 @@ pub fn deleteFile(path: []const u8) !void {
375375 }
376376}
377377
378/// Delete a directory using platform-specific syscalls
379fn deleteDir(path: []const u8) !void {
380 var path_buf: [std.fs.max_path_bytes:0]u8 = undefined;
381 if (path.len >= path_buf.len) return error.NameTooLong;
382 @memcpy(path_buf[0..path.len], path);
383 path_buf[path.len] = 0;
384
385 switch (builtin.os.tag) {
386 .windows => {
387 cwd().deleteDir(io, path) catch return error.DirNotEmpty;
388 },
389 .linux => {
390 const AT_REMOVEDIR = 0x200;
391 const rc = std.os.linux.unlinkat(std.os.linux.AT.FDCWD, &path_buf, AT_REMOVEDIR);
392 if (rc != 0) return error.DirNotEmpty;
393 },
394 else => {
395 // macOS and others - use libc
396 const result = c.rmdir(&path_buf);
397 if (result != 0) return error.DirNotEmpty;
398 },
399 }
400}
401
402378/// Delete a directory tree recursively
403379pub fn deleteTree(path: []const u8) !void {
404 // Try to delete as file first
405 deleteFile(path) catch {
406 // If that fails, try as directory
407 var dir = openDirForIteration(path) catch return;
408
409 var iter = dir.iterate();
410 while (iter.next() catch null) |entry| {
411 var child_path_buf: [std.fs.max_path_bytes]u8 = undefined;
412 const child_path = std.fmt.bufPrint(&child_path_buf, "{s}/{s}", .{ path, entry.name }) catch continue;
413 deleteTree(child_path) catch {};
414 }
415 dir.close();
416
417 // Now remove the empty directory
418 deleteDir(path) catch {};
419 };
380 // The standard implementation removes links themselves, including Windows
381 // directory reparse points. Opening a directory after deleteFile fails can
382 // follow a workspace link and recursively delete its source checkout.
383 try cwd().deleteTree(io, path);
420384}
421385
422386/// Get the current working directory as a path string
@@ -1112,6 +1076,50 @@ test "symLink creates a relative executable alias at an absolute path" {
11121076 try std.testing.expectEqualStrings("pantry", target);
11131077}
11141078
1079test "deleteTree removes a workspace directory link without deleting its sources" {
1080 const allocator = std.testing.allocator;
1081 var tmp = std.testing.tmpDir(.{});
1082 defer tmp.cleanup();
1083 try tmp.dir.createDir(io, "sources", .default_dir);
1084 try tmp.dir.writeFile(io, .{ .sub_path = "sources/build.zig", .data = "tracked workspace source" });
1085 const working_directory = try getCwdAlloc(allocator);
1086 defer allocator.free(working_directory);
1087 const link_path = try std.fs.path.join(allocator, &.{ working_directory, ".zig-cache", "tmp", tmp.sub_path[0..], "workspace" });
1088 defer allocator.free(link_path);
1089 try symLink("sources", link_path);
1090
1091 // Workspace relinking removes an existing link before recreating it.
1092 // Windows directory symlinks cannot be removed with deleteFile; following
1093 // them here empties the tracked source checkout instead of just the link.
1094 try deleteTree(link_path);
1095 const source = try tmp.dir.readFileAlloc(io, "sources/build.zig", allocator, .limited(1024));
1096 defer allocator.free(source);
1097 try std.testing.expectEqualStrings("tracked workspace source", source);
1098 try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "workspace", .{ .follow_symlinks = false }));
1099}
1100
1101test "deleteTree does not follow directory links inside a removed tree" {
1102 const allocator = std.testing.allocator;
1103 var tmp = std.testing.tmpDir(.{});
1104 defer tmp.cleanup();
1105 try tmp.dir.createDir(io, "sources", .default_dir);
1106 try tmp.dir.createDir(io, "cache", .default_dir);
1107 try tmp.dir.writeFile(io, .{ .sub_path = "sources/build.zig", .data = "keep source bytes" });
1108 try tmp.dir.writeFile(io, .{ .sub_path = "cache/owned.txt", .data = "remove cache bytes" });
1109 const working_directory = try getCwdAlloc(allocator);
1110 defer allocator.free(working_directory);
1111 const cache_path = try std.fs.path.join(allocator, &.{ working_directory, ".zig-cache", "tmp", tmp.sub_path[0..], "cache" });
1112 defer allocator.free(cache_path);
1113 const link_path = try std.fs.path.join(allocator, &.{ cache_path, "workspace" });
1114 defer allocator.free(link_path);
1115 try symLink("../sources", link_path);
1116 try deleteTree(cache_path);
1117 const source = try tmp.dir.readFileAlloc(io, "sources/build.zig", allocator, .limited(1024));
1118 defer allocator.free(source);
1119 try std.testing.expectEqualStrings("keep source bytes", source);
1120 try std.testing.expectError(error.FileNotFound, tmp.dir.statFile(io, "cache", .{}));
1121}
1122
11151123/// Read a symbolic link target
11161124pub fn readLink(path: []const u8, buf: []u8) ![]const u8 {
11171125 if (comptime is_windows) {