mise 2026.8.11

Dev tools, env vars, and tasks in one CLI
//! Host package managers (apk, apt, brew, brew-cask, flatpak, flatpak-user, mas) for the `[bootstrap.packages]` config section.
//!
//! These are host-owned, unversioned packages — deliberately separate from
//! the `Backend` system, which manages per-project, version-pinned dev tools.

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;
#[cfg(unix)]
pub(crate) mod brew;
pub(crate) mod dnf;
pub(crate) mod flatpak;
pub(crate) mod mas;
pub(crate) mod pacman;
pub(crate) mod plugin;

/// A single package entry from `[bootstrap.packages]` — the part after the
/// `manager:` prefix of a `"manager:package" = "version"` config entry.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct PackageRequest {
    /// package name as written in the spec (apt: may carry an `:arch`
    /// qualifier like "gcc:arm64"; brew/brew-cask: full name incl. "@17")
    pub name: String,
    /// version pin from the config value (`"latest"` parses to None). Each
    /// manager renders this into its native pin syntax at install time
    /// (apt: `name=version`, dnf: `name-version`).
    pub version: Option<String>,
    /// manager-specific source URL. Currently used by brew tapped formulae
    /// and casks: `[bootstrap.brew.taps]` can attach a git URL to
    /// `owner/tap/name`.
    pub tap_url: Option<String>,
}

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,
    },
    /// Installed cask whose upstream definition declares `auto_updates`.
    /// The version is the cask receipt version, not necessarily the live app
    /// bundle version after it has updated itself.
    #[cfg(unix)]
    InstalledAutoUpdates {
        version: String,
    },
    Missing,
    /// installed, but a manager-owned record needs local repair
    #[cfg_attr(windows, allow(dead_code))]
    NeedsRepair {
        installed: String,
    },
    /// installed, but the version pinned in config doesn't match
    VersionMismatch {
        installed: String,
    },
    /// The manager is available on this host, but this individual package is
    /// not supported on the current platform.
    #[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 {
    /// print what would be done without doing it
    pub dry_run: bool,
    /// force a package manager metadata refresh before installing
    pub update: bool,
}

// `?Send`: the brew manager's source-build path drives the toolset
// machinery (to provision ruby), which holds non-Send shell state across
// awaits. The driver awaits managers sequentially on one task, so the
// futures never cross threads.
#[async_trait(?Send)]
pub(crate) trait SystemPackageManager: Send + Sync {
    /// config key, e.g. "apt", "brew"
    fn name(&self) -> &str;

    /// whether this manager can run on this machine (OS + required binaries).
    /// Entries for unavailable managers are silently skipped so configs can be
    /// shared across platforms.
    fn is_available(&self) -> bool;

    /// human-readable reason `is_available()` is false, for `status`/`doctor`
    fn unavailable_reason(&self) -> String;

    /// Return why this manager cannot run, or `None` when it is available.
    ///
    /// The async form lets plugin managers resolve host binaries from mise's
    /// global toolset as well as the process PATH and shims. Built-in managers
    /// use the synchronous checks above by default.
    async fn unavailable_reason_async(&self) -> Option<String> {
        (!self.is_available()).then(|| self.unavailable_reason())
    }

    /// Query installed state. Must be side-effect free and never elevate.
    async fn installed(&self, pkgs: &[PackageRequest]) -> Result<Vec<PackageStatus>>;

    /// Whether each name exists as an installable package, positionally.
    ///
    /// This is *availability*, not installed state — [`Self::installed`]
    /// cannot answer it (apt's asks dpkg, which only knows what is already on
    /// the box). Used to resolve a plugin's candidate package names, where the
    /// same capability is packaged under different names across distro
    /// releases. Must be side-effect free and never elevate.
    ///
    /// The default reports every name as available, which makes candidate
    /// resolution pick the first one — the behavior before candidate lists
    /// existed. Managers override it where the query is cheap.
    async fn available(&self, names: &[String]) -> Result<Vec<bool>> {
        Ok(vec![true; names.len()])
    }

    /// Install the given packages (already filtered to missing, mismatched, or repairable).
    async fn install(&self, pkgs: &[PackageRequest], opts: &InstallOpts) -> Result<()>;

    /// Install with manager-specific declarative options. Managers without
    /// additional package options use the ordinary install path unchanged.
    async fn install_with_options(
        &self,
        pkgs: &[PackageRequest],
        opts: &InstallOpts,
        _manager_options: &ManagerPackageOptions,
    ) -> Result<()> {
        self.install(pkgs, opts).await
    }

    /// Upgrade the given packages (already filtered to installed ones).
    /// Defaults to `install` — for brew that is exactly right (pouring a
    /// formula whose current version differs replaces the old keg), and apt/
    /// dnf/pacman override to refresh metadata first and use their native
    /// upgrade invocation.
    async fn upgrade(&self, pkgs: &[PackageRequest], opts: &InstallOpts) -> Result<()> {
        self.install(pkgs, opts).await
    }

    /// Can `install` satisfy a version pin? pacman (Arch repos only carry
    /// the latest version) and brew (bottles only exist for a formula's
    /// current version) cannot — their pins are status-only, and the
    /// install command skips them with a warning instead of failing the
    /// rest of the batch.
    fn supports_version_pins(&self) -> bool {
        true
    }

    /// Whether this manager is supplied by a package plugin.
    #[allow(dead_code)] // used by the stacked bootstrap orchestration change
    fn is_plugin(&self) -> bool {
        false
    }
}

pub(crate) fn builtin_managers() -> Vec<Arc<dyn SystemPackageManager>> {
    vec![
        Arc::new(apk::ApkManager::new()),
        Arc::new(apt::AptManager::new()),
        #[cfg(unix)]
        Arc::new(brew::BrewManager::new()),
        #[cfg(unix)]
        Arc::new(brew::BrewCaskManager::new()),
        Arc::new(dnf::DnfManager::new()),
        Arc::new(flatpak::FlatpakManager::new()),
        Arc::new(flatpak::FlatpakManager::new_user()),
        Arc::new(mas::MasManager::new()),
        Arc::new(pacman::PacmanManager::new()),
    ]
}

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
}