use miette::{IntoDiagnostic, WrapErr, miette};
use std::path::{Path, PathBuf};
const BUCKET: &str = "v12";
const SPEC: &str = "^12.0.0";
#[cfg(windows)]
const BINARY_NAMES: &[&str] = &["node-gyp.cmd", "node-gyp.exe", "node-gyp"];
#[cfg(not(windows))]
const BINARY_NAMES: &[&str] = &["node-gyp"];
fn node_gyp_on_path() -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
for dir in std::env::split_paths(&path) {
if node_gyp_bin_exists(&dir) {
return true;
}
}
false
}
fn node_gyp_bin_exists(bin_dir: &Path) -> bool {
BINARY_NAMES.iter().any(|name| bin_dir.join(name).exists())
}
fn tool_root() -> miette::Result<PathBuf> {
let cache = aube_store::dirs::cache_dir()
.ok_or_else(|| miette!("could not resolve cache dir for node-gyp bootstrap"))?;
Ok(cache.join("tools").join("node-gyp"))
}
pub async fn ensure(project_dir: &Path) -> miette::Result<Option<PathBuf>> {
if node_gyp_on_path() {
return Ok(None);
}
let root = tool_root()?;
let tool_dir = root.join(BUCKET);
let bin_dir = tool_dir.join("node_modules").join(".bin");
if node_gyp_bin_exists(&bin_dir) {
return Ok(Some(bin_dir));
}
let lock_key = root.join(format!("{BUCKET}.lock"));
let tool_dir_blocking = tool_dir.clone();
let bin_dir_blocking = bin_dir.clone();
let project_npmrc = project_dir.join(".npmrc");
tokio::task::spawn_blocking(move || {
bootstrap_blocking(
&lock_key,
&tool_dir_blocking,
&bin_dir_blocking,
&project_npmrc,
)
})
.await
.into_diagnostic()
.wrap_err("node-gyp bootstrap task panicked")??;
Ok(Some(bin_dir))
}
fn bootstrap_blocking(
lock_key: &Path,
tool_dir: &Path,
bin_dir: &Path,
project_npmrc: &Path,
) -> miette::Result<()> {
std::fs::create_dir_all(tool_dir).into_diagnostic()?;
let _lock = xx::fslock::FSLock::new(lock_key)
.with_callback(|_| {
tracing::info!("waiting for another aube process to finish bootstrapping node-gyp");
})
.lock()
.map_err(|e| miette!("failed to acquire node-gyp bootstrap lock: {e}"))?;
if node_gyp_bin_exists(bin_dir) {
return Ok(());
}
let manifest = format!(
r#"{{"name":"aube-tool-node-gyp","private":true,"dependencies":{{"node-gyp":"{SPEC}"}}}}"#
);
aube_util::fs_atomic::atomic_write(&tool_dir.join("package.json"), manifest.as_bytes())
.into_diagnostic()?;
aube_util::fs_atomic::atomic_write(&tool_dir.join("aube-workspace.yaml"), b"")
.into_diagnostic()?;
let tool_npmrc = tool_dir.join(".npmrc");
if project_npmrc.exists() {
std::fs::copy(project_npmrc, &tool_npmrc)
.into_diagnostic()
.wrap_err_with(|| {
format!(
"failed to propagate {} to node-gyp bootstrap dir",
project_npmrc.display()
)
})?;
} else if tool_npmrc.exists() {
let _ = std::fs::remove_file(&tool_npmrc);
}
let exe = std::env::current_exe()
.into_diagnostic()
.wrap_err("could not locate current aube executable for node-gyp bootstrap")?;
tracing::info!("bootstrapping node-gyp {SPEC} into {}", tool_dir.display());
let status = std::process::Command::new(&exe)
.args(["install", "--ignore-scripts", "--silent"])
.current_dir(tool_dir)
.status()
.into_diagnostic()
.wrap_err("failed to spawn recursive aube install for node-gyp bootstrap")?;
if !status.success() {
return Err(miette!(
"recursive aube install failed while bootstrapping node-gyp (exit {:?}) — \
pre-populate {} or run `aube install` once while online",
status.code(),
tool_dir.display()
));
}
if !node_gyp_bin_exists(bin_dir) {
return Err(miette!(
"node-gyp bootstrap completed but no shim found under {}",
bin_dir.display()
));
}
Ok(())
}