Skip to content
Open
1 change: 1 addition & 0 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,7 @@ pub(crate) fn resolver_bundle_options_subset(
polyfill_node_globals: src.polyfill_node_globals,
prefer_offline_install: src.prefer_offline_install,
preserve_symlinks: src.preserve_symlinks,
preserve_symlinks_main: false,
rewrite_jest_for_tests: src.rewrite_jest_for_tests,
tsconfig_override: src.tsconfig_override.clone(),
production: src.production,
Expand Down
22 changes: 17 additions & 5 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4048,7 +4048,8 @@ impl VirtualMachine {
return Err(crate::CrateError::ModuleNotFound);
}

let is_special_source = source == MAIN_FILE_NAME || Macro::is_macro_path(source);
let is_main_entry_resolve = source == MAIN_FILE_NAME;
let is_special_source = is_main_entry_resolve || Macro::is_macro_path(source);
let mut query_string: &[u8] = b"";
let normalized_specifier = normalize_specifier_for_resolution(specifier, &mut query_string);
let top_level_dir = self.top_level_dir();
Expand Down Expand Up @@ -4157,10 +4158,21 @@ impl VirtualMachine {
let result_path = result
.path_const()
.ok_or(crate::CrateError::ModuleNotFound)?;
// SAFETY: `result_path.text` borrows the resolver's arena, which
// outlives `ResolveFunctionResult` (see the struct's lifetime-erasure
// note).
ret.path = unsafe { bun_ptr::detach_lifetime(result_path.text) };
// `--preserve-symlinks-main`: `Path::set_realpath` stashed the
// pre-realpath spelling in `.pretty`; return that for the entry.
Comment thread
robobun marked this conversation as resolved.
Outdated
let path_text = if is_main_entry_resolve
&& self.transpiler.resolver.opts.preserve_symlinks_main
&& result_path.is_symlink
&& !result_path.pretty.is_empty()
{
result_path.pretty
} else {
result_path.text
};
// SAFETY: `result_path.text`/`.pretty` borrow the resolver's arena,
// which outlives `ResolveFunctionResult` (see the struct's
// lifetime-erasure note).
ret.path = unsafe { bun_ptr::detach_lifetime(path_text) };
Comment thread
robobun marked this conversation as resolved.
ret.result = Some(result);

Ok(())
Expand Down
7 changes: 6 additions & 1 deletion src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,9 @@ extern "C" bool Bun__shouldIgnoreOneDisconnectEventListener(JSC::JSGlobalObject*
extern "C" void Bun__ensureSignalHandler();
extern "C" bool Bun__isMainThreadVM();
extern "C" void Bun__onPosixSignal(int signalNumber);
#if !OS(WINDOWS)
extern "C" void Bun__noopSignalHandler(int);
#endif

__attribute__((noinline)) static void forwardSignal(int signalNumber)
{
Expand Down Expand Up @@ -1587,7 +1590,9 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e
if (signalToContextIdsMap->find(signalNumber) != signalToContextIdsMap->end() && eventEmitter.listenerCount(eventName) == 0) {

#if !OS(WINDOWS)
if (void (*oldHandler)(int) = signal(signalNumber, SIG_DFL); oldHandler != forwardSignal) {
// SIGUSR1 stays inert (reserved); SIG_DFL would make it fatal.
void (*restoreTo)(int) = signalNumber == SIGUSR1 ? Bun__noopSignalHandler : SIG_DFL;
if (void (*oldHandler)(int) = signal(signalNumber, restoreTo); oldHandler != forwardSignal) {
// Don't uninstall the old handler if it's not the one we installed.
signal(signalNumber, oldHandler);
}
Expand Down
17 changes: 17 additions & 0 deletions src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,12 @@ extern "C" void Bun__setCTRLHandler(BOOL add)

extern "C" int32_t bun_is_stdio_null[3] = { 0, 0, 0 };

#if !OS(WINDOWS)
extern "C" void Bun__noopSignalHandler(int)
{
}
#endif

extern "C" void bun_initialize_process()
{
// Disable printf() buffering. We buffer it ourselves.
Expand Down Expand Up @@ -653,6 +659,17 @@ extern "C" void bun_initialize_process()
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
}

// Node.js reserves SIGUSR1 (debugger) so it is never fatal by default; a
// handler rather than SIG_IGN so exec()'d children revert to SIG_DFL.
Comment thread
robobun marked this conversation as resolved.
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = Bun__noopSignalHandler;
sigaction(SIGUSR1, &sa, nullptr);
}
#elif OS(WINDOWS)
for (int fd = 0; fd <= 2; ++fd) {
auto handle = reinterpret_cast<HANDLE>(uv_get_osfhandle(fd));
Expand Down
4 changes: 4 additions & 0 deletions src/resolver/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ pub struct BundleOptions {
pub polyfill_node_globals: bool,
pub prefer_offline_install: bool,
pub preserve_symlinks: bool,
/// `--preserve-symlinks-main`: `preserve_symlinks` for the entry only
/// (read by `_resolve` when source is `bun:main`).
Comment thread
robobun marked this conversation as resolved.
pub preserve_symlinks_main: bool,
pub rewrite_jest_for_tests: bool,
pub tsconfig_override: Option<Box<[u8]>>,
pub production: bool,
Expand Down Expand Up @@ -275,6 +278,7 @@ impl Default for BundleOptions {
polyfill_node_globals: false,
prefer_offline_install: false,
preserve_symlinks: false,
preserve_symlinks_main: false,
rewrite_jest_for_tests: false,
tsconfig_override: None,
output_dir: Box::default(),
Expand Down
169 changes: 85 additions & 84 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1665,98 +1665,99 @@ impl<'a> Resolver<'a> {
module_type_from_ext(name.ext).unwrap_or(options::ModuleType::Unknown);
}

if let Some(entries) = dir.get_entries_ref(self.generation) {
if let Some(query) = entries.get(name.filename) {
// SAFETY: entries_mutex held; rfs points at the process-global RealFS.
let symlink_path =
unsafe { query.entry().symlink(self.rfs_ptr(), self.store_fd) };
if !symlink_path.is_empty() {
path.set_realpath(symlink_path);
if !result.file_fd.is_valid() {
result.file_fd = query.entry().cache().fd;
}

if let Some(debug) = self.debug_logs.as_mut() {
debug.add_note_fmt(format_args!(
"Resolved symlink \"{}\" to \"{}\"",
bstr::BStr::new(path.text()),
bstr::BStr::new(symlink_path)
));
}
} else if !dir.abs_real_path.is_empty() {
// When the directory is a symlink, we don't need to call getFdPath.
let parts = [dir.abs_real_path, query.entry().base()];
let mut buf = bun_paths::PathBuffer::uninit();

// NOTE: `abs_buf` returns a borrow of `buf`; capture only the
// length so `buf` can be re-borrowed for null-termination below.
let out_len = self.fs_ref().abs_buf(&parts, &mut buf).len();

let store_fd = self.store_fd;

if !query.entry().cache().fd.is_valid() && store_fd {
buf[out_len] = 0;
// SAFETY: buf[out_len] == 0 written above
let span = bun_core::ZStr::from_buf(&buf[..], out_len);
// I/O errors propagate so `resolveAndAutoInstall` can
// return them as `Result.Union.failure` — never
// panic on EACCES/EMFILE/ELOOP here.
let file = bun_sys::open(span, bun_sys::O::RDONLY, 0)
.map_err(Into::<crate::Error>::into)?;
{
// Every cached-`Entry` rewrite takes the per-entry mutex.
let _entry_guard = query.entry().mutex.lock_guard();
query.entry().set_cache_fd(file);
}
Fs::FileSystem::set_max_fd(file.native());
}

// NOTE: snapshot `need_to_close_files` and raw-ptr the entry so
// the closure captures only Copy values — keeps `self` and
// `query.entry` reborrowable across the guard's lifetime.
let need_close = self.fs_ref().fs.need_to_close_files();
// ARENA — Entry lives in the BSSMap singleton; guard runs before
// the slot is reused (resolver mutex held). Capture as `BackRef`
// (Copy, Deref) so the closure stays Copy-only while the read is
// a safe `BackRef::get()` instead of a raw-ptr deref.
let entry_ref = bun_ptr::BackRef::<Fs::file_system::Entry>::from(
core::ptr::NonNull::new(query.entry).expect("EntryStore slot"),
);
scopeguard::defer! {
if need_close {
let e = entry_ref.get();
// Every cached-`Entry` rewrite takes the per-entry mutex.
let _entry_guard = e.mutex.lock_guard();
let fd = e.cache().fd;
if fd.is_valid() {
fd.close();
e.set_cache_fd(FD::INVALID);
}
}
}
// `--preserve-symlinks`: keep the link spelling (`__filename`);
// `dir.abs_real_path` is already gated in `dir_info_uncached`.
Comment thread
robobun marked this conversation as resolved.
Outdated
if !self.opts.preserve_symlinks
&& let Some(entries) = dir.get_entries_ref(self.generation)
&& let Some(query) = entries.get(name.filename)
{
// SAFETY: entries_mutex held; rfs points at the process-global RealFS.
let symlink_path = unsafe { query.entry().symlink(self.rfs_ptr(), self.store_fd) };
if !symlink_path.is_empty() {
path.set_realpath(symlink_path);
if !result.file_fd.is_valid() {
result.file_fd = query.entry().cache().fd;
}

let symlink =
Fs::FilenameStore::instance().append_slice(&buf[..out_len])?;
if let Some(debug) = self.debug_logs.as_mut() {
debug.add_note_fmt(format_args!(
"Resolved symlink \"{}\" to \"{}\"",
bstr::BStr::new(symlink),
bstr::BStr::new(path.text())
));
}
if let Some(debug) = self.debug_logs.as_mut() {
debug.add_note_fmt(format_args!(
"Resolved symlink \"{}\" to \"{}\"",
bstr::BStr::new(path.text()),
bstr::BStr::new(symlink_path)
));
}
} else if !dir.abs_real_path.is_empty() {
// When the directory is a symlink, we don't need to call getFdPath.
let parts = [dir.abs_real_path, query.entry().base()];
let mut buf = bun_paths::PathBuffer::uninit();

// NOTE: `abs_buf` returns a borrow of `buf`; capture only the
// length so `buf` can be re-borrowed for null-termination below.
Comment thread
robobun marked this conversation as resolved.
Outdated
let out_len = self.fs_ref().abs_buf(&parts, &mut buf).len();

let store_fd = self.store_fd;

if !query.entry().cache().fd.is_valid() && store_fd {
buf[out_len] = 0;
// SAFETY: buf[out_len] == 0 written above
let span = bun_core::ZStr::from_buf(&buf[..], out_len);
// I/O errors propagate so `resolveAndAutoInstall` can
// return them as `Result.Union.failure` — never
// panic on EACCES/EMFILE/ELOOP here.
Comment thread
robobun marked this conversation as resolved.
Outdated
let file = bun_sys::open(span, bun_sys::O::RDONLY, 0)
.map_err(Into::<crate::Error>::into)?;
{
// Every cached-`Entry` rewrite takes the per-entry mutex.
let _entry_guard = query.entry().mutex.lock_guard();
query
.entry()
.set_cache_symlink(Interned::from_static(symlink));
query.entry().set_cache_fd(file);
}
if !result.file_fd.is_valid() && store_fd {
result.file_fd = query.entry().cache().fd;
Fs::FileSystem::set_max_fd(file.native());
}

// NOTE: snapshot `need_to_close_files` and raw-ptr the entry so
// the closure captures only Copy values — keeps `self` and
// `query.entry` reborrowable across the guard's lifetime.
Comment thread
robobun marked this conversation as resolved.
Outdated
let need_close = self.fs_ref().fs.need_to_close_files();
// ARENA — Entry lives in the BSSMap singleton; guard runs before
// the slot is reused (resolver mutex held). Capture as `BackRef`
// (Copy, Deref) so the closure stays Copy-only while the read is
// a safe `BackRef::get()` instead of a raw-ptr deref.
Comment thread
robobun marked this conversation as resolved.
Outdated
let entry_ref = bun_ptr::BackRef::<Fs::file_system::Entry>::from(
core::ptr::NonNull::new(query.entry).expect("EntryStore slot"),
);
scopeguard::defer! {
if need_close {
let e = entry_ref.get();
// Every cached-`Entry` rewrite takes the per-entry mutex.
let _entry_guard = e.mutex.lock_guard();
let fd = e.cache().fd;
if fd.is_valid() {
fd.close();
e.set_cache_fd(FD::INVALID);
}
}
}

path.set_realpath(symlink);
let symlink = Fs::FilenameStore::instance().append_slice(&buf[..out_len])?;
if let Some(debug) = self.debug_logs.as_mut() {
debug.add_note_fmt(format_args!(
"Resolved symlink \"{}\" to \"{}\"",
bstr::BStr::new(symlink),
bstr::BStr::new(path.text())
));
}
{
// Every cached-`Entry` rewrite takes the per-entry mutex.
let _entry_guard = query.entry().mutex.lock_guard();
query
.entry()
.set_cache_symlink(Interned::from_static(symlink));
}
if !result.file_fd.is_valid() && store_fd {
result.file_fd = query.entry().cache().fd;
}

path.set_realpath(symlink);
}
}
}
Expand Down
33 changes: 31 additions & 2 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,11 @@
vm.argv = std::mem::take(&mut ctx.passthrough);
// `InitOptions` has no `store_fd` field, so set it on the resolver directly.
vm.transpiler.resolver.store_fd = ctx.debug.hot_reload != cli::command::HotReload::None;
vm.transpiler.resolver.opts.preserve_symlinks_main =
ctx.runtime_options.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false);
// `vm.dns_result_order` is a `u8` until the b2-cycle widens
// it to `bun_dns::Order`; the enum is `#[repr(u8)]` so `as u8` is exact.
vm.dns_result_order =
Expand Down Expand Up @@ -2870,9 +2875,33 @@
..Default::default()
});

let preserve_symlinks_main = ctx.runtime_options.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false);

Check warning on line 2881 in src/runtime/cli/run_command.rs

View check run for this annotation

Claude / Claude Code Review

preserve_symlinks_main flag/env-var check duplicated three times

The `ctx.runtime_options.preserve_symlinks_main || bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN.get().unwrap_or(false)` expression now appears three times in this file (here, line 962, and the pre-existing line 2572), with two of the three added by this PR. Per REVIEW.md ("the second time a multi-line block appears in your diff, extract a named helper and use it at EVERY parallel site"), consider a helper on `RuntimeOptions` — e.g. `fn preserve_symlinks_main_effective(&self) -> bool` — so the
Comment thread
robobun marked this conversation as resolved.
Outdated

// Re-derive the canonical absolute path from the open fd (resolves
// symlinks).
let absolute_script_path: Box<[u8]> = {
// symlinks), or under `--preserve-symlinks-main` join against cwd.
let absolute_script_path: Box<[u8]> = if preserve_symlinks_main {
let mut cwd_buf = PathBuffer::uninit();
let Ok(cwd) = bun_core::getcwd(&mut cwd_buf) else {
let _ = bun_sys::close(fd);
return false;
};
let cwd_len = cwd.as_bytes().len();
cwd_buf[cwd_len] = paths::SEP;
let joined = paths::resolve_path::join_abs_string_buf::<paths::platform::Auto>(
&cwd_buf[..cwd_len + 1],
&mut script_name_buf.0,
&[target],
);
let joined = strings::without_trailing_slash(joined);
if joined.is_empty() {
let _ = bun_sys::close(fd);
return false;
}
joined.to_vec().into_boxed_slice()
} else {
let resolved = match bun_sys::get_fd_path(fd, &mut script_name_buf) {
Ok(p) => p,
Err(_) => {
Expand Down
Loading
Loading