From bfb51434a07b2d41f655ffd95e662d455e8042d3 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Mon, 16 Mar 2026 10:06:15 +0200 Subject: [PATCH] initial impl --- .../application/application_repository.py | 4 + src/ahriman/application/handlers/add.py | 49 +++---- src/ahriman/application/handlers/rollback.py | 132 ++++++++++++++++++ src/ahriman/application/handlers/update.py | 42 +++--- src/ahriman/core/repository/package_info.py | 32 +++-- src/ahriman/web/web.py | 1 + 6 files changed, 207 insertions(+), 53 deletions(-) create mode 100644 src/ahriman/application/handlers/rollback.py diff --git a/src/ahriman/application/application/application_repository.py b/src/ahriman/application/application/application_repository.py index 400ace68..4f1931f5 100644 --- a/src/ahriman/application/application/application_repository.py +++ b/src/ahriman/application/application/application_repository.py @@ -162,6 +162,10 @@ class ApplicationRepository(ApplicationProperties): self.on_result(build_result) result.merge(build_result) + # filter packages which were prebuilt + succeeded = {package.base for package in build_result.success} + updates = filter(lambda package: package.base not in succeeded, updates) + builder = Updater.load(self.repository_id, self.configuration, self.repository) # ok so for now we split all packages into chunks and process each chunk accordingly diff --git a/src/ahriman/application/handlers/add.py b/src/ahriman/application/handlers/add.py index 6da7b9c5..172cec88 100644 --- a/src/ahriman/application/handlers/add.py +++ b/src/ahriman/application/handlers/add.py @@ -21,10 +21,10 @@ import argparse from ahriman.application.application import Application from ahriman.application.handlers.handler import Handler, SubParserAction +from ahriman.application.handlers.update import Update from ahriman.core.configuration import Configuration from ahriman.core.utils import enum_values, extract_user from ahriman.models.package_source import PackageSource -from ahriman.models.packagers import Packagers from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.repository_id import RepositoryId @@ -48,26 +48,7 @@ class Add(Handler): """ application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh) application.on_start() - - application.add(args.package, args.source, args.username) - patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else [] - for package in args.package: # for each requested package insert patch - for patch in patches: - application.reporter.package_patches_update(package, patch) - - if not args.now: - return - - packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False) - if args.changes: # generate changes if requested - application.changes(packages) - - packages = application.with_dependencies(packages, process_dependencies=args.dependencies) - packagers = Packagers(args.username, {package.base: package.packager for package in packages}) - - application.print_updates(packages, log_fn=application.logger.info) - result = application.update(packages, packagers, bump_pkgrel=args.increment) - Add.check_status(args.exit_code, not result.is_empty) + Add.perform_action(application, args) @staticmethod def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser: @@ -103,14 +84,34 @@ class Add(Handler): parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("-n", "--now", help="run update function after", action="store_true") - parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " - "-yy to force refresh even if up to date", - action="count", default=False) parser.add_argument("-s", "--source", help="explicitly specify the package source for this command", type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto) parser.add_argument("-u", "--username", help="build as user", default=extract_user()) parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append") + parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, " + "-yy to force refresh even if up to date", + action="count", default=False) + parser.set_defaults(aur=False, check_files=False, dry_run=False, local=False, manual=True, vcs=False) return parser + @staticmethod + def perform_action(application: Application, args: argparse.Namespace) -> None: + """ + perform add action + + Args: + application(Application): application instance + args(argparse.Namespace): command line args + """ + application.add(args.package, args.source, args.username) + patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else [] + for package in args.package: # for each requested package insert patch + for patch in patches: + application.reporter.package_patches_update(package, patch) + + if not args.now: + return + Update.perform_action(application, args) + arguments = [_set_package_add_parser] diff --git a/src/ahriman/application/handlers/rollback.py b/src/ahriman/application/handlers/rollback.py new file mode 100644 index 00000000..b3a71220 --- /dev/null +++ b/src/ahriman/application/handlers/rollback.py @@ -0,0 +1,132 @@ +# +# Copyright (c) 2021-2026 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import argparse + +from pathlib import Path + +from ahriman.application.application import Application +from ahriman.application.handlers.add import Add +from ahriman.application.handlers.handler import Handler, SubParserAction +from ahriman.core.configuration import Configuration +from ahriman.core.exceptions import UnknownPackageError +from ahriman.core.utils import extract_user +from ahriman.models.package import Package +from ahriman.models.package_source import PackageSource +from ahriman.models.repository_id import RepositoryId + + +class Rollback(Handler): + """ + package rollback handler + """ + + @classmethod + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + repository_id(RepositoryId): repository unique identifier + configuration(Configuration): configuration instance + report(bool): force enable or disable reporting + """ + application = Application(repository_id, configuration, report=report) + application.on_start() + + package = Rollback.package_load(application, args.package, args.version) + artifacts = Rollback.package_artifacts(application, package) + + args.package = [str(artifact) for artifact in artifacts] + Add.perform_action(application, args) + + if args.hold: + application.reporter.package_hold_update(package.base, enabled=True) + + @staticmethod + def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for package archives subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("package-rollback", help="rollback package", + description="rollback package to specified version from archives") + parser.add_argument("package", help="package base") + parser.add_argument("version", help="package version") + parser.add_argument("--hold", help="hold package afterwards", + action=argparse.BooleanOptionalAction, default=True) + parser.add_argument("-u", "--username", help="build as user", default=extract_user()) + parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False, + exit_code=False, increment=False, now=True, local=False, manual=False, refresh=False, + source=PackageSource.Archive, variable=None, vcs=False) + return parser + + @staticmethod + def package_artifacts(application: Application, package: Package) -> list[Path]: + """ + look for package artifacts and returns paths to them if any + + Args: + application(Application): application instance + package(Package): package descriptor + + Returns: + list[Path]: paths to found artifacts + + Raises: + UnknownPackageError: if artifacts do not exist + """ + # lookup for built artifacts + artifacts = application.repository.package_archives_lookup(package) + if not artifacts: + raise UnknownPackageError(package.base) from None + return artifacts + + @staticmethod + def package_load(application: Application, package_base: str, version: str) -> Package: + """ + load package from given arguments + + Args: + application(Application): application instance + package_base(str): package base + version(str): package version + + Returns: + Package: loaded package + + Raises: + UnknownPackageError: if package does not exist + """ + try: + package, _ = next(iter(application.reporter.package_get(package_base))) + package.version = version + + return package + except StopIteration: + raise UnknownPackageError(package_base) from None + + arguments = [_set_package_archives_parser] diff --git a/src/ahriman/application/handlers/update.py b/src/ahriman/application/handlers/update.py index 239ef12a..5fa10f25 100644 --- a/src/ahriman/application/handlers/update.py +++ b/src/ahriman/application/handlers/update.py @@ -48,22 +48,7 @@ class Update(Handler): """ application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh) application.on_start() - - packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, - check_files=args.check_files) - if args.changes: # generate changes if requested - application.changes(packages) - - if args.dry_run: # exit from application if no build requested - Update.check_status(args.exit_code, packages) # status code check - return - - packages = application.with_dependencies(packages, process_dependencies=args.dependencies) - packagers = Packagers(args.username, {package.base: package.packager for package in packages}) - - application.print_updates(packages, log_fn=application.logger.info) - result = application.update(packages, packagers, bump_pkgrel=args.increment) - Update.check_status(args.exit_code, not result.is_empty) + Update.perform_action(application, args) @staticmethod def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser: @@ -153,6 +138,31 @@ class Update(Handler): return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin return inner + @staticmethod + def perform_action(application: Application, args: argparse.Namespace) -> None: + """ + perform update action + + Args: + application(Application): application instance + args(argparse.Namespace): command line args + """ + packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, + check_files=args.check_files) + if args.changes: # generate changes if requested + application.changes(packages) + + if args.dry_run: # exit from application if no build requested + Update.check_status(args.exit_code, packages) # status code check + return + + packages = application.with_dependencies(packages, process_dependencies=args.dependencies) + packagers = Packagers(args.username, {package.base: package.packager for package in packages}) + + application.print_updates(packages, log_fn=application.logger.info) + result = application.update(packages, packagers, bump_pkgrel=args.increment) + Update.check_status(args.exit_code, not result.is_empty) + arguments = [ _set_repo_check_parser, _set_repo_update_parser, diff --git a/src/ahriman/core/repository/package_info.py b/src/ahriman/core/repository/package_info.py index be5f4560..e9fadedb 100644 --- a/src/ahriman/core/repository/package_info.py +++ b/src/ahriman/core/repository/package_info.py @@ -25,7 +25,6 @@ from pathlib import Path from tempfile import TemporaryDirectory from ahriman.core.alpm.pacman import Pacman -from ahriman.core.build_tools.package_version import PackageVersion from ahriman.core.build_tools.sources import Sources from ahriman.core.configuration import Configuration from ahriman.core.log import LazyLogging @@ -89,19 +88,21 @@ class PackageInfo(LazyLogging): return sorted(result) - def load_archives(self, packages: Iterable[Path]) -> list[Package]: + def load_archives(self, packages: Iterable[Path], *, newest_only: bool = True) -> list[Package]: """ load packages from list of archives Args: packages(Iterable[Path]): paths to package archives + newest_only(bool, optional): filter packages with the same base, keeping only fresh packages installed + (Default value = True) Returns: list[Package]: list of read packages """ sources = {package.base: package.remote for package, _, in self.reporter.package_get(None)} - result: dict[str, Package] = {} + result: dict[str, dict[str, Package]] = {} # we are iterating over bases, not single packages for full_path in packages: try: @@ -109,17 +110,23 @@ class PackageInfo(LazyLogging): if (source := sources.get(local.base)) is not None: # update source with remote local.remote = source - current = result.setdefault(local.base, local) - if current.version != local.version: - # force version to max of them - self.logger.warning("version of %s differs, found %s and %s", - current.base, current.version, local.version) - if PackageVersion(current).is_outdated(local, self.configuration, calculate_version=False): - current.version = local.version + loaded_versions = result.setdefault(local.base, {}) + current = loaded_versions.setdefault(local.version, local) current.packages.update(local.packages) except Exception: self.logger.exception("could not load package from %s", full_path) - return list(result.values()) + + if newest_only: + comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version) + for package_base, versions in result.items(): + newest = max(versions.values(), key=cmp_to_key(comparator)) + result[package_base] = {newest.version: newest} + + return [ + package + for versions in result.values() + for package in versions.values() + ] def package_archives(self, package_base: str) -> list[Package]: """ @@ -158,9 +165,8 @@ class PackageInfo(LazyLogging): if not archive.is_dir(): return [] - for path in filter(package_like, archive.iterdir()): + for built in self.load_archives(filter(package_like, archive.iterdir()), newest_only=False): # check if package version is the same - built = Package.from_archive(path) if built.version != package.version: continue diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index 0eab64da..a40ea234 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -104,6 +104,7 @@ def _create_watcher(path: Path, repository_id: RepositoryId) -> Watcher: package_info = PackageInfo() package_info.configuration = configuration package_info.paths = configuration.repository_paths + package_info.reporter = client package_info.repository_id = repository_id return Watcher(client, package_info)