Source code for meta_package_manager.managers.whalebrew

# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

from __future__ import annotations

import re
from typing import ClassVar

from extra_platforms import LINUX_LIKE, MACOS

from ..capabilities import search_capabilities, version_not_implemented
from ..manager import PackageManager

TYPE_CHECKING = False
if TYPE_CHECKING:
    from collections.abc import Iterator

    from ..package import Package


[docs] class Whalebrew(PackageManager): """Whalebrew, installing Docker images as commands. A package is a Docker image, identified as the listing prints it: `whalebrew/wget`, or `hello-world:linux` where a tag was given. Whalebrew writes one small YAML file per image into its install directory, named after the command the image provides, and runs the image through `docker run` each time that command is called. The command is reported as the package name. `install` needs a running Docker daemon the user can reach, and refuses an image that declares no `ENTRYPOINT`. `mpm install` looks the image up through `search` first, which only reaches the registries Whalebrew searches: install an image from anywhere else with `whalebrew install` itself, and the listing reports it like any other. Packages go to `/usr/local/bin` by default, which a regular user cannot write to on Linux. Point `WHALEBREW_INSTALL_PATH`, or `install_path` in `~/.whalebrew/config.yaml`, at a directory of your own, like `~/.whalebrew/bin`. """ # Whalebrew keys its two mutating verbs on different things: `install` takes an # image, while `uninstall` finds the package by its command or its image and # then deletes a file named after its argument, so an image fails with "no such # file or directory". The fix, whalebrew/whalebrew#323, has waited since # 2025-03. Packages are keyed on the image, the one identifier `search`, # `install` and the listing share, and `remove` looks the command up first. # # `outdated` and `upgrade`: the README upgrades a package by pulling its image # with `docker pull`, a Docker verb rather than a Whalebrew one, and nothing # reports a newer image without pulling it. operation_notes: ClassVar = { "outdated": "Nothing reports a newer image without pulling it.", "upgrade": "Upgrading is pulling the image with Docker, not a Whalebrew verb.", "upgrade_all": "Upgrading is pulling each image with Docker.", } name = "Whalebrew" repository_url = "https://github.com/whalebrew/whalebrew" platforms = LINUX_LIKE, MACOS requirement = ">=0.4.1" """The release restoring `search` after Docker Hub changed its API. The official `0.5.0` binary reports itself as a `0.4.1` build, having been compiled from a tree `git describe` places 38 commits past that tag, so a higher floor would reject it. """ version_cli_options = ("version",) version_regexes = (r"^Whalebrew[ \t]+(?P<version>\d+\.\d+\.\d+)",) """Keep the release part of the version string. ```{code-block} shell-session $ whalebrew version Whalebrew 0.4.1-38-g79b8605-dirty+2024-08-02.79b860575946278fd3215e68677d7439424d1cd1 ``` """ _INSTALLED_REGEXP = re.compile(r"^(?P<command>\S+)[ \t]+(?P<package_id>\S+)$") """One package per line: the command, then the image it runs.""" @property def installed(self) -> Iterator[Package]: """Fetch installed packages. ```{code-block} shell-session $ whalebrew list --no-headers hello-world hello-world:linux ``` """ output = self.run_cli("list", "--no-headers") for match in map(self._INSTALLED_REGEXP.match, output.splitlines()): if match: yield self.package( id=match.group("package_id"), name=match.group("command"), )
[docs] @search_capabilities(extended_support=False, exact_support=False) def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]: """Fetch matching packages. Queries the registries Whalebrew is configured with, Docker Hub's `whalebrew` organization by default, for images carrying the labels a Whalebrew package declares. The term is matched against the image name within each registry, so `whalebrew/wget` finds nothing where `wget` finds it: the namespace and tag of the query are dropped before sending. ```{caution} Search does not support extended or exact matching. ``` ```{code-block} shell-session $ whalebrew search wget whalebrew/wget ``` """ term = query.rsplit("/", 1)[-1].split(":", 1)[0] output = self.run_cli("search", term) for line in output.splitlines(): image = line.strip() if image: yield self.package(id=image)
[docs] @version_not_implemented def install(self, package_id: str, version: str | None = None) -> str: """Install one package. `--assume-yes` accepts the ports, volumes and environment an image asks for, which Whalebrew otherwise confirms on the terminal. ```{code-block} shell-session $ whalebrew install --assume-yes whalebrew/wget ``` """ return self.run_cli("install", "--assume-yes", package_id)
[docs] def remove(self, package_id: str) -> str: """Removes a package. Whalebrew deletes the file named after its argument, so the image is swapped for the command the listing pairs it with. ```{code-block} shell-session $ whalebrew uninstall --assume-yes wget ``` """ with self.acting_as("installed"): command = next( ( package.name for package in self.installed if package.id == package_id and package.name ), package_id, ) return self.run_cli("uninstall", "--assume-yes", command)