Compare commits

...

12 Commits

Author SHA1 Message Date
dave caruso
4a3b400a14 Merge remote-tracking branch 'origin/main' into dave/env-run 2024-03-27 20:58:40 -07:00
dave caruso
d934e3ef12 tweak 2024-03-27 20:02:15 -07:00
dave caruso
51a788cbe4 Merge remote-tracking branch 'origin/main' into dave/env-run 2024-03-27 19:47:53 -07:00
dave caruso
5c9270dbc2 ok 2024-03-27 17:22:56 -07:00
dave caruso
96bde2dee1 remove comment 2024-03-27 15:51:38 -07:00
dave caruso
c5fe3a0a44 make it possible to deinit createNullDelimitedEnvMap 2024-03-27 15:15:54 -07:00
dave caruso
a02a374ef1 fix window build 2024-03-26 18:41:05 -07:00
dave caruso
be9db9dd3d HashMapValue -> string
this structure had a single field in it
2024-03-26 16:41:25 -07:00
dave caruso
45286ef5e1 absord 9448's tests 2024-03-26 16:21:11 -07:00
dave caruso
ff9e7571af fix: 💥 to .conditional env vars. see pr description 2024-03-26 16:14:15 -07:00
dave caruso
c2beb127c5 a 2024-03-26 14:49:05 -07:00
dave caruso
30969221fb do not pass conditional env vars to createNullDelimitedEnvMap 2024-03-26 14:35:49 -07:00
19 changed files with 392 additions and 246 deletions

View File

