Compare commits

...

46 Commits

Author SHA1 Message Date
Meghan Denny
418c83e072 start verdaccio in ci instead of in test block 2024-03-22 20:07:39 -07:00
Meghan Denny
57b79151d3 forceWaiterThread bun-install-registry tests can be turned back on 2024-03-22 17:22:22 -07:00
Meghan Denny
65dcdff8cf windows: pass test/cli/install/bun-run-bunfig.test.ts 2024-03-22 17:21:58 -07:00
Meghan Denny
4302a43374 which: windows: need to make sure the absolute path exists 2024-03-22 17:21:30 -07:00
Meghan Denny
d71846757c shell: windows: use case insensitive env map 2024-03-22 17:21:03 -07:00
Meghan Denny
1916a68cfd make 'bun add' also not print a crash trace when pm fails cleanly 2024-03-22 17:19:55 -07:00
Meghan Denny
b475a769eb fix recurring finalize crash 2024-03-22 17:18:42 -07:00
Meghan Denny
3a8691fe65 Merge branch 'main' into nektro-patch-22129 2024-03-21 21:36:59 -07:00
Meghan Denny
e4ecd46bd6 tidy 2024-03-21 21:34:55 -07:00
Meghan Denny
d0cab7c108 these pass now 2024-03-21 21:33:41 -07:00
Meghan Denny
8817c77124 skip these because they need if statements in the shell 2024-03-21 21:33:08 -07:00
Meghan Denny
868c694157 disable the test that crashes, unrelated bug 2024-03-21 21:32:48 -07:00
Meghan Denny
51a7a01c7e import-meta.test.js: split these two
only one is failing on windows
2024-03-21 21:30:17 -07:00
Meghan Denny
30c5f39d4c windows: pass bun-run.test.ts 2024-03-21 21:29:33 -07:00
Meghan Denny
3438f9b5ab which: windows: dont check for files with no extensions but do if it matches the extensions we like 2024-03-21 21:28:59 -07:00
Meghan Denny
a2a8f95d41 which: succeed right away if given an absolute path 2024-03-21 21:28:25 -07:00
Meghan Denny
709afb9c71 shell: no need to allocate these messages 2024-03-21 21:27:22 -07:00
Meghan Denny
02db0e43df windows: use directory symlinks for directories 2024-03-21 21:26:31 -07:00
Meghan Denny
935146e9ab we actually did need this 2024-03-21 21:25:20 -07:00
Meghan Denny
b67739b7db update this check for windows too 2024-03-21 21:25:02 -07:00
Meghan Denny
632bdee900 i backed out of bun exec for now 2024-03-21 21:24:47 -07:00
Meghan Denny
7f6394e761 implement this todo 2024-03-21 21:24:28 -07:00
Meghan Denny
1936940043 more tests 2024-03-20 23:43:19 -07:00
Meghan Denny
9168a64f61 more tests 2024-03-20 23:27:12 -07:00
Meghan Denny
53ed865927 these pass now 2024-03-20 23:26:37 -07:00
Meghan Denny
65889dc803 more robust 2024-03-20 23:25:32 -07:00
Meghan Denny
1590631cf9 those arent necessary anymore 2024-03-20 23:24:45 -07:00
Meghan Denny
a6f15e4277 these weren't necessary 2024-03-20 23:24:05 -07:00
Meghan Denny
1fc547fdd9 shell: implement exit builtin 2024-03-20 23:21:41 -07:00
Meghan Denny
49ee19cb41 fix path error here 2024-03-20 23:20:06 -07:00
Meghan Denny
40dcadf0da temp skip some tests failing on windows 2024-03-20 17:54:20 -07:00
Meghan Denny
e8115529d3 fix windows build from poxix build fix 2024-03-20 17:53:19 -07:00
Meghan Denny
7c4616b2a1 Merge branch 'main' into nektro-patch-22129 2024-03-20 01:33:27 -07:00
Meghan Denny
b8b8b26f85 small fixes 2024-03-20 01:31:45 -07:00
Meghan Denny
714331909f add ensureTempNodeGypScript test 2024-03-20 01:30:55 -07:00
Meghan Denny
b3e53e1eb5 improve shebang script writing 2024-03-20 01:30:06 -07:00
Meghan Denny
5332a17551 remove bun exec 2024-03-20 01:28:38 -07:00
Meghan Denny
203022e04c use bun run instead of bun exec 2024-03-20 01:27:46 -07:00
Meghan Denny
c0e84c9076 windows: run: fix path delimeter placement 2024-03-20 01:26:16 -07:00
Meghan Denny
c861a6fcda use bun.exe_suffix in bunx 2024-03-20 01:25:37 -07:00
Meghan Denny
f1a61ceb8e fix selfExePath 2024-03-20 01:25:17 -07:00
Dylan Conway
d2ff2cc1dc fix invalid pointer 2024-03-19 23:16:07 -07:00
Meghan Denny
0b11991811 commit exec_command.zig 2024-03-19 22:23:20 -07:00
Meghan Denny
186456788e use 'bun exec' to run lifecycle scripts on windows 2024-03-19 22:20:24 -07:00
Meghan Denny
d6b815268d cli: add 'bun exec' to run bun shell 2024-03-19 22:19:55 -07:00
Meghan Denny
131bea538d memoize calls to selfExePath 2024-03-19 22:16:21 -07:00
34 changed files with 508 additions and 287 deletions

View File

@@ -253,6 +253,8 @@ jobs:
# # Core filenames will be of the form executable.pid.timestamp:
# sudo bash -c 'echo "/cores/%e.%p.%t" > /proc/sys/kernel/core_pattern'
- id: verdaccio
run: bun run verdaccio &
- id: test
name: Test (node runner)
env:

View File

@@ -422,6 +422,8 @@ jobs:
bun install --verbose
bun install --cwd=test --verbose
bun install --cwd=packages/bun-internal-test --verbose
- id: verdaccio
run: bun run verdaccio &
- id: test
name: Test (node runner)
env:

View File

@@ -409,6 +409,8 @@ jobs:
bun install --verbose
bun install --cwd=test --verbose
bun install --cwd=packages/bun-internal-test --verbose
- id: verdaccio
run: bun run verdaccio &
- id: test
name: Test (node runner)
env:

View File

@@ -406,6 +406,8 @@ jobs:
bun install --verbose
bun install --cwd=test --verbose
bun install --cwd=packages/bun-internal-test --verbose
- id: verdaccio
run: bun run verdaccio &
- id: test
name: Test (node runner)
env:

View File

@@ -430,6 +430,8 @@ jobs:
cd test && npm install
cd ../packages/bun-internal-test && npm install
cd ../..
- id: verdaccio
run: bun run verdaccio &
- id: test
name: Run tests
env:

View File

@@ -33,6 +33,7 @@
"lint:fix": "eslint './**/*.d.ts' --cache --fix",
"test": "node packages/bun-internal-test/src/runner.node.mjs ./build/bun-debug",
"test:release": "node packages/bun-internal-test/src/runner.node.mjs ./build-release/bun",
"verdaccio": "verdaccio -c test/cli/install/registry/verdaccio.yaml",
"update-known-failures": "node packages/bun-internal-test/src/update-known-windows-failures.mjs"
}
}

View File

