use aube_manifest::PackageJson;
use aube_settings::ResolveCtx;
use miette::miette;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeSource {
DevEngines,
NodeVersionFile,
Nvmrc,
PathFallback,
Embedder,
}
impl RuntimeSource {
pub fn label(self) -> &'static str {
match self {
RuntimeSource::DevEngines => "devEngines.runtime",
RuntimeSource::NodeVersionFile => ".node-version",
RuntimeSource::Nvmrc => ".nvmrc",
RuntimeSource::PathFallback => "PATH",
RuntimeSource::Embedder => "embedder",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeProvenance {
Mise,
AubeManaged,
System,
Embedder,
}
impl RuntimeProvenance {
pub fn label(self) -> &'static str {
match self {
RuntimeProvenance::Mise => "mise",
RuntimeProvenance::AubeManaged => aube_util::embedder().name,
RuntimeProvenance::System => "system",
RuntimeProvenance::Embedder => aube_util::embedder().name,
}
}
}
#[derive(Debug, Clone)]
pub struct RuntimeContext {
pub bin_dir: Option<PathBuf>,
pub bin_dir_precedes_project_bins: bool,
pub node_program: Option<PathBuf>,
pub node_execpath: Option<PathBuf>,
pub internal_node: Option<PathBuf>,
pub version: Option<String>,
pub requested: Option<String>,
pub source: RuntimeSource,
pub provenance: RuntimeProvenance,
pub env: Vec<EmbedderEnv>,
pub fresh_pin: Option<aube_runtime::PinnedNode>,
}
impl RuntimeContext {
fn path_fallback() -> RuntimeContext {
RuntimeContext {
bin_dir: None,
bin_dir_precedes_project_bins: false,
node_program: None,
node_execpath: None,
internal_node: None,
version: None,
requested: None,
source: RuntimeSource::PathFallback,
provenance: RuntimeProvenance::System,
env: Vec::new(),
fresh_pin: None,
}
}
}
static RUNTIME: tokio::sync::OnceCell<Arc<RuntimeContext>> = tokio::sync::OnceCell::const_new();
type RuntimeSlot = Arc<tokio::sync::OnceCell<Arc<RuntimeContext>>>;
tokio::task_local! {
static INSTALL_RUNTIME: RuntimeSlot;
static PER_CALL_RUNTIME: Arc<RuntimeContext>;
}
pub async fn scope<F: Future>(future: F) -> F::Output {
INSTALL_RUNTIME
.scope(Arc::new(tokio::sync::OnceCell::new()), future)
.await
}
pub fn scope_current<F: Future>(future: F) -> impl Future<Output = F::Output> {
let runtime = INSTALL_RUNTIME.try_with(Arc::clone).ok();
let per_call = per_call_runtime();
async move {
let future = async move {
match runtime {
Some(runtime) => INSTALL_RUNTIME.scope(runtime, future).await,
None => future.await,
}
};
match per_call {
Some(per_call) => PER_CALL_RUNTIME.scope(per_call, future).await,
None => future.await,
}
}
}
pub fn current() -> Option<Arc<RuntimeContext>> {
match INSTALL_RUNTIME.try_with(|runtime| runtime.get().map(Arc::clone)) {
Ok(runtime) => runtime,
Err(_) => per_call_runtime()
.or_else(|| EMBEDDER_CONTEXT.get().map(Arc::clone))
.or_else(|| RUNTIME.get().map(Arc::clone)),
}
}
fn per_call_runtime() -> Option<Arc<RuntimeContext>> {
PER_CALL_RUNTIME.try_with(Arc::clone).ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvMerge {
Replace,
Append,
}
#[derive(Debug, Clone)]
pub struct EmbedderEnv {
pub key: std::ffi::OsString,
pub val: std::ffi::OsString,
pub merge: EnvMerge,
}
#[derive(Debug, Clone, Default)]
pub struct EmbedderRuntime {
bin_dir: Option<PathBuf>,
path_unchanged: bool,
bin_dir_precedes_project_bins: bool,
node_program: Option<PathBuf>,
node_execpath: Option<PathBuf>,
internal_node: Option<PathBuf>,
version: Option<String>,
env: Vec<EmbedderEnv>,
}
impl EmbedderRuntime {
pub fn selector(bin_dir: impl Into<PathBuf>) -> Self {
Self {
bin_dir: Some(bin_dir.into()),
..Default::default()
}
}
pub fn wrapper(node_program: impl Into<PathBuf>) -> Self {
Self {
node_program: Some(node_program.into()),
bin_dir_precedes_project_bins: true,
..Default::default()
}
}
pub fn real_node(mut self, path: impl Into<PathBuf>) -> Self {
self.node_execpath = Some(path.into());
self
}
pub fn path_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.bin_dir = Some(dir.into());
self.path_unchanged = false;
self
}
pub fn without_path(mut self) -> Self {
self.path_unchanged = true;
self
}
pub fn internal_node(mut self, path: impl Into<PathBuf>) -> Self {
self.internal_node = Some(path.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn env_append(
mut self,
key: impl Into<std::ffi::OsString>,
val: impl Into<std::ffi::OsString>,
) -> Self {
self.env.push(EmbedderEnv {
key: key.into(),
val: val.into(),
merge: EnvMerge::Append,
});
self
}
pub fn env_set(
mut self,
key: impl Into<std::ffi::OsString>,
val: impl Into<std::ffi::OsString>,
) -> Self {
self.env.push(EmbedderEnv {
key: key.into(),
val: val.into(),
merge: EnvMerge::Replace,
});
self
}
fn resolve(&self) -> RuntimeContext {
let abs = |p: &PathBuf| std::path::absolute(p).unwrap_or_else(|_| p.clone());
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
let node_program = self
.node_program
.clone()
.or_else(|| self.bin_dir.as_ref().map(|dir| dir.join(node_exe)))
.map(|p| abs(&p));
let bin_dir = if self.path_unchanged {
None
} else {
self.bin_dir.clone().or_else(|| {
node_program
.as_ref()
.and_then(|p| p.parent())
.map(Path::to_path_buf)
})
}
.map(|p| abs(&p));
let node_execpath = self
.node_execpath
.clone()
.map(|p| abs(&p))
.or_else(|| node_program.clone());
let internal_node = self.internal_node.clone().map(|p| abs(&p));
RuntimeContext {
bin_dir,
bin_dir_precedes_project_bins: self.bin_dir_precedes_project_bins,
node_program,
node_execpath,
internal_node,
version: self.version.clone().filter(|v| !v.is_empty()),
requested: None,
source: RuntimeSource::Embedder,
provenance: RuntimeProvenance::Embedder,
env: self.env.clone(),
fresh_pin: None,
}
}
}
static EMBEDDER_CONTEXT: std::sync::OnceLock<Arc<RuntimeContext>> = std::sync::OnceLock::new();
pub fn set_embedder_runtime(runtime: EmbedderRuntime) {
let _ = EMBEDDER_CONTEXT.set(Arc::new(runtime.resolve()));
}
pub fn seed_install_embedder_runtime(per_call: Option<&EmbedderRuntime>) {
let ctx = match per_call {
Some(rt) => Arc::new(rt.resolve()),
None => match per_call_runtime().or_else(|| EMBEDDER_CONTEXT.get().map(Arc::clone)) {
Some(ctx) => ctx,
None => return,
},
};
let _ = INSTALL_RUNTIME.try_with(|slot| {
let _ = slot.set(ctx);
});
}
pub async fn with_embedder_runtime<F: Future>(
runtime: Option<EmbedderRuntime>,
future: F,
) -> F::Output {
let seeded_scope = |future: F| {
scope(async move {
seed_install_embedder_runtime(None);
future.await
})
};
match runtime {
Some(rt) => {
PER_CALL_RUNTIME
.scope(Arc::new(rt.resolve()), seeded_scope(future))
.await
}
None => seeded_scope(future).await,
}
}
pub fn node_program() -> PathBuf {
current()
.and_then(|c| c.node_program.clone())
.unwrap_or_else(|| PathBuf::from("node"))
}
pub fn internal_node_program() -> PathBuf {
current()
.and_then(|c| c.internal_node.clone().or_else(|| c.node_program.clone()))
.unwrap_or_else(|| PathBuf::from("node"))
}
pub fn path_entries() -> Vec<PathBuf> {
current()
.and_then(|c| c.bin_dir.clone())
.into_iter()
.collect()
}
pub(crate) fn node_child_path_entries() -> Vec<PathBuf> {
let context = current();
let mut entries = context
.as_ref()
.and_then(|c| c.bin_dir.clone())
.into_iter()
.collect::<Vec<_>>();
if let Some(dir) = crate::tool_shims::activated_shim_dir() {
if context.is_some_and(|c| c.bin_dir_precedes_project_bins) {
entries.push(dir);
} else {
entries.insert(0, dir);
}
}
entries
}
pub fn path_entries_with_project_bins(project_bins: Vec<PathBuf>) -> Vec<PathBuf> {
let Some(context) = current() else {
return project_bins;
};
aube_scripts::order_path_entries(
project_bins,
context.bin_dir.as_deref(),
context.bin_dir_precedes_project_bins,
)
}
pub fn node_execpath() -> Option<PathBuf> {
current().and_then(|c| c.node_execpath.clone().or_else(|| c.node_program.clone()))
}
pub fn apply_child_env(cmd: &mut tokio::process::Command) {
for (key, value) in child_env_vars() {
cmd.env(key, value);
}
}
pub fn child_env_vars() -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let ctx = current();
let execpath = ctx
.as_ref()
.and_then(|c| c.node_execpath.clone().or_else(|| c.node_program.clone()))
.or_else(aube_runtime::node_on_path);
let node = ctx
.as_ref()
.and_then(|c| c.node_program.clone())
.or_else(|| execpath.clone());
let mut vars: Vec<(std::ffi::OsString, std::ffi::OsString)> = Vec::new();
if let Some(execpath) = execpath {
vars.push(("npm_node_execpath".into(), execpath.into_os_string()));
}
if let Some(node) = node {
vars.push(("NODE".into(), node.into_os_string()));
}
if let Some(ctx) = ctx {
vars.extend(fold_env(&ctx.env, |key| {
std::env::var_os(key).filter(|v| !v.is_empty())
}));
}
vars
}
fn fold_env(
entries: &[EmbedderEnv],
base: impl Fn(&std::ffi::OsStr) -> Option<std::ffi::OsString>,
) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut order: Vec<std::ffi::OsString> = Vec::new();
let mut acc: std::collections::HashMap<std::ffi::OsString, std::ffi::OsString> =
std::collections::HashMap::new();
for entry in entries {
if !acc.contains_key(&entry.key) {
order.push(entry.key.clone());
}
let value = match entry.merge {
EnvMerge::Replace => entry.val.clone(),
EnvMerge::Append => {
let running = acc
.get(&entry.key)
.cloned()
.or_else(|| base(&entry.key))
.filter(|v| !v.is_empty());
match running {
Some(mut existing) => {
existing.push(" ");
existing.push(&entry.val);
existing
}
None => entry.val.clone(),
}
}
};
acc.insert(entry.key.clone(), value);
}
order
.into_iter()
.map(|key| {
let value = acc.remove(&key).unwrap_or_default();
(key, value)
})
.collect()
}
pub fn merge_node_options(base: Option<String>) -> Option<String> {
let ctx = current();
let entries = ctx.as_ref().map(|c| c.env.as_slice()).unwrap_or(&[]);
let node_options: Vec<EmbedderEnv> = entries
.iter()
.filter(|e| e.key == std::ffi::OsStr::new("NODE_OPTIONS"))
.cloned()
.collect();
if node_options.is_empty() {
return base;
}
let base_os = base.map(std::ffi::OsString::from);
fold_env(&node_options, |_| base_os.clone())
.into_iter()
.next()
.map(|(_, v)| v.to_string_lossy().into_owned())
.or_else(|| base_os.map(|b| b.to_string_lossy().into_owned()))
}
pub fn embedder_extra_env() -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let Some(ctx) = current() else {
return Vec::new();
};
let entries: Vec<EmbedderEnv> = ctx
.env
.iter()
.filter(|e| e.key != std::ffi::OsStr::new("NODE_OPTIONS"))
.cloned()
.collect();
fold_env(&entries, |_| None)
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeSettings {
pub installer: aube_runtime::InstallerMode,
pub on_fail_override: Option<aube_manifest::OnFail>,
pub mirror: Option<String>,
pub network: aube_runtime::NetworkMode,
pub switching: bool,
}
impl RuntimeSettings {
pub fn from_ctx(ctx: &ResolveCtx<'_>) -> Self {
let installer = match aube_settings::resolved::runtime_installer(ctx) {
aube_settings::resolved::RuntimeInstaller::Auto => aube_runtime::InstallerMode::Auto,
aube_settings::resolved::RuntimeInstaller::Mise => aube_runtime::InstallerMode::Mise,
aube_settings::resolved::RuntimeInstaller::Aube => aube_runtime::InstallerMode::Aube,
};
let on_fail_override =
aube_settings::resolved::runtime_on_fail(ctx).map(|forced| match forced {
aube_settings::resolved::RuntimeOnFail::Download => aube_manifest::OnFail::Download,
aube_settings::resolved::RuntimeOnFail::Error => aube_manifest::OnFail::Error,
aube_settings::resolved::RuntimeOnFail::Warn => aube_manifest::OnFail::Warn,
aube_settings::resolved::RuntimeOnFail::Ignore => aube_manifest::OnFail::Ignore,
});
RuntimeSettings {
installer,
on_fail_override,
mirror: release_mirror(ctx),
network: aube_runtime::NetworkMode::Online,
switching: aube_util::embedder().runtime_switching,
}
}
}
pub(crate) fn lockfile_node_pin(
project_dir: &Path,
manifest: &PackageJson,
parse_options: aube_lockfile::ParseOptions,
) -> Option<aube_lockfile::RuntimePin> {
let pinned = [aube_util::embedder().lockfile_basename, "pnpm-lock.yaml"]
.iter()
.any(|name| {
std::fs::read_to_string(project_dir.join(name))
.map(|s| s.contains("specifier: runtime:"))
.unwrap_or(false)
});
if !pinned {
return None;
}
let (graph, _) =
aube_lockfile::parse_lockfile_with_kind_and_options(project_dir, manifest, parse_options)
.ok()?;
graph.runtimes.get("node").cloned()
}
pub async fn ensure_for_cwd(cwd: &Path) -> miette::Result<Arc<RuntimeContext>> {
if let Some(ctx) = current() {
return Ok(ctx);
}
let project_dir = crate::dirs::find_project_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
let manifest =
aube_manifest::PackageJson::from_path_cached(&project_dir.join("package.json")).ok();
let (settings, parse_options) = crate::commands::with_settings_ctx(&project_dir, |ctx| {
(
RuntimeSettings::from_ctx(ctx),
aube_lockfile::ParseOptions {
strict_store_integrity: aube_settings::resolved::strict_store_integrity(ctx)
|| aube_settings::resolved::paranoid(ctx),
},
)
});
let pin = manifest
.as_deref()
.and_then(|m| lockfile_node_pin(&project_dir, m, parse_options));
ensure(&project_dir, manifest.as_deref(), settings, pin.as_ref()).await
}
pub(crate) async fn ensure_for_cwd_with_manifest(
cwd: &Path,
manifest: &PackageJson,
) -> miette::Result<Arc<RuntimeContext>> {
if let Some(ctx) = current() {
return Ok(ctx);
}
let project_dir = crate::dirs::find_project_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
let (settings, parse_options) = crate::commands::with_settings_ctx(&project_dir, |ctx| {
(
RuntimeSettings::from_ctx(ctx),
aube_lockfile::ParseOptions {
strict_store_integrity: aube_settings::resolved::strict_store_integrity(ctx)
|| aube_settings::resolved::paranoid(ctx),
},
)
});
let pin = lockfile_node_pin(&project_dir, manifest, parse_options);
ensure(&project_dir, Some(manifest), settings, pin.as_ref()).await
}
pub async fn ensure(
project_dir: &Path,
manifest: Option<&PackageJson>,
settings: RuntimeSettings,
lock_pin: Option<&aube_lockfile::RuntimePin>,
) -> miette::Result<Arc<RuntimeContext>> {
let lock_pin = lock_pin.cloned();
let project_dir = project_dir.to_path_buf();
let manifest = manifest.cloned();
if let Ok(runtime) = INSTALL_RUNTIME.try_with(Arc::clone) {
return runtime
.get_or_try_init(|| async {
resolve_context(project_dir, manifest, settings, lock_pin)
.await
.map(Arc::new)
})
.await
.map(Arc::clone);
}
if let Some(ctx) = EMBEDDER_CONTEXT.get() {
return Ok(Arc::clone(ctx));
}
RUNTIME
.get_or_try_init(|| async {
resolve_context(project_dir, manifest, settings, lock_pin)
.await
.map(Arc::new)
})
.await
.map(Arc::clone)
}
async fn resolve_context(
project_dir: PathBuf,
manifest: Option<PackageJson>,
settings: RuntimeSettings,
lock_pin: Option<aube_lockfile::RuntimePin>,
) -> miette::Result<RuntimeContext> {
if !settings.switching {
return Ok(RuntimeContext::path_fallback());
}
let project_dir = project_dir.as_path();
let manifest = manifest.as_ref();
let lock_pin = lock_pin.as_ref();
let dev_engines = manifest
.and_then(|m| m.dev_engines.as_ref())
.and_then(|d| d.node_runtime())
.and_then(|r| {
r.version
.as_deref()
.map(|v| (v, r.on_fail, project_dir.join("package.json")))
});
if let Some(unsupported) = manifest
.and_then(|m| m.dev_engines.as_ref())
.map(|d| d.unsupported_runtimes())
.filter(|u| !u.is_empty())
{
tracing::debug!(
runtimes = ?unsupported,
"ignoring non-node devEngines.runtime entries"
);
}
let request = aube_runtime::effective_request(
dev_engines.as_ref().map(|(v, f, p)| (*v, *f, p.as_path())),
project_dir,
)
.map_err(|e| miette!(code = e.code(), "{e}"))?;
let Some(mut request) = request else {
return Ok(RuntimeContext::path_fallback());
};
if let Some(forced) = settings.on_fail_override {
request.on_fail = forced;
}
let cfg = aube_runtime::RuntimeConfig {
installer: settings.installer,
mirror: settings.mirror.clone(),
network: settings.network,
retries: 2,
};
let source = match request.source {
aube_runtime::RequestSource::DevEngines => RuntimeSource::DevEngines,
aube_runtime::RequestSource::NodeVersionFile => RuntimeSource::NodeVersionFile,
aube_runtime::RequestSource::Nvmrc => RuntimeSource::Nvmrc,
};
let requested = request.raw.clone();
let pinned = lock_pin
.filter(|pin| pin.specifier == requested)
.map(pinned_from_lockfile)
.transpose()
.map_err(|e| miette!("{e}"))?;
let runtime = aube_runtime::NodeRuntime::new(cfg);
let resolved = runtime
.resolve(&request, pinned.as_ref(), &CliProgress::node())
.await
.map_err(|e| miette!(code = e.code(), "{e}"))?;
Ok(match resolved {
None => {
let mut ctx = RuntimeContext::path_fallback();
ctx.requested = Some(requested);
ctx.source = source;
ctx
}
Some(res) => RuntimeContext {
bin_dir: res.bin_dir.clone(),
bin_dir_precedes_project_bins: false,
node_program: Some(res.node_bin.clone()),
node_execpath: Some(res.node_bin.clone()),
internal_node: None,
version: Some(res.version.to_string()),
requested: Some(requested),
source,
provenance: match res.from {
aube_runtime::ResolvedFrom::PathEnv => RuntimeProvenance::System,
aube_runtime::ResolvedFrom::Installed(origin)
| aube_runtime::ResolvedFrom::FreshInstall(origin) => match origin {
aube_runtime::InstallOrigin::Mise => RuntimeProvenance::Mise,
aube_runtime::InstallOrigin::Aube => RuntimeProvenance::AubeManaged,
},
},
env: Vec::new(),
fresh_pin: res.fresh_pin,
},
})
}
fn release_mirror(ctx: &ResolveCtx<'_>) -> Option<String> {
let yaml_serde::Value::Mapping(map) = ctx.workspace_yaml.get("nodeDownloadMirrors")? else {
return None;
};
map.iter().find_map(|(k, v)| match (k, v) {
(yaml_serde::Value::String(key), yaml_serde::Value::String(url))
if key == "release" && !url.trim().is_empty() =>
{
Some(url.trim().to_string())
}
_ => None,
})
}
fn pinned_from_lockfile(
pin: &aube_lockfile::RuntimePin,
) -> Result<aube_runtime::PinnedNode, aube_runtime::Error> {
let version = node_semver::Version::parse(&pin.version).map_err(|e| {
aube_runtime::Error::NoMatchingVersion {
requested: format!("lockfile pin {}: {e}", pin.version),
platform_note: String::new(),
}
})?;
let mut variants = Vec::new();
for v in &pin.variants {
for t in &v.targets {
variants.push(aube_runtime::PinnedVariant {
os: t.os.clone(),
cpu: t.cpu.clone(),
libc: t.libc.clone(),
archive: v.archive.clone(),
url: v.url.clone(),
integrity_sri: v.integrity.clone(),
bin: v.bin.clone(),
prefix: v.prefix.clone(),
});
}
}
Ok(aube_runtime::PinnedNode { version, variants })
}
pub async fn refresh_lockfile_pin(
graph: &mut aube_lockfile::LockfileGraph,
manifest: &PackageJson,
settings: RuntimeSettings,
write_kind: aube_lockfile::LockfileKind,
) -> miette::Result<()> {
let declared = manifest
.dev_engines
.as_ref()
.and_then(|d| d.node_runtime())
.and_then(|r| r.version.clone());
let Some(range) = declared else {
graph.runtimes.remove("node");
return Ok(());
};
if !matches!(
write_kind,
aube_lockfile::LockfileKind::Aube | aube_lockfile::LockfileKind::Pnpm
) {
if !graph.runtimes.contains_key("node") {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_RUNTIME_PIN_NOT_RECORDED,
format = ?write_kind,
"devEngines.runtime resolved but this lockfile format cannot record a runtime pin; subsequent runs re-resolve the range"
);
}
return Ok(());
}
let Some(version) = current().and_then(|c| c.version.clone()) else {
return Ok(());
};
if graph
.runtimes
.get("node")
.is_some_and(|p| p.specifier == range && p.version == version)
{
return Ok(());
}
let fresh = current().and_then(|c| c.fresh_pin.clone());
let pin = match fresh.filter(|p| p.version.to_string() == version) {
Some(p) => p,
None => {
let cfg = aube_runtime::RuntimeConfig {
installer: settings.installer,
mirror: settings.mirror.clone(),
network: aube_runtime::NetworkMode::Online,
retries: 2,
};
let spec = aube_runtime::NodeSpec::parse(&version)
.map_err(|e| miette!(code = e.code(), "{e}"))?;
match aube_runtime::NodeRuntime::new(cfg)
.resolve_for_lockfile(&spec)
.await
{
Ok(p) => p,
Err(e) => {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_RUNTIME_PIN_NOT_RECORDED,
error = %e,
"could not fetch runtime checksums to record the lockfile pin"
);
return Ok(());
}
}
}
};
graph
.runtimes
.insert("node".to_string(), lockfile_pin_from(&pin, &range));
Ok(())
}
pub fn lockfile_pin_from(
pin: &aube_runtime::PinnedNode,
specifier: &str,
) -> aube_lockfile::RuntimePin {
aube_lockfile::RuntimePin {
specifier: specifier.to_string(),
version: pin.version.to_string(),
dev: true,
has_bin: true,
variants: pin
.variants
.iter()
.map(|v| aube_lockfile::RuntimeVariant {
targets: vec![aube_lockfile::RuntimeTarget {
os: v.os.clone(),
cpu: v.cpu.clone(),
libc: v.libc.clone(),
}],
archive: v.archive.clone(),
url: v.url.clone(),
integrity: v.integrity_sri.clone(),
bin: v.bin.clone(),
bin_is_bare_string: false,
prefix: v.prefix.clone(),
})
.collect(),
}
}
pub(crate) struct CliProgress {
tool: &'static str,
state: std::sync::Mutex<CliProgressState>,
}
#[derive(Default)]
struct CliProgressState {
version: Option<String>,
job: Option<std::sync::Arc<clx::progress::ProgressJob>>,
announced: bool,
downloaded: u64,
total: Option<u64>,
paused_for_tool: bool,
}
impl CliProgress {
pub(crate) fn node() -> Self {
Self::for_tool("Node.js")
}
pub(crate) fn aube() -> Self {
Self::for_tool(aube_util::embedder().name)
}
fn for_tool(tool: &'static str) -> Self {
CliProgress {
tool,
state: std::sync::Mutex::new(CliProgressState::default()),
}
}
fn fancy_output() -> bool {
use std::io::IsTerminal;
clx::progress::output() != clx::progress::ProgressOutput::Text
&& std::io::stderr().is_terminal()
}
fn label(&self, version: &str, phase: &str) -> String {
if phase.is_empty() {
format!("{} v{version}", self.tool)
} else {
format!("{} v{version} ({phase})", self.tool)
}
}
fn bytes_prop(state: &CliProgressState) -> String {
match state.total {
Some(total) if total > 0 => format!(
"{} / {}",
crate::progress::format_bytes(state.downloaded),
crate::progress::format_bytes(total)
),
_ => crate::progress::format_bytes(state.downloaded),
}
}
}
impl aube_runtime::DownloadProgress for CliProgress {
fn on_phase(&self, version: Option<&node_semver::Version>, phase: aube_runtime::InstallPhase) {
use aube_runtime::InstallPhase;
let mut state = self.state.lock().unwrap();
if let Some(v) = version {
state.version = Some(v.to_string());
}
let version = state.version.clone().unwrap_or_default();
match phase {
InstallPhase::Resolving => {}
InstallPhase::Downloading => {
if !Self::fancy_output() && !state.announced {
state.announced = true;
crate::progress::safe_eprintln(&format!(
"Downloading {} v{version}…",
self.tool
));
}
}
InstallPhase::Verifying => {
if let Some(job) = &state.job {
job.prop("label", &self.label(&version, "verifying…"));
}
}
InstallPhase::Extracting => {
if let Some(job) = &state.job {
job.prop("label", &self.label(&version, "extracting…"));
}
}
}
}
fn on_download_start(&self, total_bytes: Option<u64>) {
if !Self::fancy_output() {
return;
}
let mut state = self.state.lock().unwrap();
state.total = total_bytes;
let version = state.version.clone().unwrap_or_default();
let builder = clx::progress::ProgressJobBuilder::new()
.body("{{spinner()}} {{label}} {{progress_bar(flex=true)}} {{bytes}}")
.body_text(Some("{{label}} {{bytes}}"))
.prop("label", &self.label(&version, ""))
.prop("bytes", "")
.status(clx::progress::ProgressStatus::Running)
.progress_current(0)
.progress_total(total_bytes.unwrap_or(1).max(1) as usize);
state.job = Some(builder.start());
}
fn on_download_chunk(&self, bytes: u64) {
let mut state = self.state.lock().unwrap();
state.downloaded += bytes;
let bytes_text = Self::bytes_prop(&state);
if let Some(job) = &state.job {
if state.total.is_some() {
job.progress_current(state.downloaded as usize);
}
job.prop("bytes", &bytes_text);
}
}
fn on_done(&self) {
let state = self.state.lock().unwrap();
if let Some(job) = &state.job {
job.set_status(clx::progress::ProgressStatus::Done);
} else if state.announced {
crate::progress::safe_eprintln(&format!(
"{} v{} installed",
self.tool,
state.version.clone().unwrap_or_default()
));
}
}
fn on_external_tool_start(&self) {
let mut state = self.state.lock().unwrap();
if !clx::progress::is_paused() {
clx::progress::pause();
state.paused_for_tool = true;
}
}
fn on_external_tool_end(&self) {
let mut state = self.state.lock().unwrap();
if state.paused_for_tool {
clx::progress::resume();
state.paused_for_tool = false;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_context(requested: &str) -> RuntimeContext {
let mut context = RuntimeContext::path_fallback();
context.requested = Some(requested.to_string());
context
}
fn abs(p: &str) -> PathBuf {
let p = PathBuf::from(p);
std::path::absolute(&p).unwrap_or(p)
}
#[test]
fn selector_derives_one_binary_for_all_three_paths() {
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
let ctx = EmbedderRuntime::selector("/opt/mise/node/bin").resolve();
let bin = abs("/opt/mise/node/bin");
assert_eq!(ctx.bin_dir.as_deref(), Some(bin.as_path()));
assert_eq!(
ctx.node_program.as_deref(),
Some(bin.join(node_exe).as_path())
);
assert_eq!(ctx.node_execpath, ctx.node_program);
assert!(!ctx.bin_dir_precedes_project_bins);
assert_eq!(ctx.source, RuntimeSource::Embedder);
}
#[test]
fn wrapper_splits_shim_from_real_node() {
let ctx = EmbedderRuntime::wrapper("/shim/node")
.real_node("/node-24.4.1/bin/node")
.internal_node("/node-24.4.1/bin/node")
.version("24.4.1")
.env_append("NODE_OPTIONS", "--import /preload.mjs")
.resolve();
assert_eq!(ctx.bin_dir.as_deref(), Some(abs("/shim").as_path()));
assert!(ctx.bin_dir_precedes_project_bins);
assert_eq!(
ctx.node_program.as_deref(),
Some(abs("/shim/node").as_path())
);
assert_eq!(
ctx.node_execpath.as_deref(),
Some(abs("/node-24.4.1/bin/node").as_path())
);
assert_eq!(
ctx.internal_node.as_deref(),
Some(abs("/node-24.4.1/bin/node").as_path())
);
assert_eq!(ctx.version.as_deref(), Some("24.4.1"));
assert_eq!(ctx.env.len(), 1);
assert_eq!(ctx.env[0].merge, EnvMerge::Append);
}
#[test]
fn wrapper_can_leave_path_unchanged() {
let ctx = EmbedderRuntime::wrapper("/shim/node")
.real_node("/real/node")
.without_path()
.resolve();
assert_eq!(ctx.bin_dir, None);
assert_eq!(
ctx.node_program.as_deref(),
Some(abs("/shim/node").as_path())
);
assert_eq!(
ctx.node_execpath.as_deref(),
Some(abs("/real/node").as_path())
);
assert!(ctx.bin_dir_precedes_project_bins);
}
#[test]
fn last_path_builder_call_wins() {
let explicit = EmbedderRuntime::wrapper("/shim/node")
.without_path()
.path_dir("/custom/bin")
.resolve();
assert_eq!(
explicit.bin_dir.as_deref(),
Some(abs("/custom/bin").as_path())
);
let unchanged = EmbedderRuntime::wrapper("/shim/node")
.path_dir("/custom/bin")
.without_path()
.resolve();
assert_eq!(unchanged.bin_dir, None);
}
#[test]
fn selector_without_path_keeps_selected_node() {
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
let ctx = EmbedderRuntime::selector("/opt/node/bin")
.without_path()
.resolve();
assert_eq!(ctx.bin_dir, None);
assert_eq!(
ctx.node_program.as_deref(),
Some(abs("/opt/node/bin").join(node_exe).as_path())
);
assert_eq!(ctx.node_execpath, ctx.node_program);
}
#[tokio::test]
async fn internal_node_defaults_to_node_program() {
scope(async {
let rt = EmbedderRuntime::wrapper("/shim/node");
seed_install_embedder_runtime(Some(&rt));
assert_eq!(internal_node_program(), abs("/shim/node"));
})
.await;
scope(async {
let rt = EmbedderRuntime::wrapper("/shim/node").internal_node("/real/node");
seed_install_embedder_runtime(Some(&rt));
assert_eq!(internal_node_program(), abs("/real/node"));
assert_eq!(node_program(), abs("/shim/node"));
})
.await;
}
#[tokio::test]
async fn with_embedder_runtime_scopes_a_per_call_runtime() {
let seen = with_embedder_runtime(Some(EmbedderRuntime::wrapper("/shim-a/node")), async {
node_program()
})
.await;
assert_eq!(seen, abs("/shim-a/node"));
scope(async {
assert!(current().is_none());
})
.await;
}
#[tokio::test]
async fn none_arm_still_seeds_from_the_ambient_embedder_runtime() {
with_embedder_runtime(Some(EmbedderRuntime::wrapper("/shim-c/node")), async {
with_embedder_runtime(None, async {
assert_eq!(
current().map(|c| c.node_program.clone()),
Some(Some(abs("/shim-c/node")))
);
})
.await;
})
.await;
}
#[tokio::test]
async fn per_call_runtime_reaches_a_nested_install_scope() {
with_embedder_runtime(Some(EmbedderRuntime::wrapper("/shim-b/node")), async {
scope(async {
assert!(current().is_none(), "nested scope starts empty");
seed_install_embedder_runtime(None);
assert_eq!(node_program(), abs("/shim-b/node"));
})
.await;
})
.await;
}
#[tokio::test]
async fn seed_install_runtime_drives_node_program_and_path() {
scope(async {
assert!(current().is_none(), "slot starts empty");
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
let bin = abs("/opt/mise/node/bin");
let rt = EmbedderRuntime::selector("/opt/mise/node/bin");
seed_install_embedder_runtime(Some(&rt));
let ctx = current().expect("slot seeded");
assert_eq!(ctx.source, RuntimeSource::Embedder);
assert_eq!(ctx.bin_dir.as_deref(), Some(bin.as_path()));
assert_eq!(node_program(), bin.join(node_exe));
assert_eq!(path_entries(), vec![bin.clone()]);
let other = EmbedderRuntime::selector("/other");
seed_install_embedder_runtime(Some(&other));
assert_eq!(node_program(), bin.join(node_exe));
})
.await;
}
#[tokio::test]
async fn wrapper_path_leads_while_selector_follows_project_bins() {
let project_bin = abs("/project/node_modules/.bin");
let wrapper_entries =
with_embedder_runtime(Some(EmbedderRuntime::wrapper("/shim/node")), async {
path_entries_with_project_bins(vec![project_bin.clone()])
})
.await;
assert_eq!(wrapper_entries, vec![abs("/shim"), project_bin.clone()]);
let selector_entries =
with_embedder_runtime(Some(EmbedderRuntime::selector("/opt/node/bin")), async {
path_entries_with_project_bins(vec![project_bin.clone()])
})
.await;
assert_eq!(selector_entries, vec![project_bin, abs("/opt/node/bin")]);
}
#[test]
fn fold_env_composes_appends_and_honors_replace() {
fn os(s: &str) -> std::ffi::OsString {
std::ffi::OsString::from(s)
}
fn entry(k: &str, v: &str, merge: EnvMerge) -> EmbedderEnv {
EmbedderEnv {
key: os(k),
val: os(v),
merge,
}
}
let entries = vec![
entry("NODE_OPTIONS", "--a", EnvMerge::Append),
entry("NODE_OPTIONS", "--b", EnvMerge::Append),
entry("FRESH", "x", EnvMerge::Append),
];
let out = fold_env(&entries, |k| {
(k == std::ffi::OsStr::new("NODE_OPTIONS")).then(|| os("--base"))
});
assert_eq!(out[0], (os("NODE_OPTIONS"), os("--base --a --b")));
assert_eq!(out[1], (os("FRESH"), os("x")));
let entries = vec![
entry("K", "one", EnvMerge::Replace),
entry("K", "two", EnvMerge::Append),
];
let out = fold_env(&entries, |_| Some(os("IGNORED")));
assert_eq!(out, vec![(os("K"), os("one two"))]);
}
#[tokio::test]
async fn merge_node_options_appends_embedder_preload() {
scope(async {
let rt = EmbedderRuntime::wrapper("/shim/node")
.env_append("NODE_OPTIONS", "--import /preload.mjs");
seed_install_embedder_runtime(Some(&rt));
assert_eq!(
merge_node_options(Some("--enable-source-maps".into())).as_deref(),
Some("--enable-source-maps --import /preload.mjs")
);
assert_eq!(
merge_node_options(None).as_deref(),
Some("--import /preload.mjs")
);
})
.await;
}
#[tokio::test]
async fn seed_install_runtime_is_a_noop_outside_scope() {
let rt = EmbedderRuntime::selector("/opt/mise/node/bin");
seed_install_embedder_runtime(Some(&rt));
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));
let first_barrier = Arc::clone(&barrier);
let second_barrier = Arc::clone(&barrier);
let first = scope(async move {
INSTALL_RUNTIME.with(|runtime| runtime.set(Arc::new(test_context("first"))).unwrap());
first_barrier.wait().await;
tokio::spawn(scope_current(async {
current().and_then(|runtime| runtime.requested.clone())
}))
.await
.unwrap()
});
let second = scope(async move {
INSTALL_RUNTIME.with(|runtime| runtime.set(Arc::new(test_context("second"))).unwrap());
second_barrier.wait().await;
tokio::spawn(scope_current(async {
current().and_then(|runtime| runtime.requested.clone())
}))
.await
.unwrap()
});
let (first, second) = tokio::join!(first, second);
assert_eq!(first.as_deref(), Some("first"));
assert_eq!(second.as_deref(), Some("second"));
}
#[test]
fn lockfile_pin_round_trip_shapes() {
let pin = aube_runtime::PinnedNode {
version: "24.4.1".parse().unwrap(),
variants: vec![aube_runtime::PinnedVariant {
os: "darwin".into(),
cpu: "arm64".into(),
libc: None,
archive: "tarball".into(),
url: "https://nodejs.org/download/release/v24.4.1/node-v24.4.1-darwin-arm64.tar.gz"
.into(),
integrity_sri: "sha256-AAAA".into(),
bin: [("node".to_string(), "bin/node".to_string())].into(),
prefix: None,
}],
};
let lf = lockfile_pin_from(&pin, "^24.4.0");
assert_eq!(lf.specifier, "^24.4.0");
assert_eq!(lf.version, "24.4.1");
assert!(lf.dev);
let back = pinned_from_lockfile(&lf).unwrap();
assert_eq!(back, pin);
}
}