# 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
import re
from typing import Iterator
from extra_platforms import UNIX_WITHOUT_MACOS
from meta_package_manager.base import Package, PackageManager
from meta_package_manager.capabilities import (
search_capabilities,
version_not_implemented,
)
[docs]
class APT(PackageManager):
"""Base package manager shared by variation of the apt command.
Documentation:
- https://wiki.debian.org/AptCLI
- http://manpages.ubuntu.com/manpages/xenial/man8/apt.8.html
See other command equivalences at: https://wiki.archlinux.org/title/Pacman/Rosetta
"""
homepage_url = "https://wiki.debian.org/AptCLI"
platforms = UNIX_WITHOUT_MACOS
requirement = "1.0.0"
pre_args = ("--quiet",)
"""
``--quiet``: produces output suitable for logging, omitting progress indicators.
Source: https://manpages.org/apt-get/8#options
"""
version_regex = r"apt\s+(?P<version>\S+)"
"""
.. code-block:: shell-session
► apt --version
apt 2.0.6 (amd64)
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
.. code-block:: shell-session
► apt --quiet list --installed
Listing...
adduser/xenial,now 3.113+nmu3ubuntu4 all [installed]
bc/xenial,now 1.06.95-9build1 amd64 [installed]
bsdmainutils/xenial,now 9.0.6ubuntu3 amd64 [installed,automatic]
ca-certificates/xenial,now 20160104ubuntu1 all [installed]
cron/xenial,now 3.0pl1-128ubuntu2 amd64 [installed]
debconf/xenial,now 1.5.58ubuntu1 all [installed]
debianutils/xenial,now 4.7 amd64 [installed]
diffutils/xenial,now 1:3.3-3 amd64 [installed]
e2fsprogs/xenial,now 1.42.13-1ubuntu1 amd64 [installed]
ethstatus/xenial,now 0.4.3ubuntu2 amd64 [installed]
file/xenial,now 1:5.25-2ubuntu1 amd64 [installed]
findutils/xenial,now 4.6.0+git+20160126-2 amd64 [installed]
libidn2-0/jammy,now 2.3.2-2build1 amd64 [installed,automatic]
libidn2-0/jammy,now 2.3.2-2build1 i386 [installed,automatic]
"""
output = self.run_cli("list", "--installed")
regexp = re.compile(r"(\S+)\/\S+ (\S+) (\S+) .*")
for package in output.splitlines():
match = regexp.match(package)
if match:
package_id, installed_version, arch = match.groups()
yield self.package(
id=package_id, installed_version=installed_version, arch=arch
)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
.. code-block:: shell-session
► apt --quiet list --upgradable
Listing...
apt/xenial-updates 1.2.19 amd64 [upgradable from: 1.2.15ubuntu0.2]
nano/xenial-updates 2.5.3-2ubuntu2 amd64 [upgradable from: 2.5.3-2]
"""
output = self.run_cli("list", "--upgradable")
regexp = re.compile(r"(\S+)\/\S+ (\S+).*\[upgradable from: (\S+)\]")
for package in output.splitlines():
match = regexp.match(package)
if match:
package_id, latest_version, installed_version = match.groups()
yield self.package(
id=package_id,
latest_version=latest_version,
installed_version=installed_version,
)
[docs]
def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]:
"""Fetch matching packages.
.. code-block:: shell-session
► apt --quiet search abc --names-only
Sorting...
Full Text Search...
abcde/xenial 2.7.1-1 all
A Better CD Encoder
abcmidi/xenial 20160103-1 amd64
converter from ABC to MIDI format and back
berkeley-abc/xenial 1.01+20150706hgc3698e0+dfsg-2 amd64
ABC - A System for Sequential Synthesis and Verification
fuse-overlayfs/jammy,now 1.7.1-1 amd64 [installed]
implementation of overlay+shiftfs in FUSE for rootless containers
grabcd-rip/xenial 0009-1 all
rip and encode audio CDs - ripper
libakonadi-kabc4/xenial 4:4.14.10-1ubuntu2 amd64
Akonadi address book access library
.. code-block:: shell-session
► apt --quiet search ^sed$ --names-only
Sorting...
Full Text Search...
sed/xenial 2.1.9-3 all
Blah blah blah
.. code-block:: shell-session
► apt --quiet search abc --full
Sorting...
Full Text Search...
abcde/xenial 2.7.1-1 all
This package contains the essential basic system utilities.
.
Specifically, this package includes:
basename cat chgrp chmod chown chroot cksum comm cp csplit cut
dircolors dirname du echo env expand expr factor false fmt
hostid id install join link ln logname ls md5sum mkdir mkfifo
nohup od paste pathchk pinky pr printenv printf ptx pwd
sha1sum seq shred sleep sort split stat stty sum sync tac tail
tr true tsort tty uname unexpand uniq unlink users vdir wc who
(...)
midi/xenial 20160103-1 amd64
converter from ABC to MIDI format and back
"""
search_arg = "--names-only"
if exact:
# Rely on apt regexp support to speed-up exact match.
query = f"^{query}$"
# Extended search are always non-exact.
elif extended:
# Include full description in extended search to check up the match
# in the CLI output after its execution.
search_arg = "--full"
output = self.run_cli("search", query, search_arg)
regexp = re.compile(
r"""
^(?P<package_id>\S+) # A string with a char at least.
/\S+\ # A slash, any non-spaced string, then a space.
(?P<version>\S+) # Any non-spaced string.
\ # A space.
(?:.+)\n # Any content ending the line.
(?P<description> # Start of the multi-line desc group.
(?:\ \ .+\n?)+ # Line(s) of content prefixed by 2 spaces.
)
""",
re.MULTILINE | re.VERBOSE,
)
for package_id, version, description in regexp.findall(output):
yield self.package(
id=package_id,
description=description,
latest_version=version,
)
@version_not_implemented
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
.. code-block:: shell-session
► sudo apt --quiet --yes install git
"""
return self.run_cli("--yes", "install", package_id, sudo=True)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all packages (default) or only the one provided
as parameter.
.. code-block:: shell-session
► sudo apt --quiet --yes upgrade
"""
return self.build_cli("--yes", "upgrade", sudo=True)
@version_not_implemented
def upgrade_one_cli(
self,
package_id: str,
version: str | None = None,
) -> tuple[str, ...]:
"""Generates the CLI to upgrade all packages (default) or only the one provided
as parameter.
.. code-block:: shell-session
► sudo apt --quiet --yes install --only-upgrade git
"""
return self.build_cli(
"--yes",
"install",
"--only-upgrade",
package_id,
sudo=True,
)
[docs]
def sync(self) -> None:
"""Sync package metadata.
.. code-block:: shell-session
► sudo apt --quiet --yes update
Hit:1 http://archive.ubuntu.com xenial InRelease
Get:2 http://archive.ubuntu.com xenial-updates InRelease [102 kB]
Get:3 http://archive.ubuntu.com xenial-security InRelease [102 kB]
Get:4 http://archive.ubuntu.com xenial/main Translation-en [568 kB]
Fetched 6,868 kB in 2s (2,680 kB/s)
Reading package lists...
Building dependency tree...
Reading state information...
"""
self.run_cli("--yes", "update", sudo=True)
[docs]
def cleanup(self) -> None:
"""Removes things we don't need anymore.
.. code-block:: shell-session
► sudo apt --quiet --yes autoremove
► sudo apt --quiet --yes clean
"""
for command in ("autoremove", "clean"):
self.run_cli("--yes", command, sudo=True)
[docs]
class APT_Mint(APT):
"""Special version of apt for Linux Mint.
Exactly the same as its parent but implement specific version extraction.
"""
name = "Linux Mint's apt"
homepage_url = "https://github.com/kdeldycke/meta-package-manager/issues/52"
cli_names = ("apt",)
version_cli_options = ("version", "apt")
"""
.. code-block:: shell-session
► apt version apt
1.6.11
"""
[docs]
@search_capabilities(extended_support=False)
def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]:
"""Fetch matching packages.
.. caution::
Search does not supports extended matching.
.. code-block:: shell-session
► /usr/local/bin/apt --quiet search sed
v librust-slog-2.5+erased-serde-dev -
p python3-blessed - Practical wrapper
i sed - GNU stream editor
p sed:i386 - GNU stream editor
.. code-block:: shell-session
► /usr/local/bin/apt --quiet search ^sed$
i sed - GNU stream editor
p sed:i386 - GNU stream editor
"""
if exact:
# Rely on apt regexp support to speed-up exact match.
query = f"^{query}$"
output = self.run_cli("search", query)
regexp = re.compile(
r"""
\S # One non-space character.
\s+ # One space or more.
(?P<package_id>[^\s:]+) # Any non-space until whitespace or semi-colon.
(?:\:\S+)? # Optional arch suffix after package and semi-colon
\s+ # One space or more.
- # A dash.
\ ? # An optional space.
(?P<description>.+)? # Optional content string.
""",
re.VERBOSE,
)
for package_id, description in regexp.findall(output):
yield self.package(id=package_id, description=description)