pub mod policy;
pub use policy::{AllowDecision, BuildPolicy, BuildPolicyError};
use aube_manifest::PackageJson;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct ScriptSettings {
pub node_options: Option<String>,
pub script_shell: Option<PathBuf>,
pub unsafe_perm: Option<bool>,
pub shell_emulator: bool,
}
static SCRIPT_SETTINGS: std::sync::OnceLock<std::sync::RwLock<ScriptSettings>> =
std::sync::OnceLock::new();
fn script_settings_lock() -> &'static std::sync::RwLock<ScriptSettings> {
SCRIPT_SETTINGS.get_or_init(|| std::sync::RwLock::new(ScriptSettings::default()))
}
pub fn set_script_settings(settings: ScriptSettings) {
*script_settings_lock()
.write()
.expect("script settings lock poisoned") = settings;
}
fn script_settings() -> ScriptSettings {
script_settings_lock()
.read()
.expect("script settings lock poisoned")
.clone()
}
pub fn prepend_path(bin_dir: &Path) -> std::ffi::OsString {
let path = std::env::var_os("PATH").unwrap_or_default();
let mut entries = vec![bin_dir.to_path_buf()];
entries.extend(std::env::split_paths(&path));
std::env::join_paths(entries).unwrap_or(path)
}
pub fn spawn_shell(script_cmd: &str) -> tokio::process::Command {
let settings = script_settings();
#[cfg(unix)]
{
let mut cmd = tokio::process::Command::new(
settings
.script_shell
.as_deref()
.unwrap_or_else(|| Path::new("sh")),
);
cmd.arg("-c").arg(script_cmd);
apply_script_settings_env(&mut cmd, &settings);
cmd
}
#[cfg(windows)]
{
let mut cmd = tokio::process::Command::new(
settings
.script_shell
.as_deref()
.unwrap_or_else(|| Path::new("cmd.exe")),
);
if settings.script_shell.is_some() {
cmd.arg("-c").arg(script_cmd);
} else {
cmd.raw_arg("/d /s /c \"").raw_arg(script_cmd).raw_arg("\"");
}
apply_script_settings_env(&mut cmd, &settings);
cmd
}
}
fn apply_script_settings_env(cmd: &mut tokio::process::Command, settings: &ScriptSettings) {
if let Some(node_options) = settings.node_options.as_deref() {
cmd.env("NODE_OPTIONS", node_options);
}
if let Some(unsafe_perm) = settings.unsafe_perm {
cmd.env(
"npm_config_unsafe_perm",
if unsafe_perm { "true" } else { "false" },
);
}
if settings.shell_emulator {
cmd.env("npm_config_shell_emulator", "true");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleHook {
PreInstall,
Install,
PostInstall,
Prepare,
}
impl LifecycleHook {
pub fn script_name(self) -> &'static str {
match self {
Self::PreInstall => "preinstall",
Self::Install => "install",
Self::PostInstall => "postinstall",
Self::Prepare => "prepare",
}
}
}
pub const DEP_LIFECYCLE_HOOKS: [LifecycleHook; 3] = [
LifecycleHook::PreInstall,
LifecycleHook::Install,
LifecycleHook::PostInstall,
];
#[cfg(unix)]
static SAVED_STDERR_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
#[cfg(unix)]
pub fn set_saved_stderr_fd(fd: std::os::fd::RawFd) {
SAVED_STDERR_FD.store(fd, std::sync::atomic::Ordering::SeqCst);
}
#[cfg(not(unix))]
pub fn set_saved_stderr_fd(_fd: i32) {}
#[cfg(unix)]
pub fn child_stderr() -> std::process::Stdio {
let fd = SAVED_STDERR_FD.load(std::sync::atomic::Ordering::SeqCst);
if fd < 0 {
return std::process::Stdio::inherit();
}
let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
match borrowed.try_clone_to_owned() {
Ok(owned) => std::process::Stdio::from(owned),
Err(_) => std::process::Stdio::inherit(),
}
}
#[cfg(not(unix))]
pub fn child_stderr() -> std::process::Stdio {
std::process::Stdio::inherit()
}
pub async fn run_script(
script_dir: &Path,
project_root: &Path,
modules_dir_name: &str,
manifest: &PackageJson,
script_name: &str,
script_cmd: &str,
) -> Result<(), Error> {
let bin_dir = project_root.join(modules_dir_name).join(".bin");
let new_path = prepend_path(&bin_dir);
let mut cmd = spawn_shell(script_cmd);
cmd.current_dir(script_dir)
.stderr(child_stderr())
.env("PATH", &new_path)
.env("npm_lifecycle_event", script_name);
if std::env::var_os("INIT_CWD").is_none() {
cmd.env("INIT_CWD", project_root);
}
if let Some(ref name) = manifest.name {
cmd.env("npm_package_name", name);
}
if let Some(ref version) = manifest.version {
cmd.env("npm_package_version", version);
}
tracing::debug!("lifecycle: {script_name} → {script_cmd}");
let status = cmd
.status()
.await
.map_err(|e| Error::Spawn(script_name.to_string(), e.to_string()))?;
if !status.success() {
return Err(Error::NonZeroExit {
script: script_name.to_string(),
code: status.code(),
});
}
Ok(())
}
pub async fn run_root_hook(
project_dir: &Path,
modules_dir_name: &str,
manifest: &PackageJson,
hook: LifecycleHook,
) -> Result<bool, Error> {
let name = hook.script_name();
let Some(script_cmd) = manifest.scripts.get(name) else {
return Ok(false);
};
run_script(
project_dir,
project_dir,
modules_dir_name,
manifest,
name,
script_cmd,
)
.await?;
Ok(true)
}
pub fn implicit_install_script(
manifest: &PackageJson,
has_binding_gyp: bool,
) -> Option<&'static str> {
if !has_binding_gyp {
return None;
}
if manifest
.scripts
.contains_key(LifecycleHook::Install.script_name())
|| manifest
.scripts
.contains_key(LifecycleHook::PreInstall.script_name())
{
return None;
}
Some("node-gyp rebuild")
}
pub fn default_install_script(package_dir: &Path, manifest: &PackageJson) -> Option<&'static str> {
implicit_install_script(manifest, package_dir.join("binding.gyp").is_file())
}
pub fn has_dep_lifecycle_work(package_dir: &Path, manifest: &PackageJson) -> bool {
if DEP_LIFECYCLE_HOOKS
.iter()
.any(|h| manifest.scripts.contains_key(h.script_name()))
{
return true;
}
default_install_script(package_dir, manifest).is_some()
}
pub async fn run_dep_hook(
package_dir: &Path,
project_root: &Path,
modules_dir_name: &str,
manifest: &PackageJson,
hook: LifecycleHook,
) -> Result<bool, Error> {
let name = hook.script_name();
let script_cmd: &str = match manifest.scripts.get(name) {
Some(s) => s.as_str(),
None => match hook {
LifecycleHook::Install => match default_install_script(package_dir, manifest) {
Some(s) => s,
None => return Ok(false),
},
_ => return Ok(false),
},
};
run_script(
package_dir,
project_root,
modules_dir_name,
manifest,
name,
script_cmd,
)
.await?;
Ok(true)
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to spawn script {0}: {1}")]
Spawn(String, String),
#[error("script `{script}` exited with code {code:?}")]
NonZeroExit { script: String, code: Option<i32> },
}