Skip to content
Open
5 changes: 5 additions & 0 deletions src/crash_handler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,11 @@ mod draft {
} else if name == b"MissingPackageJSON" {
err_generic!("Bun could not find a package.json file to install from");
bun_core::note!("Run \"bun init\" to initialize a project");
} else if name == b"MissingShell" {
err_generic!("Bun could not find a system shell (bash, sh, or zsh) to run this script");
bun_core::note!(
"To use Bun's built-in shell, pass --shell=bun to \"bun run\" or set shell = \"bun\" under [run] in bunfig.toml"
);
} else {
// The macros need
// `:literal`, so branch on the const and call separately.
Expand Down
14 changes: 9 additions & 5 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,13 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
use_system_shell: bool,
shell_path: Option<&[u8]>,
) -> crate::Result<()> {
let shell_search_path = shell_path.unwrap_or_else(|| env.get(b"PATH").unwrap_or(b""));
let shell_bin =
Self::find_shell(shell_search_path, cwd).ok_or(crate::Error::MissingShell)?;
// `--shell=bun` must work on an image that has no system shell.
let shell_bin = if use_system_shell {
let shell_search_path = shell_path.unwrap_or_else(|| env.get(b"PATH").unwrap_or(b""));
Some(Self::find_shell(shell_search_path, cwd).ok_or(crate::Error::MissingShell)?)
} else {
None
};
Comment thread
robobun marked this conversation as resolved.
Resolved
env.map
.put(b"npm_lifecycle_event", name)
.expect("unreachable");
Expand Down Expand Up @@ -320,7 +324,7 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
Output::flush();
}

if !use_system_shell {
let Some(shell_bin) = shell_bin else {
// SAFETY: `MiniEventLoop` stores `env` as a raw `*mut`; the loader
// outlives the call (process-lifetime in `configure_env_for_run`).
let mini = bun_event_loop::MiniEventLoop::init_global(
Expand Down Expand Up @@ -364,7 +368,7 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
Global::exit(code as u32);
}
return Ok(());
}
};

use crate::api::bun_process::{Status as SpawnStatus, sync};

Expand Down
94 changes: 22 additions & 72 deletions src/runtime/node/node_os.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use core::ffi::c_int;
#[cfg(not(windows))]
use core::ffi::{c_char, c_uint, c_void};
use core::ffi::{c_uint, c_void};

use bun_core;
use bun_core::String as BunString;
Expand Down Expand Up @@ -678,82 +678,32 @@ mod _impl {
}
}

// From libuv:
// > Calling sysconf(_SC_GETPW_R_SIZE_MAX) would get the suggested size, but it
// > is frequently 1024 or 4096, so we can just use that directly. The pwent
// > will not usually be large.
// Instead of always using an allocation, first try a stack allocation
// of 4096, then fallback to heap.
let mut stack_string_bytes = [0u8; 4096];
let mut heap_bytes: Vec<u8>;
let mut string_bytes: &mut [u8] = &mut stack_string_bytes[..];
let mut using_heap = false;

// SAFETY: zeroed POD
let mut pw: libc::passwd = bun_core::ffi::zeroed();
let mut result: *mut libc::passwd = core::ptr::null_mut();

let ret: c_int = loop {
// SAFETY: valid buffers and out-pointer
let ret = unsafe {
libc::getpwuid_r(
libc::geteuid(),
&raw mut pw,
string_bytes.as_mut_ptr().cast::<c_char>(),
string_bytes.len(),
&raw mut result,
)
};

if ret == bun_sys::E::EINTR as c_int {
continue;
}

// If the system call wants more memory, double it.
if ret == bun_sys::E::ERANGE as c_int {
let len = string_bytes.len();
heap_bytes = vec![0u8; len * 2];
string_bytes = &mut heap_bytes[..];
using_heap = true;
continue;
}

break ret;
};
let _ = using_heap;

