Skip to content
Merged
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: 2 additions & 0 deletions crates/aube-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,8 @@ 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,
};
embed::add(&project_dir, &packages, options)
.await
Expand Down
2 changes: 2 additions & 0 deletions crates/aube-node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ async fn run_install(
dep_selection,
osv_transitive_check,
control: control.clone(),
// JS hosts rely on ambient node; no host-supplied runtime.
node_bin_dir: None,
},
)
.await
Expand Down
7 changes: 7 additions & 0 deletions crates/aube/src/commands/add/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ 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>,
}

/// Resolve, save, and install packages in an explicitly selected project.
Expand Down Expand Up @@ -120,6 +126,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;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This compiles — a partial move is fine here. node_bin_dir is the last use of that field, and the subsequent options.offline (a Copy bool) reads a different, still-initialized field. options.control is moved on the line just above for the same reason. Confirmed by green cargo build --workspace / clippy CI.

This reply was generated by an AI coding assistant.

if options.offline {
install_options.network_mode = aube_registry::NetworkMode::Offline;
}
Expand Down
1 change: 1 addition & 0 deletions crates/aube/src/commands/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +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,
project_dir: None,
mode: install::FrozenMode::Frozen,
dep_selection: install::DepSelection::from_flags(false, false, no_optional),
Expand Down
1 change: 1 addition & 0 deletions crates/aube/src/commands/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ pub async fn run(
};
let opts = InstallOptions {
control: install::InstallControl::default(),
embedder_node_bin_dir: None,
project_dir: Some(s.target.clone()),
mode,
dep_selection: dep_selection_for_args(&args),
Expand Down
8 changes: 8 additions & 0 deletions crates/aube/src/commands/install/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +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,
}
}
}
Expand Down Expand Up @@ -444,6 +445,12 @@ 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>,
}

impl InstallOptions {
Expand Down Expand Up @@ -488,6 +495,7 @@ impl InstallOptions {
// fallback) instead of an unconditional API hit.
osv_transitive_check: false,
control: super::InstallControl::default(),
embedder_node_bin_dir: None,
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/aube/src/commands/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,13 @@ 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;
}
Comment on lines +603 to +605

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in df3e68e. seed_embedder_node is now async and probes node --version via tokio::process::Command, so the install task no longer blocks a Tokio worker on the child spawn. The call site in install::run awaits it.

This reply was generated by an AI coding assistant.

if !opts.dry_run {
crate::runtime::ensure(
&cwd,
Expand Down
7 changes: 7 additions & 0 deletions crates/aube/src/embed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ pub struct InstallOptions {
pub osv_transitive_check: bool,
/// Invocation-scoped output, progress reporting, and cancellation.
pub control: InstallControl,
/// Directory containing the `node` executable to run lifecycle scripts on.
/// Set this when the host manages its own node runtime (e.g. mise) so
/// scripts spawn on it and find it on PATH; `None` uses aube's own runtime
/// resolution / PATH fallback.
pub node_bin_dir: Option<PathBuf>,
}

impl InstallOptions {
Expand All @@ -69,6 +74,7 @@ impl InstallOptions {
dangerously_allow_all_builds: false,
osv_transitive_check: false,
control: InstallControl::default(),
node_bin_dir: None,
}
}
}
Expand Down Expand Up @@ -112,6 +118,7 @@ pub async fn install(options: InstallOptions) -> Result<()> {
command_options.dangerously_allow_all_builds = options.dangerously_allow_all_builds;
command_options.osv_transitive_check = options.osv_transitive_check;
command_options.control = options.control;
command_options.embedder_node_bin_dir = options.node_bin_dir;
crate::commands::install::run(command_options).await
}

Expand Down
98 changes: 98 additions & 0 deletions crates/aube/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub enum RuntimeSource {
/// No requirement configured (or policy said keep the ambient
/// node) — PATH is left alone.
PathFallback,
/// A host embedding aube (e.g. mise) supplied the node runtime to
/// use for lifecycle scripts, rather than aube resolving one itself.
Embedder,
}

impl RuntimeSource {
Expand All @@ -33,6 +36,7 @@ impl RuntimeSource {
RuntimeSource::NodeVersionFile => ".node-version",
RuntimeSource::Nvmrc => ".nvmrc",
RuntimeSource::PathFallback => "PATH",
RuntimeSource::Embedder => "embedder",
}
}
}
Expand Down Expand Up @@ -127,6 +131,61 @@ pub fn current() -> Option<Arc<RuntimeContext>> {
}
}

