Commit a5a928b966

Igor Anić <igor.anic@gmail.com>
2024-02-27 19:43:30
package manager: don't strip components in tar
Unpack tar without removing leading root folder. Then find package root in unpacked tmp folder.
1 parent bc50767
Changed files (1)
src
Package
src/Package/Fetch.zig
@@ -439,7 +439,8 @@ fn runResource(
     const s = fs.path.sep_str;
     const cache_root = f.job_queue.global_cache;
     const rand_int = std.crypto.random.int(u64);
-    const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int);
+    const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int); // root of the temporary directory
+    var tmp_package_root_sub_path: ?[]const u8 = null; // package root inside temporary directory
 
     {
         const tmp_directory_path = try cache_root.join(arena, &.{tmp_dir_sub_path});
@@ -463,6 +464,34 @@ fn runResource(
 
         try unpackResource(f, resource, uri_path, tmp_directory);
 
+        // Strip leading root directory if needed.
+        if (findPackageRootSubPath(arena, tmp_directory) catch null) |sub_path| {
+            // Position tmp_directory to sub_path.
+            const handle = tmp_directory.handle.openDir(sub_path, .{ .iterate = true }) catch |err| {
+                try eb.addRootErrorMessage(.{
+                    .msg = try eb.printString("fail to open temporary directory '{s}' sub path '{s}': {s}", .{
+                        tmp_directory_path, sub_path, @errorName(err),
+                    }),
+                });
+                return error.FetchFailed;
+            };
+            tmp_package_root_sub_path = try fs.path.join(arena, &[_][]const u8{ tmp_dir_sub_path, sub_path });
+            tmp_directory.handle.close();
+            tmp_directory = .{
+                .path = try cache_root.join(arena, &.{tmp_package_root_sub_path.?}),
+                .handle = handle,
+            };
+        } else {
+            // btrfs workaround; reopen tmp_directory
+            if (native_os == .linux and f.job_queue.work_around_btrfs_bug) {
+                // https://github.com/ziglang/zig/issues/17095
+                tmp_directory.handle.close();
+                tmp_directory.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
+                    .iterate = true,
+                }) catch @panic("btrfs workaround failed");
+            }
+        }
+
         // Load, parse, and validate the unpacked build.zig.zon file. It is allowed
         // for the file to be missing, in which case this fetched package is
         // considered to be a "naked" package.
@@ -481,15 +510,6 @@ fn runResource(
 
         // Compute the package hash based on the remaining files in the temporary
         // directory.
-
-        if (native_os == .linux and f.job_queue.work_around_btrfs_bug) {
-            // https://github.com/ziglang/zig/issues/17095
-            tmp_directory.handle.close();
-            tmp_directory.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
-                .iterate = true,
-            }) catch @panic("btrfs workaround failed");
-        }
-
         f.actual_hash = try computeHash(f, tmp_directory, filter);
     }
 
@@ -503,7 +523,11 @@ fn runResource(
         .root_dir = cache_root,
         .sub_path = try arena.dupe(u8, "p" ++ s ++ Manifest.hexDigest(f.actual_hash)),
     };
-    renameTmpIntoCache(cache_root.handle, tmp_dir_sub_path, f.package_root.sub_path) catch |err| {
+    renameTmpIntoCache(
+        cache_root.handle,
+        if (tmp_package_root_sub_path) |p| p else tmp_dir_sub_path,
+        f.package_root.sub_path,
+    ) catch |err| {
         const src = try cache_root.join(arena, &.{tmp_dir_sub_path});
         const dest = try cache_root.join(arena, &.{f.package_root.sub_path});
         try eb.addRootErrorMessage(.{ .msg = try eb.printString(
@@ -512,6 +536,10 @@ fn runResource(
         ) });
         return error.FetchFailed;
     };
+    // Remove temporary directory root if that not already done in rename.
+    if (tmp_package_root_sub_path) |_| {
+        cache_root.handle.deleteTree(tmp_dir_sub_path) catch {};
+    }
 
     // Validate the computed hash against the expected hash. If invalid, this
     // job is done.
@@ -608,6 +636,19 @@ fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void {
     }
 }
 
+// Finds package root subpath.
+// Skips single root directory, returns null in all other cases.
+fn findPackageRootSubPath(allocator: Allocator, parent: Cache.Directory) !?[]const u8 {
+    var iter = parent.handle.iterate();
+    if (try iter.next()) |entry| {
+        if (try iter.next() != null) return null;
+        if (entry.kind == .directory) {
+            return try allocator.dupe(u8, entry.name);
+        }
+    }
+    return null;
+}
+
 fn queueJobsForDeps(f: *Fetch) RunError!void {
     assert(f.job_queue.recursive);
 
@@ -1700,3 +1741,37 @@ test {
     _ = Filter;
     _ = FileType;
 }
+
+test "findPackageRootSubPath" {
+    const testing = std.testing;
+
+    var root = std.testing.tmpDir(.{ .iterate = true });
+    defer root.cleanup();
+
+    // folder1
+    //     ├── folder2
+    //     ├── file1
+    //
+    try root.dir.makePath("folder1/folder2");
+    (try root.dir.createFile("folder1/file1", .{})).close();
+
+    // start at root returns folder1 as package root
+    const sub_path = (try findPackageRootSubPath(
+        testing.allocator,
+        Cache.Directory{ .path = null, .handle = root.dir },
+    )).?;
+    try testing.expectEqualStrings("folder1", sub_path);
+    testing.allocator.free(sub_path);
+
+    // start at folder1 returns null
+    try testing.expect(null == (try findPackageRootSubPath(
+        testing.allocator,
+        Cache.Directory{ .path = null, .handle = try root.dir.openDir("folder1", .{ .iterate = true }) },
+    )));
+
+    // start at folder1/folder2 returns null
+    try testing.expect(null == (try findPackageRootSubPath(
+        testing.allocator,
+        Cache.Directory{ .path = null, .handle = try root.dir.openDir("folder1/folder2", .{ .iterate = true }) },
+    )));
+}