Compare commits

...

43 Commits

Author SHA1 Message Date
arcanis e2e013a33b straight forward conftest 2026-07-13 16:33:45 +03:00
arcanis da0853b6f1 generic fixtures 2026-07-13 13:01:54 +03:00
arcanis 7db38ff9e4 reorder tests 2026-07-13 13:00:13 +03:00
arcanis 92ac036f78 migrate to hatch 2026-07-11 12:24:53 +03:00
arcanis aef7fa5f93 build: update frontend dependencies 2026-07-06 16:16:31 +03:00
arcanis c412b08135 fix: remove staleTime defaults for query client 2026-07-06 15:33:20 +03:00
arcanis a3b6372c11 fix: override head method for sse endpoint to make it actually working 2026-07-06 14:44:29 +03:00
arcanis 78288befb8 build: use python 3.13 lowerbound 2026-07-06 13:56:02 +03:00
arcanis 49cd3d43c8 fix: handle errors in sse stream 2026-06-09 01:01:08 +03:00
arcanis d9b52806c0 fix: clear subscriber map on shutdown
Even though it is not a case in the application, the interface could be
used externally
2026-06-07 11:58:04 +03:00
arcanis 774db2d780 type: fix typing ignore with the last typing update 2026-05-31 20:04:25 +03:00
arcanis 68afda4c71 feat: allow readonly events with read permission 2026-05-26 20:51:06 +03:00
arcanis fa9fa73078 docs: bump python version to 2.13 2026-05-08 10:25:24 +03:00
arcanis 3e1e24cb50 feat: SSE support (#162)
* event bus implementation

* update tests

* docs update

* review fixes

* update configs

* fix typo

* frontend changes

* install missing pacakge

* queue processing simplification
2026-05-08 10:20:03 +03:00
arcanis 18fe38c30b build: use build_date argument for docker image instead of guessing from pacman root 2026-05-05 13:45:42 +03:00
arcanis 6ee8b26bd5 feat: show aur url in interface 2026-04-22 06:56:28 +03:00
arcanis fce49f22c9 type: fix mypy warnings in updated version 2026-04-03 13:36:41 +03:00
arcanis af8e2c9e9b build: update rtd.io image 2026-03-30 19:20:03 +03:00
arcanis 1c312bb528 feat: add error boundary 2026-03-24 01:17:42 +02:00
arcanis e39194e9f6 feat: etag support 2026-03-23 23:58:56 +02:00
arcanis 21cc029c18 refactor: use usedforsecurity flag for md5 calculations 2026-03-23 23:07:31 +02:00
arcanis 40671b99d5 feat: allow to configure cors 2026-03-23 16:39:07 +02:00
arcanis 3ad2c494af docs: update docstrings 2026-03-22 17:23:16 +02:00
arcanis 34014d1cdd build: add more rules for typescript 2026-03-22 17:23:16 +02:00
arcanis 93ed2b864b docs: describe rollback procedure in docs 2026-03-22 17:23:16 +02:00
arcanis cca931ccd0 fix: handle errors on pkgbuild reading 2026-03-22 13:10:09 +02:00
arcanis 5e090cebdb refactor: reorder arguments in web ui 2026-03-22 04:03:41 +02:00
arcanis d7984c12f0 feat: package rollback support (#161)
* implement support of rollback handler

* react interface for the rollback
2026-03-22 00:36:48 +02:00
arcanis 96684d7ddd refactor: move package archive lockup to package info trait 2026-03-18 18:48:27 +02:00
arcanis 2e9837b70d build: update frontend dependencies 2026-03-15 19:18:58 +02:00
arcanis ac4a8fb2cd fix: hide ignore_list because it is ambiguous now 2026-03-15 19:06:53 +02:00
arcanis 1db8eb0ac4 fix: preserve hold status on status updates 2026-03-15 19:03:05 +02:00
arcanis dc394f7df9 feat: dynamic package hold (#160)
* add dynamic hold implementation to backend

* update frontend to support new status

* force reporter loader

* handle missing packages explicitly

* handle missing packages explicitly
2026-03-15 18:47:02 +02:00
arcanis 058f784b05 fix: center icons on buttons 2026-03-15 17:07:39 +02:00
arcanis f688768ca7 fix: fallback to timestamp in case if crypto context is not available
Previous tests have been done in either secure (HTTPS) or loopback
(localhost) environments, so I missed the case, that crypto methods
might not be available

Fixes #159
2026-03-13 17:00:06 +02:00
arcanis a09ad7617d feat: support archive listing 2026-03-12 22:26:35 +02:00
arcanis 81aeb56ba3 refactor: lightweight Package.from_archive method 2026-03-11 23:00:55 +02:00
arcanis 2cd4ef5e86 refactor: separate package_archives method from trigger 2026-03-11 13:36:59 +02:00
arcanis 998ed48dde feat: add pkgbuild subscommands 2026-03-11 13:31:19 +02:00
arcanis 021d88dc4c feat: store and show pkgbuild (implements #157) 2026-03-11 01:05:06 +02:00
arcanis 9012ee7144 feat: add dark theme support and increase contrast 2026-03-08 22:59:31 +02:00
arcanis 945ddb2942 Release 2.20.0 2026-03-08 16:51:28 +02:00
arcanis 9cd0926588 docs: add http client description 2026-03-08 16:49:46 +02:00
905 changed files with 6877 additions and 2680 deletions
+6
View File
@@ -26,6 +26,10 @@ jobs:
- uses: docker/setup-buildx-action@v3
- name: Set image date
id: args
run: echo "::set-output name=date::$(date -d yesterday +'%Y-%m-%d')"
- name: Login to docker hub
uses: docker/login-action@v3
with:
@@ -53,6 +57,8 @@ jobs:
- name: Build an image and push
uses: docker/build-push-action@v6
with:
build-args: |
BUILD_DATE=${{ steps.args.outputs.date }}
file: docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
+2 -2
View File
@@ -12,11 +12,11 @@ pacman -Syyu --noconfirm
# main dependencies
pacman -S --noconfirm devtools git npm pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
# make dependencies
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
pacman -S --noconfirm --asdeps base-devel python-build python-hatchling python-installer python-tox python-wheel
# optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then
# web server
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-aiohttp-sse-git python-cryptography python-jinja
# additional features
pacman -S --noconfirm gnupg ipython python-boto3 python-cerberus python-matplotlib rsync
fi
+5 -2
View File
@@ -103,5 +103,8 @@ docs/html/
# Frontend
node_modules/
package-lock.json
package/share/ahriman/templates/static/index.js
package/share/ahriman/templates/static/index.css
ahriman-web/package/share/ahriman/templates/static/index.js
ahriman-web/package/share/ahriman/templates/static/index.css
# local configs
/*.ini
+2
View File
@@ -2,4 +2,6 @@
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
asyncio_default_fixture_loop_scope = function
asyncio_mode = auto
pythonpath = tests
resource-path.directory-name-test-resources = ../../tests/testresources
spec_test_format = {result} {docstring_summary}
+2 -2
View File
@@ -1,9 +1,9 @@
version: 2
build:
os: ubuntu-20.04
os: ubuntu-lts-latest
tools:
python: "3.12"
python: "3.13"
apt_packages:
- graphviz
@@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0rc7" "ArcH linux ReposItory MANager"
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0" "ArcH linux ReposItory MANager"
.SH NAME
ahriman \- ArcH linux ReposItory MANager
.SH SYNOPSIS
+68
View File
@@ -0,0 +1,68 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "ahriman-core"
description = "ArcH linux ReposItory MANager, core package"
readme = "../README.md"
requires-python = ">=3.13"
license = {file = "../COPYING"}
authors = [
{name = "ahriman team"},
]
dependencies = [
"bcrypt",
"filelock",
"inflection",
"pyelftools",
"requests",
]
dynamic = ["version"]
[project.optional-dependencies]
journald = [
"systemd-python",
]
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
pacman = [
"pyalpm",
]
reports = [
"Jinja2",
]
s3 = [
"boto3",
]
shell = [
"IPython",
]
stats = [
"matplotlib",
]
unixsocket = [
"requests-unixsocket2",
]
validator = [
"cerberus",
]
[project.scripts]
ahriman = "ahriman.application.ahriman:run"
[project.urls]
Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases"
[tool.hatch.version]
path = "src/ahriman/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/ahriman"]
[tool.hatch.build.targets.wheel.shared-data]
"package/lib" = "lib"
"package/share" = "share"
@@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "2.20.0rc7"
__version__ = "2.20.0"
@@ -154,13 +154,13 @@ class Application(ApplicationPackages, ApplicationRepository):
for package_name, packager in missing.items():
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
# there is local cache, load package from it
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
leaf = Package.from_build(source_dir, self.repository.repository_id.architecture, packager)
else:
leaf = Package.from_aur(package_name, packager, include_provides=True)
portion[leaf.base] = leaf
# register package in the database
self.repository.reporter.set_unknown(leaf)
self.reporter.set_unknown(leaf)
return portion
@@ -45,8 +45,8 @@ class ApplicationRepository(ApplicationProperties):
if last_commit_sha is None:
continue # skip check in case if we can't calculate diff
changes = self.repository.package_changes(package, last_commit_sha)
self.repository.reporter.package_changes_update(package.base, changes)
if (changes := self.repository.package_changes(package, last_commit_sha)) is not None:
self.reporter.package_changes_update(package.base, changes)
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
"""
@@ -156,12 +156,15 @@ class ApplicationRepository(ApplicationProperties):
result = Result()
# process already built packages if any
built_packages = self.repository.packages_built()
if built_packages: # speedup a bit
if built_packages := self.repository.packages_built(): # speedup a bit
build_result = self.repository.process_update(built_packages, packagers)
self.on_result(build_result)
result.merge(build_result)
# filter packages which were prebuilt
succeeded = {package.base for package in build_result.success}
updates = [package for package in updates if package.base not in succeeded]
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
@@ -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]
@@ -0,0 +1,81 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import PackagePrinter
from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.repository_id import RepositoryId
class Archives(Handler):
"""
package archives handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@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=True)
match args.action:
case Action.List:
archives = application.repository.package_archives(args.package)
for package in archives:
PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info)
Archives.check_status(args.exit_code, bool(archives))
@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-archives", help="list package archive versions",
description="list available archive versions for the package")
parser.add_argument("package", help="package base")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--info", help="show additional package information",
action=argparse.BooleanOptionalAction, default=False)
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [_set_package_archives_parser]
@@ -47,14 +47,13 @@ class Change(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=True)
client = application.repository.reporter
client = Application(repository_id, configuration, report=True).reporter
match args.action:
case Action.List:
changes = client.package_changes_get(args.package)
ChangesPrinter(changes)(verbose=True, separator="")
Change.check_status(args.exit_code, not changes.is_empty)
Change.check_status(args.exit_code, changes.changes is not None)
case Action.Remove:
client.package_changes_update(args.package, Changes())
@@ -0,0 +1,93 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.repository_id import RepositoryId
class Hold(Handler):
"""
package hold 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
"""
client = Application(repository_id, configuration, report=True).reporter
match args.action:
case Action.Remove:
for package in args.package:
client.package_hold_update(package, enabled=False)
case Action.Update:
for package in args.package:
client.package_hold_update(package, enabled=True)
@staticmethod
def _set_package_hold_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for hold package subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-hold", help="hold package",
description="hold package from automatic updates")
parser.add_argument("package", help="package base", nargs="+")
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
return parser
@staticmethod
def _set_package_unhold_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for unhold package subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-unhold", help="unhold package",
description="remove package hold, allowing automatic updates")
parser.add_argument("package", help="package base", nargs="+")
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [
_set_package_hold_parser,
_set_package_unhold_parser,
]
@@ -0,0 +1,100 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
import argparse
from dataclasses import replace
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import PkgbuildPrinter
from ahriman.models.action import Action
from ahriman.models.repository_id import RepositoryId
class Pkgbuild(Handler):
"""
package pkgbuild handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@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
"""
client = Application(repository_id, configuration, report=True).reporter
match args.action:
case Action.List:
changes = client.package_changes_get(args.package)
PkgbuildPrinter(changes)(verbose=True, separator="")
Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
case Action.Remove:
changes = client.package_changes_get(args.package)
client.package_changes_update(args.package, replace(changes, pkgbuild=None))
@staticmethod
def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package pkgbuild subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
description="retrieve package PKGBUILD stored in database",
epilog="This command requests package status from the web interface "
"if it is available.")
parser.add_argument("package", help="package base")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
return parser
@staticmethod
def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package pkgbuild remove subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
description="remove the package PKGBUILD stored remotely")
parser.add_argument("package", help="package base")
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
@@ -44,8 +44,7 @@ class Reload(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=True)
client = application.repository.reporter
client = Application(repository_id, configuration, report=True).reporter
client.configuration_reload()
@staticmethod
@@ -0,0 +1,131 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
import argparse
from dataclasses import replace
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_rollback_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package rollback 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=True, 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 requested package artifacts and return paths to them
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)
return artifacts
@staticmethod
def package_load(application: Application, package_base: str, version: str) -> Package:
"""
load package from repository, while setting requested version
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)))
return replace(package, version=version)
except StopIteration:
raise UnknownPackageError(package_base) from None
arguments = [_set_package_rollback_parser]
@@ -52,7 +52,7 @@ class Status(Handler):
report(bool): force enable or disable reporting
"""
# we are using reporter here
client = Application(repository_id, configuration, report=True).repository.reporter
client = Application(repository_id, configuration, report=True).reporter
if args.ahriman:
service_status = client.status_get()
StatusPrinter(service_status.status)(verbose=args.info)
@@ -47,8 +47,7 @@ class StatusUpdate(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=True)
client = application.repository.reporter
client = Application(repository_id, configuration, report=True).reporter
match args.action:
case Action.Update if args.package:
@@ -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,
@@ -24,10 +24,11 @@ import tarfile
from collections.abc import Iterable, Iterator
from functools import cached_property
from pathlib import Path
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
from pyalpm import DB, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
from string import Template
from ahriman.core.alpm.pacman_database import PacmanDatabase
from ahriman.core.alpm.pacman_handle import PacmanHandle
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.utils import trim_package
@@ -61,16 +62,16 @@ class Pacman(LazyLogging):
self.refresh_database = refresh_database
@cached_property
def handle(self) -> Handle:
def handle(self) -> PacmanHandle:
"""
pyalpm handle
Returns:
Handle: generated pyalpm handle instance
PacmanHandle: generated pyalpm handle instance
"""
return self.__create_handle(refresh_database=self.refresh_database)
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> PacmanHandle:
"""
create lazy handle function
@@ -78,14 +79,14 @@ class Pacman(LazyLogging):
refresh_database(PacmanSynchronization): synchronize local cache to remote
Returns:
Handle: fully initialized pacman handle
PacmanHandle: fully initialized pacman handle
"""
pacman_root = self.configuration.getpath("alpm", "database")
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
root = self.configuration.getpath("alpm", "root")
handle = Handle(str(root), str(database_path))
handle = PacmanHandle(str(root), str(database_path))
for repository in self.configuration.getlist("alpm", "repositories"):
database = self.database_init(handle, repository, self.repository_id.architecture)
@@ -99,12 +100,12 @@ class Pacman(LazyLogging):
return handle
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
def database_copy(self, handle: PacmanHandle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
"""
copy database from the operating system root to the ahriman local home
Args:
handle(Handle): pacman handle which will be used for database copying
handle(PacmanHandle): pacman handle which will be used for database copying
database(DB): pacman database instance to be copied
pacman_root(Path): operating system pacman root
use_ahriman_cache(bool): use local ahriman cache instead of system one
@@ -133,12 +134,12 @@ class Pacman(LazyLogging):
with self.repository_paths.preserve_owner():
shutil.copy(src, dst)
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
def database_init(self, handle: PacmanHandle, repository: str, architecture: str) -> DB:
"""
create database instance from pacman handler and set its properties
Args:
handle(Handle): pacman handle which will be used for database initializing
handle(PacmanHandle): pacman handle which will be used for database initializing
repository(str): pacman repository name (e.g. core)
architecture(str): repository architecture
@@ -164,12 +165,12 @@ class Pacman(LazyLogging):
return database
def database_sync(self, handle: Handle, *, force: bool) -> None:
def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
"""
sync local database
Args:
handle(Handle): pacman handle which will be used for database sync
handle(PacmanHandle): pacman handle which will be used for database sync
force(bool): force database synchronization (same as ``pacman -Syy``)
"""
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
@@ -0,0 +1,81 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from pathlib import Path
from pyalpm import Handle, Package # type: ignore[import-not-found]
from tempfile import TemporaryDirectory
from typing import Any, ClassVar, Self
class PacmanHandle:
"""
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
Attributes:
handle(Handle): pyalpm handle instance
"""
_ephemeral: ClassVar[Self | None] = None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Args:
*args(Any): positional arguments for :class:`pyalpm.Handle`
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
"""
self.handle = Handle(*args, **kwargs)
@classmethod
def ephemeral(cls) -> Self:
"""
create temporary instance with no access to real databases
Returns:
Self: loaded class
"""
if cls._ephemeral is None:
# handle creates alpm version file, but we don't use it
# so it is ok to just remove it
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
cls._ephemeral = cls("/", dir_name)
return cls._ephemeral
def package_load(self, path: Path) -> Package:
"""
load package from path to the archive
Args:
path(Path): path to package archive
Returns:
Package: package instance
"""
return self.handle.load_pkg(str(path))
def __getattr__(self, item: str) -> Any:
"""
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
Args:
item(str): property name
Returns:
Any: attribute by its name
"""
return self.handle.__getattribute__(item)
@@ -104,7 +104,7 @@ class PkgbuildParser(shlex.shlex):
# ignore substitution and extend bash symbols
self.wordchars += "${}#:+-@!"
# in case of default behaviour, it will ignore, for example, segment part of url outside of quotes
# in case of default behavior, it will ignore, for example, segment part of url outside of quotes
self.commenters = ""
@staticmethod
@@ -72,7 +72,7 @@ class AUR(Remote):
parse RPC response to package list
Args:
response(dict[str, Any]): RPC response json
response(dict[str, Any]): RPC response JSON
Returns:
list[AURPackage]: list of parsed packages
@@ -74,7 +74,7 @@ class Official(Remote):
parse RPC response to package list
Args:
response(dict[str, Any]): RPC response json
response(dict[str, Any]): RPC response JSON
Returns:
list[AURPackage]: list of parsed packages
@@ -32,7 +32,7 @@ class OfficialSyncdb(Official):
updates.
This approach also has limitations, because we don't require superuser rights (neither going to download database
separately), the database file might be outdated and must be handled manually (or kind of). This behaviour might be
separately), the database file might be outdated and must be handled manually (or kind of). This behavior might be
changed in the future.
Still we leave search function based on the official repositories RPC.
@@ -72,17 +72,17 @@ class PackageArchive:
if not PackageArchive.is_elf(binary_file):
return []
elf_file = ELFFile(binary_file) # type: ignore[no-untyped-call]
elf_file = ELFFile(binary_file)
dynamic_section = next(
(section for section in elf_file.iter_sections() # type: ignore[no-untyped-call]
(section for section in elf_file.iter_sections()
if isinstance(section, DynamicSection)),
None)
if dynamic_section is None:
return []
return [
tag.needed
for tag in dynamic_section.iter_tags() # type: ignore[no-untyped-call]
tag.needed # type: ignore[attr-defined]
for tag in dynamic_section.iter_tags()
if tag.entry.d_tag == "DT_NEEDED"
]
@@ -26,6 +26,7 @@ from typing import ClassVar
from ahriman.core.exceptions import CalledProcessError
from ahriman.core.log import LazyLogging
from ahriman.core.utils import check_output, utcnow, walk
from ahriman.models.changes import Changes
from ahriman.models.package import Package
from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@@ -51,24 +52,25 @@ class Sources(LazyLogging):
}
@staticmethod
def changes(source_dir: Path, last_commit_sha: str | None) -> str | None:
def changes(source_dir: Path, last_commit_sha: str) -> Changes:
"""
extract changes from the last known commit if available
Args:
source_dir(Path): local path to directory with source files
last_commit_sha(str | None): last known commit hash
last_commit_sha(str): last known commit hash
Returns:
str | None: changes from the last commit if available or ``None`` otherwise
Changes: changes from the last commit if available
"""
if last_commit_sha is None:
return None # no previous reference found
instance = Sources()
diff = None
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
return instance.diff(source_dir, last_commit_sha)
return None
diff = instance.diff(source_dir, last_commit_sha)
pkgbuild = instance.read(source_dir, "HEAD", Path("PKGBUILD"))
return Changes(last_commit_sha, diff, pkgbuild)
@staticmethod
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
@@ -413,3 +415,21 @@ class Sources(LazyLogging):
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
else:
patch.write(sources_dir / "PKGBUILD")
def read(self, sources_dir: Path, commit_sha: str, path: Path) -> str | None:
"""
read file content from the specified commit
Args:
sources_dir(Path): local path to git repository
commit_sha(str): commit hash to read from
path(Path): path to file inside the repository
Returns:
str | None: file content at specified commit if available
"""
try:
return check_output(*self.git(), "show", f"{commit_sha}:{path}", cwd=sources_dir, logger=self.logger)
except CalledProcessError:
self.logger.exception("failed to read file %s at %s", path, commit_sha)
return None
@@ -72,7 +72,7 @@ class Configuration(configparser.RawConfigParser):
def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None:
"""
Args:
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behaviour. In case if it is set
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behavior. In case if it is set
to ``True``, the keys without values will be allowed (Default value = False)
allow_multi_key(bool, optional): if set to ``False``, then the default dictionary class will be used to
store keys internally. Otherwise, the special implementation will be used, which supports arrays
@@ -80,7 +80,7 @@ class Configuration(configparser.RawConfigParser):
"""
configparser.RawConfigParser.__init__(
self,
dict_type=ConfigurationMultiDict if allow_multi_key else dict, # type: ignore[arg-type]
dict_type=ConfigurationMultiDict if allow_multi_key else dict,
allow_no_value=allow_no_value,
strict=False,
empty_lines_in_values=not allow_multi_key,
@@ -358,6 +358,38 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"min": 0,
},
},
"cors_allow_headers": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_allow_methods": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_allow_origins": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_expose_headers": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"enable_archive_upload": {
"type": "boolean",
"coerce": "boolean",
@@ -150,6 +150,6 @@ class ShellTemplate(Template):
break
kwargs.update(mapping)
substituted = dict(generator(kwargs))
kwargs.update(dict(generator(kwargs)))
return self.safe_substitute(kwargs | substituted)
return self.safe_substitute(kwargs)

Some files were not shown because too many files have changed in this diff Show More