Compare commits

...

8 Commits

Author SHA1 Message Date
Dylan Conway
361fac9404 tests 2024-04-05 00:59:54 -07:00
Dylan Conway
09b135277e wip 2024-04-05 00:58:24 -07:00
Dylan Conway
630cc9a700 symlinkOrJunctionOnWindowsW 2024-04-04 01:16:07 -07:00
Dylan Conway
85a4da6530 extract symlinks 2024-04-04 01:14:44 -07:00
Dylan Conway
6241da9733 dont skip symlinks 2024-04-04 01:14:04 -07:00
Dylan Conway
d565aab20a delete packages 2024-04-04 01:11:19 -07:00
Dylan Conway
0eccb5f0c0 symlink test 2024-04-04 01:09:19 -07:00
Dylan Conway
0706082a2e add symlink packages 2024-04-03 21:55:04 -07:00
4 changed files with 178 additions and 42 deletions

View File

@@ -1396,6 +1396,8 @@ pub const PackageInstall = struct {
while (try walker.next()) |entry| {
if (comptime Environment.isWindows) {
switch (entry.kind) {
// TODO: use CopyFileExW for symlinks
.sym_link => {},
.directory, .file => {},
else => continue,
}
@@ -1418,13 +1420,16 @@ pub const PackageInstall = struct {
bun.MakePath.makePath(u16, destination_dir_, entry.path) catch {};
}
},
.file => {
.file, .sym_link => {
if (bun.windows.CopyFileW(src.ptr, dest.ptr, 0) == 0) {
if (bun.Dirname.dirname(u16, entry.path)) |entry_dirname| {
bun.MakePath.makePath(u16, destination_dir_, entry_dirname) catch {};
if (bun.windows.CopyFileW(src.ptr, dest.ptr, 0) != 0) {
continue;
}
// best effort
if (entry.kind == .sym_link) continue;
}
progress_.root.end();
@@ -1539,10 +1544,7 @@ pub const PackageInstall = struct {
else => {},
}
} else {
switch (entry.kind) {
.file => {},
else => continue,
}
if (entry.kind != .file and entry.kind != .sym_link) continue;
if (entry.path.len > to_copy_into1.len or entry.path.len > to_copy_into2.len) {
return error.NameTooLong;
@@ -1587,10 +1589,13 @@ pub const PackageInstall = struct {
}
}
// TODO: use CopyFileExW for symlinks
if (bun.windows.CopyFileW(src.ptr, dest.ptr, 0) != 0) {
continue;
}
if (entry.kind == .sym_link) continue;
return bun.errnoToZigErr(bun.windows.getLastErrno());
}
}
@@ -1903,7 +1908,7 @@ pub const PackageInstall = struct {
// https://github.com/npm/cli/blob/162c82e845d410ede643466f9f8af78a312296cc/workspaces/arborist/lib/arborist/reify.js#L738
// https://github.com/npm/cli/commit/0e58e6f6b8f0cd62294642a502c17561aaf46553
switch (bun.sys.symlinkOrJunctionOnWindows(to_path_z, dest_z)) {
switch (bun.sys.symlinkOrJunctionOnWindows(to_path_z, dest_z, .{ .directory = true })) {
.err => |err| {
return Result{
.fail = .{

View File

@@ -488,6 +488,8 @@ pub const Archive = struct {
const archive = stream.archive;
var count: u32 = 0;
const dir_fd = dir.fd;
var dir_path_buf: if (Environment.isWindows) bun.WPathBuffer else void = undefined;
var dir_path_len: if (Environment.isWindows) ?usize else void = if (comptime Environment.isWindows) null else {};
var w_path_buf: if (Environment.isWindows) bun.WPathBuffer else void = undefined;
@@ -577,21 +579,47 @@ pub const Archive = struct {
}
},
Kind.sym_link => {
const link_target = lib.archive_entry_symlink(entry).?;
if (comptime Environment.isWindows) {
@panic("TODO on Windows: Extracting archives containing symbolic links.");
}
std.os.symlinkatZ(link_target, dir_fd, pathname) catch |err| brk: {
switch (err) {
error.AccessDenied, error.FileNotFound => {
dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {};
break :brk try std.os.symlinkatZ(link_target, dir_fd, pathname);
},
else => {
return err;
},
const link_target = std.mem.sliceTo(lib.archive_entry_symlink_w(entry), 0);
const dir_len = dir_path_len orelse brk: {
const dir_path = bun.getFdPathW(dir_fd, &dir_path_buf) catch continue :loop;
dir_path_buf[dir_path.len] = std.fs.path.sep;
dir_path_len = dir_path.len + 1;
break :brk dir_path_len.?;
};
// TODO: the symlink target may or may not exist because it might
// still need to be extracted. For now we silently ignore errors.
// In the future, we should defer making these symlinks in order
// to determine whether the target is a file or directory, if it
// exists. Additionally, if we use junctions or the symlink target
// is an absolute path, moving the extracted directory will break
// the link.
@memcpy(dir_path_buf[dir_len..][0..pathname.len], pathname);
dir_path_buf[dir_len + pathname.len] = 0;
const dest = dir_path_buf[0 .. dir_len + pathname.len :0];
switch (bun.sys.symlinkOrJunctionOnWindowsW(dest, link_target, .{})) {
// best effort, see todo above
.err => {},
.result => {},
}
};
} else {
const link_target = lib.archive_entry_symlink(entry).?;
std.os.symlinkatZ(link_target, dir_fd, pathname) catch |err| brk: {
switch (err) {
error.AccessDenied, error.FileNotFound => {
dir.makePath(std.fs.path.dirname(path_slice) orelse return err) catch {};
break :brk try std.os.symlinkatZ(link_target, dir_fd, pathname);
},
else => {
return err;
},
}
};
}
},
Kind.file => {
const mode: bun.Mode = if (comptime Environment.isWindows) 0 else @intCast(lib.archive_entry_perm(entry));

View File

@@ -1721,16 +1721,45 @@ pub const WindowsSymlinkOptions = packed struct {
symlink_flags = 0;
}
pub var has_failed_to_create_symlink = false;
pub const ShouldUseJunction = enum(u8) {
unset = 0,
yes = 1,
no = 2,
};
pub var _should_use_junction: ShouldUseJunction = .unset;
pub fn shouldUseJunction() bool {
const value = @atomicLoad(ShouldUseJunction, &_should_use_junction, .Monotonic);
return switch (value) {
.unset => brk: {
if (bun.getenvZ("BUN_FEATURE_FLAG_USE_JUNCTIONS") != null) {
std.debug.print("USE_JUNCTION initial true\n", .{});
@atomicStore(ShouldUseJunction, &_should_use_junction, .yes, .Monotonic);
break :brk true;
}
std.debug.print("USE_JUNCTION initial false\n", .{});
@atomicStore(ShouldUseJunction, &_should_use_junction, .no, .Monotonic);
break :brk false;
},
.yes => true,
.no => false,
};
}
pub fn setShouldUseJunction() void {
@atomicStore(ShouldUseJunction, &_should_use_junction, .yes, .Monotonic);
}
};
pub fn symlinkOrJunctionOnWindows(sym: [:0]const u8, target: [:0]const u8) Maybe(void) {
if (!WindowsSymlinkOptions.has_failed_to_create_symlink) {
pub fn symlinkOrJunctionOnWindows(sym: [:0]const u8, target: [:0]const u8, options: WindowsSymlinkOptions) Maybe(void) {
if (!WindowsSymlinkOptions.shouldUseJunction()) {
var sym16: bun.WPathBuffer = undefined;
var target16: bun.WPathBuffer = undefined;
const sym_path = bun.strings.toNTPath(&sym16, sym);
const target_path = bun.strings.toNTPath(&target16, target);
switch (symlinkW(sym_path, target_path, .{ .directory = true })) {
switch (symlinkW(sym_path, target_path, options)) {
.result => {
return Maybe(void).success;
},
@@ -1738,7 +1767,30 @@ pub fn symlinkOrJunctionOnWindows(sym: [:0]const u8, target: [:0]const u8) Maybe
}
}
return sys_uv.symlinkUV(sym, target, bun.windows.libuv.UV_FS_SYMLINK_JUNCTION);
return sys_uv.symlinkUV(target, sym, bun.windows.libuv.UV_FS_SYMLINK_JUNCTION);
}
pub fn symlinkOrJunctionOnWindowsW(sym: [:0]const u16, target: [:0]const u16, options: WindowsSymlinkOptions) Maybe(void) {
if (!WindowsSymlinkOptions.shouldUseJunction()) {
switch (symlinkW(sym, target, options)) {
.result => {
return Maybe(void).success;
},
.err => {},
}
}
var sym_buf: bun.PathBuffer = undefined;
const _sym_u8 = bun.strings.convertUTF16toUTF8InBuffer(&sym_buf, sym) catch unreachable;
sym_buf[_sym_u8.len] = 0;
const sym_u8 = sym_buf[0.._sym_u8.len :0];
var target_buf: bun.PathBuffer = undefined;
const _target_u8 = bun.strings.convertUTF16toUTF8InBuffer(&target_buf, target) catch unreachable;
target_buf[_target_u8.len] = 0;
const target_u8 = target_buf[0.._target_u8.len :0];
return sys_uv.symlinkUV(target_u8, sym_u8, bun.windows.libuv.UV_FS_SYMLINK_JUNCTION);
}
pub fn symlinkW(sym: [:0]const u16, target: [:0]const u16, options: WindowsSymlinkOptions) Maybe(void) {
@@ -1764,7 +1816,7 @@ pub fn symlinkW(sym: [:0]const u16, target: [:0]const u16, options: WindowsSymli
}
if (errno.toSystemErrno()) |err| {
WindowsSymlinkOptions.has_failed_to_create_symlink = true;
WindowsSymlinkOptions.setShouldUseJunction();
return .{
.err = .{
.errno = @intFromEnum(err),

View File

@@ -18,25 +18,25 @@ var testCounter: number = 0;
var port: number = 4873;
var packageDir: string;
beforeAll(async () => {
verdaccioServer = fork(
require.resolve("verdaccio/bin/verdaccio"),
["-c", join(import.meta.dir, "verdaccio.yaml"), "-l", `${port}`],
{ silent: true, execPath: "bun" },
);
// beforeAll(async () => {
// verdaccioServer = fork(
// require.resolve("verdaccio/bin/verdaccio"),
// ["-c", join(import.meta.dir, "verdaccio.yaml"), "-l", `${port}`],
// { silent: true, execPath: "bun" },
// );
await new Promise<void>(done => {
verdaccioServer.on("message", (msg: { verdaccio_started: boolean }) => {
if (msg.verdaccio_started) {
done();
}
});
});
});
// await new Promise<void>(done => {
// verdaccioServer.on("message", (msg: { verdaccio_started: boolean }) => {
// if (msg.verdaccio_started) {
// done();
// }
// });
// });
// });
afterAll(() => {
verdaccioServer.kill();
});
// afterAll(() => {
// verdaccioServer.kill();
// });
beforeEach(async () => {
packageDir = mkdtempSync(join(realpathSync(tmpdir()), "bun-install-registry-" + testCounter++ + "-"));
@@ -288,6 +288,57 @@ test("hardlinks on windows dont fail with long paths", async () => {
expect(await exited).toBe(0);
});
test.todo("it should be able to install a package with symlinks on windows");
// for (const backend of ["hardlink", "copyfile"]) {
// for (const useJunctions of [false, true]) {
// }
// }
for (const backend of ["hardlink", "copyfile"]) {
test.only(`it should be able to extract and install symlinks [${backend}]`, async () => {
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
"missing-symlink": "github:dylan-conway/install-test#missing-symlink",
"symlink-exists": "github:dylan-conway/install-test#symlink-exists",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", `--backend=${backend}`],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env,
});
const err = await Bun.readableStreamToText(stderr);
const out = await Bun.readableStreamToText(stdout);
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("panic:");
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
"",
" + missing-symlink@github:dylan-conway/install-test#41b028c",
" + symlink-exists@github:dylan-conway/install-test#2740530",
"",
expect.stringContaining("2 packages installed"),
"",
" Blocked 2 postinstalls. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "symlink-exists", "foo"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "missing-symlink", "foo"))).toBeTrue();
});
}
test("basic 1", async () => {
await writeFile(
join(packageDir, "package.json"),