use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
const DISABLE_ENV_VAR: &str = "DENO_DISABLE_NODE_SHIM";
const ACTIVE_ENV_VAR: &str = "DENO_NODE_SHIM_ACTIVE";
const SHIM_DIR_NAME: &str = "node_compat_bin";
fn is_truthy(value: &str) -> bool {
matches!(
value.to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
fn env_disabled() -> bool {
std::env::var(DISABLE_ENV_VAR)
.map(|v| is_truthy(&v))
.unwrap_or(false)
}
pub fn subcommand_may_spawn_node(
subcommand: &crate::args::DenoSubcommand,
) -> bool {
use crate::args::DenoSubcommand::*;
matches!(
subcommand,
Run(_) | Task(_) | Test(_) | Bench(_) | Eval(_) | Repl(_) | Serve(_)
)
}
fn is_node_arg0(arg0: &OsString) -> bool {
let Some(file_name) = Path::new(arg0).file_name() else {
return false;
};
if file_name.eq_ignore_ascii_case("deno")
|| file_name.eq_ignore_ascii_case("deno.exe")
{
return false;
}
if file_name == "node" {
return true;
}
cfg!(windows) && file_name.eq_ignore_ascii_case("node.exe")
}
pub fn maybe_rewrite_node_arg0(args: Vec<OsString>) -> Vec<OsString> {
let Some(arg0) = args.first() else {
return args;
};
if !is_node_arg0(arg0) {
return args;
}
if env_disabled() {
return args;
}
let node_args: Vec<String> = args[1..]
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let parsed = match node_shim::parse_args(node_args) {
Ok(parsed) => parsed,
Err(errors) => {
#[allow(clippy::print_stderr, reason = "node shim arg parse error")]
{
if errors.len() == 1 {
eprintln!("Error: {}", errors[0]);
} else if errors.len() > 1 {
eprintln!("Errors: {}", errors.join(", "));
}
}
deno_runtime::exit(1);
}
};
let options = node_shim::TranslateOptions::for_node_cli();
let result = node_shim::translate_to_deno_args(parsed, &options);
apply_env_side_effects(&result);
let mut deno_args = result.deno_args;
#[allow(clippy::disallowed_methods, reason = "resolving the node shim exe")]
let current_exe =
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("deno"));
node_shim::resolve_run_entrypoint(¤t_exe, &mut deno_args);
deno_args.into_iter().map(OsString::from).collect()
}
fn apply_env_side_effects(result: &node_shim::TranslatedArgs) {
if result.use_system_ca {
unsafe { std::env::set_var("DENO_TLS_CA_STORE", "system") };
}
if !result.node_options.is_empty() {
let options = result.node_options.join(" ");
let merged = match std::env::var("NODE_OPTIONS") {
Ok(existing) if !existing.is_empty() => {
format!("{existing} {options}")
}
_ => options,
};
unsafe { std::env::set_var("NODE_OPTIONS", merged) };
}
if !result.trace_event_categories.is_empty() {
unsafe {
std::env::set_var(
"DENO_NODE_TRACE_EVENT_CATEGORIES",
&result.trace_event_categories,
)
};
}
}
pub fn ensure_node_on_path(deno_dir_root: &Path) -> std::io::Result<()> {
if env_disabled() {
return Ok(());
}
if std::env::var(ACTIVE_ENV_VAR)
.map(|v| is_truthy(&v))
.unwrap_or(false)
{
return Ok(());
}
if which::which("node").is_ok() {
return Ok(());
}
#[allow(clippy::disallowed_methods, reason = "resolving the node shim exe")]
let current_exe = std::env::current_exe()?;
let current_exe =
crate::util::fs::canonicalize_path(¤t_exe).unwrap_or(current_exe);
let shim_dir = deno_dir_root.join(SHIM_DIR_NAME);
let shim_name = if cfg!(windows) { "node.exe" } else { "node" };
let shim_path = shim_dir.join(shim_name);
if !shim_is_valid(&shim_path, ¤t_exe) {
std::fs::create_dir_all(&shim_dir)?;
create_shim(&shim_path, ¤t_exe)?;
}
prepend_self_path(&shim_dir);
unsafe { std::env::set_var(ACTIVE_ENV_VAR, "1") };
Ok(())
}
fn shim_is_valid(shim_path: &Path, current_exe: &Path) -> bool {
#[cfg(unix)]
{
match std::fs::read_link(shim_path) {
Ok(target) => {
target == current_exe
|| crate::util::fs::canonicalize_path(&target).ok().as_deref()
== Some(current_exe)
}
Err(_) => false,
}
}
#[cfg(windows)]
{
same_file::is_same_file(shim_path, current_exe).unwrap_or(false)
}
#[cfg(not(any(unix, windows)))]
{
let _ = (shim_path, current_exe);
false
}
}
#[cfg(unix)]
fn create_shim(shim_path: &Path, current_exe: &Path) -> std::io::Result<()> {
let tmp_path =
shim_path.with_extension(format!("tmp-{}", std::process::id()));
let _ = std::fs::remove_file(&tmp_path);
std::os::unix::fs::symlink(current_exe, &tmp_path)?;
match std::fs::rename(&tmp_path, shim_path) {
Ok(()) => Ok(()),
Err(err) => {
let _ = std::fs::remove_file(&tmp_path);
Err(err)
}
}
}
#[cfg(windows)]
fn create_shim(shim_path: &Path, current_exe: &Path) -> std::io::Result<()> {
if shim_path.exists() {
let _ = std::fs::remove_file(shim_path);
}
match std::fs::hard_link(current_exe, shim_path) {
Ok(()) => Ok(()),
Err(_) => {
let tmp_path =
shim_path.with_extension(format!("exe.tmp-{}", std::process::id()));
std::fs::copy(current_exe, &tmp_path)?;
std::fs::rename(&tmp_path, shim_path)
}
}
}
#[cfg(not(any(unix, windows)))]
fn create_shim(_shim_path: &Path, _current_exe: &Path) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"node shim is not supported on this platform",
))
}
fn prepend_self_path(dir: &Path) {
let sep = if cfg!(windows) { ';' } else { ':' };
let current = std::env::var_os("PATH").unwrap_or_default();
let already_present = std::env::split_paths(¤t).any(|p| p == dir);
if already_present {
return;
}
let mut new_path = OsString::from(dir);
if !current.is_empty() {
new_path.push(sep.to_string());
new_path.push(¤t);
}
unsafe { std::env::set_var("PATH", new_path) };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_node_arg0() {
assert!(is_node_arg0(&OsString::from("node")));
assert!(is_node_arg0(&OsString::from("/usr/local/bin/node")));
assert!(!is_node_arg0(&OsString::from("anode")));
assert!(!is_node_arg0(&OsString::from("/path/to/mynode")));
assert!(!is_node_arg0(&OsString::from("deno")));
assert!(!is_node_arg0(&OsString::from("/usr/bin/deno")));
}
#[test]
#[cfg(windows)]
fn detects_node_exe_arg0_on_windows() {
assert!(is_node_arg0(&OsString::from("node.exe")));
assert!(is_node_arg0(&OsString::from("NODE.EXE")));
assert!(!is_node_arg0(&OsString::from("deno.exe")));
}
#[test]
fn non_node_arg0_passes_through_unchanged() {
let args = vec![
OsString::from("/usr/bin/deno"),
OsString::from("run"),
OsString::from("main.ts"),
];
let result = maybe_rewrite_node_arg0(args.clone());
assert_eq!(result, args);
}
#[test]
fn truthy_values() {
assert!(is_truthy("1"));
assert!(is_truthy("true"));
assert!(is_truthy("TRUE"));
assert!(is_truthy("True"));
assert!(is_truthy("tRuE"));
assert!(is_truthy("yes"));
assert!(is_truthy("on"));
assert!(!is_truthy("0"));
assert!(!is_truthy("false"));
assert!(!is_truthy(""));
}
#[test]
fn create_shim_round_trip() {
let base = std::env::temp_dir()
.join(format!("deno_node_shim_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let fake_exe = base.join(if cfg!(windows) { "deno.exe" } else { "deno" });
std::fs::write(&fake_exe, b"binary").unwrap();
let shim_dir = base.join(SHIM_DIR_NAME);
std::fs::create_dir_all(&shim_dir).unwrap();
let shim_name = if cfg!(windows) { "node.exe" } else { "node" };
let shim_path = shim_dir.join(shim_name);
for _ in 0..2 {
create_shim(&shim_path, &fake_exe).unwrap();
assert!(shim_path.exists());
assert!(shim_is_valid(&shim_path, &fake_exe));
}
let other_exe = base.join("other");
std::fs::write(&other_exe, b"binary").unwrap();
assert!(!shim_is_valid(&shim_path, &other_exe));
let _ = std::fs::remove_dir_all(&base);
}
}