Source code for meta_package_manager.managers.am

# Copyright Kevin Deldycke <kevin@deldycke.com> 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

from typing import ClassVar

from extra_platforms import LINUX

from ..manager import PackageManager

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

    from ..package import Package


_ROW_MARKER = "β—†"
"""Character opening every application row, and nothing else in the listing."""

_NAME_COLUMN = "APPNAME"
_VERSION_COLUMN = "VERSION"
"""Header labels the column layout is read from, rather than assumed."""

_DECORATIONS = "*πŸ”’βœ“βœ– "
"""Marks the listing appends to a name or a version, carrying state rather than
identity: an asterisk for an archived or obsolete application, a padlock for a
locked or sandboxed one, and a tick or cross for the checksum verdict.
"""


[docs] class AM(PackageManager): """AppImage manager, covering the applications of its own catalog. ```{note} `am` and `appman` are the same script under two names, and only this one is wrapped. AppMan's repository holds no implementation at all, just a stub that replaces itself with this script, which then reads its own path to decide whether to run system-wide or under the user's home. Wrapping both would double-count: `am -fi` renders AppMan's applications in a second table of its own whenever AppMan is configured, so they are already reported here. See {doc}`/unsupported` for the recorded decision. ``` ```{caution} The listing's column count depends on its contents: a fifth column appears between the name and the version whenever an application resolves to a third-party catalog. The version is therefore located by reading the header rather than by counting from the left, which is also what lets a listing carrying two tables be parsed in one pass. ``` ```{note} No `install`. `am` refuses to be run under `sudo` and escalates on its own instead, priming the credential cache before it installs anything, so an install blocks on a password prompt that no flag of its own can answer. Removal is unaffected, `-R` needing no confirmation and no escalation mpm has to arrange. No `outdated` either: nothing reports a remote version without installing it, and the catalog carries no versions at all. Its maintainer declined to publish a machine-readable feed, so this is settled rather than pending, and `upgrade --all` is unaffected. No `search`: results are folded to the terminal width before they are printed, so a record wraps across lines with no marker to rejoin it, and its description is unrecoverable once wrapped. ``` Documentation: [AM](https://github.com/ivan-hc/AM). """ name = "AppImage Manager" homepage_url = "https://github.com/ivan-hc/AM" platforms = LINUX sudo = False default_sudo = False """`am` refuses outright to run under `sudo`, exiting rather than proceeding, and arranges its own escalation per privileged step instead. """ internal_sudo = True """It calls the escalation binary itself for the steps that need it, picking `sudo` or `doas` from what the host provides. """ requirement = ">=10.4" """The release that added the flag answering its confirmation prompts, which is what makes the mutating operations here unattended. """ extra_env: ClassVar = { # Belt and braces: `am` already drops its colouring when its output is # not a terminal, and honours this on top of that. "NO_COLOR": "1", } version_cli_options = ("--version",) version_regexes = (r"(?m)^(?P<version>\d+(?:\.\d+)+)$",) r"""Search a line holding nothing but the version. ```{code-block} shell-session $ am --version 10.4 ``` Anchored to a whole line because `am` prepends a multi-line warning banner on hosts that restrict user namespaces, which is the default on recent Ubuntu: a pattern matching the first number anywhere would read that banner instead. """ def _parse_listing(self, output: str) -> Iterator[Package]: """Yield one package per application row of a listing. Reads each table's header to locate the version column, so a listing holding both this manager's applications and AppMan's is parsed in one pass even when the two carry a different number of columns. """ version_index: int | None = None for line in output.splitlines(): cells = [cell.strip() for cell in line.split("|")] # A header re-declares the layout for every row that follows it. if _NAME_COLUMN in cells[0]: version_index = ( cells.index(_VERSION_COLUMN) if _VERSION_COLUMN in cells else None ) continue if not line.lstrip().startswith(_ROW_MARKER): continue package_id = cells[0].lstrip(_ROW_MARKER).strip(_DECORATIONS) if not package_id: continue version = None if version_index is not None and len(cells) > version_index: version = cells[version_index].strip(_DECORATIONS) or None yield self.package(id=package_id, installed_version=version) @property def installed(self) -> Iterator[Package]: """Fetch installed packages. `-fi` is the listing without the trailing table of unmanaged AppImages, and `--byname` fixes its order, which is otherwise by size. ```{code-block} shell-session $ am --version 10.4 ``` ```{code-block} console $ am -fi --byname YOU HAVE INSTALLED 3 PROGRAMS MANAGED BY "AM" - APPNAME | VERSION | TYPE | SIZE - ------- | ------- | ---- | ---- β—† code | 1.107.1 | dynamic-binary | 450 MiB β—† krita | 5.2.14 | appimage | 322 MiB β—† zoom | 6.4.3.827.glibc2.27 | appimage* | 288 MiB ``` ```{note} A version is genuinely optional here: an application whose updater reports none renders an empty cell, and one is yielded without a version rather than with a placeholder. ``` """ yield from self._parse_listing(self.run_cli("-fi", "--byname"))
[docs] def upgrade_all_cli(self) -> tuple[str, ...]: """Generates the CLI to upgrade all packages. `--apps` confines the run to the installed applications. The bare form additionally refreshes the catalogs and rewrites the `am` script itself, which is not what upgrading packages should mean. ```{code-block} shell-session $ am -y -u --apps ``` """ return self.build_cli("-y", "-u", "--apps")
[docs] def upgrade_one_cli( self, package_id: str, version: str | None = None, ) -> tuple[str, ...]: """Generates the CLI to upgrade the package provided as parameter. Naming an application genuinely restricts the run to it. ```{code-block} shell-session $ am -y -u firefox ``` """ return self.build_cli("-y", "-u", package_id)
[docs] def remove(self, package_id: str) -> str: """Removes a package. `-R` is the unattended removal: unlike its lowercase counterpart it asks for no confirmation, so no flag is needed to answer one. ```{code-block} shell-session $ am -R firefox ``` """ return self.run_cli("-R", package_id)
[docs] def cleanup_cache(self) -> None: """Removes things we don't need anymore. Clears the download caches, the caches applications leave in the home directory, and the launchers left behind by removals. ```{code-block} shell-session $ am -c ``` """ self.run_cli("-c")