use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{OnceLock, RwLock};
use aube_registry::client::RegistryClient;
use aube_registry::config::NpmConfig;
use miette::{Context, IntoDiagnostic, miette};
use super::{CatalogMap, config, install};
static GLOBAL_FROZEN: OnceLock<Option<install::FrozenOverride>> = OnceLock::new();
static GLOBAL_VIRTUAL_STORE: OnceLock<install::GlobalVirtualStoreFlags> = OnceLock::new();
static SKIP_AUTO_INSTALL_ON_PM_MISMATCH: AtomicBool = AtomicBool::new(false);
static REGISTRY_OVERRIDE: RwLock<Option<String>> = RwLock::new(None);
static FETCH_CLI_OVERRIDES: OnceLock<Vec<(String, String)>> = OnceLock::new();
#[derive(Copy, Clone, Debug, Default)]
pub(crate) struct GlobalOutputFlags {
pub ndjson: bool,
pub silent: bool,
}
static GLOBAL_OUTPUT: OnceLock<GlobalOutputFlags> = OnceLock::new();
pub(crate) fn set_registry_override(url: Option<String>) {
*REGISTRY_OVERRIDE.write().expect("registry lock poisoned") =
url.map(|u| aube_registry::config::normalize_registry_url_pub(&u));
}
pub(crate) fn set_fetch_cli_overrides(flags: Vec<(String, String)>) {
let _ = FETCH_CLI_OVERRIDES.set(flags);
}
pub(crate) fn fetch_cli_overrides() -> &'static [(String, String)] {
FETCH_CLI_OVERRIDES.get().map(Vec::as_slice).unwrap_or(&[])
}
pub(crate) fn set_skip_auto_install_on_package_manager_mismatch(skip: bool) {
SKIP_AUTO_INSTALL_ON_PM_MISMATCH.store(skip, Ordering::Relaxed);
}
pub(crate) fn skip_auto_install_on_package_manager_mismatch() -> bool {
SKIP_AUTO_INSTALL_ON_PM_MISMATCH.load(Ordering::Relaxed)
}
pub(crate) fn registry_override() -> Option<String> {
REGISTRY_OVERRIDE
.read()
.expect("registry lock poisoned")
.clone()
}
pub(crate) fn load_npm_config(dir: &std::path::Path) -> NpmConfig {
let mut config = NpmConfig::load(dir);
if let Some(url) = registry_override() {
config.registry = url;
}
config
}
pub(crate) fn set_global_frozen_override(flags: Option<install::FrozenOverride>) {
let _ = GLOBAL_FROZEN.set(flags);
}
pub(crate) fn set_global_virtual_store_flags(flags: install::GlobalVirtualStoreFlags) {
let _ = GLOBAL_VIRTUAL_STORE.set(flags);
}
pub(crate) fn set_global_output_flags(flags: GlobalOutputFlags) {
let _ = GLOBAL_OUTPUT.set(flags);
}
pub(crate) fn global_frozen_override() -> Option<install::FrozenOverride> {
GLOBAL_FROZEN.get().copied().unwrap_or_default()
}
pub(crate) fn global_virtual_store_flags() -> install::GlobalVirtualStoreFlags {
GLOBAL_VIRTUAL_STORE.get().copied().unwrap_or_default()
}
pub(crate) fn global_output_flags() -> GlobalOutputFlags {
GLOBAL_OUTPUT.get().copied().unwrap_or_default()
}
pub(crate) struct FileSources {
pub user_npmrc: Vec<(String, String)>,
pub project_npmrc: Vec<(String, String)>,
pub user_aube_config: Vec<(String, String)>,
pub project_aube_config: Vec<(String, String)>,
}
impl FileSources {
pub(crate) fn load(cwd: &Path) -> Self {
let npmrc = aube_registry::config::load_npmrc_entries_split(cwd);
Self {
user_npmrc: npmrc.user,
project_npmrc: npmrc.project,
user_aube_config: config::load_user_aube_config_entries(),
project_aube_config: config::load_project_aube_config_entries(cwd),
}
}
pub(crate) fn ctx<'a>(
&'a self,
workspace_yaml: &'a std::collections::BTreeMap<String, yaml_serde::Value>,
env: &'a [(String, String)],
cli: &'a [(String, String)],
) -> aube_settings::ResolveCtx<'a> {
aube_settings::ResolveCtx {
project_aube_config: &self.project_aube_config,
project_npmrc: &self.project_npmrc,
user_aube_config: &self.user_aube_config,
user_npmrc: &self.user_npmrc,
workspace_yaml,
env,
cli,
}
}
}
pub(crate) fn chained_frozen_mode(default: install::FrozenMode) -> install::FrozenMode {
match global_frozen_override() {
Some(ovr) => install::FrozenMode::from_override(Some(ovr), None),
None => default,
}
}
pub(crate) fn ensure_registry_auth(
client: &RegistryClient,
registry_url: &str,
) -> miette::Result<()> {
if client.has_resolved_auth_for(registry_url) {
Ok(())
} else {
Err(miette!(
"no auth token for {registry_url}. Run `aube login --registry {registry_url}` first."
))
}
}
pub(crate) fn open_store(cwd: &std::path::Path) -> miette::Result<aube_store::Store> {
if let Some(custom) = resolved_store_dir(cwd) {
aube_store::Store::with_root(custom.join("v1").join("files"))
.into_diagnostic()
.wrap_err("failed to open store")
} else {
aube_store::Store::default_location()
.into_diagnostic()
.wrap_err("failed to open store")
}
}
pub(crate) fn resolved_store_dir(cwd: &std::path::Path) -> Option<std::path::PathBuf> {
with_settings_ctx(cwd, |ctx| {
let raw = aube_settings::resolved::store_dir(ctx)?;
expand_setting_path(&raw, cwd)
})
}
pub(crate) fn expand_setting_path(raw: &str, cwd: &std::path::Path) -> Option<std::path::PathBuf> {
let expanded = if let Some(rest) = raw.strip_prefix("~/") {
std::path::PathBuf::from(home_dir_os()?).join(rest)
} else if raw == "~" {
std::path::PathBuf::from(home_dir_os()?)
} else {
std::path::PathBuf::from(raw)
};
Some(if expanded.is_absolute() {
expanded
} else {
cwd.join(expanded)
})
}
fn home_dir_os() -> Option<std::ffi::OsString> {
aube_util::env::home_dir().map(|p| p.into_os_string())
}
pub(crate) fn with_settings_ctx<T>(
cwd: &std::path::Path,
f: impl FnOnce(&aube_settings::ResolveCtx<'_>) -> T,
) -> T {
let files = FileSources::load(cwd);
let raw_workspace = aube_manifest::workspace::load_raw(cwd).unwrap_or_default();
let env = aube_settings::values::process_env();
let ctx = files.ctx(&raw_workspace, env, &[]);
f(&ctx)
}
pub(crate) fn make_client(cwd: &std::path::Path) -> aube_registry::client::RegistryClient {
let config = load_npm_config(cwd);
tracing::debug!("registry: {}", config.registry);
for (scope, url) in &config.scoped_registries {
tracing::debug!("scoped registry: {scope} -> {url}");
}
let policy = resolve_fetch_policy(cwd);
aube_registry::client::RegistryClient::from_config_with_policy(config, policy)
}
pub(crate) async fn run_pnpmfile_pre_resolution(
paths: &[std::path::PathBuf],
cwd: &std::path::Path,
existing: Option<&aube_lockfile::LockfileGraph>,
) -> miette::Result<()> {
if paths.is_empty() {
return Ok(());
}
let config = load_npm_config(cwd);
let mut registries = std::collections::BTreeMap::new();
registries.insert("default".to_string(), config.registry);
for (scope, url) in config.scoped_registries {
registries.insert(scope, url);
}
let store_dir = resolved_store_dir(cwd).or_else(|| {
aube_store::dirs::store_dir()
.and_then(|p| p.parent()?.parent().map(std::path::Path::to_path_buf))
});
let ctx = crate::pnpmfile::PreResolutionContext::from_existing(
cwd,
store_dir.as_deref(),
existing,
registries,
);
crate::pnpmfile::run_pre_resolution_chain(paths, cwd, &ctx)
.await
.wrap_err("pnpmfile preResolution hook failed")
}
pub(crate) fn build_resolver(
cwd: &std::path::Path,
manifest: &aube_manifest::PackageJson,
catalogs: CatalogMap,
) -> aube_resolver::Resolver {
let (ws_config, raw_workspace) = aube_manifest::workspace::load_both(cwd).unwrap_or_default();
let files = FileSources::load(cwd);
let env = aube_settings::values::process_env();
let ctx = files.ctx(&raw_workspace, env, &[]);
let target_lockfile_kind = Some(
aube_lockfile::detect_existing_lockfile_kind(cwd)
.unwrap_or(aube_lockfile::LockfileKind::Aube),
);
install::configure_resolver(
aube_resolver::Resolver::new(std::sync::Arc::new(make_client(cwd))),
cwd,
manifest,
install::ResolverConfigInputs {
settings_ctx: &ctx,
workspace_config: &ws_config,
workspace_catalogs: &catalogs,
minimum_release_age_override: None,
target_lockfile_kind,
cache_full_packuments: false,
ignore_scripts: false,
},
None,
)
}
pub(crate) fn resolve_fetch_policy(cwd: &std::path::Path) -> aube_registry::config::FetchPolicy {
let files = FileSources::load(cwd);
let workspace_yaml = aube_manifest::workspace::load_both(cwd)
.map(|(_, raw)| raw)
.unwrap_or_default();
let env = aube_settings::values::process_env();
let ctx = files.ctx(&workspace_yaml, env, fetch_cli_overrides());
aube_registry::config::FetchPolicy::from_ctx(&ctx)
}
pub(crate) fn resolved_cache_dir(cwd: &std::path::Path) -> std::path::PathBuf {
let platform_default =
|| aube_store::dirs::cache_dir().unwrap_or_else(|| std::env::temp_dir().join("aube"));
let npmrc = aube_registry::config::load_npmrc_entries(cwd);
let has_explicit = npmrc
.iter()
.any(|(k, _)| k == "cacheDir" || k == "cache-dir");
if !has_explicit {
return platform_default();
}
with_settings_ctx(cwd, |ctx| {
let raw = aube_settings::resolved::cache_dir(ctx);
expand_setting_path(&raw, cwd).unwrap_or_else(platform_default)
})
}
pub(crate) fn resolve_virtual_store_dir_max_length(ctx: &aube_settings::ResolveCtx<'_>) -> usize {
aube_settings::resolved::virtual_store_dir_max_length(ctx)
.map(|v| v as usize)
.unwrap_or(aube_lockfile::dep_path_filename::DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH)
}
pub(crate) fn resolve_virtual_store_dir_max_length_for_cwd(cwd: &std::path::Path) -> usize {
with_settings_ctx(cwd, resolve_virtual_store_dir_max_length)
}
pub(crate) fn resolve_modules_dir_name_for_cwd(cwd: &std::path::Path) -> String {
with_settings_ctx(cwd, aube_settings::resolved::modules_dir)
}
pub(crate) fn project_modules_dir(cwd: &std::path::Path) -> std::path::PathBuf {
cwd.join(resolve_modules_dir_name_for_cwd(cwd))
}
pub(crate) fn resolve_virtual_store_dir(
ctx: &aube_settings::ResolveCtx<'_>,
project_dir: &std::path::Path,
) -> std::path::PathBuf {
let default_from_modules_dir = || {
let modules_dir = aube_settings::resolved::modules_dir(ctx);
project_dir.join(modules_dir).join(".aube")
};
let has_explicit_npmrc = [
ctx.project_aube_config,
ctx.project_npmrc,
ctx.user_aube_config,
ctx.user_npmrc,
]
.iter()
.any(|entries| {
entries
.iter()
.any(|(k, _)| k == "virtualStoreDir" || k == "virtual-store-dir")
});
let has_explicit_yaml = ctx.workspace_yaml.contains_key("virtualStoreDir");
let has_explicit_env = ctx.env.iter().any(|(k, _)| {
k == "npm_config_virtual_store_dir"
|| k == "NPM_CONFIG_VIRTUAL_STORE_DIR"
|| k == "AUBE_VIRTUAL_STORE_DIR"
});
if !(has_explicit_npmrc || has_explicit_yaml || has_explicit_env) {
return default_from_modules_dir();
}
let raw = aube_settings::resolved::virtual_store_dir(ctx);
expand_setting_path(&raw, project_dir).unwrap_or_else(default_from_modules_dir)
}
pub(crate) fn resolve_virtual_store_dir_for_cwd(cwd: &std::path::Path) -> std::path::PathBuf {
with_settings_ctx(cwd, |ctx| resolve_virtual_store_dir(ctx, cwd))
}
pub(crate) fn packument_cache_dir() -> std::path::PathBuf {
let cwd = crate::dirs::cwd().unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());
resolved_cache_dir(&cwd).join("packuments-v1")
}
pub(crate) fn packument_full_cache_dir() -> std::path::PathBuf {
let cwd = crate::dirs::cwd().unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());
resolved_cache_dir(&cwd).join("packuments-full-v1")
}
#[cfg(test)]
mod resolve_virtual_store_dir_tests {
use super::resolve_virtual_store_dir;
use aube_settings::ResolveCtx;
use std::collections::BTreeMap;
use std::path::PathBuf;
fn ctx_with_env<'a>(
env: &'a [(String, String)],
ws: &'a BTreeMap<String, yaml_serde::Value>,
) -> ResolveCtx<'a> {
ResolveCtx {
project_aube_config: &[],
project_npmrc: &[],
user_aube_config: &[],
user_npmrc: &[],
workspace_yaml: ws,
env,
cli: &[],
}
}
#[test]
fn default_when_no_explicit_override() {
let env = vec![];
let ws = BTreeMap::new();
let ctx = ctx_with_env(&env, &ws);
let project = PathBuf::from("/proj");
assert_eq!(
resolve_virtual_store_dir(&ctx, &project),
PathBuf::from("/proj/node_modules/.aube"),
);
}
#[test]
fn aube_env_var_relocates_virtual_store() {
let env = vec![("AUBE_VIRTUAL_STORE_DIR".into(), ".aube".into())];
let ws = BTreeMap::new();
let ctx = ctx_with_env(&env, &ws);
let project = PathBuf::from("/proj");
assert_eq!(
resolve_virtual_store_dir(&ctx, &project),
PathBuf::from("/proj/.aube"),
);
}
#[test]
fn npm_config_env_var_relocates_virtual_store() {
let env = vec![("npm_config_virtual_store_dir".into(), ".vstore".into())];
let ws = BTreeMap::new();
let ctx = ctx_with_env(&env, &ws);
let project = PathBuf::from("/proj");
assert_eq!(
resolve_virtual_store_dir(&ctx, &project),
PathBuf::from("/proj/.vstore"),
);
}
}
#[cfg(test)]
mod package_manager_mismatch_tests {
use super::skip_auto_install_on_package_manager_mismatch;
#[test]
fn skip_auto_install_defaults_off() {
assert!(!skip_auto_install_on_package_manager_mismatch());
}
}