# 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 version_not_implemented
[docs]
class Emerge(PackageManager):
"""The Gentoo package manager.
Documentation:
- https://wiki.gentoo.org/wiki/Portage#emerge
- https://dev.gentoo.org/~zmedico/portage/doc/man/emerge.1.html
See other command equivalences at: https://wiki.archlinux.org/title/Pacman/Rosetta
"""
homepage_url = "https://wiki.gentoo.org/wiki/Portage#emerge"
platforms = UNIX_WITHOUT_MACOS
requirement = "3.0.0"
version_regex = r"Portage\s+(?P<version>\S+)"
"""
.. code-block:: shell-session
► emerge --version
Portage 3.0.30 (python 3.9.9-final-0, gcc-11.2.1, 5.15.32-gentoo-r1 x86_64)
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
.. warning::
This suppose the ``qlist`` binary is available and present on the system. We
do not search for it or try to resolves its canonical path with
:py:attr:`PackageManager.cli_path
<meta_package_manager.base.PackageManager.cli_path>`, as we do for the
reference ``emerge`` binary.
.. code-block:: shell-session
► qlist --installed --verbose --nocolor
acct-group/audio-0-r1
acct-group/cron-0
app-admin/hddtemp-0.3_beta15-r29
app-admin/perl-cleaner-2.30
app-admin/system-config-printer-1.5.16-r1
app-arch/p7zip-16.02-r8
"""
# Locate qlist.
qlist_path = self.which("qlist")
if not qlist_path:
raise FileNotFoundError(qlist_path)
output = self.run_cli(
"--installed",
"--verbose",
"--nocolor",
override_cli_path=qlist_path,
)
regexp = re.compile(
r"""
(
?P<package_id>\S+ # Non-whitespace string...
(?!-r) # ...if not directly followred by the "-r" string.
)
- # A dash.
(
?P<version>[^\s-]+ # Any non-whitespace/non-dash string.
(?:-r\d+)? # Optional revision suffix led by a -, non-grouped.
)
""",
re.VERBOSE,
)
for package in output.splitlines():
match = regexp.match(package)
if match:
package_id, installed_version = match.groups()
yield self.package(id=package_id, installed_version=installed_version)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
.. code-block:: shell-session
► emerge --update --deep --pretend --columns --color n --nospinner @world
[blocks B ] app-text/dos2unix
[ebuild N ] app-games/qstat [25c]
[ebuild R ] sys-apps/sed [2.4.7-r6]
[ebuild U] net-fs/samba [2.2.8_pre1] [2.2.7a]
[ebuild U] sys-devel/distcc [2.16] [2.13-r1] USE=ip6* -gtk
[ebuild r U] dev-libs/icu [50.1.1:0/50.1.1] [50.1-r2:0/50.1]
[ebuild r R ] dev-libs/libxml2 [2.9.0-r1:2] USE=icu
"""
output = self.run_cli(
"--update",
"--deep",
"--pretend",
"--columns",
"--color",
"n",
"--nospinner",
"@world",
)
regexp = re.compile(
r"""
\[.+\] # Update state.
\ # A space.
(?P<package_id>\S+) # Non-whitespace string.
\s+ # Any spacing.
(?:\[ # Non-matching group
# starting with a '['.
(?P<latest_version>[^\s\/:]+) # Any non-spaced string
# until a ':' or '/' is met.
\S* # Left-over parts of the version,
# after a ':' or '/'.
\])? # Optional group ending with a ']'.
\s+ # Any spacing.
(?:\[ # Non-matching group
# starting with a '['.
(?P<installed_version>[^\s\/:]+) # Any non-spaced string
# until a ':' or '/' is met.
\S* # Left-over parts of the version,
# after a ':' or '/'.
\])? # Optional group ending with a ']'.
""",
re.VERBOSE,
)
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
► emerge --search --color n --nospinner blah
[ Results for search key : blah ]
Searching...
* sys-process/htop
Latest version available: 1.0.2-r1
Latest version installed: [ Not Installed ]
Size of files: 380 KiB
Homepage: http://htop.sourceforge.net
Description: interactive process viewer
License: BSD GPL-2
* x11-drivers/nvidia-drivers
Latest version available: 455.45.01-r1
Latest version installed: [ Not Installed ]
Size of files: 180.214 KiB
Homepage: https://www.nvidia.com/Download/Find.aspx
Description: NVIDIA Accelerated Graphics Driver
License: GPL-2 NVIDIA-r2
[ Applications found : 2 ]
.. code-block:: shell-session
► emerge --search --color n --nospinner %^sed$
.. code-block:: shell-session
► emerge --searchdesc --color n --nospinner sed
.. code-block:: shell-session
► emerge --searchdesc --color n --nospinner %^sed$
"""
search_param = "--search"
if extended:
search_param = "--searchdesc"
if exact:
query = f"%^{query}$"
output = self.run_cli(search_param, "--color", "n", "--nospinner", query)
regexp = re.compile(
r"""
^\*\s+(?P<package_id>\S+)\n
\s+Latest\ version\ available:\s+(?P<latest_version>\S+)\n
(?:\s+.+\n)+?
\s+Description:\s+(?P<description>.+)\n
""",
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 emerge --color n --nospinner dev-vcs/git
"""
return self.run_cli("--color", "n", "--nospinner", 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 emerge --update --newuse --deep --color n --nospinner @world
"""
return self.build_cli(
"--update",
"--newuse",
"--deep",
"--color",
"n",
"--nospinner",
"@world",
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 emerge --update --color n --nospinner dev-vcs/git
"""
return self.build_cli(
"--update",
"--color",
"n",
"--nospinner",
package_id,
sudo=True,
)
[docs]
def sync(self) -> None:
"""Sync package metadata.
.. code-block:: shell-session
► sudo emerge --sync --color n --nospinner
"""
self.run_cli("--sync", "--color", "n", "--nospinner", sudo=True)
[docs]
def cleanup(self) -> None:
"""Removes things we don't need anymore.
An update is forced before calling the clean commands, as `pointed to by the
emerge documentation
<https://wiki.gentoo.org/wiki/Gentoo_Cheat_Sheet#Recommended_method>`_:
> As a safety measure, depclean will not remove any packages unless *all*
> required dependencies have been resolved. As a consequence, it is often
> necessary to run `emerge --update --newuse --deep @world` prior to depclean.
.. warning::
This suppose the ``eclean`` binary is available and present on the system.
We do not search for it or try to resolves its canonical path with
:py:attr:`PackageManager.cli_path
<meta_package_manager.base.PackageManager.cli_path>`, as we do for the
reference ``emerge`` binary.
.. code-block:: shell-session
► sudo emerge --update --newuse --deep --color n --nospinner @world
► sudo emerge --depclean
► sudo eclean distfiles
"""
# Forces an upgrade first, as recommended by emerge documentation.
self.upgrade()
self.run_cli("--depclean", sudo=True)
eclean_path = self.which("eclean")
if eclean_path:
self.run_cli("distfiles", override_cli_path=eclean_path, sudo=True)