if ret != 0 {
return Err(global.throw_value(
return match bun_sys::os::passwd_home_dir() {
Err(errno) => Err(global.throw_value(
bun_sys::Error::from_code(
// `ret` is a libc errno; a code outside the table is `EUNKNOWN`.
bun_sys::E::from_raw(ret as u16),
// `errno` is a libc errno; a code outside the table is `EUNKNOWN`.
bun_sys::E::from_raw(errno as u16),
bun_sys::Tag::uv_os_homedir,
)
.to_js(global),
));
}

if result.is_null() {
// bionic has no passwd entries for app uids; with HOME also unset
// (zygote/run-as), return a usable default rather than throwing.
#[cfg(target_os = "android")]
{
return Ok(BunString::static_("/data/local/tmp"));
)),
Ok(None) => {
// bionic has no passwd entries for app uids; with HOME also unset
// (zygote/run-as), return a usable default rather than throwing.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[cfg(target_os = "android")]
{
return Ok(BunString::static_("/data/local/tmp"));
}
// in uv__getpwuid_r, null result throws UV_ENOENT.
#[cfg(not(target_os = "android"))]
return Err(global.throw_value(
bun_sys::Error::from_code(bun_sys::E::ENOENT, bun_sys::Tag::uv_os_homedir)
.to_js(global),
));
}
// in uv__getpwuid_r, null result throws UV_ENOENT.
#[cfg(not(target_os = "android"))]
return Err(global.throw_value(
bun_sys::Error::from_code(bun_sys::E::ENOENT, bun_sys::Tag::uv_os_homedir)
.to_js(global),
));
}

return Ok(if !pw.pw_dir.is_null() {
// SAFETY: pw_dir is a NUL-terminated C string from getpwuid_r
BunString::clone_utf8(unsafe { bun_core::ffi::cstr(pw.pw_dir) }.to_bytes())
} else {
BunString::EMPTY
});
Ok(Some(dir)) if dir.is_empty() => Ok(BunString::EMPTY),
Ok(Some(dir)) => Ok(BunString::clone_utf8(&dir)),
};
}
}

Expand Down
48 changes: 36 additions & 12 deletions src/runtime/shell/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2159,25 +2159,49 @@ impl ShellExecEnv {
}

/// Looks up `$HOME` (`$USERPROFILE` on Windows) in `shell_env` first, then
/// `export_env`. Falls back to `""` (or `/data/local/tmp` on Android) so
/// `cd` with no args / `~` expansion never sees a null.
pub(crate) fn get_homedir(&self) -> crate::shell::env_str::EnvStr {
/// `export_env`.
fn home_env(&self) -> Option<crate::shell::env_str::EnvStr> {
use crate::shell::env_str::EnvStr;
let key = if cfg!(windows) {
EnvStr::init_slice(b"USERPROFILE")
} else {
EnvStr::init_slice(b"HOME")
};
self.shell_env
.get(key)
.or_else(|| self.export_env.get(key))
.unwrap_or_else(|| {
EnvStr::init_slice(if bun_core::env::IS_ANDROID {
b"/data/local/tmp"
} else {
b""
})
self.shell_env.get(key).or_else(|| self.export_env.get(key))
}

/// Where `cd` with no args goes: [`Self::home_env`], else `""` (or
/// `/data/local/tmp` on Android).
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) fn get_homedir(&self) -> crate::shell::env_str::EnvStr {
use crate::shell::env_str::EnvStr;
self.home_env().unwrap_or_else(|| {
EnvStr::init_slice(if bun_core::env::IS_ANDROID {
b"/data/local/tmp"
} else {
b""
})
})
}

/// The value of `~`: [`Self::home_env`], else the passwd entry (what bash
/// and `os.homedir()` use). `None` means no home is known and the `~`
/// stays. Android keeps `/data/local/tmp`: bionic synthesizes a passwd
/// entry with `/` or `/data` for every uid.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) fn get_tilde_home(&self) -> Option<crate::shell::env_str::EnvStr> {
use crate::shell::env_str::EnvStr;
if let Some(home) = self.home_env() {
return Some(home);
}
if bun_core::env::IS_ANDROID {
return Some(EnvStr::init_slice(b"/data/local/tmp"));
}
#[cfg(unix)]
if let Ok(Some(dir)) = bun_sys::os::passwd_home_dir()
&& !dir.is_empty()
{
return Some(EnvStr::init_ref_counted(dir.into_boxed_slice()));
}
None
Comment thread
robobun marked this conversation as resolved.
Resolved
}
}

