defmodule Apero.Pkg do
@moduledoc """
Cross-platform package manager abstraction.
Detects the available system package manager and provides a consistent
interface for installing, updating, querying, searching, removing, and
getting info about packages.
Supported managers: apt, apt-get, dnf, yum, pacman, brew, apk, zypper,
choco (Chocolatey), winget, pkg (FreeBSD), emerge (Gentoo).
"""
@type package_manager ::
:apt
| :apt_get
| :dnf
| :yum
| :pacman
| :brew
| :apk
| :zypper
| :choco
| :winget
| :pkg
| :emerge
| :unknown
@managers [
{:apt, "apt"},
{:apt_get, "apt-get"},
{:dnf, "dnf"},
{:yum, "yum"},
{:pacman, "pacman"},
{:brew, "brew"},
{:apk, "apk"},
{:zypper, "zypper"},
{:choco, "choco"},
{:winget, "winget"},
{:pkg, "pkg"},
{:emerge, "emerge"}
]
@doc "Detects the first available package manager on the system."
@spec detect() :: package_manager()
def detect do
Enum.find_value(@managers, :unknown, fn {pm, cmd} ->
if System.find_executable(cmd), do: pm, else: nil
end)
end
@doc """
Installs a package using the detected package manager.
Runs non-interactively.
## Options
* `:manager` — override detected package manager
"""
@spec install(binary(), keyword()) :: {:ok, binary()} | {:error, binary()}
def install(package, opts \\ []) do
pm = Keyword.get(opts, :manager, detect())
{cmd, args} = install_cmd(pm, package)
run_cmd(cmd, args)
end
@doc "Updates the package index/cache."
@spec update(keyword()) :: {:ok, binary()} | {:error, binary()}
def update(opts \\ []) do
pm = Keyword.get(opts, :manager, detect())
{cmd, args} = update_cmd(pm)
run_cmd(cmd, args)
end
@doc "Returns `true` if the package is installed."
@spec installed?(binary(), keyword()) :: boolean()
def installed?(package, opts \\ []) do
pm = Keyword.get(opts, :manager, detect())
case check_installed_cmd(pm, package) do
{cmd, args} ->
try do
match?({_out, 0}, System.cmd(cmd, args, stderr_to_stdout: true))
rescue
_ -> false
end
:unsupported ->
false
end
end
@doc """
Removes/uninstalls a package.
## Options
* `:manager` — override detected package manager
* `:purge` — remove config files too (apt only)
"""
@spec remove(binary(), keyword()) :: {:ok, binary()} | {:error, binary()}
def remove(package, opts \\ []) do
pm = Keyword.get(opts, :manager, detect())
purge = Keyword.get(opts, :purge, false)
{cmd, args} = remove_cmd(pm, package, purge)
run_cmd(cmd, args)
end
@doc """
Searches for a package by name or keyword.
## Options
* `:manager` — override detected package manager
* `:exact` — exact name match (apt-cache show vs apt-cache search)
"""
@spec search(binary(), keyword()) :: {:ok, binary()} | {:error, binary()}
def search(query, opts \\ []) do
pm = Keyword.get(opts, :manager, detect())
{cmd, args} = search_cmd(pm, query)
run_cmd(cmd, args)
end
@doc """
Gets detailed info about an installed package.
## Options
* `:manager` — override detected package manager
"""
@spec info(binary(), keyword()) :: {:ok, binary()} | {:error, binary()}
def info(package, opts \\ []) do
pm = Keyword.get(opts, :manager, detect())
case info_cmd(pm, package) do
{cmd, args} -> run_cmd(cmd, args)
:unsupported -> {:error, "info not supported for #{pm}"}
end
end
# ── Install commands ─────────────────────────────────────────
defp install_cmd(:apt, pkg), do: {"apt", ["install", "-y", pkg]}
defp install_cmd(:apt_get, pkg), do: {"apt-get", ["install", "-y", pkg]}
defp install_cmd(:dnf, pkg), do: {"dnf", ["install", "-y", pkg]}
defp install_cmd(:yum, pkg), do: {"yum", ["install", "-y", pkg]}
defp install_cmd(:pacman, pkg), do: {"pacman", ["--noconfirm", "-S", pkg]}
defp install_cmd(:brew, pkg), do: {"brew", ["install", pkg]}
defp install_cmd(:apk, pkg), do: {"apk", ["add", "--no-interaction", pkg]}
defp install_cmd(:zypper, pkg), do: {"zypper", ["--non-interactive", "install", pkg]}
defp install_cmd(:choco, pkg), do: {"choco", ["install", pkg, "-y"]}
defp install_cmd(:winget, pkg), do: {"winget", ["install", pkg, "--silent"]}
defp install_cmd(:pkg, pkg), do: {"pkg", ["install", "-y", pkg]}
defp install_cmd(:emerge, pkg), do: {"emerge", ["--noreplace", pkg]}
defp install_cmd(:unknown, pkg), do: {"echo", ["No", "package", "manager", "found", "for", pkg]}
# ── Update commands ──────────────────────────────────────────
defp update_cmd(:apt), do: {"apt", ["update"]}
defp update_cmd(:apt_get), do: {"apt-get", ["update"]}
defp update_cmd(:dnf), do: {"dnf", ["check-update"]}
defp update_cmd(:yum), do: {"yum", ["check-update"]}
defp update_cmd(:pacman), do: {"pacman", ["-Sy"]}
defp update_cmd(:brew), do: {"brew", ["update"]}
defp update_cmd(:apk), do: {"apk", ["update"]}
defp update_cmd(:zypper), do: {"zypper", ["refresh"]}
defp update_cmd(:choco), do: {"choco", ["upgrade", "all", "-y"]}
defp update_cmd(:winget), do: {"winget", ["upgrade"]}
defp update_cmd(:pkg), do: {"pkg", ["update"]}
defp update_cmd(:emerge), do: {"emerge", ["--sync"]}
defp update_cmd(:unknown), do: {"echo", ["No", "package", "manager", "detected"]}
# ── Check-installed commands ─────────────────────────────────
defp check_installed_cmd(:apt, pkg), do: {"dpkg", ["-s", pkg]}
defp check_installed_cmd(:apt_get, pkg), do: {"dpkg", ["-s", pkg]}
defp check_installed_cmd(:dnf, pkg), do: {"rpm", ["-q", pkg]}
defp check_installed_cmd(:yum, pkg), do: {"rpm", ["-q", pkg]}
defp check_installed_cmd(:pacman, pkg), do: {"pacman", ["-Qi", pkg]}
defp check_installed_cmd(:brew, pkg), do: {"brew", ["list", pkg]}
defp check_installed_cmd(:apk, pkg), do: {"apk", ["info", "-e", pkg]}
defp check_installed_cmd(:zypper, pkg), do: {"rpm", ["-q", pkg]}
defp check_installed_cmd(:choco, pkg), do: {"choco", ["list", "--local-only", pkg]}
defp check_installed_cmd(:winget, pkg), do: {"winget", ["list", pkg]}
defp check_installed_cmd(:pkg, pkg), do: {"pkg", ["info", pkg]}
defp check_installed_cmd(:emerge, pkg), do: {"equery", ["list", pkg]}
defp check_installed_cmd(:unknown, _), do: :unsupported
# ── Remove commands ──────────────────────────────────────────
defp remove_cmd(:apt, pkg, true), do: {"apt", ["purge", "-y", pkg]}
defp remove_cmd(:apt, pkg, _), do: {"apt", ["remove", "-y", pkg]}
defp remove_cmd(:apt_get, pkg, true), do: {"apt-get", ["purge", "-y", pkg]}
defp remove_cmd(:apt_get, pkg, _), do: {"apt-get", ["remove", "-y", pkg]}
defp remove_cmd(:dnf, pkg, _), do: {"dnf", ["remove", "-y", pkg]}
defp remove_cmd(:yum, pkg, _), do: {"yum", ["remove", "-y", pkg]}
defp remove_cmd(:pacman, pkg, _), do: {"pacman", ["--noconfirm", "-R", pkg]}
defp remove_cmd(:brew, pkg, _), do: {"brew", ["uninstall", pkg]}
defp remove_cmd(:apk, pkg, _), do: {"apk", ["del", pkg]}
defp remove_cmd(:zypper, pkg, _), do: {"zypper", ["--non-interactive", "remove", pkg]}
defp remove_cmd(:choco, pkg, _), do: {"choco", ["uninstall", pkg, "-y"]}
defp remove_cmd(:winget, pkg, _), do: {"winget", ["uninstall", pkg, "--silent"]}
defp remove_cmd(:pkg, pkg, _), do: {"pkg", ["remove", "-y", pkg]}
defp remove_cmd(:emerge, pkg, _), do: {"emerge", ["--unmerge", pkg]}
defp remove_cmd(:unknown, _, _), do: {"echo", ["No", "package", "manager", "detected"]}
# ── Search commands ──────────────────────────────────────────
defp search_cmd(:apt, q), do: {"apt-cache", ["search", q]}
defp search_cmd(:apt_get, q), do: {"apt-cache", ["search", q]}
defp search_cmd(:dnf, q), do: {"dnf", ["search", q]}
defp search_cmd(:yum, q), do: {"yum", ["search", q]}
defp search_cmd(:pacman, q), do: {"pacman", ["-Ss", q]}
defp search_cmd(:brew, q), do: {"brew", ["search", q]}
defp search_cmd(:apk, q), do: {"apk", ["search", q]}
defp search_cmd(:zypper, q), do: {"zypper", ["search", q]}
defp search_cmd(:choco, q), do: {"choco", ["search", q]}
defp search_cmd(:winget, q), do: {"winget", ["search", q]}
defp search_cmd(:pkg, q), do: {"pkg", ["search", q]}
defp search_cmd(:emerge, q), do: {"emerge", ["--search", q]}
defp search_cmd(:unknown, _), do: {"echo", ["No", "package", "manager", "detected"]}
# ── Info commands ────────────────────────────────────────────
defp info_cmd(:apt, pkg), do: {"apt-cache", ["show", pkg]}
defp info_cmd(:apt_get, pkg), do: {"apt-cache", ["show", pkg]}
defp info_cmd(:dnf, pkg), do: {"dnf", ["info", pkg]}
defp info_cmd(:yum, pkg), do: {"yum", ["info", pkg]}
defp info_cmd(:pacman, pkg), do: {"pacman", ["-Qi", pkg]}
defp info_cmd(:brew, pkg), do: {"brew", ["info", pkg]}
defp info_cmd(:apk, pkg), do: {"apk", ["info", pkg]}
defp info_cmd(:zypper, pkg), do: {"zypper", ["info", pkg]}
defp info_cmd(:choco, pkg), do: {"choco", ["info", pkg]}
defp info_cmd(:winget, pkg), do: {"winget", ["show", pkg]}
defp info_cmd(:pkg, pkg), do: {"pkg", ["info", pkg]}
defp info_cmd(:emerge, pkg), do: {"equery", ["metadata", pkg]}
defp info_cmd(:unknown, _), do: :unsupported
# ── Helper ───────────────────────────────────────────────────
defp run_cmd(cmd, args) when is_list(args) do
case System.cmd(cmd, args, stderr_to_stdout: true) do
{out, 0} -> {:ok, String.trim(out)}
{err, _} -> {:error, String.trim(err)}
end
rescue
e -> {:error, inspect(e)}
end
end