mirror of
https://github.com/kiviktnm/decman.git
synced 2026-09-19 12:08:28 +00:00
Use pyalpm instead of running pacman multiple times (fixes #39)
This commit is contained in:
+8
-21
@@ -8,7 +8,7 @@ AUR plugin can be used to manage AUR and custom packages. The pacman plugin mana
|
|||||||
|
|
||||||
It manages packages exactly the same way as the pacman plugin.
|
It manages packages exactly the same way as the pacman plugin.
|
||||||
|
|
||||||
> This plugin will ensure that explicitly installed packages match those defined in the decman source. If your system has explicitly installed package A, but it is not included in the source, it will be uninstalled. You don't need to list dependencies in your source as those will be handeled by pacman automatically. However, if you have inluded package B in your source and that package depends on A, this plugin will not remove A. Instead it will demote A to a dependency. This plugin will also remove all orphaned packages automatically.
|
> This plugin will ensure that explicitly installed packages match those defined in the decman source. If your system has explicitly installed package A, but it is not included in the source, it will be uninstalled. You don't need to list dependencies in your source as those will be handeled by pacman automatically. However, if you have inluded package B in your source and that package depends on A, this plugin will not remove A. Instead it will demote A to a dependency. This plugin will also remove all orphaned packages automatically. **Packages that are only optionally required by other packages are considered orphans.** This way this plugin can ensure that your system truly matches your source. You cannot install an optional dependency, and forget about it later.
|
||||||
|
|
||||||
Building of foreign packages happens in a chroot. This creates some overhead, but ensures clean builds. By default the chroot is created to `/tmp/decman/build`. If `/tmp` is a in-memory filesystem like tmpfs, make sure that the tmpfs-partition is large enough. I recommend at least 6 GB. You can also change the build directory if memory is an issue.
|
Building of foreign packages happens in a chroot. This creates some overhead, but ensures clean builds. By default the chroot is created to `/tmp/decman/build`. If `/tmp` is a in-memory filesystem like tmpfs, make sure that the tmpfs-partition is large enough. I recommend at least 6 GB. You can also change the build directory if memory is an issue.
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ If these sets change, this plugin will flag the module as changed. The module's
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
This module has partially the same configuration with pacman. You'll have to define pacman output keywords again.
|
This module has partially the same configuration with pacman. You'll have to define pacman output keywords and database options again.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
import decman
|
import decman
|
||||||
@@ -89,6 +89,12 @@ import decman
|
|||||||
decman.aur.keywords = {"pacsave", "pacnew", "warning"}
|
decman.aur.keywords = {"pacsave", "pacnew", "warning"}
|
||||||
# disable the feature
|
# disable the feature
|
||||||
decman.aur.print_highlights = False
|
decman.aur.print_highlights = False
|
||||||
|
|
||||||
|
# signature level for querying existing databases
|
||||||
|
decman.aur.database_signature_level = 2048 # pyalpm.SIG_DATABASE_OPTIONAL
|
||||||
|
|
||||||
|
# path to databases
|
||||||
|
decman.aur.database_path = "/var/lib/pacman/"
|
||||||
```
|
```
|
||||||
|
|
||||||
There are some options related to building packages.
|
There are some options related to building packages.
|
||||||
@@ -117,25 +123,6 @@ from decman.plugins import aur
|
|||||||
import decman
|
import decman
|
||||||
|
|
||||||
class MyAurAndPacmanCommands(aur.AurCommands):
|
class MyAurAndPacmanCommands(aur.AurCommands):
|
||||||
def list_orphans_foreign(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of orphaned foreign packages.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qmdtq", "--color=never"]
|
|
||||||
|
|
||||||
def list_foreign_versioned(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of installed packages and their
|
|
||||||
versions that are not from pacman repositories.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qm", "--color=never"]
|
|
||||||
|
|
||||||
def is_installable(self, pkg: str) -> list[str]:
|
|
||||||
"""
|
|
||||||
This command exits with code 0 when a package is installable from pacman repositories.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Sddp", pkg]
|
|
||||||
|
|
||||||
def install_as_dependencies(self, pkgs: set[str]) -> list[str]:
|
def install_as_dependencies(self, pkgs: set[str]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Running this command installs the given packages from pacman repositories.
|
Running this command installs the given packages from pacman repositories.
|
||||||
|
|||||||
+10
-25
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Pacman plugin can be used to manage pacman packages. The pacman plugin manages only native packages found in arch repositories. All foreign (AUR) packages are ignored by this plugin.
|
Pacman plugin can be used to manage pacman packages. The pacman plugin manages only native packages found in arch repositories. All foreign (AUR) packages are ignored by this plugin.
|
||||||
|
|
||||||
This plugin will ensure that explicitly installed packages match those defined in the decman source. If your system has explicitly installed package A, but it is not included in the source, it will be uninstalled. You don't need to list dependencies in your source as those will be handeled by pacman automatically. However, if you have inluded package B in your source and that package depends on A, this plugin will not remove A. Instead it will demote A to a dependency. This plugin will also remove all orphaned packages automatically.
|
This plugin will ensure that explicitly installed packages match those defined in the decman source. If your system has explicitly installed package A, but it is not included in the source, it will be uninstalled. You don't need to list dependencies in your source as those will be handeled by pacman automatically. However, if you have inluded package B in your source and that package depends on A, this plugin will not remove A. Instead it will demote A to a dependency. This plugin will also remove all orphaned packages automatically. **Packages that are only optionally required by other packages are considered orphans.** This way this plugin can ensure that your system truly matches your source. You cannot install an optional dependency, and forget about it later.
|
||||||
|
|
||||||
Please keep in mind that decman doesn't play well with package groups, since all packages part of that group will be installed explicitly. After the initial run decman will now try to remove those packages since it only knows that the group itself should be explicitly installed. Instead of package groups, use meta packages.
|
Please keep in mind that decman doesn't play well with package groups, since all packages part of that group will be installed explicitly. After the initial run decman will now try to remove those packages since it only knows that the group itself should be explicitly installed. Instead of package groups, use meta packages.
|
||||||
|
|
||||||
@@ -54,6 +54,12 @@ decman.pacman.keywords = {"pacsave", "pacnew", "warning"}
|
|||||||
|
|
||||||
# disable the feature
|
# disable the feature
|
||||||
decman.pacman.print_highlights = False
|
decman.pacman.print_highlights = False
|
||||||
|
|
||||||
|
# signature level for querying existing databases
|
||||||
|
decman.pacman.database_signature_level = 2048 # pyalpm.SIG_DATABASE_OPTIONAL
|
||||||
|
|
||||||
|
# path to databases
|
||||||
|
decman.pacman.database_path = "/var/lib/pacman/"
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally it's possible to override the commands this plugin uses. Create your own `PacmanCommands` class and override methods returning commands. These are the defaults.
|
Additionally it's possible to override the commands this plugin uses. Create your own `PacmanCommands` class and override methods returning commands. These are the defaults.
|
||||||
@@ -62,32 +68,11 @@ Additionally it's possible to override the commands this plugin uses. Create you
|
|||||||
from decman.plugins import pacman
|
from decman.plugins import pacman
|
||||||
|
|
||||||
class MyCommands(pacman.PacmanCommands):
|
class MyCommands(pacman.PacmanCommands):
|
||||||
def list_explicit_native(self) -> list[str]:
|
def list_pacman_repos(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Running this command outputs a newline seperated list of explicitly installed native
|
Running this command prints a newline seperated list of pacman repositories.
|
||||||
packages.
|
|
||||||
"""
|
"""
|
||||||
return ["pacman", "-Qeqn", "--color=never"]
|
return ["pacman-conf", "--repo-list"]
|
||||||
|
|
||||||
def list_explicit_foreign(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of explicitly installed foreign
|
|
||||||
packages.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qeqm", "--color=never"]
|
|
||||||
|
|
||||||
def list_orphans_native(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of orphaned native packages.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qndtq", "--color=never"]
|
|
||||||
|
|
||||||
def list_dependants(self, pkg: str) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of packages that depend on the given
|
|
||||||
package.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Rc", "--print", "--print-format", "%n", pkg]
|
|
||||||
|
|
||||||
def install(self, pkgs: set[str]) -> list[str]:
|
def install(self, pkgs: set[str]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ authors = [
|
|||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests",
|
"requests",
|
||||||
|
"pyalpm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
import pyalpm
|
||||||
|
|
||||||
import decman.config as config
|
import decman.config as config
|
||||||
import decman.core.error as errors
|
import decman.core.error as errors
|
||||||
import decman.core.module as module
|
import decman.core.module as module
|
||||||
@@ -65,6 +67,9 @@ class AUR(plugins.Plugin):
|
|||||||
self.ignored_packages: set[str] = set()
|
self.ignored_packages: set[str] = set()
|
||||||
self.commands: AurCommands = AurCommands()
|
self.commands: AurCommands = AurCommands()
|
||||||
|
|
||||||
|
self.database_signature_level = pyalpm.SIG_DATABASE_OPTIONAL
|
||||||
|
self.database_path = "/var/lib/pacman/"
|
||||||
|
|
||||||
self.aur_rpc_timeout: int = 30
|
self.aur_rpc_timeout: int = 30
|
||||||
self.print_highlights: bool = True
|
self.print_highlights: bool = True
|
||||||
self.keywords: set[str] = {
|
self.keywords: set[str] = {
|
||||||
@@ -142,7 +147,13 @@ class AUR(plugins.Plugin):
|
|||||||
package_search = PackageSearch(self.aur_rpc_timeout)
|
package_search = PackageSearch(self.aur_rpc_timeout)
|
||||||
for custom_package in self.custom_packages:
|
for custom_package in self.custom_packages:
|
||||||
package_search.add_custom_pkg(custom_package.parse(self.commands))
|
package_search.add_custom_pkg(custom_package.parse(self.commands))
|
||||||
pm = AurPacmanInterface(self.commands, self.print_highlights, self.keywords)
|
pm = AurPacmanInterface(
|
||||||
|
self.commands,
|
||||||
|
self.print_highlights,
|
||||||
|
self.keywords,
|
||||||
|
self.database_signature_level,
|
||||||
|
self.database_path,
|
||||||
|
)
|
||||||
fpm = ForeignPackageManager(
|
fpm = ForeignPackageManager(
|
||||||
store,
|
store,
|
||||||
pm,
|
pm,
|
||||||
@@ -228,6 +239,11 @@ class AUR(plugins.Plugin):
|
|||||||
output.print_error(str(error))
|
output.print_error(str(error))
|
||||||
output.print_traceback()
|
output.print_traceback()
|
||||||
return False
|
return False
|
||||||
|
except pyalpm.error as error:
|
||||||
|
output.print_error("Failed to query pacman databases with pyalpm.")
|
||||||
|
output.print_error(str(error))
|
||||||
|
output.print_traceback()
|
||||||
|
return False
|
||||||
except errors.CommandFailedError as error:
|
except errors.CommandFailedError as error:
|
||||||
output.print_error(
|
output.print_error(
|
||||||
"AUR command exited with an unexpected return code. You may have cancelled a "
|
"AUR command exited with an unexpected return code. You may have cancelled a "
|
||||||
|
|||||||
@@ -1,29 +1,11 @@
|
|||||||
|
import pyalpm
|
||||||
|
|
||||||
import decman.config as config
|
import decman.config as config
|
||||||
import decman.core.command as command
|
import decman.core.command as command
|
||||||
import decman.core.error as errors
|
|
||||||
import decman.plugins.pacman as pacman
|
import decman.plugins.pacman as pacman
|
||||||
|
|
||||||
|
|
||||||
class AurCommands(pacman.PacmanCommands):
|
class AurCommands(pacman.PacmanCommands):
|
||||||
def list_orphans_foreign(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of orphaned foreign packages.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qmdtq", "--color=never"]
|
|
||||||
|
|
||||||
def list_foreign_versioned(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of installed packages and their
|
|
||||||
versions that are not from pacman repositories.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qm", "--color=never"]
|
|
||||||
|
|
||||||
def is_installable(self, pkg: str) -> list[str]:
|
|
||||||
"""
|
|
||||||
This command exits with code 0 when a package is installable from pacman repositories.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Sddp", pkg]
|
|
||||||
|
|
||||||
def install_as_dependencies(self, pkgs: set[str]) -> list[str]:
|
def install_as_dependencies(self, pkgs: set[str]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Running this command installs the given packages from pacman repositories.
|
Running this command installs the given packages from pacman repositories.
|
||||||
@@ -144,8 +126,15 @@ class AurPacmanInterface(pacman.PacmanInterface):
|
|||||||
On failure methods raise a ``CommandFailedError``.
|
On failure methods raise a ``CommandFailedError``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, commands: AurCommands, print_highlights: bool, keywords: set[str]) -> None:
|
def __init__(
|
||||||
super().__init__(commands, print_highlights, keywords)
|
self,
|
||||||
|
commands: AurCommands,
|
||||||
|
print_highlights: bool,
|
||||||
|
keywords: set[str],
|
||||||
|
dbsiglevel: int,
|
||||||
|
dbpath: str,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(commands, print_highlights, keywords, dbsiglevel, dbpath)
|
||||||
self._installable: dict[str, bool] = {}
|
self._installable: dict[str, bool] = {}
|
||||||
self._aur_commands = commands
|
self._aur_commands = commands
|
||||||
|
|
||||||
@@ -153,51 +142,32 @@ class AurPacmanInterface(pacman.PacmanInterface):
|
|||||||
"""
|
"""
|
||||||
Returns a set of orphaned foreign packages.
|
Returns a set of orphaned foreign packages.
|
||||||
"""
|
"""
|
||||||
|
out: set[str] = set()
|
||||||
cmd = self._aur_commands.list_orphans_foreign()
|
for pkg in self._handle.get_localdb().pkgcache:
|
||||||
rc, packages_text = command.run(cmd)
|
if pkg.reason != pyalpm.PKG_REASON_DEPEND:
|
||||||
# returncode 1 means no packages exist
|
continue
|
||||||
if rc == 1:
|
if pkg.compute_requiredby():
|
||||||
return set()
|
continue
|
||||||
if rc != 0:
|
if not self._is_native(pkg.name):
|
||||||
raise errors.CommandFailedError(cmd, packages_text)
|
out.add(pkg.name)
|
||||||
|
return out
|
||||||
packages = set(packages_text.strip().split("\n"))
|
|
||||||
|
|
||||||
return packages
|
|
||||||
|
|
||||||
def is_installable(self, pkg: str) -> bool:
|
def is_installable(self, pkg: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns True if a package can be installed using pacman.
|
Returns True if a package can be installed using pacman.
|
||||||
"""
|
"""
|
||||||
if pkg in self._installable:
|
return pkg in self._name_index or pacman.strip_dependency(pkg) in self._provides_index
|
||||||
return self._installable[pkg]
|
|
||||||
|
|
||||||
returncode, _ = command.run(self._aur_commands.is_installable(pkg))
|
|
||||||
result = returncode == 0
|
|
||||||
|
|
||||||
self._installable[pkg] = result
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_versioned_foreign_packages(self) -> list[tuple[str, str]]:
|
def get_versioned_foreign_packages(self) -> list[tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Returns a list of installed packages and their versions that aren't from pacman databases,
|
Returns a list of installed packages and their versions that aren't from pacman databases,
|
||||||
basically AUR packages.
|
basically AUR packages.
|
||||||
"""
|
"""
|
||||||
cmd = self._aur_commands.list_foreign_versioned()
|
out: list[tuple[str, str]] = []
|
||||||
returncode, packages_text = command.run(cmd)
|
for pkg in self._handle.get_localdb().pkgcache:
|
||||||
packages = [
|
if not self._is_native(pkg.name):
|
||||||
(line.split(" ")[0], line.split(" ")[1]) for line in packages_text.strip().split("\n")
|
out.append((pkg.name, pkg.version))
|
||||||
]
|
return out
|
||||||
|
|
||||||
# returncode 1 means no packages exist
|
|
||||||
if returncode == 1:
|
|
||||||
return []
|
|
||||||
|
|
||||||
if returncode != 0:
|
|
||||||
raise errors.CommandFailedError(cmd, packages_text)
|
|
||||||
|
|
||||||
return packages
|
|
||||||
|
|
||||||
def install_dependencies(self, deps: set[str]):
|
def install_dependencies(self, deps: set[str]):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
@@ -11,18 +10,11 @@ import decman.config as config
|
|||||||
import decman.core.command as command
|
import decman.core.command as command
|
||||||
import decman.core.error as errors
|
import decman.core.error as errors
|
||||||
import decman.core.output as output
|
import decman.core.output as output
|
||||||
|
import decman.plugins.pacman as pacman_module
|
||||||
from decman.plugins.aur.commands import AurCommands, AurPacmanInterface
|
from decman.plugins.aur.commands import AurCommands, AurPacmanInterface
|
||||||
from decman.plugins.aur.error import AurRPCError, PKGBUILDParseError
|
from decman.plugins.aur.error import AurRPCError, PKGBUILDParseError
|
||||||
|
|
||||||
|
|
||||||
def strip_dependency(dep: str) -> str:
|
|
||||||
"""
|
|
||||||
Removes version spefications from a dependency name.
|
|
||||||
"""
|
|
||||||
rx = re.compile("(=.*|>.*|<.*)")
|
|
||||||
return rx.sub("", dep)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True, slots=True)
|
@dataclasses.dataclass(frozen=True, slots=True)
|
||||||
class PackageInfo:
|
class PackageInfo:
|
||||||
"""
|
"""
|
||||||
@@ -162,7 +154,7 @@ class PackageInfo:
|
|||||||
foreign: list[str] = []
|
foreign: list[str] = []
|
||||||
|
|
||||||
for dependency in deps:
|
for dependency in deps:
|
||||||
stripped = strip_dependency(dependency)
|
stripped = pacman_module.strip_dependency(dependency)
|
||||||
if pacman.is_installable(dependency):
|
if pacman.is_installable(dependency):
|
||||||
native.append(stripped)
|
native.append(stripped)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+111
-60
@@ -1,5 +1,8 @@
|
|||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
import pyalpm
|
||||||
|
|
||||||
import decman.config as config
|
import decman.config as config
|
||||||
import decman.core.command as command
|
import decman.core.command as command
|
||||||
import decman.core.error as errors
|
import decman.core.error as errors
|
||||||
@@ -19,6 +22,14 @@ def packages(fn):
|
|||||||
return fn
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def strip_dependency(dep: str) -> str:
|
||||||
|
"""
|
||||||
|
Removes version spefications from a dependency name.
|
||||||
|
"""
|
||||||
|
rx = re.compile("(=.*|>.*|<.*)")
|
||||||
|
return rx.sub("", dep)
|
||||||
|
|
||||||
|
|
||||||
class Pacman(plugins.Plugin):
|
class Pacman(plugins.Plugin):
|
||||||
"""
|
"""
|
||||||
Plugin that manages pacman packages added directly to ``packages`` or declared by modules via
|
Plugin that manages pacman packages added directly to ``packages`` or declared by modules via
|
||||||
@@ -40,6 +51,8 @@ class Pacman(plugins.Plugin):
|
|||||||
# "error",
|
# "error",
|
||||||
# "note",
|
# "note",
|
||||||
}
|
}
|
||||||
|
self.database_signature_level = pyalpm.SIG_DATABASE_OPTIONAL
|
||||||
|
self.database_path = "/var/lib/pacman/"
|
||||||
|
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
return shutil.which("pacman") is not None
|
return shutil.which("pacman") is not None
|
||||||
@@ -66,9 +79,15 @@ class Pacman(plugins.Plugin):
|
|||||||
def apply(
|
def apply(
|
||||||
self, store: _store.Store, dry_run: bool = False, params: list[str] | None = None
|
self, store: _store.Store, dry_run: bool = False, params: list[str] | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
pm = PacmanInterface(self.commands, self.print_highlights, self.keywords)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
pm = PacmanInterface(
|
||||||
|
self.commands,
|
||||||
|
self.print_highlights,
|
||||||
|
self.keywords,
|
||||||
|
self.database_signature_level,
|
||||||
|
self.database_path,
|
||||||
|
)
|
||||||
|
|
||||||
currently_installed_native = pm.get_native_explicit()
|
currently_installed_native = pm.get_native_explicit()
|
||||||
currently_installed_foreign = pm.get_foreign_explicit()
|
currently_installed_foreign = pm.get_foreign_explicit()
|
||||||
orphans = pm.get_native_orphans()
|
orphans = pm.get_native_orphans()
|
||||||
@@ -109,6 +128,11 @@ class Pacman(plugins.Plugin):
|
|||||||
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
pm.install(to_install)
|
pm.install(to_install)
|
||||||
|
except pyalpm.error as error:
|
||||||
|
output.print_error("Failed to query pacman databases with pyalpm.")
|
||||||
|
output.print_error(str(error))
|
||||||
|
output.print_traceback()
|
||||||
|
return False
|
||||||
except errors.CommandFailedError as error:
|
except errors.CommandFailedError as error:
|
||||||
output.print_error(
|
output.print_error(
|
||||||
"Pacman command exited with an unexpected return code. You may have cancelled a "
|
"Pacman command exited with an unexpected return code. You may have cancelled a "
|
||||||
@@ -123,32 +147,11 @@ class Pacman(plugins.Plugin):
|
|||||||
|
|
||||||
|
|
||||||
class PacmanCommands:
|
class PacmanCommands:
|
||||||
def list_explicit_native(self) -> list[str]:
|
def list_pacman_repos(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Running this command outputs a newline seperated list of explicitly installed native
|
Running this command prints a newline seperated list of pacman repositories.
|
||||||
packages.
|
|
||||||
"""
|
"""
|
||||||
return ["pacman", "-Qeqn", "--color=never"]
|
return ["pacman-conf", "--repo-list"]
|
||||||
|
|
||||||
def list_explicit_foreign(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of explicitly installed foreign
|
|
||||||
packages.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qeqm", "--color=never"]
|
|
||||||
|
|
||||||
def list_orphans_native(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of orphaned native packages.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Qndtq", "--color=never"]
|
|
||||||
|
|
||||||
def list_dependants(self, pkg: str) -> list[str]:
|
|
||||||
"""
|
|
||||||
Running this command outputs a newline seperated list of packages that depend on the given
|
|
||||||
package.
|
|
||||||
"""
|
|
||||||
return ["pacman", "-Rc", "--print", "--print-format", "%n", pkg]
|
|
||||||
|
|
||||||
def install(self, pkgs: set[str]) -> list[str]:
|
def install(self, pkgs: set[str]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
@@ -186,24 +189,66 @@ class PacmanInterface:
|
|||||||
"""
|
"""
|
||||||
High level interface for running pacman commands.
|
High level interface for running pacman commands.
|
||||||
|
|
||||||
On failure methods raise a ``CommandFailedError``.
|
On failure methods raise a ``CommandFailedError`` or ``pyalpm.error``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, commands: PacmanCommands, print_highlights: bool, keywords: set[str]
|
self,
|
||||||
|
commands: PacmanCommands,
|
||||||
|
print_highlights: bool,
|
||||||
|
keywords: set[str],
|
||||||
|
dbsiglevel: int,
|
||||||
|
dbpath: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._commands = commands
|
self._commands = commands
|
||||||
self._print_highlights = print_highlights
|
self._print_highlights = print_highlights
|
||||||
self._keywords = keywords
|
self._keywords = keywords
|
||||||
|
self._dbsiglevel = dbsiglevel
|
||||||
|
self._dbpath = dbpath
|
||||||
|
self._handle = self._create_pyalpm_handle()
|
||||||
|
self._name_index = self._create_name_index()
|
||||||
|
self._provides_index = self._create_provides_index()
|
||||||
|
|
||||||
|
def _create_pyalpm_handle(self):
|
||||||
|
root = "/"
|
||||||
|
|
||||||
|
h = pyalpm.Handle(root, self._dbpath)
|
||||||
|
|
||||||
|
cmd = self._commands.list_pacman_repos()
|
||||||
|
repos = command.prg(cmd, pty=False).strip().split("\n")
|
||||||
|
|
||||||
|
# Empty string means no DBs
|
||||||
|
if "" in repos and len(repos) == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
for repo in repos:
|
||||||
|
h.register_syncdb(repo, self._dbsiglevel)
|
||||||
|
|
||||||
|
return h
|
||||||
|
|
||||||
|
def _create_name_index(self) -> set[str]:
|
||||||
|
return {pkg.name for db in self._handle.get_syncdbs() for pkg in db.pkgcache}
|
||||||
|
|
||||||
|
def _create_provides_index(self) -> dict[str, set[str]]:
|
||||||
|
out: dict[str, set[str]] = {}
|
||||||
|
for db in self._handle.get_syncdbs():
|
||||||
|
for pkg in db.pkgcache:
|
||||||
|
for p in pkg.provides:
|
||||||
|
out.setdefault(strip_dependency(p), set()).add(pkg.name)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _is_native(self, package: str) -> bool:
|
||||||
|
return package in self._name_index
|
||||||
|
|
||||||
def get_native_explicit(self) -> set[str]:
|
def get_native_explicit(self) -> set[str]:
|
||||||
"""
|
"""
|
||||||
Returns a set of explicitly installed native packages.
|
Returns a set of explicitly installed native packages.
|
||||||
"""
|
"""
|
||||||
|
out: set[str] = set()
|
||||||
cmd = self._commands.list_explicit_native()
|
for pkg in self._handle.get_localdb().pkgcache:
|
||||||
_, packages_text = command.check_run_result(cmd, command.run(cmd))
|
if pkg.reason == pyalpm.PKG_REASON_EXPLICIT and self._is_native(pkg.name):
|
||||||
packages = set(packages_text.strip().split("\n"))
|
out.add(pkg.name)
|
||||||
|
return out
|
||||||
|
|
||||||
return packages
|
return packages
|
||||||
|
|
||||||
@@ -211,45 +256,51 @@ class PacmanInterface:
|
|||||||
"""
|
"""
|
||||||
Returns a set of orphaned native packages.
|
Returns a set of orphaned native packages.
|
||||||
"""
|
"""
|
||||||
|
out: set[str] = set()
|
||||||
cmd = self._commands.list_orphans_native()
|
for pkg in self._handle.get_localdb().pkgcache:
|
||||||
rc, packages_text = command.run(cmd)
|
if pkg.reason != pyalpm.PKG_REASON_DEPEND:
|
||||||
# returncode 1 means no packages exist
|
continue
|
||||||
if rc == 1:
|
if pkg.compute_requiredby():
|
||||||
return set()
|
continue
|
||||||
if rc != 0:
|
if self._is_native(pkg.name):
|
||||||
raise errors.CommandFailedError(cmd, packages_text)
|
out.add(pkg.name)
|
||||||
|
return out
|
||||||
packages = set(packages_text.strip().split("\n"))
|
|
||||||
|
|
||||||
return packages
|
|
||||||
|
|
||||||
def get_foreign_explicit(self) -> set[str]:
|
def get_foreign_explicit(self) -> set[str]:
|
||||||
"""
|
"""
|
||||||
Returns a set of explicitly installed foreign packages.
|
Returns a set of explicitly installed foreign packages.
|
||||||
"""
|
"""
|
||||||
cmd = self._commands.list_explicit_foreign()
|
out: set[str] = set()
|
||||||
rc, packages_text = command.run(cmd)
|
for pkg in self._handle.get_localdb().pkgcache:
|
||||||
# returncode 1 means no packages exist
|
if pkg.reason == pyalpm.PKG_REASON_EXPLICIT and not self._is_native(pkg.name):
|
||||||
if rc == 1:
|
out.add(pkg.name)
|
||||||
return set()
|
return out
|
||||||
if rc != 0:
|
|
||||||
raise errors.CommandFailedError(cmd, packages_text)
|
|
||||||
|
|
||||||
packages = set(packages_text.strip().split("\n"))
|
|
||||||
|
|
||||||
return packages
|
|
||||||
|
|
||||||
def get_dependants(self, package: str) -> set[str]:
|
def get_dependants(self, package: str) -> set[str]:
|
||||||
"""
|
"""
|
||||||
Returns a set of packages that depend on the given package.
|
Returns a set of installed packages that depend on the given package.
|
||||||
|
Includes the package itself.
|
||||||
"""
|
"""
|
||||||
|
local = self._handle.get_localdb()
|
||||||
|
|
||||||
cmd = self._commands.list_dependants(package)
|
seen: set[str] = set()
|
||||||
_, packages_text = command.check_run_result(cmd, command.run(cmd))
|
stack = [package]
|
||||||
packages = set(packages_text.strip().split("\n"))
|
|
||||||
|
|
||||||
return packages
|
while stack:
|
||||||
|
name = stack.pop()
|
||||||
|
if name in seen:
|
||||||
|
continue
|
||||||
|
seen.add(name)
|
||||||
|
|
||||||
|
pkg = local.get_pkg(name)
|
||||||
|
if pkg is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for dep in pkg.compute_requiredby():
|
||||||
|
if dep not in seen:
|
||||||
|
stack.append(dep)
|
||||||
|
|
||||||
|
return seen
|
||||||
|
|
||||||
def set_as_dependencies(self, packages: set[str]):
|
def set_as_dependencies(self, packages: set[str]):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def test_apply_respects_ignored_packages_and_protects_their_dependencies(
|
|||||||
|
|
||||||
# Fake pacman interface for foreign/native info
|
# Fake pacman interface for foreign/native info
|
||||||
class FakePM:
|
class FakePM:
|
||||||
def __init__(self, commands, print_highlights, keywords) -> None:
|
def __init__(self, commands, print_highlights, keywords, dbsiglevel, dbpath) -> None:
|
||||||
self.commands = commands
|
self.commands = commands
|
||||||
self.print_highlights = print_highlights
|
self.print_highlights = print_highlights
|
||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
@@ -139,9 +139,15 @@ def test_apply_respects_ignored_packages_and_protects_their_dependencies(
|
|||||||
def set_as_dependencies(self, pkgs: set[str]) -> None:
|
def set_as_dependencies(self, pkgs: set[str]) -> None:
|
||||||
self.set_as_deps_called_with = pkgs
|
self.set_as_deps_called_with = pkgs
|
||||||
|
|
||||||
fake_pm = FakePM(None, None, None)
|
fake_pm = FakePM(None, None, None, None, None)
|
||||||
|
|
||||||
def fake_pm_ctor(commands, print_highlights, keywords) -> FakePM:
|
def fake_pm_ctor(
|
||||||
|
commands,
|
||||||
|
print_highlights,
|
||||||
|
keywords,
|
||||||
|
dbsiglevel,
|
||||||
|
dbpath,
|
||||||
|
) -> FakePM:
|
||||||
fake_pm.commands = commands
|
fake_pm.commands = commands
|
||||||
fake_pm.print_highlights = print_highlights
|
fake_pm.print_highlights = print_highlights
|
||||||
fake_pm.keywords = keywords
|
fake_pm.keywords = keywords
|
||||||
@@ -182,7 +188,13 @@ def test_apply_respects_ignored_packages_and_protects_their_dependencies(
|
|||||||
fake_fpm = FakeFPM(None, None, None, None, None, None, None)
|
fake_fpm = FakeFPM(None, None, None, None, None, None, None)
|
||||||
|
|
||||||
def fake_fpm_ctor(
|
def fake_fpm_ctor(
|
||||||
store_arg, pm_arg, package_search_arg, commands_arg, cache_dir, build_dir, makepkg_user
|
store_arg,
|
||||||
|
pm_arg,
|
||||||
|
package_search_arg,
|
||||||
|
commands_arg,
|
||||||
|
cache_dir,
|
||||||
|
build_dir,
|
||||||
|
makepkg_user,
|
||||||
):
|
):
|
||||||
fake_fpm.store = store_arg
|
fake_fpm.store = store_arg
|
||||||
fake_fpm.pm = pm_arg
|
fake_fpm.pm = pm_arg
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from decman.plugins.aur.package import (
|
|||||||
CustomPackage,
|
CustomPackage,
|
||||||
PackageInfo,
|
PackageInfo,
|
||||||
PackageSearch,
|
PackageSearch,
|
||||||
strip_dependency,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,23 +23,6 @@ def silence_output(monkeypatch):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- strip_dependency ------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"dep,expected",
|
|
||||||
[
|
|
||||||
("foo", "foo"),
|
|
||||||
("foo=1.0", "foo"),
|
|
||||||
("bar>=2", "bar"),
|
|
||||||
("baz<3", "baz"),
|
|
||||||
("multi=1.0-2", "multi"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_strip_dependency(dep, expected):
|
|
||||||
assert strip_dependency(dep) == expected
|
|
||||||
|
|
||||||
|
|
||||||
# --- PackageInfo -----------------------------------------------------------
|
# --- PackageInfo -----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ import pytest
|
|||||||
from decman.plugins import pacman as pacman_plugin
|
from decman.plugins import pacman as pacman_plugin
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"dep,expected",
|
||||||
|
[
|
||||||
|
("foo", "foo"),
|
||||||
|
("foo=1.0", "foo"),
|
||||||
|
("bar>=2", "bar"),
|
||||||
|
("baz<3", "baz"),
|
||||||
|
("multi=1.0-2", "multi"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_strip_dependency(dep, expected):
|
||||||
|
assert pacman_plugin.strip_dependency(dep) == expected
|
||||||
|
|
||||||
|
|
||||||
class FakeStore(dict):
|
class FakeStore(dict):
|
||||||
def ensure(self, key: str, default: Any) -> None:
|
def ensure(self, key: str, default: Any) -> None:
|
||||||
if key not in self:
|
if key not in self:
|
||||||
@@ -59,7 +73,9 @@ def test_apply_dry_run_computes_sets_and_does_not_call_pacman(
|
|||||||
|
|
||||||
# Fake PacmanInterface returned by plugin module
|
# Fake PacmanInterface returned by plugin module
|
||||||
class FakePM:
|
class FakePM:
|
||||||
def __init__(self, commands, print_highlights, keywords) -> None: # noqa: D401
|
def __init__(
|
||||||
|
self, commands, print_highlights, keywords, database_signature_level, database_path
|
||||||
|
) -> None: # noqa: D401
|
||||||
self.commands = commands
|
self.commands = commands
|
||||||
self.print_highlights = print_highlights
|
self.print_highlights = print_highlights
|
||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
@@ -101,9 +117,11 @@ def test_apply_dry_run_computes_sets_and_does_not_call_pacman(
|
|||||||
def install(self, pkgs: set[str]) -> None:
|
def install(self, pkgs: set[str]) -> None:
|
||||||
self.install_called_with = pkgs
|
self.install_called_with = pkgs
|
||||||
|
|
||||||
fake_pm = FakePM(None, None, None)
|
fake_pm = FakePM(None, None, None, None, None)
|
||||||
|
|
||||||
def fake_pm_ctor(commands, print_highlights, keywords) -> FakePM:
|
def fake_pm_ctor(
|
||||||
|
commands, print_highlights, keywords, database_signature_level, database_path
|
||||||
|
) -> FakePM:
|
||||||
# constructor used in Pacman.apply
|
# constructor used in Pacman.apply
|
||||||
fake_pm.commands = commands
|
fake_pm.commands = commands
|
||||||
fake_pm.print_highlights = print_highlights
|
fake_pm.print_highlights = print_highlights
|
||||||
@@ -217,7 +235,9 @@ def test_ignored_packages_are_not_removed_or_installed(monkeypatch: pytest.Monke
|
|||||||
pacman.ignored_packages = {"ignored-installed", "ignored-uninstalled"}
|
pacman.ignored_packages = {"ignored-installed", "ignored-uninstalled"}
|
||||||
|
|
||||||
class FakePM:
|
class FakePM:
|
||||||
def __init__(self, commands, print_highlights, keywords) -> None:
|
def __init__(
|
||||||
|
self, commands, print_highlights, keywords, database_signature_level, database_path
|
||||||
|
) -> None: # noqa: D401
|
||||||
self.commands = commands
|
self.commands = commands
|
||||||
self.print_highlights = print_highlights
|
self.print_highlights = print_highlights
|
||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
@@ -252,9 +272,11 @@ def test_ignored_packages_are_not_removed_or_installed(monkeypatch: pytest.Monke
|
|||||||
def install(self, pkgs: set[str]) -> None:
|
def install(self, pkgs: set[str]) -> None:
|
||||||
self.install_called_with = pkgs
|
self.install_called_with = pkgs
|
||||||
|
|
||||||
fake_pm = FakePM(None, None, None)
|
fake_pm = FakePM(None, None, None, None, None)
|
||||||
|
|
||||||
def fake_pm_ctor(commands, print_highlights, keywords) -> FakePM:
|
def fake_pm_ctor(
|
||||||
|
commands, print_highlights, keywords, database_signature_level, database_path
|
||||||
|
) -> FakePM:
|
||||||
fake_pm.commands = commands
|
fake_pm.commands = commands
|
||||||
fake_pm.print_highlights = print_highlights
|
fake_pm.print_highlights = print_highlights
|
||||||
fake_pm.keywords = keywords
|
fake_pm.keywords = keywords
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ name = "decman"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "pyalpm" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -76,7 +77,10 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "requests" }]
|
requires-dist = [
|
||||||
|
{ name = "pyalpm" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -120,6 +124,12 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyalpm"
|
||||||
|
version = "0.10.12"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/33/fb965c94a703b2f62741a52911446fe85e7effc677d54d1a8cd0033f4848/pyalpm-0.10.12.tar.gz", hash = "sha256:8c6cb4bbba819f99fe6c2547f25a861f68905d96729caf1ea15ba6c43d4f1127", size = 52295, upload-time = "2025-05-27T10:33:58.208Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.2"
|
version = "2.19.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user