also looking at this
fix: preserve sources when removing linked workspace directories
#247
4 files
+98
-46
| @@ -375,48 +375,12 @@ pub fn deleteFile(path: []const u8) !void { | ||
| 375 | 375 | } |
| 376 | 376 | } |
| 377 | 377 | |
| 378 | /// Delete a directory using platform-specific syscalls | |
| 379 | fn 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 | ||
| 402 | 378 | /// Delete a directory tree recursively |
| 403 | 379 | pub 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); | |
| 420 | 384 | } |
| 421 | 385 | |
| 422 | 386 | /// Get the current working directory as a path string |
| @@ -1112,6 +1076,50 @@ test "symLink creates a relative executable alias at an absolute path" { | ||
| 1112 | 1076 | try std.testing.expectEqualStrings("pantry", target); |
| 1113 | 1077 | } |
| 1114 | 1078 | |
| 1079 | test "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 | ||
| 1101 | test "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 | ||
| 1115 | 1123 | /// Read a symbolic link target |
| 1116 | 1124 | pub fn readLink(path: []const u8, buf: []u8) ![]const u8 { |
| 1117 | 1125 | if (comptime is_windows) { |