use std::any::Any;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::thread::JoinHandle;
use anyhow::{Result, bail};
use colored::Colorize;
use crate::chain::mux::{LineSink, StdioSink, prefix_width, render_prefix, spawn_readers};
use crate::resolver::{CollisionPolicy, ResolutionOverrides, ResolveError, Resolver, ScriptPolicy};
use crate::tool;
use crate::types::{PackageManager, ProjectContext, TaskRunner, version_matches};
pub(crate) fn install(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
frozen: bool,
) -> Result<i32> {
install_pms(ctx, overrides, frozen, None)
}
pub(crate) fn install_pms(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
frozen: bool,
_sink: super::WarningSink<'_>,
) -> Result<i32> {
if ctx.package_managers.is_empty() {
bail!("No package manager detected.");
}
let plan = plan_install(ctx, overrides)?;
report_plan(&plan, overrides);
warn_unsupported_script_policy(&plan.pms, overrides);
let _group = super::task_group(overrides, "install");
if let (Some(nv), Some(cur)) = (&ctx.node_version, &ctx.current_node)
&& !version_matches(&nv.expected, cur)
{
eprintln!(
"{} node expected {} ({}), current {}",
"warn:".yellow().bold(),
nv.expected,
nv.source,
cur,
);
suggest_version_switch(ctx);
}
if let [pm] = plan.pms.as_slice() {
return install_single(ctx, *pm, frozen, overrides);
}
run_installs_parallel(ctx, &plan, frozen, overrides)
}
fn select_install_pms(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
) -> Result<Vec<PackageManager>, ResolveError> {
if let Some(o) = &overrides.pm {
if !ctx.package_managers.contains(&o.pm) {
return Err(ResolveError::PmOverrideNotDetected {
pm: o.pm,
origin: o.origin.clone(),
detected: ctx.package_managers.clone(),
});
}
} else {
let missing: Vec<PackageManager> = overrides
.install_pms
.iter()
.copied()
.filter(|pm| !ctx.package_managers.contains(pm))
.collect();
if !missing.is_empty() {
return Err(ResolveError::InstallPmsNotDetected {
missing,
detected: ctx.package_managers.clone(),
});
}
}
Ok(effective_install_pms(ctx, overrides))
}
fn effective_install_pms(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
) -> Vec<PackageManager> {
if let Some(o) = &overrides.pm {
return ctx
.package_managers
.iter()
.copied()
.filter(|pm| *pm == o.pm)
.collect();
}
if overrides.install_pms.is_empty() {
return ctx.package_managers.clone();
}
ctx.package_managers
.iter()
.copied()
.filter(|pm| overrides.install_pms.contains(pm))
.collect()
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct CollisionDir {
pub dir: &'static str,
pub writers: Vec<PackageManager>,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Shadowed {
pub loser: PackageManager,
pub winner: PackageManager,
pub dir: &'static str,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct InstallPlan {
pub pms: Vec<PackageManager>,
pub shadowed: Vec<Shadowed>,
pub collisions: Vec<CollisionDir>,
}
pub(crate) fn plan_install(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
) -> Result<InstallPlan, ResolveError> {
let mut plan = InstallPlan {
pms: select_install_pms(ctx, overrides)?,
shadowed: Vec::new(),
collisions: Vec::new(),
};
for install_dir in &ctx.install_dirs {
let writers: Vec<PackageManager> = install_dir
.writers
.iter()
.copied()
.filter(|pm| plan.pms.contains(pm))
.collect();
if writers.len() < 2 {
continue;
}
if overrides.on_collision == CollisionPolicy::Error {
return Err(ResolveError::InstallDirCollision {
dir: install_dir.dir,
writers,
});
}
if consented_to(&writers, overrides) {
plan.collisions.push(CollisionDir {
dir: install_dir.dir,
writers,
});
continue;
}
let winner = dir_winner(ctx, overrides, &writers);
for loser in writers.iter().copied().filter(|pm| *pm != winner) {
plan.shadowed.push(Shadowed {
loser,
winner,
dir: install_dir.dir,
});
plan.pms.retain(|pm| *pm != loser);
}
}
Ok(plan)
}
pub(crate) fn collision_warning(dir: &str, writers: &[PackageManager]) -> String {
let list = writers
.iter()
.map(|pm| pm.label())
.collect::<Vec<_>>()
.join(", ");
let first = writers.first().map_or("bun", |pm| pm.label());
format!(
"{list} all install into {dir}/, and the install allowlist names them all, so they run \
one after another over the same tree instead of one of them being skipped. Drop all but \
`{first}` from `[install].pms` (or `RUNNER_INSTALL_PMS`) to skip the redundant install."
)
}
fn consented_to(writers: &[PackageManager], overrides: &ResolutionOverrides) -> bool {
writers
.iter()
.filter(|pm| overrides.install_pms.contains(pm))
.count()
>= 2
}
fn dir_winner(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
writers: &[PackageManager],
) -> PackageManager {
let resolved = Resolver::new(ctx, overrides)
.resolve_node_pm()
.ok()
.map(|decision| decision.pm)
.filter(|pm| writers.contains(pm));
resolved.unwrap_or_else(|| {
writers
.first()
.copied()
.expect("dir_winner is only called with at least two writers")
})
}
fn report_plan(plan: &InstallPlan, overrides: &ResolutionOverrides) {
if !overrides.no_warnings {
for collision in &plan.collisions {
eprintln!(
"{} install: {}",
"warn:".yellow().bold(),
collision_warning(collision.dir, &collision.writers),
);
}
}
for shadow in &plan.shadowed {
eprintln!(
"{}",
format!(
"{}/: {} installs it, {} shadowed (run both with `[install].pms = [\"{}\", \
\"{}\"]`)",
shadow.dir,
shadow.winner.label(),
shadow.loser.label(),
shadow.winner.label(),
shadow.loser.label(),
)
.dimmed(),
);
}
}
fn install_single(
ctx: &ProjectContext,
pm: PackageManager,
frozen: bool,
overrides: &ResolutionOverrides,
) -> Result<i32> {
eprintln!("{} {}", "installing with".dimmed(), pm.label().bold());
let mut cmd = build_install_command(ctx, pm, frozen, script_directive(overrides));
super::configure_command(&mut cmd, &ctx.root, overrides);
let status = cmd.status()?;
Ok(if status.success() {
0
} else {
super::exit_code(status)
})
}
fn install_lanes(plan: &InstallPlan) -> Vec<Vec<PackageManager>> {
let mut lanes: Vec<Vec<PackageManager>> = Vec::new();
for pm in &plan.pms {
if lanes.iter().flatten().any(|queued| queued == pm) {
continue;
}
match plan
.collisions
.iter()
.find(|collision| collision.writers.contains(pm))
{
Some(collision) => lanes.push(collision.writers.clone()),
None => lanes.push(vec![*pm]),
}
}
lanes
}
fn run_installs_parallel(
ctx: &ProjectContext,
plan: &InstallPlan,
frozen: bool,
overrides: &ResolutionOverrides,
) -> Result<i32> {
let lanes = install_lanes(plan);
let names: Vec<&str> = plan.pms.iter().map(|pm| pm.label()).collect();
let width = prefix_width(&names);
let colorize = colored::control::SHOULD_COLORIZE.should_colorize();
let sink: Arc<dyn LineSink> = Arc::new(StdioSink);
let directive = script_directive(overrides);
let outcomes: Vec<Result<Option<(PackageManager, i32)>>> = std::thread::scope(|scope| {
let mut handles = Vec::with_capacity(lanes.len());
for lane in &lanes {
let sink = Arc::clone(&sink);
handles.push(scope.spawn(move || {
run_lane(
ctx, lane, frozen, overrides, &sink, width, colorize, directive,
)
}));
}
handles
.into_iter()
.map(|handle| {
handle.join().unwrap_or_else(|payload| {
bail!("install lane panicked: {}", panic_payload(&*payload))
})
})
.collect()
});
let mut failures: Vec<(PackageManager, i32)> = Vec::new();
for outcome in outcomes {
match outcome {
Ok(Some(failure)) => failures.push(failure),
Ok(None) => {}
Err(e) => return Err(e),
}
}
let index_of = |pm: PackageManager| plan.pms.iter().position(|p| *p == pm);
Ok(failures
.into_iter()
.min_by_key(|(pm, _)| index_of(*pm))
.map_or(0, |(_, code)| code))
}
#[allow(
clippy::too_many_arguments,
reason = "one lane's worth of the parallel executor's state; bundling it into a struct buys \
nothing at one call site"
)]
fn run_lane(
ctx: &ProjectContext,
lane: &[PackageManager],
frozen: bool,
overrides: &ResolutionOverrides,
sink: &Arc<dyn LineSink>,
width: usize,
colorize: bool,
directive: tool::ScriptDirective,
) -> Result<Option<(PackageManager, i32)>> {
for pm in lane {
eprintln!("{} {}", "installing with".dimmed(), pm.label().bold());
let mut cmd = build_install_command(ctx, *pm, frozen, directive);
super::configure_command(&mut cmd, &ctx.root, overrides);
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let prefix = render_prefix(pm.label(), width, colorize);
let stdout: Box<dyn std::io::Read + Send> =
Box::new(child.stdout.take().expect("stdout piped"));
let stderr: Box<dyn std::io::Read + Send> =
Box::new(child.stderr.take().expect("stderr piped"));
let readers = spawn_readers(
vec![(prefix.clone(), false, stdout), (prefix, true, stderr)],
sink,
);
let waited = match child.wait() {
Ok(status) => Ok(status),
Err(e) => {
let _ = child.kill();
let _ = child.wait();
Err(e)
}
};
for handle in readers {
join_reader_thread(handle);
}
let status = waited?;
if !status.success() {
return Ok(Some((*pm, super::exit_code(status))));
}
}
Ok(None)
}
fn join_reader_thread(handle: JoinHandle<()>) {
if let Err(payload) = handle.join() {
eprintln!(
"warn: install output reader thread panicked: {}",
panic_payload(&*payload),
);
}
}
fn panic_payload(payload: &(dyn Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
return (*message).to_string();
}
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
"non-string panic payload".to_string()
}
fn build_install_command(
ctx: &ProjectContext,
pm: PackageManager,
frozen: bool,
scripts: tool::ScriptDirective,
) -> Command {
match pm {
PackageManager::Npm => tool::npm::install_cmd(frozen, scripts),
PackageManager::Yarn => tool::yarn::install_cmd(&ctx.root, frozen, scripts),
PackageManager::Pnpm => tool::pnpm::install_cmd(frozen, scripts),
PackageManager::Bun => tool::bun::install_cmd(frozen, scripts),
PackageManager::Cargo => tool::cargo_pm::install_cmd(frozen),
PackageManager::Deno => tool::deno::install_cmd(scripts),
PackageManager::Uv => tool::uv::install_cmd(frozen),
PackageManager::Poetry => tool::poetry::install_cmd(),
PackageManager::Pipenv => tool::pipenv::install_cmd(frozen),
PackageManager::Go => tool::go_pm::install_cmd(),
PackageManager::Bundler => tool::bundler::install_cmd(),
PackageManager::Composer => tool::composer::install_cmd(scripts),
}
}
const fn script_directive(overrides: &ResolutionOverrides) -> tool::ScriptDirective {
match overrides.script_policy {
ScriptPolicy::Default => tool::ScriptDirective::Default,
ScriptPolicy::Deny => tool::ScriptDirective::Deny,
ScriptPolicy::Allow => tool::ScriptDirective::ForceOn,
}
}
const fn deny_scripts(overrides: &ResolutionOverrides) -> bool {
matches!(overrides.script_policy, ScriptPolicy::Deny)
}
const fn force_scripts(overrides: &ResolutionOverrides) -> bool {
matches!(overrides.script_policy, ScriptPolicy::Allow)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DenySupport {
ViaFlag,
DefaultDeny,
Unsupported,
}
const fn deny_support(pm: PackageManager) -> DenySupport {
match pm {
PackageManager::Npm
| PackageManager::Yarn
| PackageManager::Pnpm
| PackageManager::Bun
| PackageManager::Composer => DenySupport::ViaFlag,
PackageManager::Deno => DenySupport::DefaultDeny,
PackageManager::Cargo
| PackageManager::Uv
| PackageManager::Poetry
| PackageManager::Pipenv
| PackageManager::Go
| PackageManager::Bundler => DenySupport::Unsupported,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ForceSupport {
ViaFlag,
AlreadyRuns,
NotExpressible,
}
const fn force_support(pm: PackageManager) -> ForceSupport {
match pm {
PackageManager::Npm | PackageManager::Deno | PackageManager::Yarn => ForceSupport::ViaFlag,
PackageManager::Bun | PackageManager::Pnpm => ForceSupport::NotExpressible,
PackageManager::Composer
| PackageManager::Cargo
| PackageManager::Uv
| PackageManager::Poetry
| PackageManager::Pipenv
| PackageManager::Go
| PackageManager::Bundler => ForceSupport::AlreadyRuns,
}
}
fn warn_unsupported_script_policy(pms: &[PackageManager], overrides: &ResolutionOverrides) {
for pm in unsupported_deny_managers(pms, overrides) {
eprintln!(
"{} {} cannot skip install scripts; deny policy not applied to it",
"warn:".yellow().bold(),
pm.label(),
);
}
for pm in unforceable_managers(pms, overrides) {
let allowlist = match pm {
PackageManager::Pnpm => "pnpm.onlyBuiltDependencies",
PackageManager::Bun => "trustedDependencies",
_ => "a manifest allowlist",
};
eprintln!(
"{} {} cannot force install scripts on; it denies dependency build scripts by default \
and only the {} allowlist (which runner won't write) re-enables them",
"warn:".yellow().bold(),
pm.label(),
allowlist,
);
}
}
fn unsupported_deny_managers(
pms: &[PackageManager],
overrides: &ResolutionOverrides,
) -> Vec<PackageManager> {
if !deny_scripts(overrides) {
return Vec::new();
}
pms.iter()
.copied()
.filter(|pm| deny_support(*pm) == DenySupport::Unsupported)
.collect()
}
fn unforceable_managers(
pms: &[PackageManager],
overrides: &ResolutionOverrides,
) -> Vec<PackageManager> {
if !force_scripts(overrides) {
return Vec::new();
}
pms.iter()
.copied()
.filter(|pm| force_support(*pm) == ForceSupport::NotExpressible)
.collect()
}
fn suggest_version_switch(ctx: &ProjectContext) {
let hint = if ctx
.node_version
.as_ref()
.is_some_and(|nv| nv.source == ".nvmrc")
{
"nvm use"
} else if ctx.task_runners.contains(&TaskRunner::Mise) {
"mise install"
} else {
"switch to the expected Node version"
};
eprintln!(" {} {}", "hint:".dimmed(), hint);
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::PathBuf;
use super::{
CollisionDir, DenySupport, ForceSupport, InstallPlan, Shadowed, build_install_command,
deny_support, force_support, install_lanes, plan_install, script_directive,
select_install_pms, unforceable_managers, unsupported_deny_managers,
warn_unsupported_script_policy,
};
use crate::resolver::{
CollisionPolicy, OverrideOrigin, PmOverride, ResolutionOverrides, ResolveError,
ScriptPolicy,
};
use crate::tool::ScriptDirective;
use crate::types::{Ecosystem, InstallDir, PackageManager, ProjectContext};
fn context(pms: Vec<PackageManager>) -> ProjectContext {
ProjectContext {
root: PathBuf::from("/tmp/test"),
package_managers: pms,
task_runners: Vec::new(),
tasks: Vec::new(),
node_version: None,
current_node: None,
is_monorepo: false,
install_dirs: Vec::new(),
warnings: Vec::new(),
}
}
fn override_pm(pm: PackageManager, origin: OverrideOrigin) -> ResolutionOverrides {
ResolutionOverrides {
pm: Some(PmOverride { pm, origin }),
..Default::default()
}
}
#[test]
fn no_override_installs_with_every_detected_pm() {
let ctx = context(vec![PackageManager::Bun, PackageManager::Deno]);
let pms = select_install_pms(&ctx, &ResolutionOverrides::default())
.expect("default selection should succeed");
assert_eq!(pms, vec![PackageManager::Bun, PackageManager::Deno]);
}
#[test]
fn detected_override_installs_with_it_alone() {
let ctx = context(vec![PackageManager::Bun, PackageManager::Deno]);
let overrides = override_pm(PackageManager::Bun, OverrideOrigin::EnvVar);
let pms = select_install_pms(&ctx, &overrides).expect("detected override should filter");
assert_eq!(pms, vec![PackageManager::Bun]);
}
#[test]
fn undetected_override_errors_with_origin_and_detected_list() {
let ctx = context(vec![PackageManager::Cargo]);
let overrides = override_pm(PackageManager::Npm, OverrideOrigin::EnvVar);
let err = select_install_pms(&ctx, &overrides).expect_err("undetected override must error");
assert!(matches!(err, ResolveError::PmOverrideNotDetected { .. }));
let msg = format!("{err}");
assert!(msg.contains("RUNNER_PM"), "should name the source: {msg}");
assert!(msg.contains("cargo"), "should list detected PMs: {msg}");
}
#[test]
fn undetected_cli_override_names_the_flag() {
let ctx = context(vec![PackageManager::Cargo]);
let overrides = override_pm(PackageManager::Npm, OverrideOrigin::CliFlag);
let err = select_install_pms(&ctx, &overrides).expect_err("undetected override must error");
let msg = format!("{err}");
assert!(msg.contains("--pm"), "should name the flag: {msg}");
}
#[test]
fn install_pms_allowlist_filters_to_listed_detected_pms() {
let ctx = context(vec![
PackageManager::Bun,
PackageManager::Deno,
PackageManager::Cargo,
]);
let overrides = ResolutionOverrides {
install_pms: vec![PackageManager::Bun],
..Default::default()
};
let pms = select_install_pms(&ctx, &overrides).expect("allowlist should filter");
assert_eq!(pms, vec![PackageManager::Bun]);
}
fn colliding_context() -> ProjectContext {
let mut ctx = context(vec![PackageManager::Bun, PackageManager::Deno]);
ctx.install_dirs = vec![InstallDir {
dir: "node_modules",
writers: vec![PackageManager::Bun, PackageManager::Deno],
}];
ctx
}
#[test]
fn colliding_writers_resolve_to_one_installer_by_default() {
let ctx = colliding_context();
let plan = plan_install(&ctx, &ResolutionOverrides::default()).expect("plan");
assert_eq!(plan.pms, vec![PackageManager::Bun], "deno must not install");
assert_eq!(
plan.shadowed,
vec![Shadowed {
loser: PackageManager::Deno,
winner: PackageManager::Bun,
dir: "node_modules",
}],
);
assert!(plan.collisions.is_empty(), "resolved, so nothing to warn");
}
#[test]
fn ecosystem_pm_override_hands_the_tree_to_deno() {
let ctx = colliding_context();
let mut overrides = ResolutionOverrides::default();
overrides.pm_by_ecosystem.insert(
Ecosystem::Deno,
PmOverride {
pm: PackageManager::Deno,
origin: OverrideOrigin::ConfigFile {
path: PathBuf::from("/tmp/test/runner.toml"),
},
},
);
let plan = plan_install(&ctx, &overrides).expect("plan");
assert_eq!(plan.pms, vec![PackageManager::Deno]);
assert_eq!(
plan.shadowed,
vec![Shadowed {
loser: PackageManager::Bun,
winner: PackageManager::Deno,
dir: "node_modules",
}],
);
}
#[test]
fn naming_both_writers_runs_both_serialized_with_one_warning() {
let ctx = colliding_context();
let overrides = ResolutionOverrides {
install_pms: vec![PackageManager::Bun, PackageManager::Deno],
..Default::default()
};
let plan = plan_install(&ctx, &overrides).expect("plan");
assert_eq!(plan.pms, vec![PackageManager::Bun, PackageManager::Deno]);
assert!(plan.shadowed.is_empty(), "consent means nothing is dropped");
assert_eq!(
plan.collisions,
vec![CollisionDir {
dir: "node_modules",
writers: vec![PackageManager::Bun, PackageManager::Deno],
}],
);
assert_eq!(
install_lanes(&plan),
vec![vec![PackageManager::Bun, PackageManager::Deno]],
"consented writers still must not race over one tree",
);
}
#[test]
fn on_collision_error_refuses_to_pick() {
let ctx = colliding_context();
let overrides = ResolutionOverrides {
on_collision: CollisionPolicy::Error,
..Default::default()
};
let err = plan_install(&ctx, &overrides).expect_err("must refuse");
assert!(matches!(err, ResolveError::InstallDirCollision { .. }));
let msg = format!("{err}");
assert!(msg.contains("node_modules"), "msg: {msg}");
assert!(msg.contains("[install].pms"), "msg: {msg}");
}
#[test]
fn on_collision_error_refuses_even_when_both_were_named() {
let ctx = colliding_context();
let overrides = ResolutionOverrides {
install_pms: vec![PackageManager::Bun, PackageManager::Deno],
on_collision: CollisionPolicy::Error,
..Default::default()
};
let err = plan_install(&ctx, &overrides).expect_err("must refuse");
assert!(matches!(err, ResolveError::InstallDirCollision { .. }));
}
#[test]
fn pm_override_leaves_one_writer_and_nothing_to_say() {
let ctx = colliding_context();
let overrides = override_pm(PackageManager::Bun, OverrideOrigin::EnvVar);
let plan = plan_install(&ctx, &overrides).expect("plan");
assert_eq!(plan.pms, vec![PackageManager::Bun]);
assert!(plan.collisions.is_empty());
assert!(
plan.shadowed.is_empty(),
"nothing was dropped: deno was never in the install set to begin with"
);
}
#[test]
fn a_lone_writer_plans_clean() {
let mut ctx = context(vec![PackageManager::Bun, PackageManager::Cargo]);
ctx.install_dirs = vec![InstallDir {
dir: "node_modules",
writers: vec![PackageManager::Bun],
}];
let plan = plan_install(&ctx, &ResolutionOverrides::default()).expect("plan");
assert_eq!(plan.pms, vec![PackageManager::Bun, PackageManager::Cargo]);
assert!(plan.collisions.is_empty());
assert!(plan.shadowed.is_empty());
}
#[test]
fn lanes_serialize_shared_writers_and_leave_the_rest_parallel() {
let plan = InstallPlan {
pms: vec![
PackageManager::Bun,
PackageManager::Cargo,
PackageManager::Deno,
],
shadowed: Vec::new(),
collisions: vec![CollisionDir {
dir: "node_modules",
writers: vec![PackageManager::Bun, PackageManager::Deno],
}],
};
assert_eq!(
install_lanes(&plan),
vec![
vec![PackageManager::Bun, PackageManager::Deno],
vec![PackageManager::Cargo],
],
"bun and deno share one lane and run in order; cargo runs alongside",
);
}
#[test]
fn install_pms_allowlist_preserves_detection_order() {
let ctx = context(vec![
PackageManager::Bun,
PackageManager::Cargo,
PackageManager::Uv,
]);
let overrides = ResolutionOverrides {
install_pms: vec![PackageManager::Uv, PackageManager::Bun],
..Default::default()
};
let pms = select_install_pms(&ctx, &overrides).expect("allowlist should filter");
assert_eq!(pms, vec![PackageManager::Bun, PackageManager::Uv]);
}
#[test]
fn install_pms_undetected_entry_errors() {
let ctx = context(vec![PackageManager::Bun]);
let overrides = ResolutionOverrides {
install_pms: vec![PackageManager::Bun, PackageManager::Pnpm],
..Default::default()
};
let err = select_install_pms(&ctx, &overrides).expect_err("undetected entry must error");
assert!(matches!(err, ResolveError::InstallPmsNotDetected { .. }));
let msg = format!("{err}");
assert!(msg.contains("pnpm"), "names the missing PM: {msg}");
assert!(msg.contains("bun"), "lists detected: {msg}");
}
#[test]
fn pm_override_wins_over_install_pms_allowlist() {
let ctx = context(vec![PackageManager::Bun, PackageManager::Deno]);
let mut overrides = override_pm(PackageManager::Deno, OverrideOrigin::EnvVar);
overrides.install_pms = vec![PackageManager::Bun];
let pms = select_install_pms(&ctx, &overrides).expect("override wins");
assert_eq!(pms, vec![PackageManager::Deno]);
}
#[test]
fn empty_install_pms_installs_with_every_detected_pm() {
let ctx = context(vec![PackageManager::Bun, PackageManager::Cargo]);
let pms = select_install_pms(&ctx, &ResolutionOverrides::default())
.expect("no allowlist installs all");
assert_eq!(pms, vec![PackageManager::Bun, PackageManager::Cargo]);
}
#[test]
fn ecosystem_config_override_does_not_filter_installs() {
let ctx = context(vec![PackageManager::Bun, PackageManager::Cargo]);
let mut pm_by_ecosystem = HashMap::new();
pm_by_ecosystem.insert(
Ecosystem::Node,
PmOverride {
pm: PackageManager::Pnpm,
origin: OverrideOrigin::ConfigFile {
path: PathBuf::from("/tmp/test/runner.toml"),
},
},
);
let overrides = ResolutionOverrides {
pm_by_ecosystem,
..Default::default()
};
let pms = select_install_pms(&ctx, &overrides).expect("config must not filter installs");
assert_eq!(pms, vec![PackageManager::Bun, PackageManager::Cargo]);
}
fn install_argv(pm: PackageManager, scripts: ScriptDirective) -> Vec<String> {
let ctx = context(vec![pm]);
build_install_command(&ctx, pm, false, scripts)
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect()
}
#[test]
fn deny_support_classifies_every_pm() {
for pm in [
PackageManager::Npm,
PackageManager::Yarn,
PackageManager::Pnpm,
PackageManager::Bun,
PackageManager::Composer,
] {
assert_eq!(
deny_support(pm),
DenySupport::ViaFlag,
"{} via flag",
pm.label()
);
}
assert_eq!(deny_support(PackageManager::Deno), DenySupport::DefaultDeny);
for pm in [
PackageManager::Cargo,
PackageManager::Uv,
PackageManager::Poetry,
PackageManager::Pipenv,
PackageManager::Go,
PackageManager::Bundler,
] {
assert_eq!(
deny_support(pm),
DenySupport::Unsupported,
"{} unsupported",
pm.label(),
);
}
}
#[test]
fn force_support_classifies_every_pm() {
for pm in [
PackageManager::Npm,
PackageManager::Deno,
PackageManager::Yarn,
] {
assert_eq!(
force_support(pm),
ForceSupport::ViaFlag,
"{} via flag",
pm.label(),
);
}
for pm in [PackageManager::Bun, PackageManager::Pnpm] {
assert_eq!(
force_support(pm),
ForceSupport::NotExpressible,
"{} not expressible",
pm.label(),
);
}
for pm in [
PackageManager::Composer,
PackageManager::Cargo,
PackageManager::Uv,
PackageManager::Poetry,
PackageManager::Pipenv,
PackageManager::Go,
PackageManager::Bundler,
] {
assert_eq!(
force_support(pm),
ForceSupport::AlreadyRuns,
"{} already runs",
pm.label(),
);
}
}
#[test]
fn script_directive_lowers_every_policy() {
let directive = |policy| {
script_directive(&ResolutionOverrides {
script_policy: policy,
..ResolutionOverrides::default()
})
};
assert_eq!(directive(ScriptPolicy::Default), ScriptDirective::Default);
assert_eq!(directive(ScriptPolicy::Deny), ScriptDirective::Deny);
assert_eq!(directive(ScriptPolicy::Allow), ScriptDirective::ForceOn);
}
#[test]
fn deny_appends_skip_flag_for_flag_managers() {
assert_eq!(
install_argv(PackageManager::Npm, ScriptDirective::Deny),
["install", "--ignore-scripts"]
);
assert_eq!(
install_argv(PackageManager::Pnpm, ScriptDirective::Deny),
["install", "--ignore-scripts"]
);
assert_eq!(
install_argv(PackageManager::Bun, ScriptDirective::Deny),
["install", "--ignore-scripts"]
);
assert_eq!(
install_argv(PackageManager::Composer, ScriptDirective::Deny),
["install", "--no-scripts"]
);
}
#[test]
fn deny_is_noop_for_default_deny_and_unsupported_managers() {
assert_eq!(
install_argv(PackageManager::Deno, ScriptDirective::Deny),
["install"]
);
assert_eq!(
install_argv(PackageManager::Cargo, ScriptDirective::Deny),
["fetch"]
);
}
#[test]
fn force_on_appends_flag_for_flag_managers() {
assert_eq!(
install_argv(PackageManager::Npm, ScriptDirective::ForceOn),
["install", "--no-ignore-scripts"]
);
assert_eq!(
install_argv(PackageManager::Deno, ScriptDirective::ForceOn),
["install", "--allow-scripts"]
);
}
#[test]
fn force_on_is_noop_for_already_runs_and_unforceable_managers() {
assert_eq!(
install_argv(PackageManager::Composer, ScriptDirective::ForceOn),
["install"]
);
assert_eq!(
install_argv(PackageManager::Cargo, ScriptDirective::ForceOn),
["fetch"]
);
assert_eq!(
install_argv(PackageManager::Bun, ScriptDirective::ForceOn),
["install"]
);
assert_eq!(
install_argv(PackageManager::Pnpm, ScriptDirective::ForceOn),
["install"]
);
}
#[test]
fn default_adds_no_skip_flag() {
assert_eq!(
install_argv(PackageManager::Npm, ScriptDirective::Default),
["install"]
);
assert_eq!(
install_argv(PackageManager::Composer, ScriptDirective::Default),
["install"]
);
}
#[test]
fn deny_disclosure_fires_for_unsupported_pms_regardless_of_no_warnings() {
let pms = [PackageManager::Cargo, PackageManager::Npm];
assert!(unsupported_deny_managers(&pms, &ResolutionOverrides::default()).is_empty());
let denying = ResolutionOverrides {
script_policy: ScriptPolicy::Deny,
no_warnings: true,
..ResolutionOverrides::default()
};
assert_eq!(
unsupported_deny_managers(&pms, &denying),
vec![PackageManager::Cargo]
);
warn_unsupported_script_policy(&pms, &denying);
}
#[test]
fn force_disclosure_fires_for_unforceable_pms_regardless_of_no_warnings() {
let pms = [
PackageManager::Pnpm,
PackageManager::Npm,
PackageManager::Bun,
];
assert!(unforceable_managers(&pms, &ResolutionOverrides::default()).is_empty());
let forcing = ResolutionOverrides {
script_policy: ScriptPolicy::Allow,
no_warnings: true,
..ResolutionOverrides::default()
};
assert_eq!(
unforceable_managers(&pms, &forcing),
vec![PackageManager::Pnpm, PackageManager::Bun]
);
warn_unsupported_script_policy(&pms, &forcing);
}
}