mirror of
https://github.com/oven-sh/bun
synced 2026-02-28 04:21:04 +01:00
Compare commits
5 Commits
riskymh/di
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cee31572bf | ||
|
|
7cdd29c1f9 | ||
|
|
5b755cf5ee | ||
|
|
184263d6c7 | ||
|
|
8ec05c3766 |
@@ -3041,8 +3041,6 @@ pub const api = struct {
|
||||
|
||||
node_linker: ?bun.install.PackageManager.Options.NodeLinker = null,
|
||||
|
||||
disable_default_trusted_dependencies: ?bool = null,
|
||||
|
||||
pub fn decode(reader: anytype) anyerror!BunInstall {
|
||||
var this = std.mem.zeroes(BunInstall);
|
||||
|
||||
|
||||
@@ -499,12 +499,6 @@ pub const Bunfig = struct {
|
||||
}
|
||||
}
|
||||
|
||||
if (install_obj.get("disableDefaultTrustedDependencies")) |disable_default_trusted_expr| {
|
||||
if (disable_default_trusted_expr.asBool()) |value| {
|
||||
install.disable_default_trusted_dependencies = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (install_obj.get("lockfile")) |lockfile_expr| {
|
||||
if (lockfile_expr.get("print")) |lockfile| {
|
||||
try this.expectString(lockfile);
|
||||
|
||||
@@ -38,9 +38,7 @@ pub const UntrustedCommand = struct {
|
||||
// called alias because a dependency name is not always the package name
|
||||
const alias = dep.name.slice(buf);
|
||||
|
||||
const is_trusted = pm.lockfile.hasTrustedDependency(alias, !pm.options.disable_default_trusted_dependencies);
|
||||
|
||||
if (!is_trusted) {
|
||||
if (!pm.lockfile.hasTrustedDependency(alias)) {
|
||||
try untrusted_dep_ids.put(ctx.allocator, dep_id, {});
|
||||
}
|
||||
}
|
||||
@@ -82,7 +80,6 @@ pub const UntrustedCommand = struct {
|
||||
&node_modules_path,
|
||||
alias,
|
||||
resolution,
|
||||
!pm.options.disable_default_trusted_dependencies,
|
||||
) catch |err| {
|
||||
if (err == error.ENOENT) continue;
|
||||
return err;
|
||||
@@ -190,9 +187,7 @@ pub const TrustCommand = struct {
|
||||
|
||||
const alias = dep.name.slice(buf);
|
||||
|
||||
const is_trusted = pm.lockfile.hasTrustedDependency(alias, !pm.options.disable_default_trusted_dependencies);
|
||||
|
||||
if (!is_trusted) {
|
||||
if (!pm.lockfile.hasTrustedDependency(alias)) {
|
||||
try untrusted_dep_ids.put(ctx.allocator, dep_id, {});
|
||||
}
|
||||
}
|
||||
@@ -251,7 +246,6 @@ pub const TrustCommand = struct {
|
||||
&node_modules_path,
|
||||
alias,
|
||||
resolution,
|
||||
!pm.options.disable_default_trusted_dependencies,
|
||||
) catch |err| {
|
||||
if (err == error.ENOENT) continue;
|
||||
return err;
|
||||
@@ -262,9 +256,7 @@ pub const TrustCommand = struct {
|
||||
if (trust_all) break :brk false;
|
||||
|
||||
for (packages_to_trust.items) |package_name_from_cli| {
|
||||
const is_already_trusted = pm.lockfile.hasTrustedDependency(alias, !pm.options.disable_default_trusted_dependencies);
|
||||
|
||||
if (strings.eqlLong(package_name_from_cli, alias, true) and !is_already_trusted) {
|
||||
if (strings.eqlLong(package_name_from_cli, alias, true) and !pm.lockfile.hasTrustedDependency(alias)) {
|
||||
break :brk false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,7 +1094,7 @@ pub const PackageInstaller = struct {
|
||||
const truncated_dep_name_hash: TruncatedPackageNameHash = @truncate(dep.name_hash);
|
||||
const is_trusted, const is_trusted_through_update_request = brk: {
|
||||
if (this.trusted_dependencies_from_update_requests.contains(truncated_dep_name_hash)) break :brk .{ true, true };
|
||||
if (this.lockfile.hasTrustedDependency(alias.slice(this.lockfile.buffers.string_bytes.items), !this.options.disable_default_trusted_dependencies)) break :brk .{ true, false };
|
||||
if (this.lockfile.hasTrustedDependency(alias.slice(this.lockfile.buffers.string_bytes.items))) break :brk .{ true, false };
|
||||
break :brk .{ false, false };
|
||||
};
|
||||
|
||||
@@ -1325,7 +1325,6 @@ pub const PackageInstaller = struct {
|
||||
package_path,
|
||||
folder_name,
|
||||
resolution,
|
||||
!this.options.disable_default_trusted_dependencies,
|
||||
) catch |err| {
|
||||
if (log_level != .silent) {
|
||||
const fmt = "\n<r><red>error:<r> failed to enqueue lifecycle scripts for <b>{s}<r>: {s}\n";
|
||||
|
||||
@@ -71,8 +71,6 @@ depth: ?usize = null,
|
||||
/// isolated installs (pnpm-like) or hoisted installs (yarn-like, original)
|
||||
node_linker: NodeLinker = .auto,
|
||||
|
||||
disable_default_trusted_dependencies: bool = false,
|
||||
|
||||
pub const PublishConfig = struct {
|
||||
access: ?Access = null,
|
||||
tag: string = "",
|
||||
@@ -246,9 +244,6 @@ pub fn load(
|
||||
if (config.link_workspace_packages) |link_workspace_packages| {
|
||||
this.link_workspace_packages = link_workspace_packages;
|
||||
}
|
||||
if (config.disable_default_trusted_dependencies) |disable_default_trusted_dependencies| {
|
||||
this.disable_default_trusted_dependencies = disable_default_trusted_dependencies;
|
||||
}
|
||||
}
|
||||
|
||||
if (base.url.len == 0) base.url = Npm.Registry.default_url;
|
||||
|
||||
@@ -846,7 +846,7 @@ pub const Installer = struct {
|
||||
if (installer.trusted_dependencies_from_update_requests.contains(truncated_dep_name_hash)) {
|
||||
break :brk .{ true, true };
|
||||
}
|
||||
if (installer.lockfile.hasTrustedDependency(dep.name.slice(string_buf), !installer.manager.options.disable_default_trusted_dependencies)) {
|
||||
if (installer.lockfile.hasTrustedDependency(dep.name.slice(string_buf))) {
|
||||
break :brk .{ true, false };
|
||||
}
|
||||
break :brk .{ false, false };
|
||||
@@ -869,7 +869,6 @@ pub const Installer = struct {
|
||||
&pkg_cwd,
|
||||
dep.name.slice(string_buf),
|
||||
&pkg_res,
|
||||
!installer.manager.options.disable_default_trusted_dependencies,
|
||||
) catch |err| {
|
||||
return .failure(.{ .run_preinstall = err });
|
||||
};
|
||||
|
||||
@@ -1974,13 +1974,13 @@ pub const default_trusted_dependencies = brk: {
|
||||
break :brk &final;
|
||||
};
|
||||
|
||||
pub fn hasTrustedDependency(this: *const Lockfile, name: []const u8, check_default: bool) bool {
|
||||
pub fn hasTrustedDependency(this: *const Lockfile, name: []const u8) bool {
|
||||
if (this.trusted_dependencies) |trusted_dependencies| {
|
||||
const hash = @as(u32, @truncate(String.Builder.stringHash(name)));
|
||||
return trusted_dependencies.contains(hash);
|
||||
}
|
||||
|
||||
return if (check_default) default_trusted_dependencies.has(name) else false;
|
||||
return default_trusted_dependencies.has(name);
|
||||
}
|
||||
|
||||
pub const NameHashMap = std.ArrayHashMapUnmanaged(PackageNameHash, String, ArrayIdentityContext.U64, false);
|
||||
|
||||
@@ -267,10 +267,9 @@ pub const Scripts = extern struct {
|
||||
folder_path: *bun.AbsPath(.{ .sep = .auto }),
|
||||
folder_name: string,
|
||||
resolution: *const Resolution,
|
||||
check_default_trusted: bool,
|
||||
) !?Package.Scripts.List {
|
||||
if (this.hasAny()) {
|
||||
const add_node_gyp_rebuild_script = if (lockfile.hasTrustedDependency(folder_name, check_default_trusted) and
|
||||
const add_node_gyp_rebuild_script = if (lockfile.hasTrustedDependency(folder_name) and
|
||||
this.install.isEmpty() and
|
||||
this.preinstall.isEmpty())
|
||||
brk: {
|
||||
|
||||
@@ -85,7 +85,7 @@ pub const PatchFile = struct {
|
||||
}).asErr()) |e| return e.withoutPath();
|
||||
}
|
||||
|
||||
if (bun.sys.renameat(patch_dir, from_path, patch_dir, to_path).asErr()) |e| {
|
||||
if (bun.sys.renameatConcurrently(patch_dir, from_path, patch_dir, to_path, .{ .move_fallback = true }).asErr()) |e| {
|
||||
return e.withoutPath();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -96,9 +96,14 @@ pub const ShellMvBatchedTask = struct {
|
||||
|
||||
switch (Syscall.renameat(this.cwd, src, this.cwd, this.target)) {
|
||||
.err => |e| {
|
||||
if (e.getErrno() == .NOTDIR) {
|
||||
if (e.getErrno() == .XDEV) {
|
||||
// Cross-device move: fall back to copy + delete
|
||||
this.moveAcrossFilesystems(src, this.target);
|
||||
} else if (e.getErrno() == .NOTDIR) {
|
||||
this.err = e.withPath(this.target);
|
||||
} else this.err = e;
|
||||
} else {
|
||||
this.err = e;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@@ -120,8 +125,14 @@ pub const ShellMvBatchedTask = struct {
|
||||
ResolvePath.basename(src),
|
||||
}, .auto);
|
||||
|
||||
this.err = e.withPath(bun.default_allocator.dupeZ(u8, target_path[0..]) catch bun.outOfMemory());
|
||||
return false;
|
||||
if (e.getErrno() == .XDEV) {
|
||||
// Cross-device move: fall back to copy + delete
|
||||
this.moveAcrossFilesystems(src, target_path);
|
||||
return this.err == null;
|
||||
} else {
|
||||
this.err = e.withPath(bun.default_allocator.dupeZ(u8, target_path[0..]) catch bun.outOfMemory());
|
||||
return false;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
@@ -152,11 +163,13 @@ pub const ShellMvBatchedTask = struct {
|
||||
/// rm -rf source_file
|
||||
/// ```
|
||||
fn moveAcrossFilesystems(this: *@This(), src: [:0]const u8, dest: [:0]const u8) void {
|
||||
_ = this;
|
||||
_ = src;
|
||||
_ = dest;
|
||||
|
||||
// TODO
|
||||
// Use Bun's cross-device move functionality
|
||||
switch (Syscall.moveFileZSlowMaybe(this.cwd, src, this.cwd, dest)) {
|
||||
.err => |e| {
|
||||
this.err = e.withPath(dest);
|
||||
},
|
||||
.result => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runFromMainThread(this: *@This()) void {
|
||||
|
||||
@@ -1845,187 +1845,6 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) {
|
||||
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
|
||||
});
|
||||
|
||||
describe("disableDefaultTrustedDependencies bunfig option", async () => {
|
||||
test("install respects it", async () => {
|
||||
await verdaccio.writeBunfig(packageDir, { saveTextLockfile: false });
|
||||
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
|
||||
|
||||
// Append disableDefaultTrustedDependencies to the existing bunfig.toml
|
||||
const existingBunfig = await file(join(packageDir, "bunfig.toml")).text();
|
||||
await writeFile(join(packageDir, "bunfig.toml"), existingBunfig + "\ndisableDefaultTrustedDependencies = true");
|
||||
|
||||
await writeFile(
|
||||
packageJson,
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "1.2.3",
|
||||
dependencies: {
|
||||
// electron is in the default trusted dependencies list
|
||||
electron: "1.0.0",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
var { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
stderr: "pipe",
|
||||
env: testEnv,
|
||||
});
|
||||
|
||||
var err = await stderr.text();
|
||||
var out = await stdout.text();
|
||||
expect(err).toContain("Saved lockfile");
|
||||
expect(err).not.toContain("not found");
|
||||
expect(err).not.toContain("error:");
|
||||
expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([
|
||||
expect.stringContaining("bun install v1."),
|
||||
"",
|
||||
"+ electron@1.0.0",
|
||||
"",
|
||||
"1 package installed",
|
||||
"",
|
||||
"Blocked 1 postinstall. Run `bun pm untrusted` for details.",
|
||||
"",
|
||||
]);
|
||||
expect(await exited).toBe(0);
|
||||
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
|
||||
|
||||
// electron scripts should NOT have run
|
||||
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
|
||||
|
||||
// Now add electron to trustedDependencies and it should run
|
||||
await writeFile(
|
||||
packageJson,
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "1.2.3",
|
||||
dependencies: {
|
||||
electron: "1.0.0",
|
||||
},
|
||||
trustedDependencies: ["electron"],
|
||||
}),
|
||||
);
|
||||
|
||||
({ stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
stderr: "pipe",
|
||||
env: testEnv,
|
||||
}));
|
||||
|
||||
err = await stderr.text();
|
||||
out = await stdout.text();
|
||||
expect(err).toContain("Saved lockfile");
|
||||
expect(await exited).toBe(0);
|
||||
|
||||
// Now electron scripts SHOULD have run
|
||||
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
|
||||
});
|
||||
|
||||
test("bun pm untrusted respects it", async () => {
|
||||
await verdaccio.writeBunfig(packageDir, { saveTextLockfile: false });
|
||||
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
|
||||
|
||||
const existingBunfig = await file(join(packageDir, "bunfig.toml")).text();
|
||||
await writeFile(join(packageDir, "bunfig.toml"), existingBunfig + "\ndisableDefaultTrustedDependencies = true");
|
||||
|
||||
await writeFile(
|
||||
packageJson,
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "1.2.3",
|
||||
dependencies: {
|
||||
electron: "1.0.0",
|
||||
"uses-what-bin": "1.0.0",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
var { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
stderr: "pipe",
|
||||
env: testEnv,
|
||||
});
|
||||
|
||||
await exited;
|
||||
|
||||
({ stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "pm", "untrusted"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
stderr: "pipe",
|
||||
env: testEnv,
|
||||
}));
|
||||
|
||||
var out = await stdout.text();
|
||||
expect(await exited).toBe(0);
|
||||
|
||||
// Both packages should be listed as untrusted
|
||||
expect(out).toContain("electron");
|
||||
expect(out).toContain("uses-what-bin");
|
||||
expect(out).toContain("preinstall");
|
||||
expect(out).toContain("install");
|
||||
});
|
||||
|
||||
test("bun pm trust respects it", async () => {
|
||||
await verdaccio.writeBunfig(packageDir, { saveTextLockfile: false });
|
||||
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
|
||||
|
||||
const existingBunfig = await file(join(packageDir, "bunfig.toml")).text();
|
||||
await writeFile(join(packageDir, "bunfig.toml"), existingBunfig + "\ndisableDefaultTrustedDependencies = true");
|
||||
|
||||
await writeFile(
|
||||
packageJson,
|
||||
JSON.stringify({
|
||||
name: "foo",
|
||||
version: "1.2.3",
|
||||
dependencies: {
|
||||
electron: "1.0.0",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
var { stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
stderr: "pipe",
|
||||
env: testEnv,
|
||||
});
|
||||
|
||||
await exited;
|
||||
|
||||
// electron script should not have run
|
||||
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
|
||||
|
||||
// Trust electron
|
||||
({ stdout, stderr, exited } = spawn({
|
||||
cmd: [bunExe(), "pm", "trust", "electron"],
|
||||
cwd: packageDir,
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
stderr: "pipe",
|
||||
env: testEnv,
|
||||
}));
|
||||
|
||||
var out = await stdout.text();
|
||||
expect(await exited).toBe(0);
|
||||
|
||||
// electron script should now have run
|
||||
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe("--trust", async () => {
|
||||
test("unhoisted untrusted scripts, none at root node_modules", async () => {
|
||||
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
|
||||
|
||||
271
test/cli/install/patch.test.ts
Normal file
271
test/cli/install/patch.test.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
describe("bun patch", () => {
|
||||
it("should work across different mount points (cross-device)", async () => {
|
||||
// Skip this test on Windows as cross-device scenarios are different
|
||||
if (process.platform === "win32") {
|
||||
console.log("Skipping cross-device test on Windows");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a temporary project directory
|
||||
const testDir = tempDirWithFiles("patch-cross-device", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "patch-test",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"is-number": "^7.0.0",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Install dependencies first
|
||||
const installProcess = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [installStdout, installStderr, installExitCode] = await Promise.all([
|
||||
new Response(installProcess.stdout).text(),
|
||||
new Response(installProcess.stderr).text(),
|
||||
installProcess.exited,
|
||||
]);
|
||||
|
||||
if (installExitCode !== 0) {
|
||||
throw new Error(`Install failed: ${installStderr}`);
|
||||
}
|
||||
|
||||
// Create the patch
|
||||
const patchProcess = Bun.spawn({
|
||||
cmd: [bunExe(), "patch", "is-number"],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [patchStdout, patchStderr, patchExitCode] = await Promise.all([
|
||||
new Response(patchProcess.stdout).text(),
|
||||
new Response(patchProcess.stderr).text(),
|
||||
patchProcess.exited,
|
||||
]);
|
||||
|
||||
expect(patchExitCode).toBe(0);
|
||||
expect(patchStderr).not.toContain("operation not permitted");
|
||||
expect(patchStderr).not.toContain("failed renaming patch");
|
||||
|
||||
// Make a small change to the package
|
||||
const patchDir = join(testDir, "node_modules", "is-number");
|
||||
expect(existsSync(patchDir)).toBe(true);
|
||||
|
||||
const packageJsonPath = join(patchDir, "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
packageJson.description = "Modified for testing cross-device patch functionality";
|
||||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// Commit the patch - this is where cross-device issues would occur
|
||||
const commitProcess = Bun.spawn({
|
||||
cmd: [bunExe(), "patch", "--commit", patchDir],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [commitStdout, commitStderr, commitExitCode] = await Promise.all([
|
||||
new Response(commitProcess.stdout).text(),
|
||||
new Response(commitProcess.stderr).text(),
|
||||
commitProcess.exited,
|
||||
]);
|
||||
|
||||
if (commitExitCode !== 0) {
|
||||
console.error("Patch commit failed!");
|
||||
console.error("Exit code:", commitExitCode);
|
||||
console.error("Stdout:", commitStdout);
|
||||
console.error("Stderr:", commitStderr);
|
||||
}
|
||||
expect(commitExitCode).toBe(0);
|
||||
expect(commitStderr).not.toContain("operation not permitted");
|
||||
expect(commitStderr).not.toContain("failed renaming patch file to patches dir");
|
||||
|
||||
// Verify the patch was created successfully
|
||||
const finalPackageJson = JSON.parse(readFileSync(join(testDir, "package.json"), "utf8"));
|
||||
expect(finalPackageJson.patchedDependencies).toBeDefined();
|
||||
|
||||
// The patch key includes the version number
|
||||
const patchKey = Object.keys(finalPackageJson.patchedDependencies)[0];
|
||||
expect(patchKey).toMatch(/^is-number@/);
|
||||
|
||||
// Verify the patch file exists
|
||||
const patchFile = finalPackageJson.patchedDependencies[patchKey];
|
||||
expect(existsSync(join(testDir, patchFile))).toBe(true);
|
||||
|
||||
// Verify cross-device fallback was used (optional - shows it's working)
|
||||
if (commitStderr.includes("renameatConcurrently() failed with E.XDEV")) {
|
||||
console.log("✓ Cross-device fallback was triggered and handled correctly");
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("should handle cross-device scenarios with proper fallback", async () => {
|
||||
// Skip this test on Windows as cross-device scenarios are different
|
||||
if (process.platform === "win32") {
|
||||
console.log("Skipping cross-device fallback test on Windows");
|
||||
return;
|
||||
}
|
||||
|
||||
// This test specifically ensures that if XDEV errors occur,
|
||||
// the fallback copy mechanism works correctly
|
||||
const testDir = tempDirWithFiles("patch-xdev-fallback", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "patch-xdev-test",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"ms": "^2.1.0",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Install dependencies
|
||||
const installResult = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
await installResult.exited;
|
||||
|
||||
// Create patch
|
||||
const patchResult = Bun.spawn({
|
||||
cmd: [bunExe(), "patch", "ms"],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
await patchResult.exited;
|
||||
|
||||
// Modify the package
|
||||
const patchDir = join(testDir, "node_modules", "ms");
|
||||
const packageJsonPath = join(patchDir, "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
packageJson.description = "Testing XDEV fallback mechanism";
|
||||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// Enable debug logging to verify fallback is triggered when needed
|
||||
const debugEnv = { ...bunEnv, BUN_DEBUG_QUIET_LOGS: "0" };
|
||||
|
||||
const commitResult = Bun.spawn({
|
||||
cmd: [bunExe(), "patch", "--commit", patchDir],
|
||||
env: debugEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [commitStdout, commitStderr, commitExitCode] = await Promise.all([
|
||||
new Response(commitResult.stdout).text(),
|
||||
new Response(commitResult.stderr).text(),
|
||||
commitResult.exited,
|
||||
]);
|
||||
|
||||
if (commitExitCode !== 0) {
|
||||
console.error("Patch commit failed in fallback test!");
|
||||
console.error("Exit code:", commitExitCode);
|
||||
console.error("Stdout:", commitStdout);
|
||||
console.error("Stderr:", commitStderr);
|
||||
}
|
||||
expect(commitExitCode).toBe(0);
|
||||
|
||||
// The patch should succeed regardless of whether XDEV fallback was needed
|
||||
const finalPackageJson = JSON.parse(readFileSync(join(testDir, "package.json"), "utf8"));
|
||||
expect(finalPackageJson.patchedDependencies).toBeDefined();
|
||||
const msKey = Object.keys(finalPackageJson.patchedDependencies).find(key => key.startsWith("ms@"));
|
||||
expect(msKey).toBeDefined();
|
||||
|
||||
// If we see the debug message, that means the fallback worked
|
||||
if (commitStderr.includes("renameatConcurrently() failed with E.XDEV")) {
|
||||
console.log("✓ Cross-device fallback was triggered and handled correctly");
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("should not crash with EPERM errors from renameat operations", async () => {
|
||||
// This test ensures patch operations work correctly on all platforms
|
||||
const testDir = tempDirWithFiles("patch-eperm-test", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "patch-eperm-test",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
"lodash": "^4.17.21",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Install dependencies
|
||||
const installResult = Bun.spawn({
|
||||
cmd: [bunExe(), "install"],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const installExitCode = await installResult.exited;
|
||||
expect(installExitCode).toBe(0);
|
||||
|
||||
// Create patch
|
||||
const patchResult = Bun.spawn({
|
||||
cmd: [bunExe(), "patch", "lodash"],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const patchExitCode = await patchResult.exited;
|
||||
expect(patchExitCode).toBe(0);
|
||||
|
||||
// Modify the package
|
||||
const patchDir = join(testDir, "node_modules", "lodash");
|
||||
const packageJsonPath = join(patchDir, "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
||||
packageJson.version = "4.17.22-patched";
|
||||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// Commit patch - should not fail with EPERM or similar permission errors
|
||||
const commitResult = Bun.spawn({
|
||||
cmd: [bunExe(), "patch", "--commit", patchDir],
|
||||
env: bunEnv,
|
||||
cwd: testDir,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
const [commitStdout, commitStderr, commitExitCode] = await Promise.all([
|
||||
new Response(commitResult.stdout).text(),
|
||||
new Response(commitResult.stderr).text(),
|
||||
commitResult.exited,
|
||||
]);
|
||||
|
||||
if (commitExitCode !== 0) {
|
||||
console.error("Patch commit failed in EPERM test!");
|
||||
console.error("Exit code:", commitExitCode);
|
||||
console.error("Stdout:", commitStdout);
|
||||
console.error("Stderr:", commitStderr);
|
||||
}
|
||||
expect(commitExitCode).toBe(0);
|
||||
expect(commitStderr).not.toContain("operation not permitted");
|
||||
expect(commitStderr).not.toContain("EPERM");
|
||||
expect(commitStderr).not.toContain("failed renaming patch file to patches dir");
|
||||
|
||||
// Verify patch was applied
|
||||
const finalPackageJson = JSON.parse(readFileSync(join(testDir, "package.json"), "utf8"));
|
||||
expect(finalPackageJson.patchedDependencies).toBeDefined();
|
||||
const lodashKey = Object.keys(finalPackageJson.patchedDependencies).find(key => key.startsWith("lodash@"));
|
||||
expect(lodashKey).toBeDefined();
|
||||
}, 30000);
|
||||
});
|
||||
Reference in New Issue
Block a user