@@ -277,21 +277,18 @@ pub const StandaloneModuleGraph = struct {
}.toClean;
const cloned_executable_fd: bun.FileDescriptor = brk: {
var self_buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined;
const self_exe = std.fs.selfExePath(&self_buf) catch |err| {
const self_exeZ = bun.selfExePath() catch |err| {
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get self executable path: {s}", .{@errorName(err)});
Global.exit(1);
};
self_buf[self_exe.len] = 0;
const self_exeZ = self_buf[0..self_exe.len :0];
if (comptime Environment.isWindows) {
// copy self and then open it for writing
var in_buf: bun.WPathBuffer = undefined;
strings.copyU8IntoU16(&in_buf, self_exeZ);
in_buf[self_exe.len] = 0;
const in = in_buf[0..self_exe.len :0];
in_buf[self_exeZ.len] = 0;
const in = in_buf[0..self_exeZ.len :0];
var out_buf: bun.WPathBuffer = undefined;
strings.copyU8IntoU16(&out_buf, zname);
out_buf[zname.len] = 0;
@@ -733,10 +730,8 @@ pub const StandaloneModuleGraph = struct {
.mac => {
// Use of MAX_PATH_BYTES here is valid as the resulting path is immediately
// opened with no modification.
var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
const self_exe_path = try std.fs.selfExePath(&buf);
buf[self_exe_path.len] = 0;
const file = try std.fs.openFileAbsoluteZ(buf[0..self_exe_path.len :0].ptr, .{});
const self_exe_path = try bun.selfExePath();
const file = try std.fs.openFileAbsoluteZ(self_exe_path.ptr, .{});
return bun.toFD(file.handle);
},
.windows => {

View File

@@ -1261,9 +1261,10 @@ pub const Subprocess = struct {
} else {
subprocess.flags.has_stdin_destructor_called = false;
subprocess.weak_file_sink_stdin_ptr = pipe;
if (@intFromPtr(pipe.signal.ptr) == @intFromPtr(subprocess)) {
if (pipe.signal.ptr == @as(?*anyopaque, @ptrCast(this))) {
pipe.signal.clear();
}
return pipe.toJSWithDestructor(
globalThis,
JSC.WebCore.SinkDestructor.Ptr.init(subprocess),
@@ -1283,8 +1284,11 @@ pub const Subprocess = struct {
return switch (this.*) {
.pipe => |pipe| {
pipe.deref();
if (pipe.signal.ptr == @as(?*anyopaque, @ptrCast(this))) {
pipe.signal.clear();
}
pipe.deref();
this.* = .{ .ignore = {} };
},
.buffer => {

View File

@@ -5757,13 +5757,8 @@ pub const NodeFS = struct {
const target: [:0]u8 = args.old_path.sliceZWithForceCopy(&this.sync_error_buf, true);
// UV does not normalize slashes in symlink targets, but Node does
// See https://github.com/oven-sh/bun/issues/8273
//
// TODO: investigate if simd can be easily used here
for (target) |*c| {
if (c.* == '/') {
c.* = '\\';
}
}
bun.path.posixToPlatformInPlace(u8, target);
return Syscall.symlinkUV(
target,
args.new_path.sliceZ(&to_buf),

View File

@@ -4852,12 +4852,10 @@ pub const Process = struct {
}
pub fn getExecPath(globalObject: *JSC.JSGlobalObject) callconv(.C) JSC.JSValue {
var buf: bun.PathBuffer = undefined;
const out = std.fs.selfExePath(&buf) catch {
const out = bun.selfExePath() catch {
// if for any reason we are unable to get the executable path, we just return argv[0]
return getArgv0(globalObject);
};
return JSC.ZigString.fromUTF8(out).toValueGC(globalObject);
}
@@ -4935,7 +4933,7 @@ pub const Process = struct {
bun.String.static("bun"),
);
} else {
const exe_path = std.fs.selfExePathAlloc(allocator) catch null;
const exe_path = bun.selfExePath() catch null;
args_list.appendAssumeCapacity(
if (exe_path) |str| bun.String.fromUTF8(str) else bun.String.static("bun"),
);

View File

@@ -1555,7 +1555,7 @@ pub fn reloadProcess(
}
// we must clone selfExePath incase the argv[0] was not an absolute path (what appears in the terminal)
const exec_path = (allocator.dupeZ(u8, std.fs.selfExePathAlloc(allocator) catch unreachable) catch unreachable).ptr;
const exec_path = (allocator.dupeZ(u8, bun.selfExePath() catch unreachable) catch unreachable).ptr;
// we clone argv so that the memory address isn't the same as the libc one
const newargv = @as([*:null]?[*:0]const u8, @ptrCast(dupe_argv.ptr));
@@ -2803,6 +2803,21 @@ pub fn linuxKernelVersion() Semver.Version {
return @import("./analytics.zig").GenerateHeader.GeneratePlatform.kernelVersion();
}
pub fn selfExePath() ![:0]u8 {
const memo = struct {
var set = false;
// this is lame; open issues to fix std soon
var value: [4096]u8 = undefined;
var len: usize = 0;
};
if (memo.set) return memo.value[0..memo.len :0];
const init = try std.fs.selfExePath(&memo.value);
memo.len = init.len;
memo.value[memo.len] = 0;
memo.set = true;
return memo.value[0..memo.len :0];
}
pub const WindowsSpawnWorkaround = @import("./child_process_windows.zig");
pub const exe_suffix = if (Environment.isWindows) ".exe" else "";

View File

@@ -1218,11 +1218,11 @@ pub const Command = struct {
};
pub fn isBunX(argv0: []const u8) bool {
return strings.endsWithComptime(argv0, "bunx") or (Environment.isDebug and strings.endsWithComptime(argv0, "bunx-debug"));
return strings.endsWithComptime(argv0, "bunx" ++ bun.exe_suffix);
}
pub fn isNode(argv0: []const u8) bool {
return strings.endsWithComptime(argv0, "node");
return strings.endsWithComptime(argv0, "node" ++ bun.exe_suffix);
}
pub fn which() Tag {

View File

@@ -3,6 +3,6 @@ const PackageManager = @import("../install/install.zig").PackageManager;
pub const AddCommand = struct {
pub fn exec(ctx: Command.Context) !void {
try PackageManager.add(ctx);
try PackageManager.exec(ctx);
}
};

View File

@@ -423,18 +423,11 @@ pub const BunxCommand = struct {
debug("bunx_cache_dir: {s}", .{bunx_cache_dir});
const bin_extension = switch (Environment.os) {
.windows => ".exe",
.mac => "",
.linux => "",
.wasm => "",
};
var absolute_in_cache_dir_buf: bun.PathBuffer = undefined;
var absolute_in_cache_dir = std.fmt.bufPrint(
&absolute_in_cache_dir_buf,
bun.pathLiteral("{s}/node_modules/.bin/{s}{s}"),
.{ bunx_cache_dir, initial_bin_name, bin_extension },
.{ bunx_cache_dir, initial_bin_name, bun.exe_suffix },
) catch return error.PathTooLong;
const passthrough = passthrough_list.items;
@@ -523,7 +516,7 @@ pub const BunxCommand = struct {
if (getBinName(&this_bundler, root_dir_fd, bunx_cache_dir, initial_bin_name)) |package_name_for_bin| {
// if we check the bin name and its actually the same, we don't need to check $PATH here again
if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) {
absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}{s}", .{ bunx_cache_dir, package_name_for_bin, bin_extension }) catch unreachable;
absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}{s}", .{ bunx_cache_dir, package_name_for_bin, bun.exe_suffix }) catch unreachable;
// Only use the system-installed version if there is no version specified
if (update_request.version.literal.isEmpty()) {
@@ -572,7 +565,7 @@ pub const BunxCommand = struct {
}
var args = std.BoundedArray([]const u8, 7).fromSlice(&.{
try std.fs.selfExePathAlloc(ctx.allocator),
try bun.selfExePath(),
"add",
install_param,
"--no-summary",
@@ -635,7 +628,7 @@ pub const BunxCommand = struct {
},
}
absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}{s}"), .{ bunx_cache_dir, initial_bin_name, bin_extension }) catch unreachable;
absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, bun.pathLiteral("{s}/node_modules/.bin/{s}{s}"), .{ bunx_cache_dir, initial_bin_name, bun.exe_suffix }) catch unreachable;
// Similar to "npx":
//
@@ -663,7 +656,7 @@ pub const BunxCommand = struct {
// 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use
if (getBinNameFromTempDirectory(&this_bundler, bunx_cache_dir, result_package_name)) |package_name_for_bin| {
if (!strings.eqlLong(package_name_for_bin, initial_bin_name, true)) {
absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}{s}", .{ bunx_cache_dir, package_name_for_bin, bin_extension }) catch unreachable;
absolute_in_cache_dir = std.fmt.bufPrint(&absolute_in_cache_dir_buf, "{s}/node_modules/.bin/{s}{s}", .{ bunx_cache_dir, package_name_for_bin, bun.exe_suffix }) catch unreachable;
if (bun.which(
&path_buf,

View File

@@ -1402,7 +1402,7 @@ pub const CreateCommand = struct {
if (!create_options.skip_install) {
npm_client_ = NPMClient{
.tag = .bun,
.bin = try std.fs.selfExePathAlloc(ctx.allocator),
.bin = try bun.selfExePath(),
};
}

View File

@@ -427,7 +427,7 @@ pub const InitCommand = struct {
if (exists("package.json")) {
var process = std.ChildProcess.init(
&.{
try std.fs.selfExePathAlloc(alloc),
try bun.selfExePath(),
"install",
},
alloc,

View File

@@ -1,18 +1,8 @@
const Command = @import("../cli.zig").Command;
const bun = @import("root").bun;
const PackageManager = @import("../install/install.zig").PackageManager;
pub const InstallCommand = struct {
pub fn exec(ctx: Command.Context) !void {
PackageManager.install(ctx) catch |err| switch (err) {
error.InstallFailed,
error.InvalidPackageJSON,
=> {
const log = &bun.CLI.Cli.log_;
log.printForLogLevel(bun.Output.errorWriter()) catch {};
bun.Global.exit(1);
},
else => |e| return e,
};
try PackageManager.exec(ctx);
}
};

View File

@@ -46,7 +46,7 @@ pub const InstallCompletionsCommand = struct {
const bunx_name = if (Environment.isDebug) "bunx-debug" else "bunx";
fn installBunxSymlinkPosix(allocator: std.mem.Allocator, cwd: []const u8) !void {
fn installBunxSymlinkPosix(cwd: []const u8) !void {
var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
// don't install it if it's already there
@@ -54,7 +54,7 @@ pub const InstallCompletionsCommand = struct {
return;
// first try installing the symlink into the same directory as the bun executable
const exe = try std.fs.selfExePathAlloc(allocator);
const exe = try bun.selfExePath();
var target_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
var target = std.fmt.bufPrint(&target_buf, "{s}/" ++ bunx_name, .{std.fs.path.dirname(exe).?}) catch unreachable;
std.os.symlink(exe, target) catch {
@@ -89,7 +89,7 @@ pub const InstallCompletionsCommand = struct {
};
}
fn installBunxSymlinkWindows(_: std.mem.Allocator, _: []const u8) !void {
fn installBunxSymlinkWindows(_: []const u8) !void {
// Because symlinks are not always allowed on windows,
// `bunx.exe` on windows is a hardlink to `bun.exe`
// for this to work, we need to delete and recreate the hardlink every time
@@ -130,11 +130,11 @@ pub const InstallCompletionsCommand = struct {
}
}
fn installBunxSymlink(allocator: std.mem.Allocator, cwd: []const u8) !void {
fn installBunxSymlink(cwd: []const u8) !void {
if (Environment.isWindows) {
try installBunxSymlinkWindows(allocator, cwd);
try installBunxSymlinkWindows(cwd);
} else {
try installBunxSymlinkPosix(allocator, cwd);
try installBunxSymlinkPosix(cwd);
}
}
@@ -190,7 +190,7 @@ pub const InstallCompletionsCommand = struct {
Global.exit(fail_exit_code);
};
installBunxSymlink(allocator, cwd) catch {};
installBunxSymlink(cwd) catch {};
if (Environment.isWindows) {
installUninstallerWindows() catch {};

View File

@@ -636,7 +636,7 @@ pub const RunCommand = struct {
argv0 = bun.argv()[0];
} else if (optional_bun_path.len == 0) {
// otherwise, ask the OS for the absolute path
var self = try std.fs.selfExePath(&self_exe_bin_path_buf);
var self = try bun.selfExePath();
if (self.len > 0) {
self.ptr[self.len] = 0;
argv0 = @as([*:0]const u8, @ptrCast(self.ptr));
@@ -731,6 +731,9 @@ pub const RunCommand = struct {
}
}
}
if (PATH.items.len > 0 and PATH.items[PATH.items.len - 1] != std.fs.path.delimiter) {
try PATH.append(std.fs.path.delimiter);
}
// The reason for the extra delim is because we are going to append the system PATH
// later on. this is done by the caller, and explains why we are adding bun_node_dir
@@ -827,7 +830,7 @@ pub const RunCommand = struct {
if (this_bundler.env.get("npm_execpath") == null) {
// we don't care if this fails
if (std.fs.selfExePathAlloc(ctx.allocator)) |self_exe_path| {
if (bun.selfExePath()) |self_exe_path| {
this_bundler.env.map.putDefault("npm_execpath", self_exe_path) catch unreachable;
} else |_| {}
}

View File

@@ -756,7 +756,7 @@ pub const UpgradeCommand = struct {
}
}
const destination_executable = std.fs.selfExePath(&current_executable_buf) catch return error.UpgradeFailedMissingExecutable;
const destination_executable = bun.selfExePath() catch return error.UpgradeFailedMissingExecutable;
current_executable_buf[destination_executable.len] = 0;
const target_filename_ = std.fs.path.basename(destination_executable);

View File

@@ -388,7 +388,7 @@ fn extract(this: *const ExtractTarball, tgz_bytes: []const u8) !Install.ExtractD
.npm => folder_name[name.len + 1 ..],
else => folder_name,
},
.{},
.{ .is_directory = true },
) catch break :create_index;
}

View File

@@ -1361,19 +1361,13 @@ pub const PackageInstall = struct {
if (comptime Environment.isWindows) {
var fd_path_buf: bun.PathBuffer = undefined;
// buf[0] = '\\';
// buf[1] = '\\';
// buf[2] = '?';
// buf[3] = '\\';
// buf[0..4].* = bun.windows.nt_maxpath_prefix;
// const dest_path = try bun.getFdPath(subdir.fd, &fd_path_buf);
// strings.copyU8IntoU16(buf[4..], dest_path);
// buf[dest_path.len + 4] = '\\';
// to_copy_buf = buf[dest_path.len + 5 ..];
// buf2[0] = '\\';
// buf2[1] = '\\';
// buf2[2] = '?';
// buf2[3] = '\\';
// buf2[0..4].* = bun.windows.nt_maxpath_prefix;
// const cache_path = try bun.getFdPath(cached_package_dir.fd, &fd_path_buf);
// strings.copyU8IntoU16(buf2[4..], cache_path);
// buf2[cache_path.len + 4] = '\\';
@@ -2554,9 +2548,6 @@ pub const PackageManager = struct {
}
pub fn ensureTempNodeGypScript(this: *PackageManager) !void {
if (Environment.isWindows) {
Output.debug("TODO: VERIFY ensureTempNodeGypScript WORKS!!", .{});
}
if (this.node_gyp_tempdir_name.len > 0) return;
const tempdir = this.getTemporaryDirectory();
@@ -2592,26 +2583,34 @@ pub const PackageManager = struct {
};
defer node_gyp_file.close();
var bytes: string = switch (Environment.os) {
else => "#!/usr/bin/env node\nrequire(\"child_process\").spawnSync(\"bun\",[\"x\",\"node-gyp\",...process.argv.slice(2)],{stdio:\"inherit\"})",
.windows => "@node -e \"require('child_process').spawnSync('bun',['x','node-gyp',...process.argv.slice(2)],{stdio:'inherit'})\"",
const shebang = switch (Environment.os) {
.windows =>
\\0</* :{
\\ @echo off
\\ node %~f0 %*
\\ exit /b %errorlevel%
\\:} */0;
\\
,
else =>
\\#!/usr/bin/env node
\\
,
};
const content =
\\const child_process = require("child_process");
\\child_process.spawnSync("bun", ["x", "node-gyp", ...process.argv.slice(2)], { stdio: "inherit" });
\\
;
node_gyp_file.writeAll(shebang ++ content) catch |err| {
Output.prettyErrorln("<r><red>error<r>: <b><red>{s}<r> writing to " ++ file_name ++ " file", .{@errorName(err)});
Global.crash();
};
var index: usize = 0;
while (index < bytes.len) {
switch (bun.sys.write(bun.toFD(node_gyp_file.handle), bytes[index..])) {
.result => |written| {
index += written;
},
.err => |err| {
Output.prettyErrorln("<r><red>error<r>: <b><red>{s}<r> writing to " ++ file_name ++ " file", .{@tagName(err.getErrno())});
Global.crash();
},
}
}
// Add our node-gyp tempdir to the path
const existing_path = this.env.get("PATH") orelse "";
var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, existing_path.len + 1 + this.node_gyp_tempdir_name.len);
var PATH = try std.ArrayList(u8).initCapacity(bun.default_allocator, existing_path.len + 1 + this.temp_dir_name.len + 1 + this.node_gyp_tempdir_name.len);
try PATH.appendSlice(existing_path);
if (existing_path.len > 0 and existing_path[existing_path.len - 1] != std.fs.path.delimiter)
try PATH.append(std.fs.path.delimiter);
@@ -2620,15 +2619,16 @@ pub const PackageManager = struct {
try PATH.appendSlice(this.node_gyp_tempdir_name);
try this.env.map.put("PATH", PATH.items);
const node_gyp_abs_dir = try bun.fmt.bufPrint(&path_buf, "{s}" ++ .{std.fs.path.sep} ++ "{s}", .{
const npm_config_node_gyp = try bun.fmt.bufPrint(&path_buf, "{s}{s}{s}{s}{s}", .{
strings.withoutTrailingSlash(this.temp_dir_name),
std.fs.path.sep_str,
strings.withoutTrailingSlash(this.node_gyp_tempdir_name),
std.fs.path.sep_str,
file_name,
});
try this.env.map.putAllocKeyAndValue(this.allocator, "BUN_WHICH_IGNORE_CWD", node_gyp_abs_dir);
path_buf[node_gyp_abs_dir.len] = std.fs.path.sep;
@memcpy(path_buf[node_gyp_abs_dir.len + 1 ..][0.."node-gyp".len], "node-gyp");
const npm_config_node_gyp = path_buf[0 .. node_gyp_abs_dir.len + 1 + "node-gyp".len];
const node_gyp_abs_dir = std.fs.path.dirname(npm_config_node_gyp).?;
try this.env.map.putAllocKeyAndValue(this.allocator, "BUN_WHICH_IGNORE_CWD", node_gyp_abs_dir);
try this.env.map.putAllocKeyAndValue(this.allocator, "npm_config_node_gyp", npm_config_node_gyp);
}
@@ -7016,7 +7016,7 @@ pub const PackageManager = struct {
switch (bun.sys.sys_uv.symlinkUV(
link_path,
dest_path,
bun.windows.libuv.UV_FS_SYMLINK_JUNCTION,
bun.windows.libuv.UV_FS_SYMLINK_DIR,
)) {
.err => |err| {
Output.prettyErrorln("<r><red>error:<r> failed to create junction to node_modules in global dir due to error {}", .{err});
@@ -8128,6 +8128,19 @@ pub const PackageManager = struct {
var package_json_cwd_buf: bun.PathBuffer = undefined;
pub var package_json_cwd: string = "";
pub fn exec(ctx: Command.Context) !void {
install(ctx) catch |err| switch (err) {
error.InstallFailed,
error.InvalidPackageJSON,
=> {
const log = &bun.CLI.Cli.log_;
log.printForLogLevel(bun.Output.errorWriter()) catch {};
bun.Global.exit(1);
},
else => |e| return e,
};
}
pub inline fn install(ctx: Command.Context) !void {
var manager = try init(ctx, .install);

View File

@@ -122,7 +122,7 @@ pub const LifecycleScriptSubprocess = struct {
this.current_script_index = next_script_index;
this.has_called_process_exit = false;
const shell_bin = bun.CLI.RunCommand.findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell;
const shell_bin = bun.CLI.RunCommand.findShell(env.get("PATH") orelse "", cwd) orelse null;
var copy_script = try std.ArrayList(u8).initCapacity(manager.allocator, original_script.script.len + 1);
defer copy_script.deinit();
@@ -131,13 +131,40 @@ pub const LifecycleScriptSubprocess = struct {
const combined_script: [:0]u8 = copy_script.items[0 .. copy_script.items.len - 1 :0];
var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
var path_buf2: [bun.MAX_PATH_BYTES]u8 = undefined;
const lifecycle_script_path, var lifecycle_script_file = if (Environment.isWindows or shell_bin == null) blk: {
const tempdir = this.manager.getTemporaryDirectory();
var lifecycle_script_name = bun.span(try bun.fs.FileSystem.instance.tmpname("lifecycle-script", &path_buf, 12345));
@memcpy(path_buf[lifecycle_script_name.len..].ptr, ".bun.sh\x00");
lifecycle_script_name.len += 7;
const lifecycle_script_file = try tempdir.createFile(lifecycle_script_name, .{});
errdefer lifecycle_script_file.close();
bun.path.platformToPosixInPlace(u8, combined_script);
try lifecycle_script_file.writeAll(combined_script);
const lifecycle_script_path = try bun.getFdPath(lifecycle_script_file.handle, &path_buf2);
path_buf2[lifecycle_script_path.len] = 0;
break :blk .{
path_buf2[0..lifecycle_script_path.len :0],
@as(?std.fs.File, lifecycle_script_file),
};
} else .{ "", null };
if (lifecycle_script_file) |*f| f.close();
log("{s} - {s} $ {s}", .{ this.package_name, this.scriptName(), combined_script });
var argv = [_]?[*:0]const u8{
shell_bin,
if (Environment.isWindows) "/c" else "-c",
var argv = if (shell_bin != null and !Environment.isWindows) [_]?[*:0]const u8{
shell_bin.?,
"-c",
combined_script,
null,
} else [_]?[*:0]const u8{
try bun.selfExePath(),
"run",
lifecycle_script_path,
null,
};
if (Environment.isWindows) {
this.stdout.source = .{ .pipe = bun.default_allocator.create(uv.Pipe) catch bun.outOfMemory() };
@@ -166,6 +193,7 @@ pub const LifecycleScriptSubprocess = struct {
.windows = if (Environment.isWindows)
.{
.loop = JSC.EventLoopHandle.init(&manager.event_loop),
.verbatim_arguments = true,
}
else {},
};

View File

@@ -18,6 +18,7 @@
//! use undefined memory.
const std = @import("std");
const builtin = @import("builtin");
const string = []const u8;
const bun = @import("root").bun;
const os = std.os;
const Arena = std.heap.ArenaAllocator;
@@ -487,16 +488,23 @@ pub const CowEnvMap = Cow(EnvMap, struct {
pub const EnvMap = struct {
map: MapType,
pub const Iterator = MapType.Iterator;
const MapType = std.ArrayHashMap(EnvStr, EnvStr, struct {
pub fn hash(self: @This(), s: EnvStr) u32 {
_ = self;
if (bun.Environment.isWindows) {
return bun.CaseInsensitiveASCIIStringContext.hash(undefined, s.slice());
}
return std.array_hash_map.hashString(s.slice());
}
pub fn eql(self: @This(), a: EnvStr, b: EnvStr, b_index: usize) bool {
_ = self;
_ = b_index;
if (bun.Environment.isWindows) {
return bun.CaseInsensitiveASCIIStringContext.eql(undefined, a.slice(), b.slice(), undefined);
}
return std.array_hash_map.eqlString(a.slice(), b.slice());
}
}, true);
@@ -1148,18 +1156,21 @@ pub const Interpreter = struct {
pub fn initAndRunFromFile(mini: *JSC.MiniEventLoop, path: []const u8) !bun.shell.ExitCode {
var arena = bun.ArenaAllocator.init(bun.default_allocator);
defer arena.deinit();
const src = src: {
var file = try std.fs.cwd().openFile(path, .{});
defer file.close();
break :src try file.reader().readAllAlloc(arena.allocator(), std.math.maxInt(u32));
};
defer arena.deinit();
return initAndRunFromFileSource(mini, &arena, path, src);
}
pub fn initAndRunFromFileSource(mini: *JSC.MiniEventLoop, arena: *bun.ArenaAllocator, path: string, src: string) !bun.shell.ExitCode {
const jsobjs: []JSValue = &[_]JSValue{};
var out_parser: ?bun.shell.Parser = null;
var out_lex_result: ?bun.shell.LexResult = null;
const script = ThisInterpreter.parse(
&arena,
arena,
src,
jsobjs,
&[_]bun.String{},
@@ -1183,7 +1194,7 @@ pub const Interpreter = struct {
};
const script_heap = try arena.allocator().create(ast.Script);
script_heap.* = script;
var interp = switch (ThisInterpreter.init(.{ .mini = mini }, bun.default_allocator, &arena, script_heap, jsobjs)) {
var interp = switch (ThisInterpreter.init(.{ .mini = mini }, bun.default_allocator, arena, script_heap, jsobjs)) {
.err => |*e| {
throwShellErr(e, .{ .mini = mini });
return 1;
@@ -3784,7 +3795,9 @@ pub const Interpreter = struct {
args_slice: ?[]const [:0]const u8 = null,
cwd: bun.FileDescriptor,
impl: union(Kind) {
impl: RealImpl,
const RealImpl = union(Kind) {
cat: Cat,
touch: Touch,
mkdir: Mkdir,
@@ -3796,7 +3809,8 @@ pub const Interpreter = struct {
rm: Rm,
mv: Mv,
ls: Ls,
},
exit: Exit,
};
const Result = @import("../result.zig").Result;
@@ -3812,6 +3826,7 @@ pub const Interpreter = struct {
rm,
mv,
ls,
exit,
pub fn parentType(this: Kind) type {
_ = this;
@@ -3830,6 +3845,7 @@ pub const Interpreter = struct {
.rm => "usage: rm [-f | -i] [-dIPRrvWx] file ...\n unlink [--] file\n",
.mv => "usage: mv [-f | -i | -n] [-hv] source target\n mv [-f | -i | -n] [-v] source ... directory\n",
.ls => "usage: ls [-@ABCFGHILOPRSTUWabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n",
.exit => "usage: exit [n]\n",
};
}
@@ -3846,6 +3862,7 @@ pub const Interpreter = struct {
.rm => "rm",
.mv => "mv",
.ls => "ls",
.exit => "exit",
};
}
@@ -4007,6 +4024,7 @@ pub const Interpreter = struct {
.pwd => this.callImplWithType(Pwd, Ret, "pwd", field, args_),
.mv => this.callImplWithType(Mv, Ret, "mv", field, args_),
.ls => this.callImplWithType(Ls, Ret, "ls", field, args_),
.exit => this.callImplWithType(Exit, Ret, "exit", field, args_),
};
}
@@ -4076,26 +4094,6 @@ pub const Interpreter = struct {
};
switch (kind) {
.cat => {
cmd.exec.bltn.impl = .{
.cat = Cat{ .bltn = &cmd.exec.bltn },
};
},
.touch => {
cmd.exec.bltn.impl = .{
.touch = Touch{ .bltn = &cmd.exec.bltn },
};
},
.mkdir => {
cmd.exec.bltn.impl = .{
.mkdir = Mkdir{ .bltn = &cmd.exec.bltn },
};
},
.@"export" => {
cmd.exec.bltn.impl = .{
.@"export" = Export{ .bltn = &cmd.exec.bltn },
};
},
.rm => {
cmd.exec.bltn.impl = .{
.rm = Rm{
@@ -4112,36 +4110,10 @@ pub const Interpreter = struct {
},
};
},
.cd => {
cmd.exec.bltn.impl = .{
.cd = Cd{
.bltn = &cmd.exec.bltn,
},
};
},
.which => {
cmd.exec.bltn.impl = .{
.which = Which{
.bltn = &cmd.exec.bltn,
},
};
},
.pwd => {
cmd.exec.bltn.impl = .{
.pwd = Pwd{ .bltn = &cmd.exec.bltn },
};
},
.mv => {
cmd.exec.bltn.impl = .{
.mv = Mv{ .bltn = &cmd.exec.bltn },
};
},
.ls => {
cmd.exec.bltn.impl = .{
.ls = Ls{
.bltn = &cmd.exec.bltn,
},
};
inline else => |tag| {
cmd.exec.bltn.impl = @unionInit(RealImpl, @tagName(tag), .{
.bltn = &cmd.exec.bltn,
});
},
}
@@ -8521,6 +8493,84 @@ pub const Interpreter = struct {
}
};
};
pub const Exit = struct {
bltn: *Builtin,
state: enum {
idle,
waiting_io,
err,
done,
} = .idle,
pub fn start(this: *Exit) Maybe(void) {
const args = this.bltn.argsSlice();
switch (args.len) {
0 => {
this.bltn.done(0);
return Maybe(void).success;
},
1 => {
const first_arg = args[0][0..std.mem.len(args[0]) :0];
const exit_code: ExitCode = std.fmt.parseInt(u8, first_arg, 10) catch |err| switch (err) {
error.Overflow => @intCast((std.fmt.parseInt(usize, first_arg, 10) catch return this.fail("exit: numeric argument required")) % 256),
error.InvalidCharacter => return this.fail("exit: numeric argument required"),
};
this.bltn.done(exit_code);
return Maybe(void).success;
},
else => {
return this.fail("exit: too many arguments");
},
}
}
fn fail(this: *Exit, msg: string) Maybe(void) {
if (this.bltn.stderr.needsIO()) {
this.state = .waiting_io;
this.bltn.stderr.enqueue(this, msg);
return Maybe(void).success;
}
_ = this.bltn.writeNoIO(.stderr, msg);
this.bltn.done(1);
return Maybe(void).success;
}
pub fn next(this: *Exit) void {
while (!(this.state == .err or this.state == .done)) {
switch (this.state) {
.waiting_io => return,
.idle, .done, .err => unreachable,
}
}
if (this.state == .done) {
this.bltn.done(1);
return;
}
if (this.state == .err) {
this.bltn.done(1);
return;
}
}
pub fn onIOWriterChunk(this: *Exit, _: usize, maybe_e: ?JSC.SystemError) void {
if (comptime bun.Environment.allow_assert) {
std.debug.assert(this.state == .waiting_io);
}
if (maybe_e) |e| {
defer e.deref();
this.state = .err;
this.next();
return;
}
this.state = .done;
this.next();
}
pub fn deinit(this: *Exit) void {
_ = this;
}
};
};
/// This type is reference counted, but deinitialization is queued onto the event loop
@@ -9459,7 +9509,7 @@ inline fn fastMod(val: anytype, comptime rhs: comptime_int) @TypeOf(val) {
return val & (rhs - 1);
}
fn throwShellErr(e: *const bun.shell.ShellErr, event_loop: JSC.EventLoopHandle) void {
pub fn throwShellErr(e: *const bun.shell.ShellErr, event_loop: JSC.EventLoopHandle) void {
switch (event_loop) {
.mini => e.throwMini(),
.js => e.throwJS(event_loop.js.global),
@@ -9515,6 +9565,7 @@ pub const IOWriterChildPtr = struct {
Interpreter.Builtin.Touch,
Interpreter.Builtin.Touch.ShellTouchOutputTask,
Interpreter.Builtin.Cat,
Interpreter.Builtin.Exit,
shell.subproc.PipeReader.CapturedWriter,
});

View File

@@ -108,23 +108,20 @@ pub const ShellErr = union(enum) {
pub fn throwMini(this: @This()) void {
defer this.deinit(bun.default_allocator);
switch (this) {
.sys => {
const err = this.sys;
const str = std.fmt.allocPrint(bun.default_allocator, "bunsh: {s}: {}", .{ err.message, err.path }) catch bun.outOfMemory();
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error <b>{s}<r>", .{str});
.sys => |err| {
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error: <b>bunsh: {s}: {}<r>", .{ err.message, err.path });
bun.Global.exit(1);
},
.custom => {
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error <b>{s}<r>", .{this.custom});
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error: <b>{s}<r>", .{this.custom});
bun.Global.exit(1);
},
.invalid_arguments => {
const str = std.fmt.allocPrint(bun.default_allocator, "bunsh: invalid arguments: {s}", .{this.invalid_arguments.val}) catch bun.outOfMemory();
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error <b>{s}<r>", .{str});
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error: <b>bunsh: invalid arguments: {s}<r>", .{this.invalid_arguments.val});
bun.Global.exit(1);
},
.todo => {
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error <b>TODO: {s}<r>", .{this.todo});
bun.Output.prettyErrorln("<r><red>error<r>: Failed to due to error: <b>TODO: {s}<r>", .{this.todo});
bun.Global.exit(1);
},
}

View File

@@ -698,6 +698,10 @@ pub inline fn endsWith(self: string, str: string) bool {
return str.len == 0 or @call(bun.callmod_inline, std.mem.endsWith, .{ u8, self, str });
}
pub inline fn endsWithGeneric(comptime T: type, self: []const T, str: []const T) bool {
return str.len == 0 or @call(bun.callmod_inline, std.mem.endsWith, .{ T, self, str });
}
pub inline fn endsWithComptime(self: string, comptime str: anytype) bool {
return self.len >= str.len and eqlComptimeIgnoreLen(self[self.len - str.len .. self.len], comptime str);
}

View File

@@ -15,6 +15,23 @@ fn isValid(buf: *bun.PathBuffer, segment: []const u8, bin: []const u8) ?u16 {
// Like /usr/bin/which but without needing to exec a child process
// Remember to resolve the symlink if necessary
pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []const u8) ?[:0]const u8 {
// handle absolute paths
if (std.fs.path.isAbsolute(bin)) {
bun.copy(u8, buf, bin);
buf[bin.len] = 0;
const binZ: [:0]u8 = buf[0..bin.len :0];
if (bun.Environment.isWindows) {
(std.fs.cwd().openFile(bin, .{}) catch return null).close();
return binZ;
}
if (bun.sys.isExecutableFilePath(binZ)) return binZ;
// note that directories are often executable
// TODO: should we return null here? What about the case where ytou have
// /foo/bar/baz as a path and you're in /home/jarred?
}
if (bun.Environment.os == .windows) {
var convert_buf: bun.WPathBuffer = undefined;
const result = whichWin(&convert_buf, path, cwd, bin) orelse return null;
@@ -25,18 +42,6 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []con
}
if (bin.len == 0) return null;
// handle absolute paths
if (std.fs.path.isAbsolute(bin)) {
bun.copy(u8, buf, bin);
buf[bin.len] = 0;
const binZ: [:0]u8 = buf[0..bin.len :0];
if (bun.sys.isExecutableFilePath(binZ)) return binZ;
// note that directories are often executable
// TODO: should we return null here? What about the case where ytou have
// /foo/bar/baz as a path and you're in /home/jarred?
}
if (cwd.len > 0) {
if (isValid(buf, std.mem.trimRight(u8, cwd, std.fs.path.sep_str), bin)) |len| {
return buf[0..len :0];
@@ -53,7 +58,7 @@ pub fn which(buf: *bun.PathBuffer, path: []const u8, cwd: []const u8, bin: []con
return null;
}
const win_extensionsW = .{
const win_extensionsW = [_][:0]const u16{
bun.strings.w("exe"),
bun.strings.w("cmd"),
bun.strings.w("bat"),
@@ -77,8 +82,18 @@ pub fn endsWithExtension(str: []const u8) bool {
/// Check if the WPathBuffer holds a existing file path, checking also for windows extensions variants like .exe, .cmd and .bat (internally used by whichWin)
fn searchBin(buf: *bun.WPathBuffer, path_size: usize, check_windows_extensions: bool) ?[:0]const u16 {
if (bun.sys.existsOSPath(buf[0..path_size :0], true))
if (!bun.Environment.isWindows and bun.sys.existsOSPath(buf[0..path_size :0], true))
return buf[0..path_size :0];
if (bun.Environment.isWindows) {
const haystack = buf[0..path_size :0];
for (win_extensionsW) |ext| {
if (path_size < 4) continue;
if (!bun.strings.endsWithGeneric(u16, haystack, ext)) continue;
if (haystack[haystack.len - 4] != '.') continue;
if (!bun.sys.existsOSPath(buf[0..path_size :0], true)) continue;
return haystack;
}
}
if (check_windows_extensions) {
buf[path_size] = '.';
@@ -122,14 +137,6 @@ pub fn whichWin(buf: *bun.WPathBuffer, path: []const u8, cwd: []const u8, bin: [
const check_windows_extensions = !endsWithExtension(bin);
// handle absolute paths
if (std.fs.path.isAbsolute(bin)) {
const normalized_bin = PosixToWinNormalizer.resolveCWDWithExternalBuf(&path_buf, bin) catch return null;
const bin_utf16 = bun.strings.convertUTF8toUTF16InBuffer(buf, normalized_bin);
buf[bin_utf16.len] = 0;
return searchBin(buf, bin_utf16.len, check_windows_extensions);
}
// check if bin is in cwd
if (searchBinInPath(buf, &path_buf, cwd, bin, check_windows_extensions)) |bin_path| {
return bin_path;

View File

@@ -1,7 +1,6 @@
import { describe, expect, test } from "bun:test";
import { realpathSync, chmodSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDirWithFiles, toTOMLString } from "harness";
import { join } from "path";
describe.each(["bun run", "bun"])(`%s`, cmd => {
const runCmd = cmd === "bun" ? ["-c=bunfig.toml", "run"] : ["-c=bunfig.toml"];
@@ -17,14 +16,13 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
bun,
},
});
const which = "which";
const cwd = tempDirWithFiles("run.where.node." + cmd2, {
const cwd = tempDirWithFiles("run.where.node", {
"bunfig.toml": bunfig,
"package.json": JSON.stringify(
{
scripts: {
"where-node": `${which} node`,
"where-node": `which node`,
},
},
null,
@@ -51,11 +49,12 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
} else {
expect(realpathSync(nodeBin)).toBe(realpathSync(node));
}
expect(result.success).toBeTrue();
expect(result.exitCode).toBe(0);
});
});
describe.each(["bun", "system", "default"])(`run.shell = "%s"`, shellStr => {
if (isWindows && shellStr === "system") return; // windows always uses the bun shell now
const shell = shellStr === "default" ? (isWindows ? "bun" : "system") : shellStr;
const command_not_found =
isWindows && shell === "system" ? "is not recognized as an internal or external command" : "command not found";
@@ -85,7 +84,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
cmd: [bunExe(), ...runCmd, "startScript"],
env: bunEnv,
stderr: "pipe",
stdout: "ignore",
stdout: "pipe",
stdin: "ignore",
cwd,
});
@@ -95,7 +94,7 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
} else {
expect(result.stderr.toString().trim()).toContain("$ echo 1");
}
expect(result.success).toBeTrue();
expect(result.exitCode).toBe(0);
});
test("command not found", async () => {
const bunfig = toTOMLString({
@@ -127,11 +126,6 @@ describe.each(["bun run", "bun"])(`%s`, cmd => {
});
const err = result.stderr.toString().trim();
if (shell === "bun") {
expect(err).toStartWith("bun: ");
} else {
expect(err).not.toStartWith("bun: ");
}
expect(err).toContain(command_not_found);
expect(err).toContain("this-should-start-with-bun-in-the-error-message");
expect(result.success).toBeFalse();

View File

@@ -1,10 +1,9 @@
// @known-failing-on-windows: 1 failing
import { file, spawn, spawnSync } from "bun";
import { afterEach, beforeEach, expect, it, describe } from "bun:test";
import { bunEnv, bunExe, bunEnv as env, isWindows } from "harness";
import { mkdtemp, realpath, rm, writeFile, exists } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { join, sep } from "path";
import { readdirSorted } from "./dummy.registry";
let run_dir: string;
@@ -311,7 +310,7 @@ it("should download dependencies to run local file", async () => {
import { file } from "bun";
import decompress from "decompress@4.2.1";
const buffer = await file("${filePath}").arrayBuffer();
const buffer = await file("${filePath.replaceAll(sep, "/")}").arrayBuffer();
for (const entry of await decompress(Buffer.from(buffer))) {
console.log(\`\${entry.type}: \${entry.path}\`);
}

View File

@@ -1,11 +1,10 @@
import { file, spawn } from "bun";
import { bunExe, bunEnv as env, toBeValidBin, toHaveBins } from "harness";
import { join } from "path";
import { bunExe, bunEnv as env, isWindows, toBeValidBin, toHaveBins, writeShebangScript } from "harness";
import { join, sep } from "path";
import { mkdtempSync, realpathSync } from "fs";
import { rm, writeFile, mkdir, exists, cp } from "fs/promises";
import { readdirSorted } from "../dummy.registry";
import { tmpdir } from "os";
import { fork, ChildProcess } from "child_process";
import { beforeAll, afterAll, beforeEach, afterEach, test, expect, describe } from "bun:test";
expect.extend({
@@ -13,31 +12,10 @@ expect.extend({
toHaveBins,
});
var verdaccioServer: ChildProcess;
var testCounter: number = 0;
var port: number = 4873;
var packageDir: string;
beforeAll(async () => {
verdaccioServer = fork(
await import.meta.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();
}
});
});
});
afterAll(() => {
verdaccioServer.kill();
});
beforeEach(async () => {
packageDir = mkdtempSync(join(realpathSync(tmpdir()), "bun-install-registry-" + testCounter++ + "-"));
await writeFile(
@@ -869,7 +847,7 @@ test("it should install with missing bun.lockb, node_modules, and/or cache", asy
expect(err).not.toContain("panic:");
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
"",
expect.stringContaining("Checked 19 installs across 23 packages (no changes)"),
"Checked 19 installs across 23 packages (no changes)",
]);
expect(await exited).toBe(0);
@@ -2049,7 +2027,7 @@ test("it should re-populate .bin folder if package is reinstalled", async () =>
"",
" + what-bin@1.5.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe(
@@ -2182,7 +2160,10 @@ test("missing package on reinstall, some with binaries", async () => {
).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin));
});
for (const forceWaiterThread of [false, true]) {
for (const forceWaiterThread of [
false,
true,
]) {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => {
test("root package with all lifecycle scripts", async () => {
@@ -2297,7 +2278,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + all-lifecycle-scripts@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!");
@@ -2352,7 +2333,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + all-lifecycle-scripts@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!");
@@ -2466,7 +2447,7 @@ for (const forceWaiterThread of [false, true]) {
expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse();
});
test("dependency lifecycle scripts run before root lifecycle scripts", async () => {
test.skipIf(isWindows)("dependency lifecycle scripts run before root lifecycle scripts", async () => {
const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]';
await writeFile(
join(packageDir, "package.json"),
@@ -2545,7 +2526,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + all-lifecycle-scripts@1.0.0",
"",
expect.stringContaining("1 package installed"),
expect.stringContaining(" 1 package installed"),
"",
" Blocked 3 postinstalls. Run `bun pm untrusted` for details.",
"",
@@ -2794,7 +2775,7 @@ for (const forceWaiterThread of [false, true]) {
" + lifecycle-postinstall@1.0.0",
"",
// @ts-ignore
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
@@ -2819,7 +2800,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + lifecycle-postinstall@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(err).not.toContain("Saved lockfile");
expect(err).not.toContain("not found");
@@ -2879,12 +2860,13 @@ for (const forceWaiterThread of [false, true]) {
" + another-init-cwd@1.0.0",
" + lifecycle-init-cwd@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir + "/");
expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir + "/");
expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir + "/");
const packageDir_expected = packageDir.replaceAll(sep, "/") + "/";
expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir_expected);
expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir_expected);
expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir_expected);
});
test("failing lifecycle script should print output", async () => {
@@ -2923,7 +2905,7 @@ for (const forceWaiterThread of [false, true]) {
name: "fooooooooo",
version: "1.0.0",
scripts: {
preinstall: `${bunExe().replace(/\\/g, "\\\\")} -e "throw new Error('Oops!')"`,
preinstall: `${bunExe()} -e "throw new Error('Oops!')"`,
},
}),
);
@@ -2943,6 +2925,43 @@ for (const forceWaiterThread of [false, true]) {
expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1');
});
test("exit 0 in lifecycle scripts works", async () => {
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
postinstall: "exit 0",
prepare: "exit 0",
postprepare: "exit 0",
},
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: testEnv,
});
const err = await new Response(stderr).text();
expect(err).toContain("No packages! Deleted empty lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("panic:");
const out = await new Response(stdout).text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
"",
expect.stringContaining("done"),
"",
]);
expect(await exited).toBe(0);
});
test("--ignore-scripts should skip lifecycle scripts", async () => {
await writeFile(
join(packageDir, "package.json"),
@@ -3111,7 +3130,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + node-gyp@1.5.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "build.node"))).toBeTrue();
@@ -3169,7 +3188,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + node-gyp@1.5.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "build.node"))).toBeTrue();
@@ -3209,7 +3228,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + node-gyp@1.5.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "build.node"))).toBeFalse();
@@ -3297,7 +3316,7 @@ for (const forceWaiterThread of [false, true]) {
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue();
});
test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => {
test.skipIf(isWindows)("root lifecycle scripts should wait for dependency lifecycle scripts", async () => {
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
@@ -3383,7 +3402,7 @@ for (const forceWaiterThread of [false, true]) {
test("reach max concurrent scripts", async () => {
const scripts = {
"preinstall": `${bunExe().replace(/\\/g, "\\\\")} -e "Bun.sleepSync(500)"`,
"preinstall": `${bunExe()} -e "Bun.sleepSync(500)"`,
};
const dependenciesList = await createPackagesWithScripts(4, scripts);
@@ -3412,9 +3431,9 @@ for (const forceWaiterThread of [false, true]) {
expect(await exited).toBe(0);
});
test("stress test", async () => {
test.skip("stress test", async () => {
const dependenciesList = await createPackagesWithScripts(500, {
"postinstall": `${bunExe().replace(/\\/g, "\\\\")} --version`,
"postinstall": `${bunExe()} --version`,
});
// the script is quick, default number for max concurrent scripts
@@ -3443,7 +3462,7 @@ for (const forceWaiterThread of [false, true]) {
expect(await exited).toBe(0);
});
test("it should install and use correct binary version", async () => {
test.skip("it should install and use correct binary version", async () => {
// this should install `what-bin` in two places:
//
// - node_modules/.bin/what-bin@1.5.0
@@ -3591,6 +3610,40 @@ for (const forceWaiterThread of [false, true]) {
expect(await exited).toBe(0);
});
test("npm_config_node_gyp should be set and usable in lifecycle scripts: basic", async () => {
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
name: "foo",
scripts: {
install: "echo $npm_config_node_gyp > npm_config_node_gyp.txt",
},
}),
);
const { stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env: testEnv,
});
const err = await new Response(stderr).text();
expect(err).not.toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("panic:");
expect(err).toContain("v");
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "npm_config_node_gyp.txt"))).toBeTrue();
const ext = isWindows ? ".cmd" : "";
expect(await file(join(packageDir, "npm_config_node_gyp.txt")).text()).toEndWith(`${sep}node-gyp${ext}\n`);
});
test("npm_config_node_gyp should be set and usable in lifecycle scripts", async () => {
await writeFile(
join(packageDir, "package.json"),
@@ -3611,13 +3664,13 @@ for (const forceWaiterThread of [false, true]) {
env: testEnv,
});
expect(await exited).toBe(0);
const err = await new Response(stderr).text();
expect(err).not.toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("panic:");
expect(err).toContain("v");
expect(await exited).toBe(0);
});
// if this test fails, `electron` might be removed from the default list
@@ -3651,7 +3704,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + electron@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(out).not.toContain("Blocked");
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
@@ -3820,7 +3873,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + electron@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
@@ -4014,7 +4067,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" installed no-deps@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue();
@@ -4052,7 +4105,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" installed no-deps@2.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue();
@@ -4288,7 +4341,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + electron@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
@@ -4346,7 +4399,7 @@ for (const forceWaiterThread of [false, true]) {
name: "foo",
version: "1.0.0",
scripts: {
postinstall: 'node -p \'require("fs").writeFileSync("postinstall.txt", "postinstall")\'',
postinstall: `node -p 'require("fs").writeFileSync("postinstall.txt","postinstall")'`,
},
}),
);
@@ -4375,6 +4428,41 @@ for (const forceWaiterThread of [false, true]) {
expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue();
});
test("ensureTempNodeGypScript works", async () => {
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
preinstall: "node-gyp --version",
},
}),
);
const originalPath = env.PATH;
env.PATH = "";
let { stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env,
});
env.PATH = originalPath;
let err = await Bun.readableStreamToText(stderr);
expect(err).toContain("No packages! Deleted empty lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(err).not.toContain("panic:");
expect(await exited).toBe(0);
});
test("bun pm trust and untrusted on missing package", async () => {
await writeFile(
@@ -4599,7 +4687,7 @@ for (const forceWaiterThread of [false, true]) {
"",
" + uses-what-bin@1.0.0",
"",
expect.stringContaining("1 package installed"),
expect.stringContaining(" 1 package installed"),
"",
" Blocked 1 postinstall. Run `bun pm untrusted` for details.",
"",
@@ -4619,20 +4707,20 @@ for (const forceWaiterThread of [false, true]) {
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await Bun.readableStreamToText(stdout);
expect(out).toContain("./node_modules/uses-what-bin @1.0.0");
expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep));
expect(await exited).toBe(0);
});
}
});
describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => {
test("install", async () => {
test.skip("install", async () => {
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
preinstall: `${bunExe().replace(/\\/g, "\\\\")} -e "Bun.sleepSync(1000)"`,
preinstall: `${bunExe()} -e "Bun.sleepSync(1000)"`,
},
}),
);
@@ -4650,7 +4738,7 @@ for (const forceWaiterThread of [false, true]) {
expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000);
});
test("bun pm trust", async () => {
test.skip("bun pm trust", async () => {
await writeFile(
join(packageDir, "package.json"),
JSON.stringify({
@@ -4822,12 +4910,10 @@ test("it should be able to find binary in node_modules/.bin from parent director
await cp(join(packageDir, "bunfig.toml"), join(packageDir, "morePackageDir", "bunfig.toml"));
await await writeFile(
await writeShebangScript(
join(packageDir, "node_modules", ".bin", "missing-bin"),
`#!/usr/bin/env node
require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");
`,
{ mode: 0o777 },
"node",
`require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");`,
);
const { stdout, stderr, exited } = spawn({
@@ -4849,7 +4935,7 @@ require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");
"",
" + what-bin@1.0.0",
"",
expect.stringContaining("1 package installed"),
" 1 package installed",
]);
expect(await exited).toBe(0);
expect(await file(join(packageDir, "morePackageDir", "missing-bin.txt")).text()).toBe("missing-bin@WHAT");

View File

@@ -1,7 +1,7 @@
import { gc as bunGC, unsafe, which } from "bun";
import { describe, test, expect, afterAll, beforeAll } from "bun:test";
import { readlink, readFile } from "fs/promises";
import { isAbsolute } from "path";
import { readlink, readFile, writeFile } from "fs/promises";
import { isAbsolute, sep } from "path";
import { openSync, closeSync } from "node:fs";
export const isMacOS = process.platform === "darwin";
@@ -231,9 +231,8 @@ export async function runWithErrorPromise(cb: () => unknown): Promise<Error | un
}
export function fakeNodeRun(dir: string, file: string | string[], env?: Record<string, string>) {
var path = require("path");
const result = Bun.spawnSync([bunExe(), "--bun", "node", ...(Array.isArray(file) ? file : [file])], {
cwd: dir ?? path.dirname(file),
cwd: dir,
env: {
...bunEnv,
NODE_ENV: undefined,
@@ -552,8 +551,26 @@ export function toTOMLString(opts: object) {
const props = makeFlatPropertyMap(opts);
let ret = "";
for (const [key, value] of Object.entries(props)) {
if (value === undefined) continue;
ret += `${key} = ${JSON.stringify(value)}` + "\n";
if (!value) continue;
ret += `${key} = ${JSON.stringify(value)}\n`;
}
return ret;
}
const shebang_posix = (program: string) => `#!/usr/bin/env ${program}
`;
const shebang_windows = (program: string) => `0</* :{
@echo off
${program} %~f0 %*
exit /b %errorlevel%
:} */0;
`;
export function writeShebangScript(path: string, program: string, data: string) {
if (!isWindows) {
return writeFile(path, shebang_posix(program) + "\n" + data, { mode: 0o777 });
} else {
return writeFile(path + ".cmd", shebang_windows(program) + "\n" + data);
}
}

View File

@@ -1,6 +1,7 @@
import { spawnSync, spawn } from "bun";
import { describe, expect, it } from "bun:test";
import { bunExe } from "harness";
import { bunExe, bunEnv } from "harness";
import { join } from "lodash";
describe("should work for static input", () => {
const inputs = [
@@ -11,6 +12,7 @@ describe("should work for static input", () => {
"Hello\nWorld\n",
"1",
"💕 Red Heart ✨ Sparkles 🔥 Fire\n💕 Red Heart ✨ Sparkles\n💕 Red Heart\n💕\n\nnormal",
"a\n§\nb",
];
for (let input of inputs) {
@@ -18,9 +20,7 @@ describe("should work for static input", () => {
const { stdout } = spawnSync({
cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run.ts"],
stdin: Buffer.from(input),
env: {
BUN_DEBUG_QUIET_LOGS: "1",
},
env: bunEnv,
});
expect(stdout.toString()).toBe(input.replaceAll("\n", ""));
});
@@ -36,6 +36,7 @@ describe("should work for streaming input", () => {
"Hello\nWorld\n",
"1",
"💕 Red Heart ✨ Sparkles 🔥 Fire\n 💕 Red Heart ✨ Sparkles\n 💕 Red Heart\n 💕 \n\nnormal",
"a\n§\nb",
];
for (let input of inputs) {
@@ -44,9 +45,7 @@ describe("should work for streaming input", () => {
cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run.ts"],
stdin: "pipe",
stdout: "pipe",
env: {
BUN_DEBUG_QUIET_LOGS: "1",
},
env: bunEnv,
});
const { stdout, stdin } = proc;
stdin.write(input.slice(0, (input.length / 2) | 0));
@@ -68,9 +67,7 @@ it("can use the console iterator more than once", async () => {
cmd: [bunExe(), import.meta.dir + "/" + "console-iterator-run-2.ts"],
stdin: "pipe",
stdout: "pipe",
env: {
BUN_DEBUG_QUIET_LOGS: "1",
},
env: bunEnv,
});
const { stdout, stdin } = proc;
stdin.write("hello\nworld\nbreak\nanother\nbreak\n");

View File

@@ -255,16 +255,18 @@ it("import.meta paths have the correct slash", () => {
expect(import.meta.url).not.toInclude("\\");
});
it("import.meta is correct in a module that was imported with a query param", async () => {
it("import.meta is correct in a module that was imported with a query param esm", async () => {
const esm = (await import("./other.js?foo=bar")).default;
const cjs = require("./other-cjs.js?foo=bar").meta;
expect(esm.url).toBe(new URL("./other.js?foo=bar", import.meta.url).toString());
expect(cjs.url).toBe(new URL("./other-cjs.js?foo=bar", import.meta.url).toString());
expect(esm.path).toBe(join(import.meta.dir, "./other.js"));
expect(cjs.path).toBe(join(import.meta.dir, "./other-cjs.js"));
expect(esm.dir).toBe(import.meta.dir);
expect(cjs.dir).toBe(import.meta.dir);
expect(esm.file).toBe("other.js");
});
it("import.meta is correct in a module that was imported with a query param cjs", async () => {
const cjs = require("./other-cjs.js?foo=bar").meta;
expect(cjs.url).toBe(new URL("./other-cjs.js?foo=bar", import.meta.url).toString());
expect(cjs.path).toBe(join(import.meta.dir, "./other-cjs.js"));
expect(cjs.dir).toBe(import.meta.dir);
expect(cjs.file).toBe("other-cjs.js");
});

View File

@@ -0,0 +1,22 @@
import { $ } from "bun";
import { describe, test, expect } from "bun:test";
import { TestBuilder } from "../test_builder";
import { sortedShellOutput } from "../util";
import { join } from "path";
describe("exit", async () => {
TestBuilder.command`exit`.exitCode(0).runAsTest("works");
describe("argument sets exit code", async () => {
for (const arg of [0, 2, 11]) {
TestBuilder.command`exit ${arg}`.exitCode(arg).runAsTest(`${arg}`);
}
});
TestBuilder.command`exit 3 5`.exitCode(1).stderr("exit: too many arguments").runAsTest("too many arguments");
TestBuilder.command`exit 62757836`.exitCode(204).runAsTest("exit code wraps u8");
// prettier-ignore
TestBuilder.command`exit abc`.exitCode(1).stderr("exit: numeric argument required").runAsTest("numeric argument required");
});