use std::future::Future;
use miette::{IntoDiagnostic, WrapErr, miette};
use super::install;
#[derive(Clone, Copy, Debug)]
pub(crate) struct LazyInstallRuntime {
worker_threads: usize,
max_blocking_threads: usize,
}
impl LazyInstallRuntime {
pub(crate) fn new(worker_threads: usize, max_blocking_threads: usize) -> Self {
Self {
worker_threads,
max_blocking_threads,
}
}
}
tokio::task_local! {
static LAZY_INSTALL_RUNTIME: LazyInstallRuntime;
}
pub(crate) async fn with_lazy_install_runtime<F: Future>(
config: LazyInstallRuntime,
future: F,
) -> F::Output {
LAZY_INSTALL_RUNTIME.scope(config, future).await
}
async fn run_with_install_runtime<T, F, Fut>(operation: F) -> miette::Result<T>
where
T: Send + 'static,
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = miette::Result<T>> + 'static,
{
let Ok(config) = LAZY_INSTALL_RUNTIME.try_with(|config| *config) else {
return operation().await;
};
tokio::task::spawn_blocking(move || {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(config.worker_threads)
.max_blocking_threads(config.max_blocking_threads)
.enable_all()
.build()
.into_diagnostic()
.wrap_err("failed to build lazy install runtime")?;
runtime.block_on(operation())
})
.await
.into_diagnostic()
.wrap_err("lazy install runtime task failed")?
}
pub(crate) async fn ensure_installed(no_install: bool) -> miette::Result<()> {
ensure_installed_in(no_install, None).await
}
pub(crate) async fn ensure_installed_in(
no_install: bool,
base_dir: Option<&std::path::Path>,
) -> miette::Result<()> {
if no_install {
return Ok(());
}
if super::skip_auto_install_on_package_manager_mismatch() {
return Ok(());
}
if std::env::var_os("npm_lifecycle_event").is_some() {
return Ok(());
}
let initial_cwd = match base_dir {
Some(dir) => dir.to_path_buf(),
None => crate::dirs::cwd()?,
};
let cwd = crate::dirs::find_workspace_root(&initial_cwd)
.or_else(|| crate::dirs::find_project_root(&initial_cwd))
.unwrap_or(initial_cwd);
let (skip_auto_install, optimistic_repeat, verify_mode) =
super::with_settings_ctx(&cwd, |ctx| {
(
aube_settings::resolved::aube_no_auto_install(ctx),
aube_settings::resolved::optimistic_repeat_install(ctx),
parse_verify_deps_before_run(&aube_settings::resolved::verify_deps_before_run(ctx)),
)
});
if skip_auto_install {
return Ok(());
}
let g = super::global_frozen_override();
let needs = if optimistic_repeat {
crate::state::check_needs_install(&cwd)
} else {
Some("optimisticRepeatInstall=false".to_string())
};
let Some(reason) = needs.or_else(|| g.map(|o| format!("global {} flag", o.cli_flag()))) else {
return Ok(());
};
match verify_mode {
VerifyDepsBeforeRun::Skip => return Ok(()),
VerifyDepsBeforeRun::Warn => {
eprintln!("Dependencies need install before run: {reason}");
return Ok(());
}
VerifyDepsBeforeRun::Error => {
return Err(miette!(
"dependencies need install before run: {reason}\nRun `{}`, or set verifyDepsBeforeRun=install to let {} do it automatically.",
aube_util::cmd("install"),
aube_util::prog()
));
}
VerifyDepsBeforeRun::Install => {}
}
eprintln!("Auto-installing: {reason}");
let mode = super::chained_frozen_mode(install::FrozenMode::Prefer);
let mut opts = install::InstallOptions::with_mode(mode);
opts.strict_no_lockfile = matches!(g, Some(install::FrozenOverride::Frozen));
opts.project_dir = Some(cwd);
run_with_install_runtime(|| install::run(opts)).await?;
Ok(())
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum VerifyDepsBeforeRun {
Install,
Warn,
Error,
Skip,
}
fn parse_verify_deps_before_run(raw: &str) -> VerifyDepsBeforeRun {
match raw.trim().to_ascii_lowercase().as_str() {
"false" | "0" => VerifyDepsBeforeRun::Skip,
"warn" => VerifyDepsBeforeRun::Warn,
"error" => VerifyDepsBeforeRun::Error,
"prompt" | "install" => VerifyDepsBeforeRun::Install,
_ => VerifyDepsBeforeRun::Install,
}
}
#[cfg(test)]
mod verify_deps_tests {
use super::{VerifyDepsBeforeRun, parse_verify_deps_before_run};
#[test]
fn verify_deps_parser_preserves_existing_aliases_and_fallback() {
assert_eq!(
parse_verify_deps_before_run(" false "),
VerifyDepsBeforeRun::Skip
);
assert_eq!(
parse_verify_deps_before_run("WARN"),
VerifyDepsBeforeRun::Warn
);
assert_eq!(
parse_verify_deps_before_run("error"),
VerifyDepsBeforeRun::Error
);
assert_eq!(
parse_verify_deps_before_run("prompt"),
VerifyDepsBeforeRun::Install
);
assert_eq!(
parse_verify_deps_before_run("unknown"),
VerifyDepsBeforeRun::Install
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lazy_install_switches_from_current_thread_to_multi_thread() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread test runtime should build");
let ambient = runtime
.block_on(run_with_install_runtime(|| async {
Ok(tokio::runtime::Handle::current().runtime_flavor())
}))
.expect("ambient operation should run");
assert_eq!(ambient, tokio::runtime::RuntimeFlavor::CurrentThread);
let lazy = runtime
.block_on(with_lazy_install_runtime(
LazyInstallRuntime::new(1, 2),
run_with_install_runtime(|| async {
Ok(tokio::runtime::Handle::current().runtime_flavor())
}),
))
.expect("lazy operation should run");
assert_eq!(lazy, tokio::runtime::RuntimeFlavor::MultiThread);
}
}