Embedding in Rust
Use the aube::embed facade to install dependencies and run commands inside a Rust host. The host owns its runtime, output handling, and cancellation.
Add the dependency
Add aube without its binary-only default features:
[dependencies]
aube = { version = "2", default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }The embedding application owns its Tokio runtime and global allocator. aube does not install an allocator when used as a library.
Initialize the host
Define a process-lifetime host profile and register it before starting any aube operation:
use aube::embed::{self, Host};
static HOST: Host = Host {
name: "mytool",
display_name: "My Tool",
vendor: None,
version: env!("CARGO_PKG_VERSION"),
user_agent: concat!("mytool/", env!("CARGO_PKG_VERSION")),
self_names: &["mytool"],
compatible_names: &["pnpm"],
lockfile_basename: "mytool-lock.yaml",
workspace_yaml: None,
manifest_namespace: "mytool",
env_prefix: None,
config_env_prefix: None,
cache_namespace: "mytool",
data_namespace: "mytool",
canonical_lockfile_always_wins: false,
runtime_switching: false,
self_engines_check: false,
self_update_enabled: false,
};
embed::initialize(
&HOST,
vec![("nodeLinker".to_owned(), "hoisted".to_owned())],
);Initialization is process-global and first-write-wins. Setting defaults have the lowest precedence, so users can still override them through normal aube configuration sources. Host fields, by contrast, are decisions of the embedding application and are not user-configurable.
Forward aube's private argv
Lifecycle scripts reach back for the tools that ran them, and both paths land on std::env::current_exe() — your binary, not aube's. Aube writes shims that re-enter itself through it, marked with a private first argument. Intercept those two before your own argument parser sees them:
fn main() -> std::process::ExitCode {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(String::as_str) {
// `node-gyp` shims: bootstrap aube's cached node-gyp and print its path.
Some("__node-gyp-bootstrap") => {
let dir = args.get(2).map_or_else(|| ".".into(), std::path::PathBuf::from);
match runtime.block_on(embed::bootstrap_node_gyp(&dir)) {
Ok(path) => println!("{}", path.display()),
Err(err) => { eprintln!("{err:?}"); return 1.into(); }
}
return 0.into();
}
// `npm_execpath`: a package script re-invoking its package manager.
Some(embed::CLI_TRAMPOLINE_ARG) => return (aube::cli_main(&HOST) as u8).into(),
_ => {}
}
// ... your own CLI
}cli_main strips the __aube-cli token itself, so forward the argv unchanged.
Dispatch both. The shims fall back to a node-gyp on PATH only when AUBE_NODE_GYP_EXE is absent, and aube always sets it — so a host that exports the shims without answering __node-gyp-bootstrap turns a native-addon build into a hard failure rather than a fallback. Skipping __aube-cli is milder but the same shape: a package that runs ${npm_execpath} run verify lands in your CLI, which is the misrouting the token exists to prevent.
Install a project
Always select the project directory explicitly. InstallControl::silent() is appropriate when the host does not want aube to write directly to the terminal.
use aube::embed::{self, InstallControl, InstallOptions};
let mut options = InstallOptions::new(&project_dir);
options.ignore_scripts = true;
options.control = InstallControl::silent();
embed::install(options).await?;Lifecycle scripts remain subject to aube's build policy. Set ignore_scripts when the host requires scripts to be disabled regardless of project policy.
Add packages
add holds the workspace project lock across both the package.json mutation and installation:
use aube::embed::{self, AddToProjectOptions, InstallControl};
let packages = vec!["typescript@latest".to_owned()];
embed::add(
&project_dir,
&packages,
AddToProjectOptions {
save_dev: true,
save_exact: true,
ignore_scripts: true,
control: InstallControl::silent(),
..Default::default()
},
).await?;Set save_dev, save_optional, or save_peer to select the manifest section. Combining save_peer with save_dev writes both sections, matching the CLI. Both install option types also expose osv_transitive_check when a host wants to force a live transitive OSV check for an otherwise unchanged lockfile. Offline mode always disables that live request. Set dangerously_allow_all_builds to bypass the lifecycle build allowlist for the invocation; ignore_scripts still disables scripts entirely.
Node runtime
By default aube resolves its own Node for lifecycle scripts. A host that manages Node itself describes how Node should be invoked with an EmbedderRuntime.
A host that merely selects a toolchain (a version manager) uses selector — the bin directory goes on PATH after project-local .bin directories, and its node is both NODE and npm_node_execpath:
use aube::embed::EmbedderRuntime;
let runtime = EmbedderRuntime::selector("/opt/mytool/node-24.4.1/bin");A host that wraps Node — an instrumenting runtime, a transpiling loader, a sandbox — uses wrapper. The shim is NODE and leads PATH so a script's node / $NODE stays wrapped even when a dependency provides a node bin, while real_node is exported as npm_node_execpath so node-gyp builds against the real binary. Use env_append to add a NODE_OPTIONS preload without dropping the user's value (env_set overwrites):
let runtime = EmbedderRuntime::wrapper("/opt/mytool/shim/node")
.real_node("/opt/mytool/node-24.4.1/bin/node")
.internal_node("/opt/mytool/node-24.4.1/bin/node") // aube's own spawns skip the wrapper
.version("24.4.1") // supplied, not probed
.env_append("NODE_OPTIONS", "--import /opt/mytool/preload.mjs");Use .without_path() when the host should set NODE and use the wrapper for direct aube spawns but intentionally leave bare node resolution to the inherited PATH. Calling .path_dir(...) later restores an explicit PATH entry; the last path builder call wins.
internal_node splits off the node aube's internal machinery spawns — pnpmfile hooks, the security scanner, version probes — so those hot paths run on the real binary while user-facing spawns (scripts, NODE, aube node) stay wrapped. It defaults to the wrapper program.
Apply it per call, or register it once so every spawn path — install lifecycle scripts, dlx, exec, run, node — is covered:
// Per call.
let mut options = InstallOptions::new(&project_dir);
options.runtime = Some(runtime.clone());
embed::install(options).await?;
// Or once at startup (first-write-wins; a per-call `runtime` still wins for
// that install).
embed::set_embedder_runtime(runtime);Every field is optional and unset reproduces standalone aube's behavior. The runtime is honored regardless of the host's runtime_switching flag — an explicit invocation is an override, not a resolution.
Run scripts, binaries, and Node
With a runtime registered, a host can drive the run-class commands in process. Each resolves its project from the directory passed in — never the process working directory — and returns the child's exit code:
embed::run(&project_dir, "build", vec![], None).await?; // package script
embed::exec(&project_dir, "eslint", vec![".".into()], None).await?; // node_modules/.bin
embed::dlx(&project_dir, vec!["cowsay".into(), "hi".into()], vec![], None).await?;
embed::node(&project_dir, vec!["--eval".into(), "console.log(1)".into()], None).await?;The trailing parameter is an optional per-call EmbedderRuntime. None uses the process-wide registration; pass Some(runtime) when the runtime varies per invocation (e.g. a host that provisions a fresh shim directory per command), since set_embedder_runtime is set-once. Unlike the CLI, embed::node supervises a child rather than replacing the host process.
Progress and cancellation
Implement InstallReporter and pass it through InstallControl::events. report must be non-blocking; enqueue the event when crossing into another runtime or thread.
use aube::embed::{InstallControl, InstallEvent, InstallReporter};
use std::sync::Arc;
struct Reporter;
impl InstallReporter for Reporter {
fn report(&self, event: InstallEvent) {
// Enqueue the event for the host.
let _ = event;
}
}
let control = InstallControl::events(Arc::new(Reporter));
let cancellation_handle = control.clone();
options.control = control;
// Call this from the host's abort handler.
cancellation_handle.cancel();Each invocation has independent event and cancellation state. Installs for unrelated projects can run concurrently; operations within the same workspace wait on its project lock. Cancelling add restores the project manifest and lockfile snapshots before returning the cancellation error. Other installation failures preserve the manifest change so the host can retry install without repeating the add operation.
Errors
Embedding operations return miette::Result. Extract the stable code without parsing the rendered message:
if let Err(error) = embed::install(options).await {
let code = embed::error_code(&error);
let message = error.to_string();
let diagnostic = format!("{error:?}");
}Code identifiers and meanings are stable once published. Human-readable messages and rendered diagnostics may evolve.
Migrating to aube 2
aube_resolver::ResolutionMode includes LowestDirect and is #[non_exhaustive]. Downstream matches need a wildcard arm so future resolution modes can be added without another source-breaking enum change.
Binding installed commands to Node
An embedding tool manager can make installed Node CLIs independent of the caller's selected Node with EmbedderRuntime::bind_bins_to:
use aube::embed::{EmbedderInstallOverrides, EmbedderRuntime, InstallOptions};
let mut options = InstallOptions::new("/tools/my-cli");
// Use the same runtime for approved install scripts and native-addon builds.
options.runtime = Some(
EmbedderRuntime::selector("/runtimes/node/25/bin")
.bind_bins_to("/runtimes/node/25/bin/node"),
);
let overrides = EmbedderInstallOverrides {
use_global_virtual_store: Some(false),
..Default::default()
};
aube::embed::install_with_overrides(options, overrides).await?;The same runtime builder works with AddToProjectOptions::runtime. Node-backed commands invoke that executable directly; their inherited PATH is unchanged. Commands they spawn through PATH can therefore use the project's Node, while internal workers using process.execPath use the application's Node. Native executables and scripts using other interpreters keep their existing launch behavior.
The executable path must be absolute UTF-8 without NUL or newlines. Aube keeps it as written rather than resolving symlinks: /runtimes/node/25/bin/node can follow a host-managed major-version symlink. A missing executable fails instead of silently falling back to Node on PATH. Changing or removing the binding regenerates launchers on the next install, including frozen installs.
Runtime-bound installs disable the global virtual store, even if enabled in settings or overrides, so their launchers cannot retarget another project's commands. Package contents can still share the content-addressable store.
The host owns runtime selection, installation, compatibility, retention, and lockfile policy. Use an exact runtime path for reproducible pins; moving a symlink can require rebuilding native addons. This API does not enable automatic runtime binding for standalone aube installs. It provides the launcher support for a host such as mise to do so. Simple Node shebang flags are preserved; quoted shebang arguments and environment assignments currently produce an error when binding.