Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 crates/aube-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ fn add_impl(
osv_transitive_check: input.osv_transitive_check && !input.offline,
control,
// C ABI hosts rely on ambient node; no host-supplied runtime.
node_bin_dir: None,
runtime: None,
};
embed::add(&project_dir, &packages, options)
.await
Expand Down
2 changes: 1 addition & 1 deletion crates/aube-node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ async fn run_install(
osv_transitive_check,
control: control.clone(),
// JS hosts rely on ambient node; no host-supplied runtime.
node_bin_dir: None,
runtime: None,
},
)
.await
Expand Down
78 changes: 70 additions & 8 deletions crates/aube-scripts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,20 @@ pub struct ScriptSettings {
/// system one while project-local binaries still win. `None` when
/// no runtime switching is active.
pub node_bin_dir: Option<PathBuf>,
/// The resolved node executable, exported as `npm_node_execpath` /
/// `NODE` (npm parity) for every script.
pub node_exe: Option<PathBuf>,
/// The node exported as `NODE` (npm parity) — the program a
/// script's `$NODE` / bare `node` re-spawns. A wrapper's shim; the
/// real binary otherwise.
pub node_program: Option<PathBuf>,
/// The node exported as `npm_node_execpath` — the *real* binary
/// node-gyp reads to find Node's install prefix. Equals
/// [`Self::node_program`] unless a wrapper split them apart.
pub node_execpath: Option<PathBuf>,
/// Extra environment an embedder contributes to every script,
/// applied last (after `env_clear`) so it survives the jail. Merge
/// semantics against aube's own values are resolved upstream; these
/// are plain overrides. (`NODE_OPTIONS` folding happens through
/// [`Self::node_options`].)
pub extra_env: Vec<(std::ffi::OsString, std::ffi::OsString)>,
/// The top-level package-manager command, exported as `npm_command`
/// (npm/pnpm parity): `"run-script"` for `aube run`, `"install"`
/// for install lifecycle hooks, `"rebuild"`, `"pack"`, etc. `None`
Expand Down Expand Up @@ -607,11 +618,21 @@ fn apply_script_settings_env(cmd: &mut tokio::process::Command, settings: &Scrip
if let Some(exe) = aube_exe.as_deref() {
cmd.env("npm_execpath", exe);
}
// `npm_node_execpath` / `NODE`: the node binary scripts should use
// — the switched runtime's node, or the ambient `node` on PATH.
// Set here (not at spawn) so it survives the jail's `env_clear`.
if let Some(node_exe) = settings.node_exe.as_deref() {
cmd.env("npm_node_execpath", node_exe).env("NODE", node_exe);
// `npm_node_execpath` / `NODE`: the node binaries scripts should use
// — the switched runtime's node, or the ambient `node` on PATH. `NODE`
// is the program a script re-spawns (a wrapper's shim); the execpath
// is the *real* binary node-gyp reads for Node's install prefix. They
// coincide unless a wrapping embedder split them. Set here (not at
// spawn) so they survive the jail's `env_clear`.
let node_execpath = settings
.node_execpath
.as_deref()
.or(settings.node_program.as_deref());
if let Some(execpath) = node_execpath {
cmd.env("npm_node_execpath", execpath);
}
if let Some(node) = settings.node_program.as_deref().or(node_execpath) {
cmd.env("NODE", node);
}
// `npm_command`: the top-level PM command (run-script / install / …).
if let Some(command) = settings.command.as_deref() {
Expand Down Expand Up @@ -671,6 +692,14 @@ fn apply_script_settings_env(cmd: &mut tokio::process::Command, settings: &Scrip
}
cmd.env("NODE_USE_ENV_PROXY", "1");
}
// Embedder-contributed env, applied last (after `env_clear`, so it
// survives the jail) and after every aube-set var, so a wrapping
// host has the final say. `NODE_OPTIONS` is not among these — it is
// folded into `settings.node_options` upstream to preserve the
// user's value.
for (key, value) in &settings.extra_env {
cmd.env(key, value);
}
}

/// Apply the manifest-derived `npm_package_*` env (name, version,
Expand Down Expand Up @@ -1513,6 +1542,39 @@ mod jail_tests {
assert_eq!(env("NO_PROXY"), None);
assert_eq!(env("NODE_USE_ENV_PROXY"), None);
}

#[test]
fn wrapper_node_and_execpath_are_stamped_distinctly() {
// A wrapping embedder: `NODE` is the shim (so `$NODE` stays
// wrapped) while `npm_node_execpath` is the real binary node-gyp
// reads. Embedder `extra_env` lands last.
let env = proxy_env(ScriptSettings {
node_program: Some(PathBuf::from("/shim/node")),
node_execpath: Some(PathBuf::from("/real/node-24.4.1/bin/node")),
extra_env: vec![("MYTOOL_WRAPPED".into(), "1".into())],
..Default::default()
});
assert_eq!(env("NODE").as_deref(), Some("/shim/node"));
assert_eq!(
env("npm_node_execpath").as_deref(),
Some("/real/node-24.4.1/bin/node")
);
assert_eq!(env("MYTOOL_WRAPPED").as_deref(), Some("1"));
}

#[test]
fn node_execpath_falls_back_to_node_program() {
// A selector supplies only `node_program`; both vars point at it.
let env = proxy_env(ScriptSettings {
node_program: Some(PathBuf::from("/opt/node/bin/node")),
..Default::default()
});
assert_eq!(env("NODE").as_deref(), Some("/opt/node/bin/node"));
assert_eq!(
env("npm_node_execpath").as_deref(),
Some("/opt/node/bin/node")
);
}
}

#[cfg(all(test, windows))]
Expand Down
15 changes: 8 additions & 7 deletions crates/aube/src/commands/add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ pub struct AddToProjectOptions {
pub osv_transitive_check: bool,
/// Invocation-scoped output and cancellation controls for embedders.
pub control: install::InstallControl,
/// Directory containing the `node` executable an embedding host wants
/// lifecycle scripts to run on (see
/// [`crate::commands::install::InstallOptions::embedder_node_bin_dir`]).
/// `None` keeps aube's own runtime resolution. Matches the
/// `node_bin_dir` field on [`crate::embed::InstallOptions`].
pub node_bin_dir: Option<std::path::PathBuf>,
/// How an embedding host wants Node invoked for lifecycle scripts (see
/// [`crate::commands::install::InstallOptions::embedder_runtime`]).
/// `None` falls back to the process-wide
/// [`crate::runtime::set_embedder_runtime`], else aube's own runtime
/// resolution. Matches the `runtime` field on
/// [`crate::embed::InstallOptions`].
pub runtime: Option<crate::runtime::EmbedderRuntime>,
}

/// Resolve, save, and install packages in an explicitly selected project.
Expand Down Expand Up @@ -126,7 +127,7 @@ pub async fn add_to_project(
options.dangerously_allow_all_builds,
);
install_options.control = options.control;
install_options.embedder_node_bin_dir = options.node_bin_dir;
install_options.embedder_runtime = options.runtime;
if options.offline {
install_options.network_mode = aube_registry::NetworkMode::Offline;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/aube/src/commands/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub async fn run(args: CiArgs) -> miette::Result<()> {
// Propagate --ignore-scripts so root lifecycle hooks are skipped.
let opts = install::InstallOptions {
control: install::InstallControl::default(),
embedder_node_bin_dir: None,
embedder_runtime: None,
project_dir: None,
mode: install::FrozenMode::Frozen,
dep_selection: install::DepSelection::from_flags(false, false, no_optional),
Expand Down
2 changes: 1 addition & 1 deletion crates/aube/src/commands/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ pub async fn run(
};
let opts = InstallOptions {
control: install::InstallControl::default(),
embedder_node_bin_dir: None,
embedder_runtime: None,
project_dir: Some(s.target.clone()),
mode,
dep_selection: dep_selection_for_args(&args),
Expand Down
18 changes: 16 additions & 2 deletions crates/aube/src/commands/dlx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use miette::{Context, IntoDiagnostic, miette};
use std::collections::BTreeMap;
use std::sync::Arc;

#[derive(Debug, Args)]
#[derive(Debug, Default, Args)]
// dlx forwards everything after `<command>` to the bin it runs, including
// `--help` and `--version`. Let clap auto-inject its own `-h`/`--help` and
// `--version` handlers and they'd silently swallow those flags before they
Expand Down Expand Up @@ -80,6 +80,17 @@ pub struct DlxArgs {
/// 3. Exec `<tmp>/node_modules/.bin/<command>` from the user's original cwd.
/// 4. tempfile removes the scratch dir on drop.
pub async fn run(args: DlxArgs) -> miette::Result<Option<i32>> {
run_in(args, None).await
}

/// `dlx` rooted at an explicit `base_dir` instead of the process cwd for
/// runtime resolution and the local-bin fast path — the in-process
/// embedding entry. `None` reproduces the CLI behavior. The transient
/// install still runs in its own scratch project regardless.
pub async fn run_in(
args: DlxArgs,
base_dir: Option<std::path::PathBuf>,
) -> miette::Result<Option<i32>> {
args.network.install_overrides();
args.lockfile.install_overrides();
args.virtual_store.install_overrides();
Expand Down Expand Up @@ -152,7 +163,10 @@ pub async fn run(args: DlxArgs) -> miette::Result<Option<i32>> {
// local-bin bin and `aubx` honor the project's .nvmrc / devEngines. The
// fast path replaces the image immediately, so this must run before it or
// a local `dlx` bin would launch with the ambient runtime.
let initial_cwd = crate::dirs::cwd()?;
let initial_cwd = match base_dir {
Some(dir) => dir,
None => crate::dirs::cwd()?,
};
crate::runtime::ensure_for_cwd(&initial_cwd).await?;

let bin_name = bin_name_for(&command);
Expand Down
2 changes: 1 addition & 1 deletion crates/aube/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ fn runtime_section(warnings: &mut Vec<String>) -> Section {
));
}
}
if let Some(bin) = &ctx.node_bin {
if let Some(bin) = &ctx.node_program {
s.push("node-bin", display_path_owned(bin.clone()));
}
Comment on lines +172 to 174
}
Expand Down
28 changes: 25 additions & 3 deletions crates/aube/src/commands/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use clap::Args;
use miette::{Context, IntoDiagnostic, miette};
use std::path::Path;

#[derive(Debug, Args)]
#[derive(Debug, Default, Args)]
pub struct ExecArgs {
/// Binary name
pub bin: String,
Expand Down Expand Up @@ -75,13 +75,30 @@ pub struct ExecArgs {
pub async fn run(
exec_args: ExecArgs,
filter: aube_workspace::selector::EffectiveFilter,
) -> miette::Result<Option<i32>> {
run_in(exec_args, filter, None).await
}

/// `exec` rooted at an explicit `base_dir` instead of the process cwd.
/// `None` reproduces the CLI behavior (resolve from the process cwd);
/// `Some(dir)` is the in-process embedding entry — the project is
/// resolved from `dir`, so concurrent embed calls in different projects
/// don't race on process-global cwd.
pub async fn run_in(
exec_args: ExecArgs,
filter: aube_workspace::selector::EffectiveFilter,
base_dir: Option<std::path::PathBuf>,
) -> miette::Result<Option<i32>> {
exec_args.network.install_overrides();
exec_args.lockfile.install_overrides();
exec_args.virtual_store.install_overrides();
let effective_cwd = match base_dir {
Some(dir) => dir,
None => crate::dirs::cwd()?,
};
// Resolve the project's Node runtime before anything spawns (see
// run.rs for the warm-path rationale).
crate::runtime::ensure_for_cwd(&crate::dirs::cwd()?).await?;
crate::runtime::ensure_for_cwd(&effective_cwd).await?;
let ExecArgs {
bin,
args,
Expand All @@ -100,7 +117,12 @@ pub async fn run(
network: _,
virtual_store: _,
} = exec_args;
let cwd = crate::dirs::project_root()?;
let cwd = crate::dirs::find_project_root(&effective_cwd).ok_or_else(|| {
miette!(
"no package.json found in {} or any parent directory",
effective_cwd.display()
)
})?;
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

ensure_installed(no_install).await?;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved

Expand Down
18 changes: 10 additions & 8 deletions crates/aube/src/commands/install/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ impl InstallArgs {
// every install — neither needs the caller to opt in.
osv_transitive_check: false,
control: super::InstallControl::default(),
embedder_node_bin_dir: None,
embedder_runtime: None,
}
}
}
Expand Down Expand Up @@ -445,12 +445,14 @@ pub struct InstallOptions {
/// embedders can select structured events or silence independently for
/// each concurrent install.
pub control: super::InstallControl,
/// Directory containing the `node` executable an embedding host wants
/// lifecycle scripts to run on. When set, it seeds the install's runtime
/// slot ([`crate::runtime::seed_embedder_node`]) so scripts spawn on that
/// node and find it on PATH — aube does no runtime resolution of its own.
/// `None` keeps aube's normal runtime switching / PATH fallback.
pub embedder_node_bin_dir: Option<std::path::PathBuf>,
/// How an embedding host wants Node invoked for lifecycle scripts. When
/// set, it seeds the install's runtime slot
/// ([`crate::runtime::seed_install_embedder_runtime`]) so scripts invoke
/// that Node and find it on PATH — aube does no runtime resolution of its
/// own. Takes precedence over any process-wide
/// [`crate::runtime::set_embedder_runtime`]. `None` falls back to the
/// process-wide runtime, else aube's normal switching / PATH fallback.
pub embedder_runtime: Option<crate::runtime::EmbedderRuntime>,
}

impl InstallOptions {
Expand Down Expand Up @@ -495,7 +497,7 @@ impl InstallOptions {
// fallback) instead of an unconditional API hit.
osv_transitive_check: false,
control: super::InstallControl::default(),
embedder_node_bin_dir: None,
embedder_runtime: None,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/aube/src/commands/install/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ pub(super) async fn import_local_source(
"buildDir": build_dir,
"locator": format!("{pkg_name}@{}", local.specifier()),
});
let status = tokio::process::Command::new(crate::runtime::node_program())
let status = tokio::process::Command::new(crate::runtime::internal_node_program())
.arg("-e")
.arg(aube_resolver::YARN_EXEC_WRAPPER)
.arg(&script)
Expand Down
13 changes: 6 additions & 7 deletions crates/aube/src/commands/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,13 +596,12 @@ async fn run_inner(opts: InstallOptions, cwd: std::path::PathBuf) -> miette::Res
let lockfile_parse_options = aube_lockfile::ParseOptions {
strict_store_integrity: strict_store_integrity_setting,
};
// An embedding host (mise) can supply the node runtime for lifecycle
// scripts. Seed it into the scoped runtime slot before `ensure` runs —
// `ensure` returns early when the slot is set, so aube skips its own
// runtime resolution and scripts spawn on the host's node.
if let Some(bin_dir) = &opts.embedder_node_bin_dir {
crate::runtime::seed_embedder_node(bin_dir.clone()).await;
}
// An embedding host (mise, or a wrapper) can describe how Node is
// invoked for lifecycle scripts. Seed it into the scoped runtime slot
// before `ensure` runs — `ensure` returns early when the slot is set,
// so aube skips its own runtime resolution and scripts invoke the
// host's Node. A per-call runtime wins over the process-wide one.
crate::runtime::seed_install_embedder_runtime(opts.embedder_runtime.as_ref());
if !opts.dry_run {
crate::runtime::ensure(
&cwd,
Expand Down
2 changes: 1 addition & 1 deletion crates/aube/src/commands/install/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1267,7 +1267,7 @@ mod finalize_lockfile_graph_tests {
use super::*;

fn node_available() -> bool {
std::process::Command::new(crate::runtime::node_program())
std::process::Command::new(crate::runtime::internal_node_program())
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
Expand Down
Loading
Loading