Expand Down
25 changes: 16 additions & 9 deletions src/runtime/shell/states/Expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,18 @@ impl Expansion {

// All sub-atoms expanded — post-process leading tilde then finish.
if leading_tilde {
let home = me.base.shell().get_homedir();
let home = me.base.shell().get_tilde_home();
let home_bytes: &[u8] = home.as_ref().map_or(b"~", |home| home.slice());
let len_before = me.current_out.len();
match me.current_out.first() {
Some(b'/') | Some(b'\\') => {
me.current_out.splice(0..0, home.slice().iter().copied());
me.current_out.splice(0..0, home_bytes.iter().copied());
}
Some(_) => me.current_out.insert(0, b'~'),
// `~""` expands to $HOME,
// but `~$unset` expands to nothing (word is dropped).
None if me.has_quoted_empty => {
me.current_out.extend_from_slice(home.slice());
me.current_out.extend_from_slice(home_bytes);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
None => {}
Comment thread
robobun marked this conversation as resolved.
Resolved
}
Expand All @@ -250,7 +251,9 @@ impl Expansion {
*off += prepended;
}
}
home.deref();
if let Some(home) = home {
home.deref();
}
}
// Brace expansion
// first, then glob, else flush as a single word.
Expand Down Expand Up @@ -518,12 +521,16 @@ impl Expansion {
out.push(b',');
}
ast::SimpleAtom::Tilde => {
if expand_tilde {
let home = shell.get_homedir();
out.extend_from_slice(home.slice());
home.deref();
match if expand_tilde {
shell.get_tilde_home()
} else {
out.push(b'~');
None
} {
Some(home) => {
out.extend_from_slice(home.slice());
home.deref();
}
None => out.push(b'~'),
}
}
ast::SimpleAtom::CmdSubst(_) => return true,
Expand Down
1 change: 1 addition & 0 deletions src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ pub mod signal_code;
pub use signal_code::SignalCode;
pub mod tmp;
pub use tmp::Tmpfile;
pub mod os;
// `windows/mod.rs` is `#![cfg(windows)]`-gated internally; on POSIX this
// declares an empty module so `bun_sys::windows::*` paths still resolve under
// `#[cfg(windows)]` arms in dependents.
Expand Down
58 changes: 58 additions & 0 deletions src/sys/os.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! `node:os` platform queries.

#[cfg(unix)]
use core::ffi::{c_char, c_int};

/// The effective user's home directory from the passwd database
/// (`getpwuid_r(geteuid())`). `Ok(None)`: no entry for this uid; an entry
/// with no `pw_dir` yields an empty path. `Err` is the errno `getpwuid_r`
/// returned.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[cfg(unix)]
pub fn passwd_home_dir() -> Result<Option<Vec<u8>>, c_int> {
// From libuv:
// > Calling sysconf(_SC_GETPW_R_SIZE_MAX) would get the suggested size, but it
// > is frequently 1024 or 4096, so we can just use that directly. The pwent
// > will not usually be large.
Comment thread
robobun marked this conversation as resolved.
Resolved
let mut stack = [0u8; 4096];
let mut heap: Vec<u8>;
let mut buf: &mut [u8] = &mut stack;
let mut pw: libc::passwd = bun_core::ffi::zeroed();
let mut result: *mut libc::passwd = core::ptr::null_mut();
loop {
// SAFETY: `pw`/`result` are stack out-params; `buf` is writable for `buf.len()`.
let rc = unsafe {
libc::getpwuid_r(
libc::geteuid(),
&raw mut pw,
buf.as_mut_ptr().cast::<c_char>(),
buf.len(),
&raw mut result,
)
};
if rc == libc::EINTR {
continue;
}
if rc == libc::ERANGE {
let len = buf.len();
heap = vec![0u8; len * 2];
buf = &mut heap;
continue;
Comment thread
robobun marked this conversation as resolved.
Resolved
}
if rc != 0 {
return Err(rc);
}
break;
}
if result.is_null() {
return Ok(None);
}
if pw.pw_dir.is_null() {
return Ok(Some(Vec::new()));
}
// SAFETY: on success `pw_dir` is a NUL-terminated string inside `buf`.
Ok(Some(
unsafe { core::ffi::CStr::from_ptr(pw.pw_dir) }
.to_bytes()
.to_vec(),
))
}
Loading
Loading