Merge pull request #15 from oatmealraisin/formatters

Add ruff and isort
This commit is contained in:
Kivi Kaitaniemi
2025-01-25 12:42:14 +02:00
committed by GitHub
6 changed files with 472 additions and 346 deletions
+7 -1
View File
@@ -11,8 +11,14 @@ authors = [
{name = "Kivi Kaitaniemi"} {name = "Kivi Kaitaniemi"}
] ]
dependencies = [ dependencies = [
"requests" "requests",
] ]
[project.scripts] [project.scripts]
decman = "decman.app:main" decman = "decman.app:main"
[dependency-groups]
dev = [
"isort>=5.13.2",
"ruff>=0.9.3",
]
+39 -28
View File
@@ -2,12 +2,13 @@
Module for writing system configurations for decman. Module for writing system configurations for decman.
""" """
import typing
import pwd
import grp import grp
import shutil
import os import os
import pwd
import shutil
import subprocess import subprocess
import typing
import decman.error import decman.error
@@ -20,9 +21,11 @@ class UserRaisedError(Exception):
super().__init__(message) super().__init__(message)
def sh(sh_cmd: str, def sh(
sh_cmd: str,
user: typing.Optional[str] = None, user: typing.Optional[str] = None,
env_overrides: typing.Optional[dict[str, str]] = None): env_overrides: typing.Optional[dict[str, str]] = None,
):
""" """
Shortcut for running a shell command. Shortcut for running a shell command.
""" """
@@ -49,16 +52,20 @@ def sh(sh_cmd: str,
f"Running user defined shell command failed because the user {user} doesn't exist." f"Running user defined shell command failed because the user {user} doesn't exist."
) from e ) from e
with subprocess.Popen(sh_cmd, shell=True, group=gid, user=uid, with subprocess.Popen(
env=env) as process: sh_cmd, shell=True, group=gid, user=uid, env=env
) as process:
if process.wait() != 0: if process.wait() != 0:
raise decman.error.UserFacingError( raise decman.error.UserFacingError(
f"Running user shell command '{sh_cmd}' as {user} failed.") f"Running user shell command '{sh_cmd}' as {user} failed."
)
def prg(command: list[str], def prg(
command: list[str],
user: typing.Optional[str] = None, user: typing.Optional[str] = None,
env_overrides: typing.Optional[dict[str, str]] = None): env_overrides: typing.Optional[dict[str, str]] = None,
):
""" """
Shortcut for running a program. Shortcut for running a program.
""" """
@@ -74,7 +81,8 @@ def prg(command: list[str],
subprocess.run(command, check=True, env=env) subprocess.run(command, check=True, env=env)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise decman.error.UserFacingError( raise decman.error.UserFacingError(
f"Running user defined program '{command}' failed.") from e f"Running user defined program '{command}' failed."
) from e
else: else:
try: try:
uid = pwd.getpwnam(user).pw_uid uid = pwd.getpwnam(user).pw_uid
@@ -84,11 +92,11 @@ def prg(command: list[str],
f"Running user defined program failed because the user {user} doesn't exist." f"Running user defined program failed because the user {user} doesn't exist."
) from e ) from e
with subprocess.Popen(command, group=gid, user=uid, with subprocess.Popen(command, group=gid, user=uid, env=env) as process:
env=env) as process:
if process.wait() != 0: if process.wait() != 0:
raise decman.error.UserFacingError( raise decman.error.UserFacingError(
f"Running user program '{command}' as {user} failed.") f"Running user program '{command}' as {user} failed."
)
class File: class File:
@@ -127,9 +135,7 @@ class File:
if group is not None: if group is not None:
self.gid = grp.getgrnam(group).gr_gid self.gid = grp.getgrnam(group).gr_gid
def copy_to(self, def copy_to(self, target: str, variables: typing.Optional[dict[str, str]] = None):
target: str,
variables: typing.Optional[dict[str, str]] = None):
""" """
Copies the contents of this file to the target file. Copies the contents of this file to the target file.
""" """
@@ -138,8 +144,9 @@ class File:
target_directory = os.path.dirname(target) target_directory = os.path.dirname(target)
def create_missing_dirs(dirct: str, uid: typing.Optional[int], def create_missing_dirs(
gid: typing.Optional[int]): dirct: str, uid: typing.Optional[int], gid: typing.Optional[int]
):
if not os.path.isdir(dirct): if not os.path.isdir(dirct):
parent_dir = os.path.dirname(dirct) parent_dir = os.path.dirname(dirct)
if not os.path.isdir(parent_dir): if not os.path.isdir(parent_dir):
@@ -161,8 +168,7 @@ class File:
os.chmod(target, self.permissions) os.chmod(target, self.permissions)
def _write_content(self, target: str, variables: dict[str, str]): def _write_content(self, target: str, variables: dict[str, str]):
if self.source_file is not None and (self.bin_file if self.source_file is not None and (self.bin_file or len(variables) == 0):
or len(variables) == 0):
shutil.copy(self.source_file, target) shutil.copy(self.source_file, target)
elif self.bin_file and self.content is not None: elif self.bin_file and self.content is not None:
with open(target, "wb") as file: with open(target, "wb") as file:
@@ -177,7 +183,9 @@ class File:
with open(target, "wt", encoding=self.encoding) as file: with open(target, "wt", encoding=self.encoding) as file:
file.write(content) file.write(content)
else: else:
assert self.content is not None, "Content should be set since source_file was not set." assert self.content is not None, (
"Content should be set since source_file was not set."
)
content = self.content content = self.content
for var, value in variables.items(): for var, value in variables.items():
content = content.replace(var, value) content = content.replace(var, value)
@@ -217,10 +225,12 @@ class Directory:
if group is not None: if group is not None:
self.gid = grp.getgrnam(group).gr_gid self.gid = grp.getgrnam(group).gr_gid
def copy_to(self, def copy_to(
self,
target_directory: str, target_directory: str,
variables: typing.Optional[dict[str, str]] = None, variables: typing.Optional[dict[str, str]] = None,
only_print: bool = False) -> list[str]: only_print: bool = False,
) -> list[str]:
""" """
Copies the files in this directory to the target directory. Copies the files in this directory to the target directory.
@@ -233,14 +243,15 @@ class Directory:
for src_dir, _, src_files in os.walk("."): for src_dir, _, src_files in os.walk("."):
for src_file in src_files: for src_file in src_files:
src_path = os.path.join(src_dir, src_file) src_path = os.path.join(src_dir, src_file)
file = File(source_file=src_path, file = File(
source_file=src_path,
bin_file=self.bin_files, bin_file=self.bin_files,
encoding=self.encoding, encoding=self.encoding,
owner=self.owner, owner=self.owner,
group=self.group, group=self.group,
permissions=self.permissions) permissions=self.permissions,
target = os.path.normpath( )
os.path.join(target_directory, src_path)) target = os.path.normpath(os.path.join(target_directory, src_path))
created.append(target) created.append(target)
if not only_print: if not only_print:
+69 -47
View File
@@ -8,9 +8,9 @@ import sys
import traceback import traceback
import decman import decman
import decman.config as conf
import decman.error as err import decman.error as err
import decman.lib as l import decman.lib as l
import decman.config as conf
from decman.lib import fpm from decman.lib import fpm
@@ -23,53 +23,62 @@ def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="decman", prog="decman",
description= description="Declarative package & configuration manager for Arch Linux",
"Declarative package & configuration manager for Arch Linux", epilog="See more help at: https://github.com/kiviktnm/decman",
epilog="See more help at: https://github.com/kiviktnm/decman") )
parser.add_argument("--source", parser.add_argument(
action="store", "--source", action="store", help="python file containing configuration"
help="python file containing configuration") )
parser.add_argument( parser.add_argument(
"--print", "--print",
"--dry-run", "--dry-run",
action="store_true", action="store_true",
default=False, default=False,
help="print what would happen as a result of running decman") help="print what would happen as a result of running decman",
parser.add_argument("--debug", )
action="store_true", parser.add_argument(
default=False, "--debug", action="store_true", default=False, help="show debug output"
help="show debug output") )
parser.add_argument( parser.add_argument(
"--no-packages", "--no-packages",
action="store_true", action="store_true",
default=False, default=False,
help="don't upgrade any packages (including foreign packages)") help="don't upgrade any packages (including foreign packages)",
parser.add_argument("--no-foreign-packages", )
parser.add_argument(
"--no-foreign-packages",
action="store_true", action="store_true",
default=False, default=False,
help="don't upgrade foreign packages") help="don't upgrade foreign packages",
parser.add_argument("--no-files", )
parser.add_argument(
"--no-files", action="store_true", default=False, help="don't install any files"
)
parser.add_argument(
"--no-systemd-units",
action="store_true", action="store_true",
default=False, default=False,
help="don't install any files") help="don't enable/disable systemd units",
parser.add_argument("--no-systemd-units", )
parser.add_argument(
"--no-commands",
action="store_true", action="store_true",
default=False, default=False,
help="don't enable/disable systemd units") help="don't run user specified commands",
parser.add_argument("--no-commands", )
parser.add_argument(
"--upgrade-devel",
action="store_true", action="store_true",
default=False, default=False,
help="don't run user specified commands") help="upgrade devel packages",
parser.add_argument("--upgrade-devel", )
action="store_true",
default=False,
help="upgrade devel packages")
parser.add_argument( parser.add_argument(
"--force-build", "--force-build",
action="store_true", action="store_true",
default=False, default=False,
help="force building of packages that are already cached") help="force building of packages that are already cached",
)
args = parser.parse_args() args = parser.parse_args()
@@ -105,8 +114,7 @@ def main():
l.print_debug(line) l.print_debug(line)
errored = True errored = True
except decman.UserRaisedError as user_error: except decman.UserRaisedError as user_error:
l.print_error( l.print_error(f"Error encountered while running the source: {user_error}")
f"Error encountered while running the source: {user_error}")
errored = True errored = True
# Save even when an error has occurred, since this avoids repeating steps like building pkgs. # Save even when an error has occurred, since this avoids repeating steps like building pkgs.
@@ -158,13 +166,23 @@ def _set_up(store: l.Store, args):
content = file.read() content = file.read()
except OSError as e: except OSError as e:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to read source file '{store.source_file}'.") from e f"Failed to read source file '{store.source_file}'."
) from e
os.chdir(source_dir) os.chdir(source_dir)
sys.path.append(".") sys.path.append(".")
exec(content) exec(content)
return args.print, not args.no_packages, not args.no_foreign_packages, not args.no_files, not args.no_systemd_units, not args.no_commands, args.upgrade_devel, args.force_build return (
args.print,
not args.no_packages,
not args.no_foreign_packages,
not args.no_files,
not args.no_systemd_units,
not args.no_commands,
args.upgrade_devel,
args.force_build,
)
class Core: class Core:
@@ -173,7 +191,16 @@ class Core:
""" """
def __init__(self, store: l.Store, opts): def __init__(self, store: l.Store, opts):
self.only_print, self.update_packages, self.update_foreign_packages, self.update_files, self.update_units, self.run_commands, self.upgrade_devel, self.force_build = opts (
self.only_print,
self.update_packages,
self.update_foreign_packages,
self.update_files,
self.update_units,
self.run_commands,
self.upgrade_devel,
self.force_build,
) = opts
self.store = store self.store = store
self.source = _resolve_source() self.source = _resolve_source()
@@ -183,10 +210,10 @@ class Core:
for upkg in self.source.all_user_pkgs(): for upkg in self.source.all_user_pkgs():
self.fpkg_search.add_user_pkg( self.fpkg_search.add_user_pkg(
fpm.PackageInfo.from_user_package(upkg, self.pacman)) fpm.PackageInfo.from_user_package(upkg, self.pacman)
)
self.fpm = fpm.ForeignPackageManager(store, self.pacman, self.fpm = fpm.ForeignPackageManager(store, self.pacman, self.fpkg_search)
self.fpkg_search)
def run(self): def run(self):
""" """
@@ -219,8 +246,7 @@ class Core:
to_disable = self.source.units_to_disable(self.store) to_disable = self.source.units_to_disable(self.store)
l.print_list("Disabling systemd units:", to_disable) l.print_list("Disabling systemd units:", to_disable)
if to_disable: if to_disable:
l.print_info( l.print_info("Disabled systemd units won't be stopped automatically.")
"Disabled systemd units won't be stopped automatically.")
if not self.only_print: if not self.only_print:
self.systemctl.disable_units(to_disable) self.systemctl.disable_units(to_disable)
@@ -242,15 +268,14 @@ class Core:
if not self.only_print: if not self.only_print:
self.pacman.upgrade() self.pacman.upgrade()
if conf.enable_fpm and self.update_foreign_packages: if conf.enable_fpm and self.update_foreign_packages:
self.fpm.upgrade(self.upgrade_devel, self.force_build, self.fpm.upgrade(
self.source.ignored_packages) self.upgrade_devel, self.force_build, self.source.ignored_packages
)
def _install_pkgs(self): def _install_pkgs(self):
currently_installed = self.pacman.get_installed() currently_installed = self.pacman.get_installed()
to_install_pacman = self.source.pacman_packages_to_install( to_install_pacman = self.source.pacman_packages_to_install(currently_installed)
currently_installed) to_install_fpm = self.source.foreign_packages_to_install(currently_installed)
to_install_fpm = self.source.foreign_packages_to_install(
currently_installed)
l.print_list("Installing pacman packages:", to_install_pacman) l.print_list("Installing pacman packages:", to_install_pacman)
@@ -269,9 +294,7 @@ class Core:
all_created = self.source.create_all_files(self.only_print) all_created = self.source.create_all_files(self.only_print)
to_remove = self.source.files_to_remove(self.store, all_created) to_remove = self.source.files_to_remove(self.store, all_created)
l.print_list("Ensured files are up to date:", l.print_list("Ensured files are up to date:", all_created, elements_per_line=1)
all_created,
elements_per_line=1)
l.print_list("Removing files:", to_remove, elements_per_line=1) l.print_list("Removing files:", to_remove, elements_per_line=1)
if self.only_print: if self.only_print:
@@ -290,8 +313,7 @@ class Core:
to_enable = self.source.units_to_enable(self.store) to_enable = self.source.units_to_enable(self.store)
l.print_list("Enabling systemd units:", to_enable) l.print_list("Enabling systemd units:", to_enable)
if to_enable: if to_enable:
l.print_info( l.print_info("Enabled systemd units won't be started automatically.")
"Enabled systemd units won't be started automatically.")
if not self.only_print: if not self.only_print:
self.systemctl.enable_units(to_enable) self.systemctl.enable_units(to_enable)
+13 -13
View File
@@ -64,8 +64,7 @@ class Commands:
Running this command installs the given packages from pacman repositories. Running this command installs the given packages from pacman repositories.
The packages are installed as dependencies. The packages are installed as dependencies.
""" """
return ["pacman", "-S", "--color=always", "--needed", "--asdeps" return ["pacman", "-S", "--color=always", "--needed", "--asdeps"] + deps
] + deps
def is_installable(self, pkg: str) -> list[str]: def is_installable(self, pkg: str) -> list[str]:
""" """
@@ -110,8 +109,7 @@ class Commands:
""" """
return ["systemctl", "--user", "-M", f"{user}@", "disable"] + units return ["systemctl", "--user", "-M", f"{user}@", "disable"] + units
def compare_versions(self, installed_version: str, def compare_versions(self, installed_version: str, new_version: str) -> list[str]:
new_version: str) -> list[str]:
""" """
Running this command outputs -1 when the installed version is older than the new version. Running this command outputs -1 when the installed version is older than the new version.
""" """
@@ -160,27 +158,29 @@ class Commands:
Running this command installs the given packages to the given chroot. Running this command installs the given packages to the given chroot.
""" """
return [ return [
"arch-nspawn", chroot_dir, "pacman", "-S", "--needed", "arch-nspawn",
"--noconfirm" chroot_dir,
"pacman",
"-S",
"--needed",
"--noconfirm",
] + packages ] + packages
def remove_chroot_packages(self, chroot_dir: str, packages: list[str]): def remove_chroot_packages(self, chroot_dir: str, packages: list[str]):
""" """
Running this command removes the given packages from the given chroot. Running this command removes the given packages from the given chroot.
""" """
return ["arch-nspawn", chroot_dir, "pacman", "-Rsu", "--noconfirm" return ["arch-nspawn", chroot_dir, "pacman", "-Rsu", "--noconfirm"] + packages
] + packages
def make_chroot_pkg(self, chroot_wd_dir: str, user: str, def make_chroot_pkg(
pkgfiles_to_install: list[str]) -> list[str]: self, chroot_wd_dir: str, user: str, pkgfiles_to_install: list[str]
) -> list[str]:
""" """
Running this command creates a package file using the given chroot. Running this command creates a package file using the given chroot.
The package is created as the user and the pkg_files_to_install are installed The package is created as the user and the pkg_files_to_install are installed
in the chroot before the package is created. in the chroot before the package is created.
""" """
makechrootpkg_cmd = [ makechrootpkg_cmd = ["makechrootpkg", "-c", "-r", chroot_wd_dir, "-U", user]
"makechrootpkg", "-c", "-r", chroot_wd_dir, "-U", user
]
for pkgfile in pkgfiles_to_install: for pkgfile in pkgfiles_to_install:
makechrootpkg_cmd += ["-I", pkgfile] makechrootpkg_cmd += ["-I", pkgfile]
+108 -72
View File
@@ -2,17 +2,18 @@
Library module for decman. Library module for decman.
""" """
import threading
import sys
import shutil
import subprocess
import json import json
import os import os
import typing import shutil
import subprocess
import sys
import threading
import time import time
import typing
import decman
import decman.config as conf import decman.config as conf
import decman.error as err import decman.error as err
import decman
_DECMAN_MSG_TAG = "[\033[1;35mDECMAN\033[m]" _DECMAN_MSG_TAG = "[\033[1;35mDECMAN\033[m]"
_RED_PREFIX = "\033[91m" _RED_PREFIX = "\033[91m"
@@ -60,12 +61,14 @@ def print_summary(msg: str):
print(f"{_DECMAN_MSG_TAG} {_CYAN_PREFIX}SUMMARY{_RESET_SUFFIX}: {msg}") print(f"{_DECMAN_MSG_TAG} {_CYAN_PREFIX}SUMMARY{_RESET_SUFFIX}: {msg}")
def print_list(msg: str, def print_list(
msg: str,
l: list[str], l: list[str],
elements_per_line: typing.Optional[int] = None, elements_per_line: typing.Optional[int] = None,
max_line_width: typing.Optional[int] = None, max_line_width: typing.Optional[int] = None,
limit_to_term_size: bool = True, limit_to_term_size: bool = True,
level: int = SUMMARY): level: int = SUMMARY,
):
""" """
Prints a summary message to the user along with a list of elements. Prints a summary message to the user along with a list of elements.
@@ -89,8 +92,11 @@ def print_list(msg: str,
max_line_width = 2**32 # Big enough to basically be unlimited max_line_width = 2**32 # Big enough to basically be unlimited
if limit_to_term_size: if limit_to_term_size:
max_line_width = shutil.get_terminal_size().columns - len( max_line_width = (
_SPACING) - len(_CONTINUATION_PREFIX) shutil.get_terminal_size().columns
- len(_SPACING)
- len(_CONTINUATION_PREFIX)
)
lines = [f"{l.pop(0)}"] lines = [f"{l.pop(0)}"]
index = 0 index = 0
@@ -131,10 +137,9 @@ def print_debug(msg: str):
print(f"{_DECMAN_MSG_TAG} {_GRAY_PREFIX}DEBUG{_RESET_SUFFIX}: {msg}") print(f"{_DECMAN_MSG_TAG} {_GRAY_PREFIX}DEBUG{_RESET_SUFFIX}: {msg}")
def prompt_number(msg: str, def prompt_number(
min_num: int, msg: str, min_num: int, max_num: int, default: typing.Optional[int] = None
max_num: int, ) -> int:
default: typing.Optional[int] = None) -> int:
""" """
Prompts the user for a integer. Prompts the user for a integer.
""" """
@@ -263,11 +268,12 @@ class Store:
if latest_path is None: if latest_path is None:
return None return None
assert latest_version is not None, "If latest_path is set, then latest_version is set." assert latest_version is not None, (
"If latest_path is set, then latest_version is set."
)
return (latest_version, latest_path) return (latest_version, latest_path)
def add_package_to_cache(self, package: str, version: str, def add_package_to_cache(self, package: str, version: str, path_to_built_pkg: str):
path_to_built_pkg: str):
""" """
Adds a built package to the package file cache. Tries to remove excess cached packages. Adds a built package to the package file cache. Tries to remove excess cached packages.
""" """
@@ -332,14 +338,13 @@ class Store:
d = { d = {
"source_file": self.source_file, "source_file": self.source_file,
"allow_running_source_without_prompt": "allow_running_source_without_prompt": self.allow_running_source_without_prompt,
self.allow_running_source_without_prompt,
"enabled_systemd_units": self.enabled_systemd_units, "enabled_systemd_units": self.enabled_systemd_units,
"enabled_user_systemd_units": self._enabled_user_systemd_units, "enabled_user_systemd_units": self._enabled_user_systemd_units,
"enabled_modules": self.enabled_modules, "enabled_modules": self.enabled_modules,
"created_files": self.created_files, "created_files": self.created_files,
"package_file_cache": self._package_file_cache, "package_file_cache": self._package_file_cache,
"pkgbuild_git_commits": self.pkgbuild_latest_reviewed_commits "pkgbuild_git_commits": self.pkgbuild_latest_reviewed_commits,
} }
try: try:
@@ -370,7 +375,8 @@ class Store:
store.source_file = d.get("source_file", None) store.source_file = d.get("source_file", None)
store.allow_running_source_without_prompt = d.get( store.allow_running_source_without_prompt = d.get(
"allow_running_source_without_prompt", False) "allow_running_source_without_prompt", False
)
store.enabled_systemd_units = d.get( store.enabled_systemd_units = d.get(
"enabled_systemd_units", "enabled_systemd_units",
[], [],
@@ -390,12 +396,10 @@ class Store:
return store return store
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print_error(f"{e}") print_error(f"{e}")
raise err.UserFacingError( raise err.UserFacingError("Failed to parse decman store json.") from e
"Failed to parse decman store json.") from e
except OSError as e: except OSError as e:
print_error(f"{e}") print_error(f"{e}")
raise err.UserFacingError( raise err.UserFacingError("Failed to read saved decman store.") from e
"Failed to read saved decman store.") from e
class Source: class Source:
@@ -455,7 +459,8 @@ class Source:
""" """
for module in self.modules: for module in self.modules:
if module.enabled and module.version != store.enabled_modules.get( if module.enabled and module.version != store.enabled_modules.get(
module.name, module.version): module.name, module.version
):
module.after_version_change() module.after_version_change()
elif module.enabled and module.name not in store.enabled_modules: elif module.enabled and module.name not in store.enabled_modules:
module.after_version_change() module.after_version_change()
@@ -467,8 +472,10 @@ class Source:
""" """
created_files = [] created_files = []
def install_files(files: dict[str, decman.File], def install_files(
variables: typing.Optional[dict[str, str]] = None): files: dict[str, decman.File],
variables: typing.Optional[dict[str, str]] = None,
):
for target, file in files.items(): for target, file in files.items():
created_files.append(target) created_files.append(target)
@@ -481,19 +488,24 @@ class Source:
except OSError as e: except OSError as e:
print_error(f"{e}") print_error(f"{e}")
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to install file to {target}.") from e f"Failed to install file to {target}."
) from e
def install_dirs(dirs: dict[str, decman.Directory], def install_dirs(
variables: typing.Optional[dict[str, str]] = None): dirs: dict[str, decman.Directory],
variables: typing.Optional[dict[str, str]] = None,
):
for target, directory in dirs.items(): for target, directory in dirs.items():
try: try:
print_debug(f"Installing directory to {target}.") print_debug(f"Installing directory to {target}.")
created_files.extend( created_files.extend(
directory.copy_to(target, variables, only_print)) directory.copy_to(target, variables, only_print)
)
except OSError as e: except OSError as e:
print_error(f"{e}") print_error(f"{e}")
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to install directory to {target}.") from e f"Failed to install directory to {target}."
) from e
install_files(self.files) install_files(self.files)
install_dirs(self.directories) install_dirs(self.directories)
@@ -531,8 +543,7 @@ class Source:
return all_dirs return all_dirs
def files_to_remove(self, store: Store, def files_to_remove(self, store: Store, created_files: list[str]) -> list[str]:
created_files: list[str]) -> list[str]:
""" """
Returns all files that should be removed. Returns all files that should be removed.
""" """
@@ -587,8 +598,7 @@ class Source:
result[user] = entry result[user] = entry
return result return result
def packages_to_remove( def packages_to_remove(self, currently_installed_packages: list[str]) -> list[str]:
self, currently_installed_packages: list[str]) -> list[str]:
""" """
Returns all packages that should be removed. This includes pacman, aur and user packages. Returns all packages that should be removed. This includes pacman, aur and user packages.
""" """
@@ -601,7 +611,8 @@ class Source:
return result return result
def pacman_packages_to_install( def pacman_packages_to_install(
self, currently_installed_packages: list[str]) -> list[str]: self, currently_installed_packages: list[str]
) -> list[str]:
""" """
Returns all pacman packages that should be installed. Returns all pacman packages that should be installed.
""" """
@@ -614,7 +625,8 @@ class Source:
return result return result
def foreign_packages_to_install( def foreign_packages_to_install(
self, currently_installed_packages: list[str]) -> list[str]: self, currently_installed_packages: list[str]
) -> list[str]:
""" """
Returns all aur and user packages that should be installed. Returns all aur and user packages that should be installed.
""" """
@@ -704,11 +716,16 @@ class Pacman:
""" """
try: try:
packages = subprocess.run( packages = (
subprocess.run(
conf.commands.list_pkgs(), conf.commands.list_pkgs(),
check=True, check=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
).stdout.decode().strip().split('\n') )
.stdout.decode()
.strip()
.split("\n")
)
return packages return packages
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
@@ -722,9 +739,12 @@ class Pacman:
if dep in self._installable: if dep in self._installable:
return self._installable[dep] return self._installable[dep]
result = subprocess.run(conf.commands.is_installable(dep), result = (
check=False, subprocess.run(
capture_output=True).returncode == 0 conf.commands.is_installable(dep), check=False, capture_output=True
).returncode
== 0
)
self._installable[dep] = result self._installable[dep] = result
return result return result
@@ -734,18 +754,23 @@ class Pacman:
basically AUR packages. basically AUR packages.
""" """
try: try:
output = subprocess.run( output = (
subprocess.run(
conf.commands.list_foreign_pkgs_versioned(), conf.commands.list_foreign_pkgs_versioned(),
check=True, check=True,
stdout=subprocess.PIPE).stdout.decode().strip().split('\n') stdout=subprocess.PIPE,
)
.stdout.decode()
.strip()
.split("\n")
)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to get foreign packages using '{error.cmd}'. Output: {error.stdout}." f"Failed to get foreign packages using '{error.cmd}'. Output: {error.stdout}."
) from error ) from error
try: try:
return [(line.split(" ")[0], line.split(" ")[1]) return [(line.split(" ")[0], line.split(" ")[1]) for line in output]
for line in output]
except IndexError as error: except IndexError as error:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to parse foreign packages from pacman output. Output: {output}" f"Failed to parse foreign packages from pacman output. Output: {output}"
@@ -759,7 +784,8 @@ class Pacman:
return return
returncode, output = echo_and_capture_command( returncode, output = echo_and_capture_command(
conf.commands.install_pkgs(packages)) conf.commands.install_pkgs(packages)
)
if returncode != 0: if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to install packages using pacman. Process exited with code {returncode}." f"Failed to install packages using pacman. Process exited with code {returncode}."
@@ -768,9 +794,11 @@ class Pacman:
print_highlighted_pacman_messages(output) print_highlighted_pacman_messages(output)
try: try:
subprocess.run(conf.commands.set_as_explicitly_installed(packages), subprocess.run(
conf.commands.set_as_explicitly_installed(packages),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
"Failed to set packages as explicitly installed using pacman." "Failed to set packages as explicitly installed using pacman."
@@ -783,8 +811,7 @@ class Pacman:
if not deps: if not deps:
return return
returncode, output = echo_and_capture_command( returncode, output = echo_and_capture_command(conf.commands.install_deps(deps))
conf.commands.install_deps(deps))
if returncode != 0: if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to install packages as dependencies using pacman. Process exited with code {returncode}." f"Failed to install packages as dependencies using pacman. Process exited with code {returncode}."
@@ -801,7 +828,8 @@ class Pacman:
return return
returncode, output = echo_and_capture_command( returncode, output = echo_and_capture_command(
conf.commands.install_files(files)) conf.commands.install_files(files)
)
if returncode != 0: if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to install package files using pacman. Process exited with code {returncode}." f"Failed to install package files using pacman. Process exited with code {returncode}."
@@ -814,7 +842,8 @@ class Pacman:
subprocess.run( subprocess.run(
conf.commands.set_as_explicitly_installed(as_explicit), conf.commands.set_as_explicitly_installed(as_explicit),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
if conf.suppress_command_output: if conf.suppress_command_output:
print_error("Output:") print_error("Output:")
@@ -842,8 +871,7 @@ class Pacman:
if not packages: if not packages:
return return
returncode, output = echo_and_capture_command( returncode, output = echo_and_capture_command(conf.commands.remove(packages))
conf.commands.remove(packages))
if returncode != 0: if returncode != 0:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to remove packages using pacman. Process exited with code {returncode}." f"Failed to remove packages using pacman. Process exited with code {returncode}."
@@ -879,10 +907,9 @@ def echo_and_capture_command(program: list[str]) -> tuple[int, str]:
Returns a tuple containing the return code of the program as well as all output of the program. Returns a tuple containing the return code of the program as well as all output of the program.
""" """
with subprocess.Popen(program, with subprocess.Popen(
stdin=subprocess.PIPE, program, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
stdout=subprocess.PIPE, ) as process:
stderr=subprocess.STDOUT) as process:
os.set_blocking(process.stdout.fileno(), False) os.set_blocking(process.stdout.fileno(), False)
output_thread = _OutputCapturingThread(process.stdout) output_thread = _OutputCapturingThread(process.stdout)
@@ -915,7 +942,6 @@ def echo_and_capture_command(program: list[str]) -> tuple[int, str]:
class _OutputCapturingThread(threading.Thread): class _OutputCapturingThread(threading.Thread):
def __init__(self, stream): def __init__(self, stream):
super().__init__() super().__init__()
self._stream = stream self._stream = stream
@@ -948,12 +974,15 @@ class Systemd:
return return
try: try:
subprocess.run(conf.commands.enable_units(units), subprocess.run(
conf.commands.enable_units(units),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to enable systemd units: {units}") from error f"Failed to enable systemd units: {units}"
) from error
self.state.enabled_systemd_units += units self.state.enabled_systemd_units += units
def disable_units(self, units: list[str]): def disable_units(self, units: list[str]):
@@ -964,12 +993,15 @@ class Systemd:
return return
try: try:
subprocess.run(conf.commands.disable_units(units), subprocess.run(
conf.commands.disable_units(units),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to disable systemd units: {units}") from error f"Failed to disable systemd units: {units}"
) from error
for unit in units: for unit in units:
try: try:
self.state.enabled_systemd_units.remove(unit) self.state.enabled_systemd_units.remove(unit)
@@ -984,9 +1016,11 @@ class Systemd:
return return
try: try:
subprocess.run(conf.commands.enable_user_units(units, user), subprocess.run(
conf.commands.enable_user_units(units, user),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to enable systemd units: {units} for {user}." f"Failed to enable systemd units: {units} for {user}."
@@ -1003,9 +1037,11 @@ class Systemd:
return return
try: try:
subprocess.run(conf.commands.disable_user_units(units, user), subprocess.run(
conf.commands.disable_user_units(units, user),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
raise err.UserFacingError( raise err.UserFacingError(
f"Failed to disable systemd units: {units} for {user}." f"Failed to disable systemd units: {units} for {user}."
+181 -130
View File
@@ -11,18 +11,18 @@ Terminology:
- all dependencies: normal dependencies and build dependencies combined - all dependencies: normal dependencies and build dependencies combined
""" """
import shutil
import subprocess
import os import os
import re import re
import shutil
import subprocess
import typing import typing
import requests import requests
import decman import decman
import decman.config as conf import decman.config as conf
import decman.lib as l
import decman.error as err import decman.error as err
import decman.lib as l
def strip_dependency(dep: str) -> str: def strip_dependency(dep: str) -> str:
@@ -58,10 +58,18 @@ class PackageInfo:
In case of AUR packages, these are fetched from AUR RPC. In case of AUR packages, these are fetched from AUR RPC.
""" """
def __init__(self, pkgname: str, pkgbase: str, version: str, def __init__(
provides: list[str], dependencies: list[str], self,
make_dependencies: list[str], check_dependencies: list[str], pkgname: str,
git_url: str, pacman: l.Pacman): pkgbase: str,
version: str,
provides: list[str],
dependencies: list[str],
make_dependencies: list[str],
check_dependencies: list[str],
git_url: str,
pacman: l.Pacman,
):
self.pkgname = pkgname self.pkgname = pkgname
self.pkgbase = pkgbase self.pkgbase = pkgbase
self.version = version self.version = version
@@ -79,22 +87,23 @@ class PackageInfo:
if pacman.is_installable(dep): if pacman.is_installable(dep):
self.pacman_dependencies.append(dep) self.pacman_dependencies.append(dep)
else: else:
self.foreign_dependencies_stripped.append( self.foreign_dependencies_stripped.append(strip_dependency(dep))
strip_dependency(dep))
for make_dep in make_dependencies: for make_dep in make_dependencies:
if pacman.is_installable(make_dep): if pacman.is_installable(make_dep):
self.pacman_make_dependencies.append(make_dep) self.pacman_make_dependencies.append(make_dep)
else: else:
self.foreign_make_dependencies_stripped.append( self.foreign_make_dependencies_stripped.append(
strip_dependency(make_dep)) strip_dependency(make_dep)
)
for check_dep in check_dependencies: for check_dep in check_dependencies:
if pacman.is_installable(check_dep): if pacman.is_installable(check_dep):
self.pacman_check_dependencies.append(check_dep) self.pacman_check_dependencies.append(check_dep)
else: else:
self.foreign_check_dependencies_stripped.append( self.foreign_check_dependencies_stripped.append(
strip_dependency(check_dep)) strip_dependency(check_dep)
)
def pkg_file_prefix(self) -> str: def pkg_file_prefix(self) -> str:
""" """
@@ -103,8 +112,9 @@ class PackageInfo:
return f"{self.pkgname}-{self.version}" return f"{self.pkgname}-{self.version}"
@staticmethod @staticmethod
def from_user_package(user_package: decman.UserPackage, def from_user_package(
pacman: l.Pacman) -> "PackageInfo": user_package: decman.UserPackage, pacman: l.Pacman
) -> "PackageInfo":
""" """
Converts a UserPackage to PackageInfo Converts a UserPackage to PackageInfo
""" """
@@ -132,8 +142,11 @@ class ForeignPackage:
def __eq__(self, value: object, /) -> bool: def __eq__(self, value: object, /) -> bool:
if isinstance(value, self.__class__): if isinstance(value, self.__class__):
return self.name == value.name \ return (
and self._all_recursive_foreign_deps == value._all_recursive_foreign_deps self.name == value.name
and self._all_recursive_foreign_deps
== value._all_recursive_foreign_deps
)
return False return False
def __hash__(self) -> int: def __hash__(self) -> int:
@@ -145,8 +158,7 @@ class ForeignPackage:
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.name}" return f"{self.name}"
def add_foreign_dependency_packages(self, def add_foreign_dependency_packages(self, package_names: typing.Iterable[str]):
package_names: typing.Iterable[str]):
""" """
Adds dependencies to the package. Adds dependencies to the package.
""" """
@@ -174,8 +186,7 @@ class DepNode:
Returns True if the given package name is in the parents of this DepNode. Returns True if the given package name is in the parents of this DepNode.
""" """
for name, parent in self.parents.items(): for name, parent in self.parents.items():
if name == pkgname or parent.is_pkgname_in_parents_recursive( if name == pkgname or parent.is_pkgname_in_parents_recursive(pkgname):
pkgname):
return True return True
return False return False
@@ -189,15 +200,15 @@ class DepGraph:
self.package_nodes: dict[str, DepNode] = {} self.package_nodes: dict[str, DepNode] = {}
self._childless_node_names = set() self._childless_node_names = set()
def add_requirement(self, child_pkgname: str, def add_requirement(self, child_pkgname: str, parent_pkgname: typing.Optional[str]):
parent_pkgname: typing.Optional[str]):
""" """
Adds a connection between two packages, creating the child package if it doesn't exist. Adds a connection between two packages, creating the child package if it doesn't exist.
The parent is the package that requires the child package. The parent is the package that requires the child package.
""" """
child_node = self.package_nodes.get( child_node = self.package_nodes.get(
child_pkgname, DepNode(ForeignPackage(child_pkgname))) child_pkgname, DepNode(ForeignPackage(child_pkgname))
)
self.package_nodes[child_pkgname] = child_node self.package_nodes[child_pkgname] = child_node
if len(child_node.children) == 0: if len(child_node.children) == 0:
@@ -212,7 +223,8 @@ class DepGraph:
raise err.UserFacingError( raise err.UserFacingError(
f"Foreign package dependency cycle detected involving '{child_pkgname}' \ f"Foreign package dependency cycle detected involving '{child_pkgname}' \
and '{parent_pkgname}'. Foreign package dependencies are also required \ and '{parent_pkgname}'. Foreign package dependencies are also required \
during package building and therefore dependency cycles cannot be handled.") during package building and therefore dependency cycles cannot be handled."
)
parent_node.children[child_pkgname] = child_node parent_node.children[child_pkgname] = child_node
child_node.parents[parent_pkgname] = parent_node child_node.parents[parent_pkgname] = parent_node
@@ -230,8 +242,7 @@ during package building and therefore dependency cycles cannot be handled.")
childless_node = self.package_nodes[childless_node_name] childless_node = self.package_nodes[childless_node_name]
for parent in childless_node.parents.values(): for parent in childless_node.parents.values():
new_deps = childless_node.pkg.get_all_recursive_foreign_dep_pkgs( new_deps = childless_node.pkg.get_all_recursive_foreign_dep_pkgs()
)
new_deps.add(childless_node.pkg.name) new_deps.add(childless_node.pkg.name)
parent.pkg.add_foreign_dependency_packages(new_deps) parent.pkg.add_foreign_dependency_packages(new_deps)
del parent.children[childless_node_name] del parent.children[childless_node_name]
@@ -270,8 +281,7 @@ class ExtendedPackageSearch:
times, because then those methods don't have to make new AUR RPC requests. times, because then those methods don't have to make new AUR RPC requests.
""" """
packages = list( packages = list(filter(lambda p: p not in self._package_info_cache, packages))
filter(lambda p: p not in self._package_info_cache, packages))
if len(packages) == 0: if len(packages) == 0:
return return
@@ -281,8 +291,7 @@ class ExtendedPackageSearch:
max_pkgs_per_request = 200 max_pkgs_per_request = 200
while packages: while packages:
to_request = map(lambda p: f"arg[]={p}", to_request = map(lambda p: f"arg[]={p}", packages[:max_pkgs_per_request])
packages[:max_pkgs_per_request])
packages = packages[max_pkgs_per_request:] packages = packages[max_pkgs_per_request:]
url = f"https://aur.archlinux.org/rpc/v5/info?{'&'.join(to_request)}" url = f"https://aur.archlinux.org/rpc/v5/info?{'&'.join(to_request)}"
@@ -293,8 +302,7 @@ class ExtendedPackageSearch:
d = request.json() d = request.json()
if d["type"] == "error": if d["type"] == "error":
raise err.UserFacingError( raise err.UserFacingError(f"AUR RPC returned error: {d['error']}")
f"AUR RPC returned error: {d['error']}")
for result in d["results"]: for result in d["results"]:
pkgname = result["Name"] pkgname = result["Name"]
@@ -304,8 +312,7 @@ class ExtendedPackageSearch:
for user_package in self._user_packages: for user_package in self._user_packages:
if user_package.pkgname == pkgname: if user_package.pkgname == pkgname:
l.print_debug( l.print_debug(f"'{pkgname}' found in user packages.")
f"'{pkgname}' found in user packages.")
self._package_info_cache[pkgname] = user_package self._package_info_cache[pkgname] = user_package
break break
else: # if not in user_packages then: else: # if not in user_packages then:
@@ -317,9 +324,9 @@ class ExtendedPackageSearch:
make_dependencies=result.get("MakeDepends", []), make_dependencies=result.get("MakeDepends", []),
check_dependencies=result.get("CheckDepends", []), check_dependencies=result.get("CheckDepends", []),
provides=result.get("Provides", []), provides=result.get("Provides", []),
git_url= git_url=f"https://aur.archlinux.org/{result['PackageBase']}.git",
f"https://aur.archlinux.org/{result['PackageBase']}.git", pacman=self._pacman,
pacman=self._pacman) )
self._package_info_cache[pkgname] = info self._package_info_cache[pkgname] = info
l.print_debug("Request completed.") l.print_debug("Request completed.")
@@ -355,8 +362,7 @@ class ExtendedPackageSearch:
d = request.json() d = request.json()
if d["type"] == "error": if d["type"] == "error":
raise err.UserFacingError( raise err.UserFacingError(f"AUR RPC returned error: {d['error']}")
f"AUR RPC returned error: {d['error']}")
if d["resultcount"] == 0: if d["resultcount"] == 0:
l.print_debug(f"'{package}' not found.") l.print_debug(f"'{package}' not found.")
@@ -373,9 +379,9 @@ class ExtendedPackageSearch:
make_dependencies=result.get("MakeDepends", []), make_dependencies=result.get("MakeDepends", []),
check_dependencies=result.get("CheckDepends", []), check_dependencies=result.get("CheckDepends", []),
provides=result.get("Provides", []), provides=result.get("Provides", []),
git_url= git_url=f"https://aur.archlinux.org/{result['PackageBase']}.git",
f"https://aur.archlinux.org/{result['PackageBase']}.git", pacman=self._pacman,
pacman=self._pacman) )
self._package_info_cache[package] = info self._package_info_cache[package] = info
@@ -386,8 +392,7 @@ class ExtendedPackageSearch:
f"Failed to fetch package information for {package} from AUR RPC." f"Failed to fetch package information for {package} from AUR RPC."
) from e ) from e
def find_provider( def find_provider(self, stripped_dependency: str) -> typing.Optional[PackageInfo]:
self, stripped_dependency: str) -> typing.Optional[PackageInfo]:
""" """
Finds a provider for a dependency. Finds a provider for a dependency.
@@ -425,10 +430,13 @@ class ExtendedPackageSearch:
return pkg return pkg
if len(user_pkg_results) > 1: if len(user_pkg_results) > 1:
return self._choose_provider(stripped_dependency, user_pkg_results, return self._choose_provider(
"user packages") stripped_dependency, user_pkg_results, "user packages"
)
url = f"https://aur.archlinux.org/rpc/v5/search/{stripped_dependency}?by=provides" url = (
f"https://aur.archlinux.org/rpc/v5/search/{stripped_dependency}?by=provides"
)
l.print_debug( l.print_debug(
f"Requesting providers for '{stripped_dependency}' from AUR. URL = {url}" f"Requesting providers for '{stripped_dependency}' from AUR. URL = {url}"
) )
@@ -437,8 +445,7 @@ class ExtendedPackageSearch:
d = request.json() d = request.json()
if d["type"] == "error": if d["type"] == "error":
raise err.UserFacingError( raise err.UserFacingError(f"AUR RPC returned error: {d['error']}")
f"AUR RPC returned error: {d['error']}")
if d["resultcount"] == 0: if d["resultcount"] == 0:
l.print_debug(f"'{stripped_dependency}' not found.") l.print_debug(f"'{stripped_dependency}' not found.")
@@ -461,8 +468,9 @@ class ExtendedPackageSearch:
f"Failed to search for {stripped_dependency} from AUR RPC." f"Failed to search for {stripped_dependency} from AUR RPC."
) from e ) from e
def _choose_provider(self, dep: str, possible_providers: list[str], def _choose_provider(
where: str) -> typing.Optional[PackageInfo]: self, dep: str, possible_providers: list[str], where: str
) -> typing.Optional[PackageInfo]:
min_selection = 1 min_selection = 1
max_selection = len(possible_providers) max_selection = len(possible_providers)
l.print_summary( l.print_summary(
@@ -478,7 +486,8 @@ class ExtendedPackageSearch:
f"Select a provider [{min_selection}-{max_selection}] (default: {min_selection}): ", f"Select a provider [{min_selection}-{max_selection}] (default: {min_selection}): ",
min_selection, min_selection,
max_selection, max_selection,
default=min_selection) default=min_selection,
)
info = self.get_package_info(possible_providers[selection - 1]) info = self.get_package_info(possible_providers[selection - 1])
if info is not None: if info is not None:
@@ -541,16 +550,17 @@ class ForeignPackageManager:
Class for dealing with foreign packages. Class for dealing with foreign packages.
""" """
def __init__(self, store: l.Store, pacman: l.Pacman, def __init__(self, store: l.Store, pacman: l.Pacman, search: ExtendedPackageSearch):
search: ExtendedPackageSearch):
self._store = store self._store = store
self._pacman = pacman self._pacman = pacman
self._search = search self._search = search
def upgrade(self, def upgrade(
self,
upgrade_devel: bool = False, upgrade_devel: bool = False,
force: bool = False, force: bool = False,
ignored_pkgs: typing.Optional[set[str]] = None): ignored_pkgs: typing.Optional[set[str]] = None,
):
""" """
Upgrades all foreign packages. Upgrades all foreign packages.
""" """
@@ -561,11 +571,9 @@ class ForeignPackageManager:
all_foreign_pkgs = self._pacman.get_versioned_foreign_packages() all_foreign_pkgs = self._pacman.get_versioned_foreign_packages()
all_explicit_pkgs = set(self._pacman.get_installed()) all_explicit_pkgs = set(self._pacman.get_installed())
l.print_debug( l.print_debug(f"Foreign packages to check for upgrades: {all_foreign_pkgs}")
f"Foreign packages to check for upgrades: {all_foreign_pkgs}")
self._search.try_caching_packages( self._search.try_caching_packages(list(map(lambda p: p[0], all_foreign_pkgs)))
list(map(lambda p: p[0], all_foreign_pkgs)))
as_explicit = [] as_explicit = []
as_deps = [] as_deps = []
@@ -579,8 +587,7 @@ class ForeignPackageManager:
f"Failed to find '{pkg}' from AUR or user provided packages." f"Failed to find '{pkg}' from AUR or user provided packages."
) )
if self.should_upgrade_package(pkg, ver, info.version, if self.should_upgrade_package(pkg, ver, info.version, upgrade_devel):
upgrade_devel):
if pkg in all_explicit_pkgs: if pkg in all_explicit_pkgs:
as_explicit.append(pkg) as_explicit.append(pkg)
else: else:
@@ -592,10 +599,12 @@ class ForeignPackageManager:
self.install(as_explicit, as_deps, force) self.install(as_explicit, as_deps, force)
def install(self, def install(
self,
foreign_pkgs: list[str], foreign_pkgs: list[str],
foreign_dep_pkgs: typing.Optional[list[str]] = None, foreign_dep_pkgs: typing.Optional[list[str]] = None,
force: bool = False): force: bool = False,
):
""" """
Installs the given foreign packages and their dependencies (both pacman/AUR). Installs the given foreign packages and their dependencies (both pacman/AUR).
""" """
@@ -607,39 +616,44 @@ class ForeignPackageManager:
return return
resolved_dependencies = self.resolve_dependencies( resolved_dependencies = self.resolve_dependencies(
foreign_pkgs, foreign_dep_pkgs) foreign_pkgs, foreign_dep_pkgs
)
l.print_list( l.print_list(
"The following foreign packages will be installed explicitly:", "The following foreign packages will be installed explicitly:",
list(resolved_dependencies.foreign_pkgs), list(resolved_dependencies.foreign_pkgs),
level=l.SUMMARY) level=l.SUMMARY,
)
l.print_list( l.print_list(
"The following foreign packages will be installed as dependencies:", "The following foreign packages will be installed as dependencies:",
list(resolved_dependencies.foreign_dep_pkgs), list(resolved_dependencies.foreign_dep_pkgs),
level=l.SUMMARY) level=l.SUMMARY,
)
l.print_list( l.print_list(
"The following foreign packages will be built in order to install other packages. They will not be installed:", "The following foreign packages will be built in order to install other packages. They will not be installed:",
list(resolved_dependencies.foreign_build_dep_pkgs), list(resolved_dependencies.foreign_build_dep_pkgs),
level=l.SUMMARY) level=l.SUMMARY,
)
if not l.prompt_confirm("Proceed?", default=True): if not l.prompt_confirm("Proceed?", default=True):
raise err.UserFacingError("Installing aborted.") raise err.UserFacingError("Installing aborted.")
l.print_summary("Installing foreign package dependencies from pacman.") l.print_summary("Installing foreign package dependencies from pacman.")
self._pacman.install_dependencies( self._pacman.install_dependencies(list(resolved_dependencies.pacman_deps))
list(resolved_dependencies.pacman_deps))
try: try:
with PackageBuilder(self._search, self._store, with PackageBuilder(
resolved_dependencies) as builder: self._search, self._store, resolved_dependencies
) as builder:
while resolved_dependencies.build_order: while resolved_dependencies.build_order:
to_build = resolved_dependencies.build_order.pop(0) to_build = resolved_dependencies.build_order.pop(0)
pkgbase = resolved_dependencies.get_pkgbase(to_build) pkgbase = resolved_dependencies.get_pkgbase(to_build)
package_names = resolved_dependencies.get_pkgs_with_common_pkgbase( package_names = resolved_dependencies.get_pkgs_with_common_pkgbase(
to_build) to_build
)
packages = [ packages = [
resolved_dependencies.packages[pkgname] resolved_dependencies.packages[pkgname]
@@ -663,16 +677,17 @@ class ForeignPackageManager:
if package_files_to_install or force: if package_files_to_install or force:
l.print_summary("Installing foreign packages.") l.print_summary("Installing foreign packages.")
self._pacman.install_files(package_files_to_install, self._pacman.install_files(
as_explicit=list( package_files_to_install,
resolved_dependencies.foreign_pkgs)) as_explicit=list(resolved_dependencies.foreign_pkgs),
)
else: else:
l.print_summary("No packages to install.") l.print_summary("No packages to install.")
def resolve_dependencies( def resolve_dependencies(
self, self,
foreign_pkgs: list[str], foreign_pkgs: list[str],
foreign_dep_pkgs: typing.Optional[list[str]] = None foreign_dep_pkgs: typing.Optional[list[str]] = None,
) -> ResolvedDependencies: ) -> ResolvedDependencies:
""" """
Resolves foreign dependencies of foreign packages. Resolves foreign dependencies of foreign packages.
@@ -690,7 +705,7 @@ class ForeignPackageManager:
graph = DepGraph() graph = DepGraph()
for name in (foreign_pkgs + foreign_dep_pkgs): for name in foreign_pkgs + foreign_dep_pkgs:
graph.add_requirement(name, None) graph.add_requirement(name, None)
seen_packages = set(foreign_pkgs + foreign_dep_pkgs) seen_packages = set(foreign_pkgs + foreign_dep_pkgs)
@@ -709,8 +724,7 @@ class ForeignPackageManager:
add_to.add(dep_info.pkgname) add_to.add(dep_info.pkgname)
l.print_debug( l.print_debug(f"Adding dependency {dep_info.pkgname} to package {pkgname}.")
f"Adding dependency {dep_info.pkgname} to package {pkgname}.")
graph.add_requirement(dep_info.pkgname, pkgname) graph.add_requirement(dep_info.pkgname, pkgname)
if dep_info.pkgname not in seen_packages: if dep_info.pkgname not in seen_packages:
to_process.append(dep_info.pkgname) to_process.append(dep_info.pkgname)
@@ -728,10 +742,14 @@ class ForeignPackageManager:
result.pacman_deps.update(info.pacman_dependencies) result.pacman_deps.update(info.pacman_dependencies)
result.add_pkgbase_info(pkgname, info.pkgbase) result.add_pkgbase_info(pkgname, info.pkgbase)
build_deps = info.foreign_make_dependencies_stripped + info.foreign_check_dependencies_stripped build_deps = (
info.foreign_make_dependencies_stripped
+ info.foreign_check_dependencies_stripped
)
self._search.try_caching_packages( self._search.try_caching_packages(
info.foreign_dependencies_stripped + build_deps) info.foreign_dependencies_stripped + build_deps
)
for depname in info.foreign_dependencies_stripped: for depname in info.foreign_dependencies_stripped:
process_dep(pkgname, depname, result.foreign_dep_pkgs) process_dep(pkgname, depname, result.foreign_dep_pkgs)
@@ -758,26 +776,29 @@ class ForeignPackageManager:
return result return result
def should_upgrade_package(self, def should_upgrade_package(
self,
package: str, package: str,
installed_version: str, installed_version: str,
fetched_version: str, fetched_version: str,
upgrade_devel=False) -> bool: upgrade_devel=False,
) -> bool:
""" """
Returns True if a package should be upgraded. Returns True if a package should be upgraded.
""" """
if upgrade_devel and is_devel(package): if upgrade_devel and is_devel(package):
l.print_debug( l.print_debug(f"Package {package} is devel package. It should be upgraded.")
f"Package {package} is devel package. It should be upgraded.")
return True return True
try: try:
result = int( result = int(
subprocess.run(conf.commands.compare_versions( subprocess.run(
installed_version, fetched_version), conf.commands.compare_versions(installed_version, fetched_version),
check=True, check=True,
stdout=subprocess.PIPE).stdout.decode()) stdout=subprocess.PIPE,
).stdout.decode()
)
should_upgrade = result < 0 should_upgrade = result < 0
l.print_debug( l.print_debug(
f"Installed version is: {installed_version}. Available version is {fetched_version}. Should upgrade: {should_upgrade}" f"Installed version is: {installed_version}. Available version is {fetched_version}. Should upgrade: {should_upgrade}"
@@ -786,7 +807,8 @@ class ForeignPackageManager:
except (ValueError, subprocess.CalledProcessError) as error: except (ValueError, subprocess.CalledProcessError) as error:
l.print_error(f"{error}") l.print_error(f"{error}")
raise err.UserFacingError( raise err.UserFacingError(
"Failed to compare versions using vercmp.") from error "Failed to compare versions using vercmp."
) from error
class PackageBuilder: class PackageBuilder:
@@ -796,8 +818,12 @@ class PackageBuilder:
always_included_packages = ["base-devel", "git"] always_included_packages = ["base-devel", "git"]
def __init__(self, search: ExtendedPackageSearch, store: l.Store, def __init__(
resolved_deps: ResolvedDependencies): self,
search: ExtendedPackageSearch,
store: l.Store,
resolved_deps: ResolvedDependencies,
):
self._search = search self._search = search
self._store = store self._store = store
self._resolved_deps = resolved_deps self._resolved_deps = resolved_deps
@@ -850,7 +876,8 @@ class PackageBuilder:
os.chdir(pkgbuild_dir) os.chdir(pkgbuild_dir)
git_url_info = self._search.get_package_info( git_url_info = self._search.get_package_info(
self._resolved_deps.get_some_pkgname(pkgbase)) self._resolved_deps.get_some_pkgname(pkgbase)
)
# Because all dependencies and packages should be resolved during the creation # Because all dependencies and packages should be resolved during the creation
# of ResolvedDependencies. git_url should not be None. # of ResolvedDependencies. git_url should not be None.
@@ -869,16 +896,16 @@ class PackageBuilder:
mkarchroot_env_vars = os.environ.copy() mkarchroot_env_vars = os.environ.copy()
try: try:
del mkarchroot_env_vars["GNUPGHOME"] del mkarchroot_env_vars["GNUPGHOME"]
l.print_debug( l.print_debug("Removed GNUPGHOME variable from mkarchroot environment.")
"Removed GNUPGHOME variable from mkarchroot environment.")
except KeyError: except KeyError:
pass pass
subprocess.run(conf.commands.make_chroot(self.chroot_dir, subprocess.run(
list(self._pkgs_in_chroot)), conf.commands.make_chroot(self.chroot_dir, list(self._pkgs_in_chroot)),
env=mkarchroot_env_vars, env=mkarchroot_env_vars,
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
def remove_build_environment(self): def remove_build_environment(self):
""" """
@@ -886,8 +913,9 @@ class PackageBuilder:
""" """
shutil.rmtree(conf.build_dir) shutil.rmtree(conf.build_dir)
def build_packages(self, package_base: str, packages: list[ForeignPackage], def build_packages(
force: bool): self, package_base: str, packages: list[ForeignPackage], force: bool
):
""" """
Builds package(s) with the same package base. Builds package(s) with the same package base.
@@ -906,8 +934,7 @@ class PackageBuilder:
l.print_info(f"Building '{' '.join(package_names)}'.") l.print_info(f"Building '{' '.join(package_names)}'.")
chroot_new_pacman_pkgs, chroot_pkg_files = self._get_chroot_packages( chroot_new_pacman_pkgs, chroot_pkg_files = self._get_chroot_packages(packages)
packages)
pkgbuild_dir = self.pkgbase_dir_map[package_base] pkgbuild_dir = self.pkgbase_dir_map[package_base]
os.chdir(pkgbuild_dir) os.chdir(pkgbuild_dir)
@@ -918,19 +945,24 @@ class PackageBuilder:
l.print_info("Installing build dependencies to chroot.") l.print_info("Installing build dependencies to chroot.")
subprocess.run(conf.commands.install_chroot_packages( subprocess.run(
conf.commands.install_chroot_packages(
self.chroot_dir, self.chroot_dir,
chroot_new_pacman_pkgs + PackageBuilder.always_included_packages), chroot_new_pacman_pkgs + PackageBuilder.always_included_packages,
),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
l.print_info("Making package.") l.print_info("Making package.")
subprocess.run(conf.commands.make_chroot_pkg(self.chroot_wd_dir, subprocess.run(
conf.makepkg_user, conf.commands.make_chroot_pkg(
chroot_pkg_files), self.chroot_wd_dir, conf.makepkg_user, chroot_pkg_files
),
check=True, check=True,
capture_output=conf.quiet_output) capture_output=conf.quiet_output,
)
for pkgname in package_names: for pkgname in package_names:
file = self._find_pkgfile(pkgname, pkgbuild_dir) file = self._find_pkgfile(pkgname, pkgbuild_dir)
@@ -958,10 +990,11 @@ class PackageBuilder:
for p in chroot_new_pacman_pkgs: for p in chroot_new_pacman_pkgs:
if p not in self._pkgs_in_chroot: if p not in self._pkgs_in_chroot:
to_remove.append(strip_dependency(p)) to_remove.append(strip_dependency(p))
subprocess.run(conf.commands.remove_chroot_packages( subprocess.run(
self.chroot_dir, to_remove), conf.commands.remove_chroot_packages(self.chroot_dir, to_remove),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
l.print_info(f"Finished building: '{' '.join(package_names)}'.") l.print_info(f"Finished building: '{' '.join(package_names)}'.")
@@ -1030,8 +1063,10 @@ class PackageBuilder:
for foreign_pkg in chroot_foreign_pkgs: for foreign_pkg in chroot_foreign_pkgs:
entry = self._store.get_package(foreign_pkg) entry = self._store.get_package(foreign_pkg)
assert entry is not None, "Build order determines that the dependencies are built \ assert entry is not None, (
"Build order determines that the dependencies are built \
before and thus are found in the cache." before and thus are found in the cache."
)
_, file = entry _, file = entry
@@ -1070,39 +1105,55 @@ before and thus are found in the cache."
The user is prompted to review the PKGBUILD and confirm if the package should be built. The user is prompted to review the PKGBUILD and confirm if the package should be built.
""" """
try: try:
subprocess.run(conf.commands.git_clone(git_url, "."), subprocess.run(
conf.commands.git_clone(git_url, "."),
check=True, check=True,
capture_output=conf.suppress_command_output) capture_output=conf.suppress_command_output,
)
if l.prompt_confirm(f"Review PKGBUILD or show diff for {pkgbase}?", if l.prompt_confirm(
default=True): f"Review PKGBUILD or show diff for {pkgbase}?", default=True
latest_reviewed_commit = self._store.pkgbuild_latest_reviewed_commits.get( ):
pkgbase) latest_reviewed_commit = (
self._store.pkgbuild_latest_reviewed_commits.get(pkgbase)
)
git_commit_ids = subprocess.run( git_commit_ids = (
subprocess.run(
conf.commands.git_log_commit_ids(), conf.commands.git_log_commit_ids(),
check=True, check=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
).stdout.decode().strip().split('\n') )
.stdout.decode()
.strip()
.split("\n")
)
if latest_reviewed_commit is None or latest_reviewed_commit not in git_commit_ids: if (
latest_reviewed_commit is None
or latest_reviewed_commit not in git_commit_ids
):
for file in os.scandir("."): for file in os.scandir("."):
if file.is_file() and not file.name.startswith("."): if file.is_file() and not file.name.startswith("."):
subprocess.run(conf.commands.review_file( subprocess.run(
file.path), conf.commands.review_file(file.path), check=True
check=True) )
else: else:
subprocess.run( subprocess.run(
conf.commands.git_diff(latest_reviewed_commit), conf.commands.git_diff(latest_reviewed_commit), check=True
check=True) )
if l.prompt_confirm("Build this package?", default=True): if l.prompt_confirm("Build this package?", default=True):
commit_id = subprocess.run( commit_id = (
subprocess.run(
conf.commands.git_get_commit_id(), conf.commands.git_get_commit_id(),
check=True, check=True,
capture_output=True).stdout.decode().strip() capture_output=True,
self._store.pkgbuild_latest_reviewed_commits[ )
pkgbase] = commit_id .stdout.decode()
.strip()
)
self._store.pkgbuild_latest_reviewed_commits[pkgbase] = commit_id
else: else:
raise err.UserFacingError("Building aborted.") raise err.UserFacingError("Building aborted.")