/// Seed the current install's runtime slot with a node binary supplied by an
/// embedding host (e.g. mise), so lifecycle scripts spawn on that node and
/// find it on PATH instead of relying on an ambient `node`.
///
/// `bin_dir` is the directory containing the `node` executable; it is
/// prepended to the script PATH ([`path_entries`]) and its `node` is used as
/// [`node_program`]. Must be called inside a [`scope`] (the install task) and
/// before [`ensure`] runs — `ensure` returns early when the slot is already
/// set, so this override wins without aube probing for its own runtime. A
/// no-op outside a scope or if the slot is already populated.
pub async fn seed_embedder_node(bin_dir: PathBuf) {
// No-op outside an install scope, or when the slot is already seeded.
// Check first so the path/version probing below is skipped entirely in
// those cases (a later `set` would be a no-op anyway).
let should_seed = INSTALL_RUNTIME
.try_with(|slot| slot.get().is_none())
.unwrap_or(false);
if !should_seed {
return;
}
// Absolutize: lifecycle scripts may run with a different working
// directory, so both `node_program()` and the prepended PATH entry must
// resolve independently of cwd. `absolute` doesn't require the dir to
// exist or touch symlinks; fall back to the input if it errors.
let bin_dir = std::path::absolute(&bin_dir).unwrap_or(bin_dir);
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
let node_bin = bin_dir.join(node_exe);
// Probe the supplied node for its version so engine checks and
// virtual-store hashing key off the *same* node as lifecycle scripts,
// instead of `effective_node_version` falling back to an ambient `node`.
// Async spawn so the install task doesn't block a Tokio worker on the
// child process.
let version = tokio::process::Command::new(&node_bin)
.arg("--version")
.output()
.await
.ok()
.filter(|out| out.status.success())
.and_then(|out| String::from_utf8(out.stdout).ok())
.map(|v| v.trim().trim_start_matches('v').to_string())
.filter(|v| !v.is_empty());
let ctx = RuntimeContext {
node_bin: Some(node_bin),
bin_dir: Some(bin_dir),
version,
requested: None,
source: RuntimeSource::Embedder,
provenance: RuntimeProvenance::Mise,
fresh_pin: None,
};
Comment on lines +175 to +183
let _ = INSTALL_RUNTIME.try_with(|slot| {
let _ = slot.set(Arc::new(ctx));
});
}
Comment thread
cursor[bot] marked this conversation as resolved.

/// The node executable spawn sites should use: the switched runtime's
/// binary when one resolved, otherwise bare `"node"` (PATH lookup at
/// spawn time, today's behavior).
Expand Down Expand Up @@ -753,6 +812,45 @@ mod tests {
context
}

#[tokio::test]
async fn seed_embedder_node_drives_node_program_and_path() {
scope(async {
assert!(current().is_none(), "slot starts empty");
let bin_dir = PathBuf::from("/opt/mise/node/bin");
seed_embedder_node(bin_dir.clone()).await;

// The seed absolutizes the dir; compute the expected the same way
// so this holds on Windows (where `/opt/...` isn't absolute).
let expected = std::path::absolute(&bin_dir).unwrap_or(bin_dir);
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };

let ctx = current().expect("slot seeded");
assert_eq!(ctx.source, RuntimeSource::Embedder);
assert_eq!(ctx.bin_dir.as_deref(), Some(expected.as_path()));
assert_eq!(node_program(), expected.join(node_exe));
assert_eq!(path_entries(), vec![expected.clone()]);

// `ensure`-style early return: a second seed does not clobber the
// first (OnceCell is set once).
seed_embedder_node(PathBuf::from("/other")).await;
assert_eq!(node_program(), expected.join(node_exe));
})
.await;
}

#[tokio::test]
async fn seed_embedder_node_is_a_noop_outside_scope() {
// No install scope active: the seed can't set a task-local slot, so it
// must not panic and must not leak into a later scope. Asserting via a
// fresh `scope` reads the (empty) task-local, not the process-wide
// `RUNTIME`, so this stays deterministic regardless of test order.
seed_embedder_node(PathBuf::from("/opt/mise/node/bin")).await;
scope(async {
assert!(current().is_none(), "seed outside a scope must not leak in");
})
.await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn install_runtime_is_isolated_and_propagated() {
let barrier = Arc::new(tokio::sync::Barrier::new(2));
Expand Down
Loading