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
2 changes: 1 addition & 1 deletion src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1664,7 +1664,7 @@ impl<'a> BundleOptions<'a> {
origin: bun_url::OwnedURL::from_href(Box::default()),
output_dir_handle: None,
root_dir: Box::default(),
preserve_symlinks: false,
preserve_symlinks: transform.preserve_symlinks.unwrap_or(false),
preserve_extensions: false,
production: false,
output_format: Format::Esm,
Expand Down
46 changes: 42 additions & 4 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3988,6 +3988,20 @@ impl VirtualMachine {
slice
}

/// Canonicalize the entry point's path without going through the
/// resolver, whose preserve-symlinks mode shapes its shared caches.
/// `None` when the path can't be opened; the caller falls back to the
/// resolved spelling.
Comment thread
robobun marked this conversation as resolved.
fn realpath_for_main(&mut self, path: &[u8]) -> Option<&'static [u8]> {
let boxed = bun_sys::realpath_by_open(path)?;
// SAFETY: `boxed`'s heap allocation has a stable address for as long
// as the owning `Box` lives in `resolved_path_dups` (drained in
// `destroy()`).
let slice: &'static [u8] = unsafe { core::mem::transmute::<&[u8], &'static [u8]>(&*boxed) };
self.resolved_path_dups.push(boxed);
Some(slice)
}

/// Note: `is_a_file_path` is a runtime
/// arg to avoid duplicating the body for both monomorphizations.
pub(crate) fn _resolve(
Expand Down Expand Up @@ -4082,6 +4096,17 @@ impl VirtualMachine {
top_level_dir
};

// Node applies --preserve-symlinks-main / NODE_PRESERVE_SYMLINKS_MAIN
// (not --preserve-symlinks) to the entry point, which is resolved from
// the synthetic main module here (workers included). Applied after the
// resolve (see `path_text` below) rather than by flipping
// `opts.preserve_symlinks` for one call: that option shapes cached
// `DirInfo` entries, and the dir cache is keyed by path only.
Comment thread
robobun marked this conversation as resolved.
let main_preserve: Option<bool> = (source == MAIN_FILE_NAME).then(|| {
bun_options_types::context::try_get()
.is_some_and(|c| c.runtime_options.preserve_symlinks_main_enabled())
});
Comment thread
robobun marked this conversation as resolved.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// A `loop`
// returning the resolver result; `retry_on_not_found` is consumed on
// the first miss.
Expand Down Expand Up @@ -4168,10 +4193,23 @@ 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) };
let path_text: &[u8] = match main_preserve {
// The resolver realpathed the entry and `set_realpath` kept the
// link spelling in `pretty`; that spelling is the entry's identity.
Comment thread
robobun marked this conversation as resolved.
Some(true) if result_path.is_symlink && !result_path.pretty.is_empty() => {
result_path.pretty
}
// The preserve-mode resolver kept the entry's link path, but Node
// realpaths the entry when only the general flag is on.
Comment thread
robobun marked this conversation as resolved.
Some(false) if self.transpiler.resolver.opts.preserve_symlinks => self
.realpath_for_main(result_path.text)
.unwrap_or(result_path.text),
_ => 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) };
ret.result = Some(result);

