use std::collections::HashMap;
use eyre::{Result, bail};
use crate::config::Settings;
use crate::system::ManagerPackages;
use crate::system::packages::{InstallOpts, PackageState, PackageStatus};
use crate::ui::prompt;
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum Action {
Install,
Upgrade,
}
impl Action {
fn verb(self) -> &'static str {
match self {
Action::Install => "install",
Action::Upgrade => "upgrade",
}
}
}
pub(crate) struct DriverOpts {
pub manager: Option<String>,
pub explicit: bool,
pub allow_unavailable_manager: bool,
pub dry_run: bool,
pub update: bool,
pub yes: bool,
}
fn unavailable_manager_is_error(d: &DriverOpts) -> bool {
(d.manager.is_some() || d.explicit) && !d.allow_unavailable_manager
}
fn unavailable_package_reason<'a>(
d: &DriverOpts,
statuses: &'a [PackageStatus],
) -> Option<&'a str> {
if !d.explicit {
return None;
}
statuses
.iter()
.find_map(|status| status.state.unavailable_reason())
}
pub(crate) async fn run(mgrs: Vec<ManagerPackages>, action: Action, d: &DriverOpts) -> Result<()> {
if let Some(only) = &d.manager
&& !mgrs.iter().any(|mp| mp.manager.name() == only)
{
if let Some(enabled) = &Settings::get().system_packages.managers
&& !enabled.contains(only)
{
bail!(
"manager '{only}' is excluded by the system_packages.managers setting \
(currently: {})",
enabled.join(", ")
);
}
bail!("no packages requested for manager '{only}'");
}
if mgrs.is_empty() {
info!("no bootstrap packages configured in [bootstrap.packages]");
return Ok(());
}
let opts = InstallOpts {
dry_run: d.dry_run,
update: d.update,
};
for mp in mgrs {
if let Some(only) = &d.manager
&& mp.manager.name() != only
{
continue;
}
let name = mp.manager.name();
if mp.disabled {
if d.manager.is_some() {
bail!("manager '{name}' is excluded by the system_packages.managers setting");
}
debug!("{name}: skipping, excluded by system_packages.managers");
continue;
}
if let Some(reason) = mp.manager.unavailable_reason_async().await {
if unavailable_manager_is_error(d) {
bail!("{name} is not available: {}", reason);
}
debug!("{name}: skipping, {reason}");
continue;
}
let statuses = mp.manager.installed(&mp.requests).await?;
if let Some(reason) = unavailable_package_reason(d, &statuses) {
bail!("{reason}");
}
let mut targets: Vec<_> = statuses
.iter()
.filter(|s| match action {
Action::Install => !s.state.is_installed() && !s.state.is_unavailable(),
Action::Upgrade => {
!matches!(s.state, PackageState::Missing) && !s.state.is_unavailable()
}
})
.collect();
let missing = statuses
.iter()
.filter(|status| matches!(status.state, PackageState::Missing))
.count();
if action == Action::Upgrade && missing > 0 {
warn!(
"{name}: {missing} package(s) not installed — run `mise bootstrap packages apply` first"
);
}
if !mp.manager.supports_version_pins() {
targets.retain(|status| {
if status.request.version.is_some()
&& !matches!(status.state, PackageState::NeedsRepair { .. })
{
warn!(
"{name}: cannot {} pinned version '{}', skipping",
action.verb(),
status.request
);
false
} else {
true
}
});
}
let installed = statuses
.iter()
.filter(|status| status.state.is_installed())
.count();
if action == Action::Install && installed > 0 {
info!("{name}: {installed} package(s) already installed");
}
if targets.is_empty() {
continue;
}
let targets = targets
.into_iter()
.map(|status| status.request.clone())
.collect::<Vec<_>>();
let list = targets.iter().map(|r| r.to_string()).collect::<Vec<_>>();
if !d.dry_run && !d.yes && console::user_attended_stderr() {
let msg = format!("{name}: {} {}?", action.verb(), list.join(", "));
if !prompt::confirm(msg)?.is_yes() {
info!("{name}: skipped");
continue;
}
}
match action {
Action::Install => {
mp.manager
.install_with_options(&targets, &opts, &mp.options)
.await?;
if !d.dry_run {
info!("{name}: installed {}", list.join(", "));
}
}
Action::Upgrade => {
let prior: HashMap<String, String> = statuses
.iter()
.filter_map(|s| match &s.state {
PackageState::Installed { version }
| PackageState::NeedsRepair { installed: version }
| PackageState::VersionMismatch { installed: version } => {
Some((s.request.name.clone(), version.clone()))
}
#[cfg(unix)]
PackageState::InstalledAutoUpdates { version } => {
Some((s.request.name.clone(), version.clone()))
}
PackageState::Missing => None,
#[cfg(unix)]
PackageState::Unavailable { .. } => None,
})
.collect();
mp.manager.upgrade(&targets, &opts).await?;
if !d.dry_run {
let after = mp.manager.installed(&targets).await?;
let changed: Vec<String> = after
.iter()
.filter_map(|s| match &s.state {
PackageState::Installed { version }
| PackageState::NeedsRepair { installed: version }
| PackageState::VersionMismatch { installed: version } => {
let old = prior.get(&s.request.name)?;
(old != version)
.then(|| format!("{} {old} -> {version}", s.request.name))
}
#[cfg(unix)]
PackageState::InstalledAutoUpdates { version } => {
let old = prior.get(&s.request.name)?;
(old != version)
.then(|| format!("{} {old} -> {version}", s.request.name))
}
PackageState::Missing => None,
#[cfg(unix)]
PackageState::Unavailable { .. } => None,
})
.collect();
if changed.is_empty() {
info!("{name}: already up to date");
} else {
info!("{name}: upgraded {}", changed.join(", "));
}
}
}
}
}
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use crate::system::packages::PackageRequest;
#[test]
fn explicit_packages_reject_unavailable_entries_but_manager_filters_skip_them() {
let explicit_opts = DriverOpts {
manager: None,
explicit: true,
allow_unavailable_manager: true,
dry_run: false,
update: false,
yes: true,
};
let statuses = vec![PackageStatus {
request: PackageRequest {
name: "example".to_string(),
version: None,
tap_url: None,
},
state: PackageState::Unavailable {
reason: "unsupported on this platform".to_string(),
},
}];
assert!(!unavailable_manager_is_error(&explicit_opts));
assert_eq!(
unavailable_package_reason(&explicit_opts, &statuses),
Some("unsupported on this platform")
);
let manager_opts = DriverOpts {
manager: Some("brew-cask".to_string()),
explicit: false,
allow_unavailable_manager: false,
dry_run: false,
update: false,
yes: true,
};
assert!(unavailable_manager_is_error(&manager_opts));
assert_eq!(unavailable_package_reason(&manager_opts, &statuses), None);
}
}