use std::sync::Arc;
use async_trait::async_trait;
use crate::result::Result;
use crate::system::ManagerPackageOptions;
pub(crate) mod apk;
pub(crate) mod apt;
pub(crate) mod aur;
#[cfg(unix)]
pub(crate) mod brew;
pub(crate) mod dnf;
pub(crate) mod flatpak;
pub(crate) mod mas;
pub(crate) mod nix;
pub(crate) mod pacman;
pub(crate) mod plugin;
pub(crate) mod scoop;
pub(crate) mod winget;
pub(crate) mod zypper;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct PackageRequest {
pub name: String,
pub version: Option<String>,
pub tap_url: Option<String>,
pub desired: PackageDesiredState,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub(crate) enum PackageDesiredState {
#[default]
Present,
Absent,
}
impl std::fmt::Display for PackageRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.version {
Some(v) => write!(f, "{}@{}", self.name, v),
None => write!(f, "{}", self.name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PackageState {
Installed {
version: String,
},
#[cfg(unix)]
InstalledAutoUpdates {
version: String,
},
Missing,
NeedsRepair {
installed: String,
},
VersionMismatch {
installed: String,
},
#[cfg(unix)]
Unavailable {
reason: String,
},
}
impl PackageState {
pub(crate) fn is_installed(&self) -> bool {
match self {
Self::Installed { .. } => true,
#[cfg(unix)]
Self::InstalledAutoUpdates { .. } => true,
_ => false,
}
}
pub(crate) fn auto_updates(&self) -> bool {
match self {
#[cfg(unix)]
Self::InstalledAutoUpdates { .. } => true,
_ => false,
}
}
#[cfg(unix)]
pub(crate) fn unavailable(reason: impl Into<String>) -> Self {
Self::Unavailable {
reason: reason.into(),
}
}
pub(crate) fn is_unavailable(&self) -> bool {
#[cfg(unix)]
if matches!(self, Self::Unavailable { .. }) {
return true;
}
false
}
pub(crate) fn unavailable_reason(&self) -> Option<&str> {
#[cfg(unix)]
if let Self::Unavailable { reason } = self {
return Some(reason);
}
None
}
}
#[derive(Debug, Clone)]
pub(crate) struct PackageStatus {
pub request: PackageRequest,
pub state: PackageState,
}
#[derive(Debug, Default)]
pub(crate) struct InstallOpts {
pub dry_run: bool,
pub update: bool,
}
#[async_trait(?Send)]
pub(crate) trait SystemPackageManager: Send + Sync {
fn name(&self) -> &str;
fn is_available(&self) -> bool;
fn unavailable_reason(&self) -> String;
async fn unavailable_reason_async(&self) -> Option<String> {
(!self.is_available()).then(|| self.unavailable_reason())
}
async fn installed(&self, pkgs: &[PackageRequest]) -> Result<Vec<PackageStatus>>;
async fn prepare_mutation(&self, _pkgs: &[PackageRequest]) -> Result<()> {
Ok(())
}
async fn available(&self, names: &[String]) -> Result<Vec<bool>> {
Ok(vec![true; names.len()])
}
async fn install(&self, pkgs: &[PackageRequest], opts: &InstallOpts) -> Result<()>;
async fn remove(&self, _pkgs: &[PackageRequest], _opts: &InstallOpts) -> Result<()> {
eyre::bail!(
"{} does not support declarative package removal",
self.name()
)
}
fn supports_remove(&self) -> bool {
false
}
async fn installed_with_options(
&self,
pkgs: &[PackageRequest],
_manager_options: &ManagerPackageOptions,
) -> Result<Vec<PackageStatus>> {
self.installed(pkgs).await
}
async fn upgrade_with_options(
&self,
pkgs: &[PackageRequest],
opts: &InstallOpts,
_manager_options: &ManagerPackageOptions,
) -> Result<()> {
self.upgrade(pkgs, opts).await
}
async fn install_with_options(
&self,
pkgs: &[PackageRequest],
opts: &InstallOpts,
_manager_options: &ManagerPackageOptions,
) -> Result<()> {
self.install(pkgs, opts).await
}
async fn upgrade(&self, pkgs: &[PackageRequest], opts: &InstallOpts) -> Result<()> {
self.install(pkgs, opts).await
}
fn supports_version_pins(&self) -> bool {
true
}
fn is_plugin(&self) -> bool {
false
}
fn package_identity(&self, _name: &str) -> Option<String> {
None
}
}
pub(crate) fn check_name_conflicts(
manager: &dyn SystemPackageManager,
pkgs: &[PackageRequest],
) -> Result<()> {
let mgr = manager.name();
let identities = pkgs
.iter()
.map(|pkg| manager.package_identity(&pkg.name))
.collect::<Vec<_>>();
for (index, pkg) in pkgs.iter().enumerate() {
let Some(identity) = &identities[index] else {
continue;
};
let rest = pkgs[index + 1..].iter().zip(&identities[index + 1..]);
for (other, other_identity) in rest {
if other_identity.as_ref() != Some(identity) {
continue;
}
if pkg.desired != other.desired {
let (present, absent) = match pkg.desired {
PackageDesiredState::Present => (&pkg.name, &other.name),
PackageDesiredState::Absent => (&other.name, &pkg.name),
};
eyre::bail!(
"[bootstrap.packages]: '{mgr}:{present}' and '{mgr}:{absent}' name the same \
package but ask for opposite states; declare it once"
);
}
if pkg.desired == PackageDesiredState::Present && pkg.version != other.version {
eyre::bail!(
"[bootstrap.packages]: '{mgr}:{}' and '{mgr}:{}' name the same package but \
ask for different versions ({} and {}); declare it once",
pkg.name,
other.name,
pkg.version.as_deref().unwrap_or("latest"),
other.version.as_deref().unwrap_or("latest"),
);
}
}
}
Ok(())
}
pub(crate) fn builtin_managers() -> Vec<Arc<dyn SystemPackageManager>> {
vec![
Arc::new(apk::ApkManager::new()),
Arc::new(apt::AptManager::new()),
Arc::new(aur::AurManager::new()),
#[cfg(unix)]
Arc::new(brew::BrewManager::new()),
#[cfg(unix)]
Arc::new(brew::BrewCaskManager::new()),
#[cfg(unix)]
Arc::new(brew::BrewCaskManager::new_macos_app()),
Arc::new(dnf::DnfManager::new()),
Arc::new(flatpak::FlatpakManager::new()),
Arc::new(flatpak::FlatpakManager::new_user()),
Arc::new(mas::MasManager::new()),
Arc::new(nix::NixManager),
Arc::new(pacman::PacmanManager::new()),
Arc::new(scoop::ScoopManager::new()),
Arc::new(winget::WingetManager::new()),
Arc::new(zypper::ZypperManager),
]
}
pub(crate) fn is_builtin_manager_name(name: &str) -> bool {
builtin_managers()
.iter()
.any(|manager| manager.name() == name)
}
pub(crate) fn all_managers() -> Vec<Arc<dyn SystemPackageManager>> {
let mut managers = builtin_managers();
let builtins = managers
.iter()
.map(|manager| manager.name().to_string())
.collect::<std::collections::HashSet<_>>();
let Some(plugins) = crate::toolset::install_state::try_list_plugins() else {
return managers;
};
for (name, plugin_type) in plugins.iter() {
if *plugin_type != crate::plugins::PluginType::Package {
continue;
}
if builtins.contains(name) {
warn!(
"package plugin '{name}' collides with a built-in package manager; ignoring plugin"
);
continue;
}
match plugin::PackagePluginManager::new(name.clone()) {
Ok(manager) => managers.push(Arc::new(manager)),
Err(err) => warn!("failed to load package plugin '{name}': {err:#}"),
}
}
managers
}
#[cfg(test)]
mod tests {
use super::*;
struct VerbatimManager;
#[async_trait(?Send)]
impl SystemPackageManager for VerbatimManager {
fn name(&self) -> &str {
"verbatim"
}
fn is_available(&self) -> bool {
true
}
fn unavailable_reason(&self) -> String {
unreachable!()
}
async fn installed(&self, _pkgs: &[PackageRequest]) -> Result<Vec<PackageStatus>> {
unreachable!()
}
async fn install(&self, _pkgs: &[PackageRequest], _opts: &InstallOpts) -> Result<()> {
unreachable!()
}
}
struct FoldingManager;
#[async_trait(?Send)]
impl SystemPackageManager for FoldingManager {
fn name(&self) -> &str {
"folding"
}
fn is_available(&self) -> bool {
true
}
fn unavailable_reason(&self) -> String {
unreachable!()
}
fn package_identity(&self, name: &str) -> Option<String> {
Some(name.to_ascii_lowercase())
}
async fn installed(&self, _pkgs: &[PackageRequest]) -> Result<Vec<PackageStatus>> {
unreachable!()
}
async fn install(&self, _pkgs: &[PackageRequest], _opts: &InstallOpts) -> Result<()> {
unreachable!()
}
}
fn request(name: &str, version: Option<&str>, desired: PackageDesiredState) -> PackageRequest {
PackageRequest {
name: name.to_string(),
version: version.map(str::to_string),
tap_url: None,
desired,
}
}
#[test]
fn folded_names_with_opposite_states_are_rejected() {
let pkgs = vec![
request("Git.Git", None, PackageDesiredState::Present),
request("git.git", None, PackageDesiredState::Absent),
];
let err = check_name_conflicts(&FoldingManager, &pkgs).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("'folding:Git.Git'"), "{msg}");
assert!(msg.contains("'folding:git.git'"), "{msg}");
assert!(msg.contains("opposite states"), "{msg}");
let reversed = vec![pkgs[1].clone(), pkgs[0].clone()];
assert_eq!(
check_name_conflicts(&FoldingManager, &reversed)
.unwrap_err()
.to_string(),
msg
);
}
#[test]
fn folded_names_with_different_versions_are_rejected() {
let pkgs = vec![
request("Git.Git", Some("2.43.0"), PackageDesiredState::Present),
request("git.git", None, PackageDesiredState::Present),
];
let msg = check_name_conflicts(&FoldingManager, &pkgs)
.unwrap_err()
.to_string();
assert!(
msg.contains("different versions (2.43.0 and latest)"),
"{msg}"
);
}
#[test]
fn folded_names_that_agree_are_left_alone() {
let present = vec![
request("Git.Git", Some("2.43.0"), PackageDesiredState::Present),
request("git.git", Some("2.43.0"), PackageDesiredState::Present),
];
assert!(check_name_conflicts(&FoldingManager, &present).is_ok());
let absent = vec![
request("Git.Git", None, PackageDesiredState::Absent),
request("git.git", Some("2.43.0"), PackageDesiredState::Absent),
];
assert!(check_name_conflicts(&FoldingManager, &absent).is_ok());
}
#[test]
fn verbatim_names_that_differ_only_by_case_are_two_packages() {
let pkgs = vec![
request("Git", None, PackageDesiredState::Present),
request("git", None, PackageDesiredState::Absent),
];
assert!(check_name_conflicts(&VerbatimManager, &pkgs).is_ok());
}
#[test]
fn winget_folds_package_ids() {
let winget = winget::WingetManager::new();
assert_eq!(
winget.package_identity("Git.Git"),
Some("git.git".to_string())
);
}
#[test]
fn apt_compares_package_names_verbatim() {
assert_eq!(apt::AptManager::new().package_identity("Git"), None);
}
}