use miette::{Context, IntoDiagnostic, miette};
use super::bin_linking::{dep_modules_dir_for, materialized_pkg_dir};
use super::node_gyp_bootstrap;
use super::side_effects_cache::{
SideEffectsCacheConfig, SideEffectsCacheEntry, SideEffectsCacheRestore,
};
pub(super) async fn run_root_lifecycle(
project_dir: &std::path::Path,
modules_dir_name: &str,
manifest: &aube_manifest::PackageJson,
hook: aube_scripts::LifecycleHook,
) -> miette::Result<()> {
if !manifest.scripts.contains_key(hook.script_name()) {
return Ok(());
}
tracing::debug!("Running {} script...", hook.script_name());
aube_scripts::run_root_hook(project_dir, modules_dir_name, manifest, hook)
.await
.map_err(|e| {
miette!("root {} script failed: {e}", hook.script_name())
})?;
Ok(())
}
pub(crate) fn build_policy_from_sources(
manifest: &aube_manifest::PackageJson,
workspace: &aube_manifest::WorkspaceConfig,
dangerously_allow_all_builds: bool,
) -> (
aube_scripts::BuildPolicy,
Vec<aube_scripts::BuildPolicyError>,
) {
let mut merged = manifest.pnpm_allow_builds();
for (k, v) in workspace.allow_builds_raw() {
merged.insert(k, v);
}
let mut only_built = manifest.pnpm_only_built_dependencies();
only_built.extend(workspace.only_built_dependencies.iter().cloned());
only_built.extend(manifest.trusted_dependencies());
let mut never_built = manifest.pnpm_never_built_dependencies();
never_built.extend(workspace.never_built_dependencies.iter().cloned());
aube_scripts::BuildPolicy::from_config(
&merged,
&only_built,
&never_built,
dangerously_allow_all_builds,
)
}
#[derive(Debug, Clone)]
pub(crate) struct JailBuildPolicy {
enabled: bool,
denylist: aube_scripts::BuildPolicy,
grants: Vec<(String, aube_manifest::JailBuildPermission)>,
}
impl JailBuildPolicy {
pub(crate) fn from_settings(
ctx: &aube_settings::ResolveCtx<'_>,
workspace: &aube_manifest::WorkspaceConfig,
) -> (Self, Vec<String>) {
let enabled =
aube_settings::resolved::jail_builds(ctx) || aube_settings::resolved::paranoid(ctx);
let jail_exclusions = aube_settings::resolved::jail_build_exclusions(ctx);
let (denylist, denylist_warnings) = aube_scripts::BuildPolicy::denylist(&jail_exclusions);
let mut warnings = denylist_warnings
.into_iter()
.map(|warning| format!("jailBuildExclusions: {warning}"))
.collect::<Vec<_>>();
let grants = workspace
.jail_build_permissions
.iter()
.filter_map(|(pattern, grant)| {
if let Err(err) = aube_scripts::pattern_matches(pattern, "", "") {
warnings.push(format!("jailBuildPermissions: {err}"));
return None;
}
Some((pattern.clone(), grant.clone()))
})
.collect();
(
Self {
enabled,
denylist,
grants,
},
warnings,
)
}
fn should_jail(&self, name: &str, version: &str) -> bool {
self.enabled
&& !matches!(
self.denylist.decide(name, version),
aube_scripts::AllowDecision::Deny
)
}
fn jail_for(
&self,
name: &str,
version: &str,
package_dir: &std::path::Path,
project_dir: &std::path::Path,
) -> Option<aube_scripts::ScriptJail> {
if !self.should_jail(name, version) {
return None;
}
let mut env = Vec::new();
let mut read_paths = Vec::new();
let mut write_paths = Vec::new();
let mut network = false;
for (pattern, grant) in &self.grants {
match aube_scripts::pattern_matches(pattern, name, version) {
Ok(true) => {
env.extend(grant.env.iter().cloned());
read_paths.extend(
grant
.read
.iter()
.map(|path| resolve_jail_grant_path(project_dir, path)),
);
write_paths.extend(
grant
.write
.iter()
.map(|path| resolve_jail_grant_path(project_dir, path)),
);
network |= grant.network;
}
Ok(false) => {}
Err(_) => {}
}
}
Some(
aube_scripts::ScriptJail::new(package_dir)
.with_env(env)
.with_read_paths(read_paths)
.with_write_paths(write_paths)
.with_network(network),
)
}
}
fn resolve_jail_grant_path(project_dir: &std::path::Path, raw: &str) -> std::path::PathBuf {
let path = raw.trim();
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME")
{
return std::path::PathBuf::from(home).join(rest);
}
let path = std::path::Path::new(path);
if path.is_absolute() {
path.to_path_buf()
} else {
project_dir.join(path)
}
}
pub(super) fn resolve_link_strategy(
cwd: &std::path::Path,
ctx: &aube_settings::ResolveCtx<'_>,
) -> miette::Result<aube_linker::LinkStrategy> {
let package_import_method_cli =
aube_settings::values::string_from_cli("packageImportMethod", ctx.cli);
let auto_probe = || {
let store_dir = super::super::open_store(cwd)
.map(|s| s.root().to_path_buf())
.ok();
match store_dir.as_deref() {
Some(sd) => aube_linker::Linker::detect_strategy_cross(sd, cwd),
None => aube_linker::Linker::detect_strategy(cwd),
}
};
let strategy = if let Some(cli) = package_import_method_cli.as_deref() {
match cli.trim().to_ascii_lowercase().as_str() {
"" | "auto" => auto_probe(),
"hardlink" => aube_linker::LinkStrategy::Hardlink,
"copy" => aube_linker::LinkStrategy::Copy,
"clone-or-copy" => aube_linker::LinkStrategy::Reflink,
"clone" => {
tracing::warn!(
"package-import-method=clone: reflink will silently fall back to copy \
if the filesystem does not support it (strict enforcement is a known TODO)"
);
aube_linker::LinkStrategy::Reflink
}
other => {
return Err(miette!(
"unknown --package-import-method value `{other}`; expected `auto`, `hardlink`, `copy`, `clone`, or `clone-or-copy`"
));
}
}
} else {
match aube_settings::resolved::package_import_method(ctx) {
aube_settings::resolved::PackageImportMethod::Auto => auto_probe(),
aube_settings::resolved::PackageImportMethod::Hardlink => {
aube_linker::LinkStrategy::Hardlink
}
aube_settings::resolved::PackageImportMethod::Copy => aube_linker::LinkStrategy::Copy,
aube_settings::resolved::PackageImportMethod::CloneOrCopy => {
aube_linker::LinkStrategy::Reflink
}
aube_settings::resolved::PackageImportMethod::Clone => {
tracing::warn!(
"package-import-method=clone: reflink will silently fall back to copy \
if the filesystem does not support it (strict enforcement is a known TODO)"
);
aube_linker::LinkStrategy::Reflink
}
}
};
Ok(strategy)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_dep_lifecycle_scripts(
project_dir: &std::path::Path,
modules_dir_name: &str,
aube_dir: &std::path::Path,
graph: &aube_lockfile::LockfileGraph,
policy: &aube_scripts::BuildPolicy,
virtual_store_dir_max_length: usize,
child_concurrency: usize,
placements: Option<&aube_linker::HoistedPlacements>,
side_effects_cache: SideEffectsCacheConfig<'_>,
jail_policy: &JailBuildPolicy,
) -> miette::Result<usize> {
#[derive(Clone)]
struct BuildJob {
name: String,
registry_name: String,
version: String,
package_dir: std::path::PathBuf,
dep_modules_dir: std::path::PathBuf,
manifest: aube_manifest::PackageJson,
cache_entry: Option<SideEffectsCacheEntry>,
}
let mut jobs: Vec<BuildJob> = Vec::new();
for (dep_path, pkg) in &graph.packages {
match policy.decide(pkg.registry_name(), &pkg.version) {
aube_scripts::AllowDecision::Allow => {}
aube_scripts::AllowDecision::Deny | aube_scripts::AllowDecision::Unspecified => {
continue;
}
}
let package_dir = materialized_pkg_dir(
aube_dir,
dep_path,
&pkg.name,
virtual_store_dir_max_length,
placements,
);
if !package_dir.exists() {
tracing::debug!(
"allowBuilds: skipping {} — {} not on disk",
pkg.name,
package_dir.display()
);
continue;
}
let pkg_json_path = package_dir.join("package.json");
let pkg_json_content = match std::fs::read_to_string(&pkg_json_path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => {
return Err(miette!(
"failed to read package.json for {} at {}: {}",
pkg.name,
pkg_json_path.display(),
e
));
}
};
let dep_manifest = aube_manifest::PackageJson::parse(&pkg_json_path, pkg_json_content)
.map_err(miette::Report::new)
.wrap_err_with(|| format!("failed to parse package.json for {}", pkg.name))?;
if !aube_scripts::has_dep_lifecycle_work(&package_dir, &dep_manifest) {
continue;
}
let cache_entry = side_effects_cache
.root()
.map(|root| SideEffectsCacheEntry::new(root, &pkg.name, &pkg.version, &package_dir))
.transpose()?;
let dep_modules_dir = dep_modules_dir_for(&package_dir, &pkg.name);
jobs.push(BuildJob {
name: pkg.name.clone(),
registry_name: pkg.registry_name().to_string(),
version: pkg.version.clone(),
package_dir,
dep_modules_dir,
manifest: dep_manifest,
cache_entry,
});
}
if jobs.is_empty() {
return Ok(0);
}
let node_gyp_bin_dir = std::sync::Arc::new(node_gyp_bootstrap::ensure(project_dir).await?);
let concurrency = child_concurrency.max(1);
let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
let project_dir = project_dir.to_path_buf();
let modules_dir_name = modules_dir_name.to_string();
let should_restore_side_effects_cache = side_effects_cache.should_restore();
let should_save_side_effects_cache = side_effects_cache.should_save();
let overwrite_side_effects_cache = side_effects_cache.overwrite_existing();
let jail_policy = std::sync::Arc::new((*jail_policy).clone());
let mut set: tokio::task::JoinSet<miette::Result<usize>> = tokio::task::JoinSet::new();
for job in jobs {
let sem = semaphore.clone();
let project_dir = project_dir.clone();
let modules_dir_name = modules_dir_name.clone();
let node_gyp_bin_dir = node_gyp_bin_dir.clone();
let jail_policy = jail_policy.clone();
set.spawn(async move {
let _permit = sem.acquire().await.unwrap();
if should_restore_side_effects_cache && let Some(cache_entry) = job.cache_entry.clone()
{
let package_dir = job.package_dir.clone();
let restore_result = tokio::task::spawn_blocking(move || {
cache_entry.restore_if_available(&package_dir)
})
.await
.map_err(|e| {
miette!(
"side-effects-cache restore task panicked for {}@{}: {e}",
job.name,
job.version
)
})?;
match restore_result? {
SideEffectsCacheRestore::Restored | SideEffectsCacheRestore::AlreadyApplied => {
return Ok(0);
}
SideEffectsCacheRestore::Miss => {}
}
}
let tool_dirs: Vec<&std::path::Path> = node_gyp_bin_dir
.as_ref()
.as_deref()
.map(|p| vec![p])
.unwrap_or_default();
let jail = jail_policy.jail_for(
&job.registry_name,
&job.version,
&job.package_dir,
&project_dir,
);
let _jail_home_cleanup = jail.as_ref().map(aube_scripts::ScriptJailHomeCleanup::new);
let mut ran_here = 0usize;
for hook in aube_scripts::DEP_LIFECYCLE_HOOKS {
let did_run = aube_scripts::run_dep_hook(
&job.package_dir,
&job.dep_modules_dir,
&project_dir,
&modules_dir_name,
&job.manifest,
hook,
&tool_dirs,
jail.as_ref(),
)
.await
.map_err(|e| {
miette!(
"lifecycle script {} failed for {}@{}: {}",
hook.script_name(),
job.name,
job.version,
e
)
})?;
if did_run {
tracing::debug!(
"ran {} for {}@{}",
hook.script_name(),
job.name,
job.version
);
ran_here += 1;
}
}
if should_save_side_effects_cache
&& ran_here > 0
&& let Some(cache_entry) = job.cache_entry.clone()
{
let package_dir = job.package_dir.clone();
let save_result = tokio::task::spawn_blocking(move || {
cache_entry.save(&package_dir, overwrite_side_effects_cache)
})
.await
.map_err(|e| {
miette!(
"side-effects-cache save task panicked for {}@{}: {e}",
job.name,
job.version
)
})
.and_then(|r| r);
if let Err(e) = save_result {
tracing::debug!(
"side-effects-cache: ignoring cache save error for {}@{}: {e}",
job.name,
job.version
);
}
}
Ok(ran_here)
});
}
let mut ran = 0usize;
while let Some(res) = set.join_next().await {
ran += res.into_diagnostic()??;
}
Ok(ran)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn import_verified_tarball(
store: &aube_store::Store,
bytes: &[u8],
display_name: &str,
registry_name: &str,
version: &str,
integrity: Option<&str>,
verify_integrity: bool,
strict_integrity: bool,
strict_pkg_content_check: bool,
) -> miette::Result<aube_store::PackageIndex> {
if verify_integrity {
if let Some(expected) = integrity {
aube_store::verify_integrity(bytes, expected)
.map_err(|e| miette!("{display_name}@{version}: {e}"))?;
} else if strict_integrity {
return Err(miette!(
"{display_name}@{version}: registry response has no `dist.integrity` and `strict-store-integrity` is on. Refusing to import unverified bytes."
));
} else {
tracing::warn!(
"{display_name}@{version}: registry response has no `dist.integrity`, importing without content verification. Set `strict-store-integrity=true` to refuse instead."
);
}
}
let index = store
.import_tarball(bytes)
.map_err(|e| miette!("failed to import {display_name}@{version}: {e}"))?;
if strict_pkg_content_check {
aube_store::validate_pkg_content(&index, registry_name, version)
.map_err(|e| miette!("{display_name}@{version}: {e}"))?;
}
if let Err(e) = store.save_index(registry_name, version, integrity, &index) {
tracing::warn!("Failed to cache index for {display_name}@{version}: {e}");
}
Ok(index)
}
pub(super) fn validate_required_scripts(
project_dir: &std::path::Path,
manifest: &aube_manifest::PackageJson,
required: &[String],
) -> miette::Result<()> {
if required.is_empty() {
return Ok(());
}
let mut missing = Vec::new();
collect_missing_required_scripts(".", manifest, required, &mut missing);
for pkg_dir in aube_workspace::find_workspace_packages(project_dir)
.map_err(|e| miette!("failed to discover workspace packages: {e}"))?
{
let manifest_path = pkg_dir.join("package.json");
let pkg_manifest = aube_manifest::PackageJson::from_path(&manifest_path)
.map_err(miette::Report::new)
.wrap_err_with(|| format!("failed to read {}", manifest_path.display()))?;
let label = pkg_manifest
.name
.as_deref()
.map(str::to_string)
.unwrap_or_else(|| {
pkg_dir
.strip_prefix(project_dir)
.unwrap_or(&pkg_dir)
.display()
.to_string()
});
collect_missing_required_scripts(&label, &pkg_manifest, required, &mut missing);
}
if missing.is_empty() {
Ok(())
} else {
Err(miette!(
"requiredScripts check failed:\n{}",
missing
.into_iter()
.map(|(pkg, script)| format!(" - {pkg} is missing `{script}`"))
.collect::<Vec<_>>()
.join("\n")
))
}
}
fn collect_missing_required_scripts(
label: &str,
manifest: &aube_manifest::PackageJson,
required: &[String],
missing: &mut Vec<(String, String)>,
) {
for script in required {
if !manifest.scripts.contains_key(script) {
missing.push((label.to_string(), script.clone()));
}
}
}
pub(super) fn unreviewed_dep_builds(
aube_dir: &std::path::Path,
graph: &aube_lockfile::LockfileGraph,
policy: &aube_scripts::BuildPolicy,
virtual_store_dir_max_length: usize,
placements: Option<&aube_linker::HoistedPlacements>,
) -> miette::Result<Vec<String>> {
let mut unreviewed = Vec::new();
for (dep_path, pkg) in &graph.packages {
if !matches!(
policy.decide(pkg.registry_name(), &pkg.version),
aube_scripts::AllowDecision::Unspecified
) {
continue;
}
let package_dir = materialized_pkg_dir(
aube_dir,
dep_path,
&pkg.name,
virtual_store_dir_max_length,
placements,
);
if !package_dir.exists() {
continue;
}
let pkg_json_path = package_dir.join("package.json");
let pkg_json_content = match std::fs::read_to_string(&pkg_json_path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => {
return Err(miette!(
"failed to read package.json for {} at {}: {}",
pkg.name,
pkg_json_path.display(),
e
));
}
};
let dep_manifest = aube_manifest::PackageJson::parse(&pkg_json_path, pkg_json_content)
.map_err(miette::Report::new)
.wrap_err_with(|| format!("failed to parse package.json for {}", pkg.name))?;
if aube_scripts::has_dep_lifecycle_work(&package_dir, &dep_manifest) {
unreviewed.push(pkg.spec_key());
}
}
unreviewed.sort();
unreviewed.dedup();
Ok(unreviewed)
}