@@ -1688,7 +1688,6 @@ pub const Subprocess = struct {
}
if (args != .zero and args.isObject()) {
// This must run before the stdio parsing happens
if (args.getTruthy(globalThis, "ipc")) |val| {
if (val.isCell() and val.isCallable(globalThis.vm())) {
@@ -1848,9 +1847,8 @@ pub const Subprocess = struct {
}
if (!override_env and env_array.items.len == 0) {
env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch |err|
env_array = jsc_vm.bundler.env.map.createEnvArrayList(allocator) catch |err|
return globalThis.handleError(err, "in Bun.spawn");
env_array.capacity = env_array.items.len;
}
inline for (0..stdio.len) |fd_index| {
@@ -1949,8 +1947,8 @@ pub const Subprocess = struct {
var spawned = switch (bun.spawn.spawnProcess(
&spawn_options,
@ptrCast(argv.items.ptr),
@ptrCast(env_array.items.ptr),
@ptrCast(argv.items[0 .. argv.items.len - 1 :null].ptr),
@ptrCast(env_array.items[0 .. env_array.items.len - 1 :null].ptr),
) catch |err| {
process_allocator.destroy(subprocess);
spawn_options.deinit();

View File

@@ -2172,7 +2172,7 @@ pub const EventLoopHandle = union(enum) {
this.loop().unref();
}
pub inline fn createNullDelimitedEnvMap(this: @This(), alloc: Allocator) ![:null]?[*:0]u8 {
pub inline fn createNullDelimitedEnvMap(this: @This(), alloc: Allocator) !bun.DotEnv.Map.NullDelimitedEnvMap {
return switch (this) {
.js => this.js.virtual_machine.bundler.env.map.createNullDelimitedEnvMap(alloc),
.mini => this.mini.env.?.map.createNullDelimitedEnvMap(alloc),

View File

@@ -769,8 +769,8 @@ pub const VirtualMachine = struct {
if (map.map.fetchSwapRemove("BUN_INTERNAL_IPC_FD")) |kv| {
if (Environment.isWindows) {
this.initIPCInstance(kv.value.value);
} else if (std.fmt.parseInt(i32, kv.value.value, 10) catch null) |fd| {
this.initIPCInstance(kv.value);
} else if (std.fmt.parseInt(i32, kv.value, 10) catch null) |fd| {
this.initIPCInstance(bun.toFD(fd));
} else {
Output.printErrorln("Failed to parse BUN_INTERNAL_IPC_FD", .{});

View File

@@ -183,7 +183,7 @@ fn dumpSourceString(specifier: string, written: []const u8) void {
},
};
const dir = std.fs.cwd().makeOpenPath(base_name, .{}) catch |e| {
Output.debug("Failed to dump source string: {}", .{e});
Output.debugWarn("Failed to dump source string: {}", .{e});
return;
};
BunDebugHolder.dir = dir;
@@ -196,12 +196,12 @@ fn dumpSourceString(specifier: string, written: []const u8) void {
.windows => bun.path.windowsFilesystemRoot(dir_path).len,
};
var parent = dir.makeOpenPath(dir_path[root_len..], .{}) catch |e| {
Output.debug("Failed to dump source string: makeOpenPath({s}[{d}..]) {}", .{ dir_path, root_len, e });
Output.debugWarn("Failed to dump source string: makeOpenPath({s}[{d}..]) {}", .{ dir_path, root_len, e });
return;
};
defer parent.close();
parent.writeFile(std.fs.path.basename(specifier), written) catch |e| {
Output.debug("Failed to dump source string: writeFile {}", .{e});
Output.debugWarn("Failed to dump source string: writeFile {}", .{e});
return;
};
} else {

View File

@@ -133,7 +133,7 @@ pub const Run = struct {
try @import("./bun.js/config.zig").configureTransformOptionsForBunVM(ctx.allocator, ctx.args),
null,
);
try bundle.runEnvLoader();
try bundle.runEnvLoader(false);
const mini = JSC.MiniEventLoop.initGlobal(bundle.env);
mini.top_level_dir = ctx.args.absolute_working_dir orelse "";
return try bun.shell.Interpreter.initAndRunFromFile(mini, entry_path);
@@ -147,7 +147,7 @@ pub const Run = struct {
try bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", &ctx, .RunCommand);
}
if (strings.endsWithComptime(entry_path, comptime if (Environment.isWindows) ".sh" else ".bun.sh")) {
if (strings.endsWithComptime(entry_path, ".sh")) {
const exit_code = try bootBunShell(&ctx, entry_path);
Global.exitWide(exit_code);
return;
@@ -226,10 +226,7 @@ pub const Run = struct {
const node_env_entry = try b.env.map.getOrPutWithoutValue("NODE_ENV");
if (!node_env_entry.found_existing) {
node_env_entry.key_ptr.* = try b.env.allocator.dupe(u8, node_env_entry.key_ptr.*);
node_env_entry.value_ptr.* = .{
.value = try b.env.allocator.dupe(u8, "development"),
.conditional = false,
};
node_env_entry.value_ptr.* = try b.env.allocator.dupe(u8, "development");
}
b.configureRouter(false) catch {

View File

@@ -520,7 +520,7 @@ pub const Bundler = struct {
bundler.configureLinkerWithAutoJSX(true);
}
pub fn runEnvLoader(this: *Bundler) !void {
pub fn runEnvLoader(this: *Bundler, is_script_runner: bool) !void {
switch (this.options.env.behavior) {
.prefix, .load_all, .load_all_without_inlining => {
// Step 1. Load the project root.
@@ -541,11 +541,11 @@ pub const Bundler = struct {
}
if (this.options.isTest() or this.env.isTest()) {
try this.env.load(dir, this.options.env.files, .@"test");
try this.env.load(dir, this.options.env.files, .@"test", is_script_runner);
} else if (this.options.production) {
try this.env.load(dir, this.options.env.files, .production);
try this.env.load(dir, this.options.env.files, .production, is_script_runner);
} else {
try this.env.load(dir, this.options.env.files, .development);
try this.env.load(dir, this.options.env.files, .development, is_script_runner);
}
},
.disable => {
@@ -584,7 +584,7 @@ pub const Bundler = struct {
this.options.env.prefix = "BUN_";
}
try this.runEnvLoader();
try this.runEnvLoader(false);
this.options.jsx.setProduction(this.env.isProduction());

View File

@@ -602,10 +602,13 @@ pub const BunxCommand = struct {
debug("installing package: {s}", .{bun.fmt.fmtSlice(argv_to_use, " ")});
this_bundler.env.map.put("BUN_INTERNAL_BUNX_INSTALL", "true") catch bun.outOfMemory();
const envp = try this_bundler.env.map.createNullDelimitedEnvMap(bun.default_allocator);
defer envp.deinit(bun.default_allocator);
const spawn_result = switch ((bun.spawnSync(&.{
.argv = argv_to_use,
.envp = try this_bundler.env.map.createNullDelimitedEnvMap(bun.default_allocator),
.envp = envp.envp,
.cwd = bunx_cache_dir,
.stderr = .inherit,

View File

@@ -347,13 +347,14 @@ pub const RunCommand = struct {
Output.flush();
}
const envp = try env.map.createNullDelimitedEnvMap(bun.default_allocator);
defer envp.deinit(bun.default_allocator);
const spawn_result = switch ((bun.spawnSync(&.{
.argv = &argv,
.argv0 = shell_bin.ptr,
// TODO: remember to free this when we add --filter or --concurrent
// in the meantime we don't need to free it.
.envp = try env.map.createNullDelimitedEnvMap(bun.default_allocator),
.envp = envp.envp,
.cwd = cwd,
.stderr = .inherit,
@@ -513,14 +514,15 @@ pub const RunCommand = struct {
argv = try array_list.toOwnedSlice();
}
const envp = try env.map.createNullDelimitedEnvMap(bun.default_allocator);
defer envp.deinit(bun.default_allocator);
const silent = ctx.debug.silent;
const spawn_result = bun.spawnSync(&.{
.argv = argv,
.argv0 = executableZ,
// TODO: remember to free this when we add --filter or --concurrent
// in the meantime we don't need to free it.
.envp = try env.map.createNullDelimitedEnvMap(bun.default_allocator),
.envp = envp.envp,
.cwd = cwd,
.stderr = .inherit,
@@ -850,7 +852,7 @@ pub const RunCommand = struct {
this_bundler.configureLinker();
var root_dir_info = this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch |err| {
const root_dir_info = this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch |err| {
if (!log_errors) return error.CouldntReadCurrentDirectory;
if (Output.enable_ansi_colors) {
ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {};
@@ -876,26 +878,10 @@ pub const RunCommand = struct {
if (env == null) {
this_bundler.env.loadProcess();
if (this_bundler.env.get("NODE_ENV")) |node_env| {
if (strings.eqlComptime(node_env, "production")) {
this_bundler.options.production = true;
}
}
if (this_bundler.env.isProduction())
this_bundler.options.production = true;
// TODO: evaluate if we can skip running this in nested calls to bun run
// The reason why it's unclear:
// - Some scripts may do NODE_ENV=production bun run foo
// This would cause potentially a different .env file to be loaded
this_bundler.runEnvLoader() catch {};
if (root_dir_info.getEntries(0)) |dir| {
// Run .env again if it exists in a parent dir
if (this_bundler.options.production) {
this_bundler.env.load(dir, this_bundler.options.env.files, .production) catch {};
} else {
this_bundler.env.load(dir, this_bundler.options.env.files, .development) catch {};
}
}
this_bundler.runEnvLoader(true) catch {};
}
this_bundler.env.map.putDefault("npm_config_local_prefix", this_bundler.fs.top_level_dir) catch unreachable;
@@ -1055,11 +1041,8 @@ pub const RunCommand = struct {
{
this_bundler.env.loadProcess();
if (this_bundler.env.get("NODE_ENV")) |node_env| {
if (strings.eqlComptime(node_env, "production")) {
this_bundler.options.production = true;
}
}
if (this_bundler.env.isProduction())
this_bundler.options.production = true;
}
const ResultList = bun.StringArrayHashMap(void);

View File

@@ -663,10 +663,7 @@ pub const TestCommand = struct {
const node_env_entry = try env_loader.map.getOrPutWithoutValue("NODE_ENV");
if (!node_env_entry.found_existing) {
node_env_entry.key_ptr.* = try env_loader.allocator.dupe(u8, node_env_entry.key_ptr.*);
node_env_entry.value_ptr.* = .{
.value = try env_loader.allocator.dupe(u8, "test"),
.conditional = false,
};
node_env_entry.value_ptr.* = try env_loader.allocator.dupe(u8, "test");
}
try vm.bundler.configureDefines();

View File

@@ -189,7 +189,6 @@ pub const Loader = struct {
}
fn loadCCachePathImpl(this: *Loader, fs: *Fs.FileSystem) !void {
// if they have ccache installed, put it in env variable `CMAKE_CXX_COMPILER_LAUNCHER` so
// cmake can use it to hopefully speed things up
var buf: [bun.MAX_PATH_BYTES]u8 = undefined;
@@ -204,18 +203,12 @@ pub const Loader = struct {
const cxx_gop = try this.map.getOrPutWithoutValue("CMAKE_CXX_COMPILER_LAUNCHER");
if (!cxx_gop.found_existing) {
cxx_gop.key_ptr.* = try this.allocator.dupe(u8, cxx_gop.key_ptr.*);
cxx_gop.value_ptr.* = .{
.value = try this.allocator.dupe(u8, ccache_path),
.conditional = false,
};
cxx_gop.value_ptr.* = try this.allocator.dupe(u8, ccache_path);
}
const c_gop = try this.map.getOrPutWithoutValue("CMAKE_C_COMPILER_LAUNCHER");
if (!c_gop.found_existing) {
c_gop.key_ptr.* = try this.allocator.dupe(u8, c_gop.key_ptr.*);
c_gop.value_ptr.* = .{
.value = try this.allocator.dupe(u8, ccache_path),
.conditional = false,
};
c_gop.value_ptr.* = try this.allocator.dupe(u8, ccache_path);
}
}
}
@@ -335,7 +328,7 @@ pub const Loader = struct {
if (behavior == .prefix) {
while (iter.next()) |entry| {
const value: string = entry.value_ptr.value;
const value: string = entry.value_ptr.*;
if (strings.startsWith(entry.key_ptr.*, prefix)) {
const key_str = std.fmt.allocPrint(key_allocator, "process.env.{s}", .{entry.key_ptr.*}) catch unreachable;
@@ -386,12 +379,11 @@ pub const Loader = struct {
}
} else {
while (iter.next()) |entry| {
const value: string = entry.value_ptr.value;
const key = std.fmt.allocPrint(key_allocator, "process.env.{s}", .{entry.key_ptr.*}) catch unreachable;
e_strings[0] = js_ast.E.String{
.data = if (entry.value_ptr.value.len > 0)
@as([*]u8, @ptrFromInt(@intFromPtr(entry.value_ptr.value.ptr)))[0..value.len]
.data = if (entry.value_ptr.len > 0)
entry.value_ptr.*
else
&[_]u8{},
};
@@ -468,13 +460,14 @@ pub const Loader = struct {
dir: *Fs.FileSystem.DirEntry,
env_files: []const []const u8,
comptime suffix: DotEnvFileSuffix,
is_script_runner: bool,
) !void {
const start = std.time.nanoTimestamp();
if (env_files.len > 0) {
try this.loadExplicitFiles(env_files);
} else {
try this.loadDefaultFiles(dir, suffix);
try this.loadDefaultFiles(dir, suffix, is_script_runner);
}
if (!this.quiet) this.printLoaded(start);
@@ -492,8 +485,7 @@ pub const Loader = struct {
var iter = std.mem.splitBackwardsScalar(u8, arg_value, ',');
while (iter.next()) |file_path| {
if (file_path.len > 0) {
try this.loadEnvFileDynamic(file_path, false, true);
Analytics.Features.dotenv = true;
try this.loadEnvFileDynamic(file_path, false);
}
}
}
@@ -508,61 +500,61 @@ pub const Loader = struct {
this: *Loader,
dir: *Fs.FileSystem.DirEntry,
comptime suffix: DotEnvFileSuffix,
/// Only '.env' and '.env.local' are loaded when running a script
/// mostly for backwards compatibility with older behavior of bun.
/// See https://github.com/oven-sh/bun/pull/9642
is_script_runner: bool,
) !void {
const dir_handle: std.fs.Dir = std.fs.cwd();
switch (comptime suffix) {
.development => {
if (dir.hasComptimeQuery(".env.development.local")) {
try this.loadEnvFile(dir_handle, ".env.development.local", false, true);
Analytics.Features.dotenv = true;
}
},
.production => {
if (dir.hasComptimeQuery(".env.production.local")) {
try this.loadEnvFile(dir_handle, ".env.production.local", false, true);
Analytics.Features.dotenv = true;
}
},
.@"test" => {
if (dir.hasComptimeQuery(".env.test.local")) {
try this.loadEnvFile(dir_handle, ".env.test.local", false, true);
Analytics.Features.dotenv = true;
}
},
if (!is_script_runner) {
switch (comptime suffix) {
.development => {
if (dir.hasComptimeQuery(".env.development.local")) {
try this.loadEnvFile(dir_handle, ".env.development.local", false);
}
},
.production => {
if (dir.hasComptimeQuery(".env.production.local")) {
try this.loadEnvFile(dir_handle, ".env.production.local", false);
}
},
.@"test" => {
if (dir.hasComptimeQuery(".env.test.local")) {
try this.loadEnvFile(dir_handle, ".env.test.local", false);
}
},
}
}
if (comptime suffix != .@"test") {
if (dir.hasComptimeQuery(".env.local")) {
try this.loadEnvFile(dir_handle, ".env.local", false, false);
Analytics.Features.dotenv = true;
try this.loadEnvFile(dir_handle, ".env.local", false);
}
}
switch (comptime suffix) {
.development => {
if (dir.hasComptimeQuery(".env.development")) {
try this.loadEnvFile(dir_handle, ".env.development", false, true);
Analytics.Features.dotenv = true;
}
},
.production => {
if (dir.hasComptimeQuery(".env.production")) {
try this.loadEnvFile(dir_handle, ".env.production", false, true);
Analytics.Features.dotenv = true;
}
},
.@"test" => {
if (dir.hasComptimeQuery(".env.test")) {
try this.loadEnvFile(dir_handle, ".env.test", false, true);
Analytics.Features.dotenv = true;
}
},
}
if (!is_script_runner) {
switch (comptime suffix) {
.development => {
if (dir.hasComptimeQuery(".env.development")) {
try this.loadEnvFile(dir_handle, ".env.development", false);
}
},
.production => {
if (dir.hasComptimeQuery(".env.production")) {
try this.loadEnvFile(dir_handle, ".env.production", false);
}
},
.@"test" => {
if (dir.hasComptimeQuery(".env.test")) {
try this.loadEnvFile(dir_handle, ".env.test", false);
}
},
}
if (dir.hasComptimeQuery(".env")) {
try this.loadEnvFile(dir_handle, ".env", false, false);
Analytics.Features.dotenv = true;
if (dir.hasComptimeQuery(".env")) {
try this.loadEnvFile(dir_handle, ".env", false);
}
}
}
@@ -636,7 +628,6 @@ pub const Loader = struct {
dir: std.fs.Dir,
comptime base: string,
comptime override: bool,
comptime conditional: bool,
) !void {
if (@field(this, base) != null) {
return;
@@ -714,7 +705,6 @@ pub const Loader = struct {
this.map,
override,
false,
conditional,
);
@field(this, base) = source;
@@ -724,7 +714,6 @@ pub const Loader = struct {
this: *Loader,
file_path: []const u8,
comptime override: bool,
comptime conditional: bool,
) !void {
if (this.custom_files_loaded.contains(file_path)) {
return;
@@ -786,7 +775,6 @@ pub const Loader = struct {
this.map,
override,
false,
conditional,
);
try this.custom_files_loaded.put(file_path, source);
@@ -1011,8 +999,9 @@ const Parser = struct {
map: *Map,
comptime override: bool,
comptime is_process: bool,
comptime conditional: bool,
) void {
Analytics.Features.dotenv = true;
var count = map.map.count();
while (this.pos < this.src.len) {
const key = this.parseKey(true) orelse {
@@ -1027,25 +1016,19 @@ const Parser = struct {
// https://github.com/oven-sh/bun/issues/1262
if (comptime !override) continue;
} else {
allocator.free(entry.value_ptr.value);
allocator.free(entry.value_ptr.*);
}
}
entry.value_ptr.* = .{
.value = allocator.dupe(u8, value) catch unreachable,
.conditional = conditional,
};
entry.value_ptr.* = allocator.dupe(u8, value) catch bun.outOfMemory();
}
if (comptime !is_process) {
var it = map.iterator();
while (it.next()) |entry| {
if (count > 0) {
count -= 1;
} else if (expandValue(map, entry.value_ptr.value)) |value| {
allocator.free(entry.value_ptr.value);
entry.value_ptr.* = .{
.value = allocator.dupe(u8, value) catch unreachable,
.conditional = conditional,
};
} else if (expandValue(map, entry.value_ptr.*)) |value| {
allocator.free(entry.value_ptr.*);
entry.value_ptr.* = allocator.dupe(u8, value) catch bun.outOfMemory();
}
}
}
@@ -1057,61 +1040,148 @@ const Parser = struct {
map: *Map,
comptime override: bool,
comptime is_process: bool,
comptime conditional: bool,
) void {
var parser = Parser{ .src = source.contents };
parser._parse(allocator, map, override, is_process, conditional);
parser._parse(allocator, map, override, is_process);
}
};
pub const Map = struct {
const HashTableValue = struct {
value: string,
conditional: bool,
};
// On Windows, environment variables are case-insensitive. So we use a case-insensitive hash map.
// An issue with this exact implementation is unicode characters can technically appear in these
// keys, and we use a simple toLowercase function that only applies to ascii, so this will make
// some strings collide.
const HashTable = (if (Environment.isWindows) bun.CaseInsensitiveASCIIStringArrayHashMap else bun.StringArrayHashMap)(HashTableValue);
const HashTable = (if (Environment.isWindows) bun.CaseInsensitiveASCIIStringArrayHashMap else bun.StringArrayHashMap)(string);
const GetOrPutResult = HashTable.GetOrPutResult;
map: HashTable,
pub fn createNullDelimitedEnvMap(this: *Map, arena: std.mem.Allocator) ![:null]?[*:0]u8 {
/// Creates a environment block for use in Posix Spawn APIs.
/// The return value is a struct with a field '.envp' which can be passed to a posix api.
/// Call .deinit with the same allocator to free the memory.
pub fn createNullDelimitedEnvMap(this: *Map, alloc: std.mem.Allocator) !NullDelimitedEnvMap {
var env_map = &this.map;
const envp_count = env_map.count();
const envp_buf = try arena.allocSentinel(?[*:0]u8, envp_count, null);
var envp_count: usize = 0;
var total_bytes: usize = 0;
{
var it = env_map.iterator();
while (it.next()) |pair| {
envp_count += 1;
// env line is 'KEY=VALUE\x00'
total_bytes += (pair.key_ptr.len + pair.value_ptr.len + "=\x00".len);
}
}
total_bytes += (envp_count + 1) * @sizeOf(?[*:0]const u8); // +1 for the null ptr after the pointer list
// Instead of creating separate allocations for each string, let's allocate
// enough bytes for everything. The buffer contains the pointers, and then
// all the string bytes afterwards. For this to pass the ptrCast a few
// lines later, we have to make a pointer-aligned allocation.
const buf = try alloc.alignedAlloc(u8, @alignOf(?[*:0]const u8), total_bytes);
comptime std.debug.assert(@TypeOf(buf.ptr) == NullDelimitedEnvMap.BufPtrType);
const envp = envp: {
const p: [*]?[*:0]const u8 = @ptrCast(buf.ptr);
p[envp_count] = null;
break :envp p[0..envp_count :null];
};
var fba = std.heap.FixedBufferAllocator.init(buf[@sizeOf(usize) * (envp_count + 1) ..]);
const string_alloc = fba.allocator();
{
var it = env_map.iterator();
var i: usize = 0;
while (it.next()) |pair| : (i += 1) {
const env_buf = try arena.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.value.len + 1, 0);
bun.copy(u8, env_buf, pair.key_ptr.*);
env_buf[pair.key_ptr.len] = '=';
bun.copy(u8, env_buf[pair.key_ptr.len + 1 ..], pair.value_ptr.value);
envp_buf[i] = env_buf.ptr;
while (it.next()) |pair| {
const variable_buf = string_alloc.allocSentinel(
u8,
pair.key_ptr.len + pair.value_ptr.len + 1,
0,
) catch unreachable; // all bytes were pre-allocated.
@memcpy(variable_buf[0..pair.key_ptr.len], pair.key_ptr.*);
variable_buf[pair.key_ptr.len] = '=';
@memcpy(variable_buf[pair.key_ptr.len + 1 ..], pair.value_ptr.*);
envp[i] = variable_buf.ptr;
i += 1;
}
if (comptime Environment.allow_assert) std.debug.assert(i == envp_count);
}
return envp_buf;
if (comptime Environment.allow_assert) {
std.debug.assert(fba.end_index == fba.buffer.len); // incorrect counting above. every pointer should be byte-aligned
std.debug.assert(@intFromPtr(envp.ptr) == @intFromPtr(buf.ptr));
}
return .{
.envp = envp,
// In order to properly free this, the Zig allocator must be passed
// the original slice it was given. The pointer is technically not
// enough information.
.allocation_size = total_bytes,
};
}
/// Creates a environment block for use in Posix Spawn APIs. It is assumed that the caller
/// will want to add other strings to this block, **so there is no null terminator** in the
/// return value.
///
/// If passing this into a Posix Spawn API, you will need to append a `null`, and then
/// use `list.items[0.. list.items.len - 1 :null]` to get a null-terminated array.
///
/// Unlike `createNullDelimitedEnvMap`, this does a separate allocation for each string,
/// meaning you likely want to use an arena allocator for the input so you can quickly
/// free everything.
pub fn createEnvArrayList(this: *Map, alloc: std.mem.Allocator) !std.ArrayListUnmanaged(?[*:0]const u8) {
const envp_count = this.map.count();
// Allocate an extra entry so a caller that wants a null pointer does not have to resize the ArrayList.
var list = try std.ArrayListUnmanaged(?[*:0]const u8).initCapacity(alloc, envp_count + 1);
errdefer {
for (list.items) |item| {
alloc.free(bun.span(
item orelse unreachable, // no null values are added
));
}
list.deinit(alloc);
}
var it = this.map.iterator();
while (it.next()) |pair| {
const variable_buf = try alloc.allocSentinel(u8, pair.key_ptr.len + pair.value_ptr.len + 1, 0);
@memcpy(variable_buf[0..pair.key_ptr.len], pair.key_ptr.*);
variable_buf[pair.key_ptr.len] = '=';
@memcpy(variable_buf[pair.key_ptr.len + 1 ..], pair.value_ptr.*);
list.appendAssumeCapacity(variable_buf.ptr);
}
return list;
}
pub const NullDelimitedEnvMap = struct {
envp: [:null]?[*:0]const u8,
allocation_size: usize,
const BufPtrType = [*]align(@alignOf(?[*:0]const u8)) u8;
pub inline fn deinit(this: NullDelimitedEnvMap, alloc: std.mem.Allocator) void {
alloc.free(
@as(BufPtrType, @ptrCast(this.envp.ptr))[0..this.allocation_size],
);
}
};
/// Returns a wrapper around the std.process.EnvMap that does not duplicate the memory of
/// the keys and values, but instead points into the memory of the bun env map.
///
/// To prevent
/// To prevent mutation, the return value is a wrapper struct that can only
/// return a *const std.process.EnvMap.
pub fn stdEnvMap(this: *Map, allocator: std.mem.Allocator) !StdEnvMapWrapper {
var env_map = std.process.EnvMap.init(allocator);
var iter = this.map.iterator();
while (iter.next()) |entry| {
// Allow var from .env.development or .env.production to be loaded again
if (!entry.value_ptr.conditional) {
try env_map.hash_map.put(entry.key_ptr.*, entry.value_ptr.value);
}
try env_map.hash_map.put(entry.key_ptr.*, entry.value_ptr.*);
}
return .{ .unsafe_map = env_map };
@@ -1139,7 +1209,7 @@ pub const Map = struct {
if (i + 7 >= result.len) return error.TooManyEnvironmentVariables;
result[i] = '=';
i += 1;
i += bun.strings.convertUTF8toUTF16InBuffer(result[i..], pair.value_ptr.*.value).len;
i += bun.strings.convertUTF8toUTF16InBuffer(result[i..], pair.value_ptr.*).len;
if (i + 5 >= result.len) return error.TooManyEnvironmentVariables;
result[i] = 0;
i += 1;
@@ -1168,18 +1238,12 @@ pub const Map = struct {
if (Environment.isWindows and Environment.allow_assert) {
std.debug.assert(bun.strings.indexOfChar(key, '\x00') == null);
}
try this.map.put(key, .{
.value = value,
.conditional = false,
});
try this.map.put(key, value);
}
pub inline fn putAllocKeyAndValue(this: *Map, allocator: std.mem.Allocator, key: string, value: string) !void {
const gop = try this.map.getOrPut(key);
gop.value_ptr.* = .{
.value = try allocator.dupe(u8, value),
.conditional = false,
};
gop.value_ptr.* = try allocator.dupe(u8, value);
if (!gop.found_existing) {
gop.key_ptr.* = try allocator.dupe(u8, key);
}
@@ -1187,20 +1251,14 @@ pub const Map = struct {
pub inline fn putAllocKey(this: *Map, allocator: std.mem.Allocator, key: string, value: string) !void {
const gop = try this.map.getOrPut(key);
gop.value_ptr.* = .{
.value = value,
.conditional = false,
};
gop.value_ptr.* = value;
if (!gop.found_existing) {
gop.key_ptr.* = try allocator.dupe(u8, key);
}
}
pub inline fn putAllocValue(this: *Map, allocator: std.mem.Allocator, key: string, value: string) !void {
try this.map.put(key, .{
.value = try allocator.dupe(u8, value),
.conditional = false,
});
try this.map.put(key, try allocator.dupe(u8, value));
}
pub inline fn getOrPutWithoutValue(this: *Map, key: string) !GetOrPutResult {
@@ -1232,21 +1290,11 @@ pub const Map = struct {
this: *const Map,
key: string,
) ?string {
return if (this.map.get(key)) |entry| entry.value else null;
return this.map.get(key);
}
pub inline fn putDefault(this: *Map, key: string, value: string) !void {
_ = try this.map.getOrPutValue(key, .{
.value = value,
.conditional = false,
});
}
pub inline fn getOrPut(this: *Map, key: string, value: string) !void {
_ = try this.map.getOrPutValue(key, .{
.value = value,
.conditional = false,
});
_ = try this.map.getOrPutValue(key, value);
}
pub fn remove(this: *Map, key: string) void {

View File

@@ -2112,10 +2112,7 @@ pub const PackageManager = struct {
const init_cwd_gop = try this.env.map.getOrPutWithoutValue("INIT_CWD");
if (!init_cwd_gop.found_existing) {
init_cwd_gop.key_ptr.* = try ctx.allocator.dupe(u8, init_cwd_gop.key_ptr.*);
init_cwd_gop.value_ptr.* = .{
.value = try ctx.allocator.dupe(u8, FileSystem.instance.top_level_dir),
.conditional = false,
};
init_cwd_gop.value_ptr.* = try ctx.allocator.dupe(u8, FileSystem.instance.top_level_dir);
}
this.env.loadCCachePath(this_bundler.fs);
@@ -6672,7 +6669,7 @@ pub const PackageManager = struct {
};
env.loadProcess();
try env.load(entries_option.entries, &[_][]u8{}, .production);
try env.load(entries_option.entries, &[_][]u8{}, .production, false);
var cpu_count = @as(u32, @truncate(((try std.Thread.getCpuCount()) + 1)));
@@ -10324,13 +10321,12 @@ pub const PackageManager = struct {
try PATH.appendSlice(original_path);
}
this_bundler.env.map.put("PATH", PATH.items) catch unreachable;
const envp = try this_bundler.env.map.createNullDelimitedEnvMap(this.allocator);
this_bundler.env.map.put("PATH", PATH.items) catch bun.outOfMemory();
const env = try this_bundler.env.map.createNullDelimitedEnvMap(this.allocator);
try this_bundler.env.map.put("PATH", original_path);
PATH.deinit();
try LifecycleScriptSubprocess.spawnPackageScripts(this, list, envp, log_level);
try LifecycleScriptSubprocess.spawnPackageScripts(this, list, env, log_level);
}
};

View File

@@ -25,7 +25,7 @@ pub const LifecycleScriptSubprocess = struct {
stderr: OutputReader = OutputReader.init(@This()),
has_called_process_exit: bool = false,
manager: *PackageManager,
envp: [:null]?[*:0]u8,
env: bun.DotEnv.Map.NullDelimitedEnvMap,
timer: ?Timer = null,
@@ -174,7 +174,7 @@ pub const LifecycleScriptSubprocess = struct {
};
this.remaining_fds = 0;
var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(&argv), this.envp)).unwrap();
var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(&argv), this.env.envp)).unwrap();
if (comptime Environment.isPosix) {
if (spawned.stdout) |stdout| {
@@ -391,18 +391,19 @@ pub const LifecycleScriptSubprocess = struct {
this.stderr.deinit();
}
this.env.deinit(this.manager.allocator);
this.destroy();
}
pub fn spawnPackageScripts(
manager: *PackageManager,
list: Lockfile.Package.Scripts.List,
envp: [:null]?[*:0]u8,
env: bun.DotEnv.Map.NullDelimitedEnvMap,
comptime log_level: PackageManager.Options.LogLevel,
) !void {
var lifecycle_subprocess = LifecycleScriptSubprocess.new(.{
.manager = manager,
.envp = envp,
.env = env,
.scripts = list,
.package_name = list.package_name,
});

View File

@@ -1112,7 +1112,7 @@ pub const Printer = struct {
};
env_loader.loadProcess();
try env_loader.load(entries_option.entries, &[_][]u8{}, .production);
try env_loader.load(entries_option.entries, &[_][]u8{}, .production, false);
var log = logger.Log.init(allocator);
try options.load(
allocator,

View File

@@ -568,17 +568,21 @@ pub noinline fn print(comptime fmt: string, args: anytype) callconv(std.builtin.
/// BUN_DEBUG_foo=1
/// To enable all logs, set the environment variable
/// BUN_DEBUG_ALL=1
const _log_fn = fn (comptime fmt: string, args: anytype) void;
pub fn scoped(comptime tag: anytype, comptime disabled: bool) _log_fn {
const LogFunction = fn (comptime fmt: string, args: anytype) void;
pub fn createScope(comptime tag: anytype, comptime disabled: bool) type {
const tagname = switch (@TypeOf(tag)) {
@Type(.EnumLiteral) => @tagName(tag),
[]const u8 => tag,
else => @compileError("Output.scoped expected @Type(.EnumLiteral) or []const u8, you gave: " ++ @typeName(@Type(tag))),
else => @compileError("Output.scoped expected @Type(.EnumLiteral) or []const u8, you gave: " ++ @typeName(@TypeOf(tag))),
};
if (comptime !Environment.isDebug or !Environment.isNative) {
return struct {
pub fn isVisible() bool {
return false;
}
pub fn log(comptime _: string, _: anytype) void {}
}.log;
};
}
return struct {
@@ -590,6 +594,21 @@ pub fn scoped(comptime tag: anytype, comptime disabled: bool) _log_fn {
var evaluated_disable = false;
var lock = std.Thread.Mutex{};
pub fn isVisible() bool {
if (!evaluated_disable) {
evaluated_disable = true;
if (bun.getenvZ("BUN_DEBUG_ALL") != null or
bun.getenvZ("BUN_DEBUG_" ++ tagname) != null)
{
really_disable = false;
} else if (bun.getenvZ("BUN_DEBUG_QUIET_LOGS")) |val| {
really_disable = really_disable or !strings.eqlComptime(val, "0");
}
}
return !really_disable;
}
/// Debug-only logs which should not appear in release mode
/// To enable a specific log at runtime, set the environment variable
/// BUN_DEBUG_${TAG} to 1
@@ -606,18 +625,7 @@ pub fn scoped(comptime tag: anytype, comptime disabled: bool) _log_fn {
return;
}
if (!evaluated_disable) {
evaluated_disable = true;
if (bun.getenvZ("BUN_DEBUG_ALL") != null or
bun.getenvZ("BUN_DEBUG_" ++ tagname) != null)
{
really_disable = false;
} else if (bun.getenvZ("BUN_DEBUG_QUIET_LOGS")) |val| {
really_disable = really_disable or !strings.eqlComptime(val, "0");
}
}
if (really_disable)
if (!isVisible())
return;
if (!out_set) {
@@ -650,7 +658,11 @@ pub fn scoped(comptime tag: anytype, comptime disabled: bool) _log_fn {
};
}
}
}.log;
};
}
pub fn scoped(comptime tag: anytype, comptime disabled: bool) LogFunction {
return createScope(tag, disabled).log;
}
// Valid "colors":
@@ -868,9 +880,11 @@ pub inline fn warn(comptime fmt: []const u8, args: anytype) void {
prettyErrorln("<yellow>warn<r><d>:<r> " ++ fmt, args);
}
const debug_scope = Output.createScope(.debug_warn, false);
/// Print a yellow warning message, only in debug mode
pub inline fn debugWarn(comptime fmt: []const u8, args: anytype) void {
if (Environment.isDebug) {
if (Environment.isDebug and debug_scope.isVisible()) {
prettyErrorln("<yellow>debug warn<r><d>:<r> " ++ fmt, args);
flush();
}

View File

@@ -1043,7 +1043,7 @@ pub const Interpreter = struct {
var iter = env_loader.iterator();
while (iter.next()) |entry| {
const value = EnvStr.initSlice(entry.value_ptr.value);
const value = EnvStr.initSlice(entry.value_ptr.*);
const key = EnvStr.initSlice(entry.key_ptr.*);
export_env.insert(key, value);
}
@@ -3423,8 +3423,6 @@ pub const Interpreter = struct {
var arena = &this.spawn_arena;
var arena_allocator = arena.allocator();
var spawn_args = Subprocess.SpawnArgs.default(arena, this.base.interpreter.event_loop, false);
spawn_args.argv = std.ArrayListUnmanaged(?[*:0]const u8){};
spawn_args.cmd_parent = this;
spawn_args.cwd = this.base.shell.cwdZ();

View File

@@ -610,10 +610,7 @@ pub const ShellSubprocess = struct {
cmd_parent: ?*ShellCmd = null,
override_env: bool = false,
env_array: std.ArrayListUnmanaged(?[*:0]const u8) = .{
.items = &.{},
.capacity = 0,
},
env_array: std.ArrayListUnmanaged(?[*:0]const u8) = .{},
cwd: []const u8,
stdio: [3]Stdio = .{
.ignore,
@@ -686,10 +683,7 @@ pub const ShellSubprocess = struct {
.arena = arena,
.override_env = false,
.env_array = .{
.items = &.{},
.capacity = 0,
},
.env_array = .{},
.cwd = event_loop.topLevelDir(),
.stdio = .{
.{ .ignore = {} },
@@ -698,7 +692,7 @@ pub const ShellSubprocess = struct {
},
.lazy = false,
.PATH = event_loop.env().get("PATH") orelse "",
.argv = undefined,
.argv = .{},
.detached = false,
// .ipc_mode = IPCMode.none,
// .ipc_callback = .zero,
@@ -708,6 +702,7 @@ pub const ShellSubprocess = struct {
out.stdio[1] = .{ .pipe = {} };
out.stdio[2] = .{ .pipe = {} };
}
return out;
}
@@ -752,21 +747,20 @@ pub const ShellSubprocess = struct {
pub fn spawnAsync(
event_loop: JSC.EventLoopHandle,
shellio: *ShellIO,
spawn_args_: SpawnArgs,
spawn_args: SpawnArgs,
out: **@This(),
) bun.shell.Result(void) {
var cloned_spawn_args = spawn_args;
var arena = @import("root").bun.ArenaAllocator.init(bun.default_allocator);
defer arena.deinit();
var spawn_args = spawn_args_;
_ = switch (spawnMaybeSyncImpl(
.{
.is_sync = false,
},
event_loop,
arena.allocator(),
&spawn_args,
&cloned_spawn_args,
shellio,
out,
)) {
@@ -789,11 +783,34 @@ pub const ShellSubprocess = struct {
) bun.shell.Result(*@This()) {
const is_sync = config.is_sync;
if (!spawn_args.override_env and spawn_args.env_array.items.len == 0) {
// spawn_args.env_array.items = jsc_vm.bundler.env.map.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory();
spawn_args.env_array.items = event_loop.createNullDelimitedEnvMap(allocator) catch bun.outOfMemory();
spawn_args.env_array.capacity = spawn_args.env_array.items.len;
}
const override_env = !spawn_args.override_env and spawn_args.env_array.items.len == 0;
const env: bun.DotEnv.Map.NullDelimitedEnvMap = brk: {
if (override_env) {
break :brk event_loop.createNullDelimitedEnvMap(allocator) catch {
return .{ .err = .{
.custom = bun.default_allocator.dupe(u8, "out of memory") catch bun.outOfMemory(),
} };
};
} else {
// Make sure this is a null-terminated
if (spawn_args.env_array.items.len == 0 or
spawn_args.env_array.getLast() != null)
{
spawn_args.env_array.append(allocator, null) catch {
return .{ .err = .{
.custom = bun.default_allocator.dupe(u8, "out of memory") catch bun.outOfMemory(),
} };
};
}
break :brk .{
.envp = spawn_args.env_array.items[0 .. spawn_args.env_array.items.len - 1 :null],
// undefined is fine because `env.deinit` is only called if the `override_env` path is taken.
.allocation_size = undefined,
};
}
};
defer if (override_env) env.deinit(allocator);
var should_close_memfd = Environment.isLinux;
@@ -845,14 +862,10 @@ pub const ShellSubprocess = struct {
return .{ .err = .{ .custom = bun.default_allocator.dupe(u8, "out of memory") catch bun.outOfMemory() } };
};
spawn_args.env_array.append(allocator, null) catch {
return .{ .err = .{ .custom = bun.default_allocator.dupe(u8, "out of memory") catch bun.outOfMemory() } };
};
var spawn_result = switch (bun.spawn.spawnProcess(
&spawn_options,
@ptrCast(spawn_args.argv.items.ptr),
@ptrCast(spawn_args.env_array.items.ptr),
env.envp,
) catch |err| {
return .{ .err = .{ .custom = std.fmt.allocPrint(bun.default_allocator, "Failed to spawn process: {s}", .{@errorName(err)}) catch bun.outOfMemory() } };
}) {

View File

@@ -710,6 +710,99 @@ test("NODE_ENV default is not propogated in bun run", () => {
expect(bunRunAsScript(tmp, "show-env", {}).stdout).toBe("");
});
for (const shell of ["system", "bun"]) {
const isWindowsCMD = isWindows && shell === "system";
const show_env_script = isWindowsCMD //
? "echo ENV_FILE_NAME=%ENV_FILE_NAME%, NODE_ENV=%NODE_ENV%"
: "echo ENV_FILE_NAME=$ENV_FILE_NAME, NODE_ENV=$NODE_ENV";
describe(`script runner with ${shell} shell`, () => {
test("does not pass variables from .env files into scripts", () => {
const tmp = tempDirWithFiles("script-runner-env", {
"package.json": '{"scripts":{"show-env":"' + show_env_script + '"}}',
".env.development": "ENV_FILE_NAME=.env.development",
".env.production": "ENV_FILE_NAME=.env.production",
".env.test": "ENV_FILE_NAME=.env.test",
".env": "ENV_FILE_NAME=.env",
});
expect(bunRunAsScript(tmp, "show-env", {}, ["--shell=" + shell]).stdout).toBe("ENV_FILE_NAME=, NODE_ENV=");
});
test(".env.local is loaded by the script runner", () => {
const tmp = tempDirWithFiles("script-runner-env", {
"package.json": '{"scripts":{"show-env":"' + show_env_script + '"}}',
".env.local": "ENV_FILE_NAME=.env.local",
".env.development": "ENV_FILE_NAME=.env.development",
".env.production": "ENV_FILE_NAME=.env.production",
".env.test": "ENV_FILE_NAME=.env.test",
".env": "ENV_FILE_NAME=.env",
});
expect(bunRunAsScript(tmp, "show-env", {}, ["--shell=" + shell]).stdout).toBe(
"ENV_FILE_NAME=.env.local, NODE_ENV=",
);
});
for (const { NODE_ENV, expected, env_file } of [
{
NODE_ENV: "production",
expected: "production",
env_file: ".env.production",
},
{
NODE_ENV: "development",
expected: "development",
env_file: ".env.development",
},
{
NODE_ENV: undefined,
expected: "",
env_file: ".env.development",
},
]) {
test("explicit NODE_ENV=" + NODE_ENV, () => {
const tmp = tempDirWithFiles("script-runner-env", {
"package.json": '{"scripts":{"show-env":"' + show_env_script + '"}}',
".env.development": "ENV_FILE_NAME=.env.development",
".env.production": "ENV_FILE_NAME=.env.production",
".env.test": "ENV_FILE_NAME=.env.test",
".env": "ENV_FILE_NAME=.env",
});
expect(bunRunAsScript(tmp, "show-env", { NODE_ENV }, ["--shell=" + shell]).stdout).toBe(
"ENV_FILE_NAME=, NODE_ENV=" + expected,
);
});
// This is already covered in isolation by the '.env file is loaded' describe
// but it is nice to have just a couple e2e tests combining script runner AND the runtime.
test("e2e NODE_ENV=" + NODE_ENV, () => {
const run_index_script = isWindowsCMD
? `set NODE_ENV=${NODE_ENV} && bun run index.ts`
: `NODE_ENV=${NODE_ENV} bun run index.ts`;
const tmp = tempDirWithFiles("script-runner-env", {
"package.json": '{"scripts":{"start":"' + run_index_script + '"}}',
"index.ts": "console.log(`ENV_FILE_NAME=${process.env.ENV_FILE_NAME}, NODE_ENV=${process.env.NODE_ENV}`);",
".env.development": "ENV_FILE_NAME=.env.development",
".env.production": "ENV_FILE_NAME=.env.production",
".env.test": "ENV_FILE_NAME=.env.test",
".env": "ENV_FILE_NAME=.env",
});
expect(bunRunAsScript(tmp, "start", {}, ["--shell=" + shell]).stdout).toBe(
"ENV_FILE_NAME=" + env_file + ", NODE_ENV=" + NODE_ENV,
);
});
}
});
}
const todoOnPosix = process.platform !== "win32" ? test.todo : test;
todoOnPosix("setting process.env coerces the value to a string", () => {
// @ts-expect-error

View File

@@ -168,8 +168,13 @@ export function bunTest(file: string, env?: Record<string, string>) {
};
}
export function bunRunAsScript(dir: string, script: string, env?: Record<string, string>) {
const result = Bun.spawnSync([bunExe(), `run`, `${script}`], {
export function bunRunAsScript(
dir: string,
script: string,
env?: Record<string, string | undefined>,
execArgv?: string[],
) {
const result = Bun.spawnSync([bunExe(), ...(execArgv ?? []), `run`, `${script}`], {
cwd: dir,
env: {
...bunEnv,