Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,10 +632,8 @@ impl<'a> Transpiler<'a> {
self.resolver.opts.set_production(true);
}

// Load the project root for .env file discovery. If the cwd
// (or a parent) is unreadable, readDirInfo may return null;
// bail out of .env file loading in that case, but process
// env vars were already loaded above.
// If the cwd (or a parent) is unreadable, readDirInfo returns
// null; skip .env file loading, process env vars are loaded.
let top_level_dir = Fs::FileSystem::get().top_level_dir();
let dir_info = match self.resolver.read_dir_info(top_level_dir) {
Ok(Some(d)) => d,
Expand All @@ -646,24 +644,6 @@ impl<'a> Transpiler<'a> {
merge_tsconfig_jsx_into(&tsconfig, &mut self.options.jsx);
}

// Copy the listing's basenames out under `entries_mutex`,
// refreshing it at our generation in the same critical
// section: concurrent resolvers rewrite the `DirEntry` map in
// place under that lock, and `dot_env::Loader::load` does
// file I/O between probes.
let dir = {
let _entries_lock = bun_resolver::fs::FileSystem::get()
.fs
.entries_mutex
.lock_guard();
match dir_info.get_entries_ref_locked(self.resolver.generation) {
Some(entries) => dot_env::DirEntryKeys(
entries.data.iter().map(|(k, _)| Box::from(&**k)).collect(),
),
None => return Ok(()),
}
};

// `Env.files: Box<[Box<[u8]>]>` but `Loader::load`
// wants `&[&[u8]]`. Re-borrow into a small Vec; the explicit
// `--env-file` list is bounded (CLI args), not hot-path.
Expand All @@ -676,7 +656,7 @@ impl<'a> Transpiler<'a> {
} else {
dot_env::DotEnvFileSuffix::Development
};
env.load(&dir, &env_files, suffix, skip_default_env)?;
env.load(&env_files, suffix, skip_default_env)?;
}
DotEnvBehavior::disable => {
env.load_process()?;
Expand Down
80 changes: 26 additions & 54 deletions src/dotenv/env_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,6 @@ impl DefaultEnvFile {
}
}