Ok(())
Expand Down
18 changes: 17 additions & 1 deletion src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ impl WebWorker {
utf8_slice.slice(),
error_message,
temp_log,
false,
)
} {
preloads.push(preload.to_vec().into_boxed_slice());
Expand Down Expand Up @@ -1060,6 +1061,7 @@ impl WebWorker {
&self.unresolved_specifier,
&mut resolve_error,
vm_log,
true,
)
} {
Some(p) => p,
Expand Down Expand Up @@ -1618,6 +1620,7 @@ unsafe fn resolve_entry_point_specifier<'s>(
str: &'s [u8],
error_message: &mut BunString,
log: &mut bun_ast::Log,
is_main_entry: bool,
) -> Option<&'s [u8]> {
// SAFETY: per fn contract; read-only field.
if let Some(graph) = unsafe { (*parent).standalone_module_graph } {
Expand Down Expand Up @@ -1740,7 +1743,20 @@ unsafe fn resolve_entry_point_specifier<'s>(
// `filename_store` (`Path<'static>`), NOT `resolved_entry_point` itself —
// copy the slice out and let `resolved_entry_point` drop on the stack.
match resolved_entry_point.path_const() {
Some(entry_path) => Some(entry_path.text),
Some(entry_path) => {
// Node applies --preserve-symlinks-main to a worker's entry too
// (but not to preloads): recover the link spelling that
// `set_realpath` stashed in `pretty`. (With --preserve-symlinks
// off and -main on, the resolver above ran in realpath mode.)
Comment thread
robobun marked this conversation as resolved.
let preserve_main = is_main_entry
&& bun_options_types::context::try_get()
.is_some_and(|c| c.runtime_options.preserve_symlinks_main_enabled());
if preserve_main && entry_path.is_symlink && !entry_path.pretty.is_empty() {
Some(entry_path.pretty)
} else {
Some(entry_path.text)
}
}
Comment thread
robobun marked this conversation as resolved.
None => {
*error_message = BunString::static_(b"Worker entry point is missing");
None
Expand Down
10 changes: 10 additions & 0 deletions src/options_types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,16 @@ pub struct HeapProf {
pub dir: Box<[u8]>,
}

impl RuntimeOptions {
/// `--preserve-symlinks-main` or `NODE_PRESERVE_SYMLINKS_MAIN=1`.
pub fn preserve_symlinks_main_enabled(&self) -> bool {
self.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false)
}
}

impl Default for RuntimeOptions {
// See `ContextData::default` — folded into the single startup call site.
#[inline(always)]
Expand Down
6 changes: 5 additions & 1 deletion src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1661,7 +1661,11 @@ 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) {
// With --preserve-symlinks, the link path stays the module's
// identity (matching Node), so skip resolving it to the target.
Comment thread
robobun marked this conversation as resolved.
if !self.opts.preserve_symlinks
&& 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 =
Expand Down
66 changes: 49 additions & 17 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2550,18 +2550,11 @@ impl RunCommand {
bstr::BStr::new(target_name),
bstr::BStr::new(fs_top_level_dir),
);
// Temporarily honor `--preserve-symlinks-main` / NODE_PRESERVE_SYMLINKS_MAIN
// for this one resolve.
let preserve_symlinks_main = ctx.runtime_options.preserve_symlinks_main_enabled();
let resolution: ::core::result::Result<bun_resolver::Result, bun_resolver::Error> = {
let saved_preserve = this_transpiler.resolver.opts.preserve_symlinks;
this_transpiler.resolver.opts.preserve_symlinks =
ctx.runtime_options.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false);
// SAFETY: `Transpiler::init` always sets `fs`; resolver-cache lifetime.
let top_level_dir = unsafe { (*this_transpiler.fs).top_level_dir };
let resolved = match this_transpiler.resolver.resolve(
match this_transpiler.resolver.resolve(
top_level_dir,
target_name,
bun_ast::ImportKind::EntryPointRun,
Expand All @@ -2576,9 +2569,7 @@ impl RunCommand {
bun_ast::ImportKind::EntryPointRun,
)
}
};
this_transpiler.resolver.opts.preserve_symlinks = saved_preserve;
resolved
}
};
// (path, loader) — captured if the resolve hit a real file whose
// loader Bun cannot execute (e.g. `.css`); used by the `log_errors`
Expand All @@ -2597,9 +2588,29 @@ impl RunCommand {
.unwrap_or(Loader::Tsx);
if loader.can_be_run_by_bun() || loader == Loader::Html || loader == Loader::Md {
bun_core::scoped_log!(RUN_LOG, "Resolved to: `{}`", bstr::BStr::new(path.text));
// borrowck — `boot_and_handle_error` takes
// `&mut ctx`; copy `path.text` out of the resolver borrow.
let text: Box<[u8]> = path.text.to_vec().into_boxed_slice();
// Node applies --preserve-symlinks-main / NODE_PRESERVE_SYMLINKS_MAIN
// (not --preserve-symlinks) to the entry point. Adjust the
// resolved spelling here instead of flipping
// `opts.preserve_symlinks` for one resolve: that option
// shapes the process-lifetime DirInfo cache, which is
// keyed by path only.
// (Also copies `path.text` out of the resolver borrow for
// the `&mut ctx` call below.)
Comment thread
robobun marked this conversation as resolved.
let text: Box<[u8]> =
if preserve_symlinks_main && path.is_symlink && !path.pretty.is_empty() {
// The resolver realpathed the entry; `set_realpath`
// kept the link spelling in `pretty`.
Comment thread
robobun marked this conversation as resolved.
path.pretty.to_vec().into_boxed_slice()
} else if !preserve_symlinks_main
&& this_transpiler.resolver.opts.preserve_symlinks
{
// The preserve-mode resolver kept the link path,
// but the entry must be realpathed.
Comment thread
robobun marked this conversation as resolved.
bun_sys::realpath_by_open(path.text)
.unwrap_or_else(|| path.text.to_vec().into_boxed_slice())
} else {
path.text.to_vec().into_boxed_slice()
};
return Ok(Self::boot_and_handle_error(ctx, &text, Some(loader)));
} else {
bun_core::scoped_log!(
Expand Down Expand Up @@ -2861,8 +2872,29 @@ impl RunCommand {
});

// Re-derive the canonical absolute path from the open fd (resolves
// symlinks).
let absolute_script_path: Box<[u8]> = {
// symlinks). With --preserve-symlinks-main / NODE_PRESERVE_SYMLINKS_MAIN,
// keep the link path as the entry's identity instead (matching Node).
Comment thread
robobun marked this conversation as resolved.
let preserve_symlinks_main = ctx.runtime_options.preserve_symlinks_main_enabled();
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 mut abs_buf = PathBuffer::uninit();
let joined = paths::resolve_path::join_abs_string_buf::<paths::platform::Auto>(
&cwd_buf[..cwd_len + 1],
&mut abs_buf.0,
&[&script_name_buf[..open_len]],
);
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
19 changes: 19 additions & 0 deletions src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7684,6 +7684,25 @@ fn get_fd_path_freebsd_linuxulator<'a>(
Ok(&mut out.0[..len])
}

/// Canonicalize `path` by opening it read-only and deriving the fd's
/// absolute path via [`get_fd_path`]. `None` when the path is too long or
/// cannot be opened.
Comment thread
robobun marked this conversation as resolved.
pub fn realpath_by_open(path: &[u8]) -> Option<Box<[u8]>> {
let mut buf = bun_paths::path_buffer_pool::get();
if path.len() >= buf.len() {
return None;
}
buf[..path.len()].copy_from_slice(path);
buf[path.len()] = 0;
// SAFETY: `buf[path.len()] == 0` written above.
let z = ZStr::from_buf(&buf[..], path.len());
let fd = open(z, O::RDONLY, 0).ok()?;
let mut out = bun_paths::path_buffer_pool::get();
let resolved = get_fd_path(fd, &mut out);
let _ = close(fd);
resolved.ok().map(|p| p.to_vec().into_boxed_slice())
}

/// fd → absolute path. Linux: readlink `/proc/self/fd/N`;
/// macOS: `fcntl(F_GETPATH)`; Windows: `GetFinalPathNameByHandle`.
pub fn get_fd_path<'a>(fd: Fd, out: &'a mut bun_paths::PathBuffer) -> Maybe<&'a mut [u8]> {
Expand Down
Loading
Loading