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
}
pub(crate) fn node_gyp_bin_exists(bin_dir: &Path) -> bool {
BINARY_NAMES.iter().any(|name| bin_dir.join(name).exists())
}
fn primary_binary_name() -> &'static str {
BINARY_NAMES[0]
}
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);
}
ensure_cached(project_dir).await.map(Some)
}
pub async fn ensure_cached(project_dir: &Path) -> miette::Result<PathBuf> {
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(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(bin_dir)
}
pub(crate) fn lazy_shim_bin_dir(project_bin_dir: &Path) -> miette::Result<Option<PathBuf>> {
if node_gyp_bin_exists(project_bin_dir) || node_gyp_on_path() {
return Ok(None);
}
let shim_dir = tool_root()?.join("lazy-bin");
std::fs::create_dir_all(&shim_dir).into_diagnostic()?;
write_lazy_shims(&shim_dir)?;
Ok(Some(shim_dir))
}
pub(crate) async fn print_bootstrapped_binary(project_dir: &Path) -> miette::Result<()> {
let bin_dir = ensure_cached(project_dir).await?;
println!("{}", bin_dir.join(primary_binary_name()).display());
Ok(())
}
fn write_lazy_shims(shim_dir: &Path) -> miette::Result<()> {
let sh = r#"#!/usr/bin/env sh
set -eu
real="$("$AUBE_NODE_GYP_EXE" __node-gyp-bootstrap "$AUBE_NODE_GYP_PROJECT_DIR")"
exec "$real" "$@"
"#;
let sh_path = shim_dir.join("node-gyp");
aube_util::fs_atomic::atomic_write(&sh_path, sh.as_bytes()).into_diagnostic()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&sh_path, std::fs::Permissions::from_mode(0o755))
.into_diagnostic()?;
}
#[cfg(windows)]
{
let cmd = r#"@echo off
for /f "usebackq delims=" %%i in (`"%AUBE_NODE_GYP_EXE%" __node-gyp-bootstrap "%AUBE_NODE_GYP_PROJECT_DIR%"`) do set "AUBE_REAL_NODE_GYP=%%i"
if not defined AUBE_REAL_NODE_GYP exit /b 1
"%AUBE_REAL_NODE_GYP%" %*
"#;
aube_util::fs_atomic::atomic_write(&shim_dir.join("node-gyp.cmd"), cmd.as_bytes())
.into_diagnostic()?;
}
Ok(())
}
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(())
}