/// Directory-entry probe used by `Loader::load`. `bun_dotenv` sits below
/// `bun_resolver` in the crate graph, so the directory listing is taken
/// generically; the only operation `load_default_files` performs is a lookup
/// of a known-at-compile-time filename. Callers snapshot the resolver's
/// listing into a [`DirEntryKeys`] (the live `DirEntry` map may be rewritten
/// in place by a concurrent resolver, so `load` must not probe it directly).
pub trait DirEntryProbe {
/// The argument MUST already be ASCII-lowercase.
fn has_comptime_query(&self, query_lower: &'static [u8]) -> bool;
}

/// Directory-listing basenames copied out under the resolver's
/// `entries_mutex`, probed between the `.env` file reads in `Loader::load`.
pub struct DirEntryKeys(pub Vec<Box<[u8]>>);

impl DirEntryProbe for DirEntryKeys {
fn has_comptime_query(&self, query_lower: &'static [u8]) -> bool {
self.0.iter().any(|k| **k == *query_lower)
}
}

/// Canonical definition; re-exported as
/// `bun_options_types::schema::api::DotEnvBehavior` for higher tiers.
#[repr(u32)]
Expand Down Expand Up @@ -624,9 +603,9 @@ impl Loader {
Parser::parse_bytes::<OVERWRITE, false, EXPAND>(str, &mut self.map, &mut value_buffer)
}

pub fn load<D: DirEntryProbe + ?Sized>(
/// Load the `--env-file` list, or else the default `.env*` files of the cwd.
pub fn load(
&mut self,
dir: &D,
env_files: &[&[u8]],
suffix: DotEnvFileSuffix,
skip_default_env: bool,
Expand All @@ -649,7 +628,7 @@ impl Loader {
// See https://github.com/oven-sh/bun/issues/9635#issuecomment-2021350123
// for more details on how this edge case works.
if !skip_default_env {
self.load_default_files(suffix, dir, &mut value_buffer)?;
self.load_default_files(suffix, &mut value_buffer)?;
}
}

Expand Down Expand Up @@ -690,61 +669,52 @@ impl Loader {
// Load .env.development if development
// Load .env.production if !development
// .env goes last
fn load_default_files<D: DirEntryProbe + ?Sized>(
fn load_default_files(
&mut self,
suffix: DotEnvFileSuffix,
dir: &D,
value_buffer: &mut Vec<u8>,
) -> crate::Result<()> {
let dir_handle = bun_sys::Fd::cwd();

match suffix {
DotEnvFileSuffix::Development => self.try_load_default(
dir,
dir_handle,
DefaultEnvFile::DevelopmentLocal,
value_buffer,
)?,
DotEnvFileSuffix::Production => self.try_load_default(
dir,
dir_handle,
DefaultEnvFile::ProductionLocal,
value_buffer,
)?,
DotEnvFileSuffix::Development => {
self.try_load_default(dir_handle, DefaultEnvFile::DevelopmentLocal, value_buffer)?
}
DotEnvFileSuffix::Production => {
self.try_load_default(dir_handle, DefaultEnvFile::ProductionLocal, value_buffer)?
}
DotEnvFileSuffix::Test => {
self.try_load_default(dir, dir_handle, DefaultEnvFile::TestLocal, value_buffer)?
self.try_load_default(dir_handle, DefaultEnvFile::TestLocal, value_buffer)?
}
}

if suffix != DotEnvFileSuffix::Test {
self.try_load_default(dir, dir_handle, DefaultEnvFile::Local, value_buffer)?;
self.try_load_default(dir_handle, DefaultEnvFile::Local, value_buffer)?;
}

match suffix {
DotEnvFileSuffix::Development => {
self.try_load_default(dir, dir_handle, DefaultEnvFile::Development, value_buffer)?
self.try_load_default(dir_handle, DefaultEnvFile::Development, value_buffer)?
}
DotEnvFileSuffix::Production => {
self.try_load_default(dir, dir_handle, DefaultEnvFile::Production, value_buffer)?
self.try_load_default(dir_handle, DefaultEnvFile::Production, value_buffer)?
}
DotEnvFileSuffix::Test => {
self.try_load_default(dir, dir_handle, DefaultEnvFile::Test, value_buffer)?
self.try_load_default(dir_handle, DefaultEnvFile::Test, value_buffer)?
}
}

self.try_load_default(dir, dir_handle, DefaultEnvFile::Env, value_buffer)
self.try_load_default(dir_handle, DefaultEnvFile::Env, value_buffer)
}

#[inline]
fn try_load_default<D: DirEntryProbe + ?Sized>(
fn try_load_default(
&mut self,
dir: &D,
dir_handle: bun_sys::Fd,
env_file: DefaultEnvFile,
value_buffer: &mut Vec<u8>,
) -> crate::Result<()> {
if dir.has_comptime_query(env_file.name()) {
self.load_env_file::<false>(dir_handle, env_file, value_buffer)?;
if self.load_env_file::<false>(dir_handle, env_file, value_buffer)? {
analytics::Features::dotenv_inc();
}
Ok(())
Expand Down Expand Up @@ -784,14 +754,15 @@ impl Loader {
Output::flush();
}

/// Returns whether a file of that name exists.
pub(crate) fn load_env_file<const OVERRIDE: bool>(
&mut self,
dir: bun_sys::Fd,
env_file: DefaultEnvFile,
value_buffer: &mut Vec<u8>,
) -> crate::Result<()> {
) -> crate::Result<bool> {
if self.default_files_loaded.contains(env_file) {
return Ok(());
return Ok(true);
}
let base = env_file.name();

Expand All @@ -802,11 +773,12 @@ impl Loader {
Err(err) => {
use bun_sys::E;
match err.get_errno() {
E::ENOENT | E::ENOTDIR => return Ok(false),
// A unix socket: ENXIO on Linux, EOPNOTSUPP on macOS and FreeBSD.
E::EISDIR | E::ENOENT | E::ENXIO | E::EOPNOTSUPP => {
E::EISDIR | E::ENXIO | E::EOPNOTSUPP => {
// prevent retrying
self.default_files_loaded.insert(env_file);
return Ok(());
return Ok(true);
}
E::EBUSY | E::EACCES => {
if !self.quiet {
Expand All @@ -818,7 +790,7 @@ impl Loader {
}
// prevent retrying
self.default_files_loaded.insert(env_file);
return Ok(());
return Ok(true);
}
_ => return Err(err.into()),
}
Expand All @@ -842,7 +814,7 @@ impl Loader {
}

self.default_files_loaded.insert(env_file);
Ok(())
Ok(true)
}

pub(crate) fn load_env_file_dynamic<const OVERRIDE: bool>(
Expand Down
5 changes: 2 additions & 3 deletions src/dotenv/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ pub mod no_proxy;
pub use error::{Error, Result};

pub use env_loader::{
DirEntryKeys, DotEnvBehavior, DotEnvFileSuffix, HAS_NO_CLEAR_SCREEN_CLI_FLAG, HashTable,
HashTableValue, INSTANCE, Loader, Map, NullDelimitedEnvMap, S3Credentials, instance,
set_instance,
DotEnvBehavior, DotEnvFileSuffix, HAS_NO_CLEAR_SCREEN_CLI_FLAG, HashTable, HashTableValue,
INSTANCE, Loader, Map, NullDelimitedEnvMap, S3Credentials, instance, set_instance,
};

/// `dotenv::map::{HashTable, Entry}` namespace expected by `install_jsc::ini_jsc` et al.
Expand Down
71 changes: 31 additions & 40 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,10 @@ bun_output::declare_scope!(PackageManager, hidden);
pub struct PackageManager {
pub(crate) cache_directory: Option<bun_sys::Dir>,
pub(crate) cache_directory_path: ZBox, // owned; process lifetime via the leaked singleton
pub root_dir: &'static fs::DirEntry,
/// The project directory, interned in `DirnameStore`, without a trailing separator.
pub root_dir: &'static [u8],
/// `root_dir` had a `node_modules` entry when the manager was created.
pub root_dir_has_node_modules: bool,
// allocator dropped per §Allocators (was `bun.default_allocator`). For the
// handful of sites that allocated AST nodes via `Expr.allocate(manager.allocator, …)`
// — i.e. nodes that must outlive `Expr.Data.Store.reset()` across workspace
Expand Down Expand Up @@ -1555,10 +1558,20 @@ fn http_thread_on_init_error(err: http::InitError, opts: &http::http_thread::Ini
// allocate / get singleton
// ──────────────────────────────────────────────────────────────────────────

fn root_dir_has_node_modules(root_dir: &[u8]) -> bool {
let mut buf = bun_paths::path_buffer_pool::get();
resolve_path::join_abs_string_buf_z_checked::<platform::Auto>(
root_dir,
&mut buf[..],
&[b"node_modules".as_slice()],
)
.is_some_and(|path| bun_sys::lstat(path).is_ok())
}

fn allocate_package_manager(
options: Options,
log: *mut bun_ast::Log,
root_dir: &'static fs::DirEntry,
root_dir: &'static [u8],
env: &mut dot_env::Loader,
// NUL-terminated
original_package_json_path: Vec<u8>,
Expand Down Expand Up @@ -1616,6 +1629,10 @@ fn allocate_package_manager(
wr!(patch_task_fifo, PatchTaskFifo::init());
wr!(log, log);
wr!(root_dir, root_dir);
wr!(
root_dir_has_node_modules,
root_dir_has_node_modules(root_dir)
);
wr!(ast_arena, bun_alloc::Arena::new());
wr!(env, Some(bun_ptr::BackRef::new_mut(env)));
wr!(
Expand Down Expand Up @@ -2370,12 +2387,7 @@ pub fn init(
if !cli.env_files.is_empty() {
// Load these before the cwd changes, because their paths are relative to it.
env.load_process()?;
env.load(
&dot_env::DirEntryKeys(Vec::new()),
cli.env_files,
dot_env::DotEnvFileSuffix::Production,
true,
)?;
env.load(cli.env_files, dot_env::DotEnvFileSuffix::Production, true)?;
}

const CONFIG_TAG: bun_options_types::command_tag::Tag =
Expand Down Expand Up @@ -2614,34 +2626,18 @@ pub fn init(
let _ = root_package_json_file.close();

let top_level_dir = fs.top_level_dir();
// Returns the resolver's BSSMap-owned
// `*EntriesOption` slot.
let entries_option = FileSystem::instance()
.read_directory(top_level_dir, 0, true)?
.try_entries()?;

env.load_process()?;
// This is read before the project's `.env` files are loaded, so they cannot set it.
let env_ignores_scripts = env_ignores_scripts(env);
if cli.env_files.is_empty() && !cli.no_env_file && !ctx.args.disable_default_env_files {
// `.data` must only be probed while `entries_mutex` is held.
let env_probe_keys = {
let _entries_lock = FileSystem::get().fs.entries_mutex.lock_guard();
dot_env::DirEntryKeys(
entries_option
.data
.iter()
.map(|(k, _)| Box::from(&**k))
.collect(),
)
};
// Follows `BUN_ENV` / `NODE_ENV` like `bun run`, but defaults to the production set.
let suffix = match env.get_node_env() {
Some(b"development") => dot_env::DotEnvFileSuffix::Development,
Some(b"test") => dot_env::DotEnvFileSuffix::Test,
_ => dot_env::DotEnvFileSuffix::Production,
};
env.load(&env_probe_keys, &[], suffix, false)?;
env.load(&[], suffix, false)?;
}
// `bunx` sets these for this command alone. They are removed because lifecycle scripts and the
// security scanner get a copy of this map.
Expand Down Expand Up @@ -2709,7 +2705,7 @@ pub fn init(
let manager_ptr = allocate_package_manager(
options,
ctx.log,
entries_option,
top_level_dir,
env,
original_package_json_path_buf,
root_package_json_path,
Expand Down Expand Up @@ -2965,15 +2961,14 @@ fn init_with_runtime_once(
PackageManager::set_verbose_install(true);
}

// Read the root directory BEFORE allocating the singleton. This is
// Open the root directory BEFORE allocating the singleton. This is
// user-reachable failure (the cwd may have been deleted out from under the
// process, or be unreadable: ENOENT/EACCES/ENOTDIR), and bailing out here
// leaves `holder::RAW_PTR` null rather than pointing at an uninitialized
// manager. Returns the resolver's BSSMap-owned `*EntriesOption` slot.
// manager.
let top_level_dir = FileSystem::get().top_level_dir();
let root_dir = FileSystem::instance()
.read_directory(top_level_dir, 0, true)?
.try_entries()?;
let _ = bun_sys::close(bun_sys::open_dir_for_iteration(Fd::cwd(), top_level_dir)?);
let root_dir = top_level_dir;

// A file inside `node_modules` belongs to the project that installed the package.
let importer_dir = match strings::index_of(importer_dir, bun_paths::NODE_MODULES_NEEDLE) {
Expand Down Expand Up @@ -3138,14 +3133,9 @@ fn init_with_runtime_once(
// temporarily moving the boxed lockfile out so the `&mut PackageManager`
// passed in does not alias the `&mut Lockfile` receiver.
manager.lockfile.init_empty();
if let Ok(fs::EntriesOption::Entries(entries)) =
FileSystem::instance().read_directory(project_dir, 0, false)
&& {
// `.data` probes must hold `entries_mutex`.
let _entries_lock = FileSystem::get().fs.entries_mutex.lock_guard();
entries.has_comptime_query(b"bun.lock") || entries.has_comptime_query(b"bun.lockb")
}
&& let Ok(dir) = bun_sys::Dir::open(project_dir)
if let Ok(dir) = bun_sys::Dir::open(project_dir)
&& (bun_sys::lstatat(dir.fd(), bun_core::zstr!("bun.lock")).is_ok()
|| bun_sys::lstatat(dir.fd(), bun_core::zstr!("bun.lockb")).is_ok())
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
{
let mut lockfile = core::mem::replace(&mut manager.lockfile, Box::new(Lockfile::default()));
match lockfile.load_from_dir::<false>(dir.fd(), Some(&mut *manager), log) {
Expand All @@ -3156,7 +3146,8 @@ fn init_with_runtime_once(
// Workspace paths are relative to `root_dir`. A lockfile records them relative to its own
// directory, and `parse_with_json` makes them relative to the working directory.
if manager.lockfile.packages.len() > 0 {
manager.root_dir = entries;
manager.root_dir = FileSystem::get().dirname_store().append(project_dir)?;
manager.root_dir_has_node_modules = root_dir_has_node_modules(project_dir);
// Auto-install ignores an override that points away from the registry, so it also drops
// what such an override resolved to in the lockfile.
use crate::lockfile_real::package::PackageColumns as _;
Expand Down
5 changes: 1 addition & 4 deletions src/install/PackageManager/security_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,7 @@ pub(crate) fn perform_security_scan_after_resolution(
return Ok(None);
}

let has_node_modules = {
let _entries_lock = FileSystem::get().fs.entries_mutex.lock_guard();
manager.root_dir.has_comptime_query(b"node_modules")
};
let has_node_modules = manager.root_dir_has_node_modules;
// A command that names the packages it adds or updates scans only those packages and `seeds`.
// The install that wrote the lockfile and `node_modules` already scanned everything else.
let scan_all = manager.update_requests.is_empty()
Expand Down
Loading
Loading