Compare commits

...

19 Commits

Author SHA1 Message Date
d98211e5e5 add possibility to run full update
In case if packages are not set from web, the spawner will run full
repository update
2022-10-31 02:41:24 +02:00
b97c8928e1 add daemon subcommand
This command emulates default systemd timer and can be useful in docker
container in order to run 24/7
2022-10-31 01:38:01 +02:00
649df81aa5 implement single-function patches (#69) 2022-10-30 03:11:03 +03:00
ad7cdb7d95 drop ahriman.core.triggers.Trigger.run method
In order to force new triggers to use on_result method, the old method
has been removed. However, default on_result method still checks if the
old method exists and tries to run it
2022-10-19 20:07:31 +03:00
4bb598d2eb fix rtd docs building
Commit 6633766cc3 introduced kw_only
dataclasess which require python 3.10+
2022-10-18 02:24:18 +03:00
f47be6cab0 disallow no values in configuration
This option could lead to missing warnings about missing or invalid
configuration values because code usually expects that values are exists
and not empty unless it is explicitly specified.

However, pacman configuration still requires this option in order to be
able to deal with boolean values
2022-10-18 02:13:01 +03:00
342b3cb652 Add gitremote triggers (#68)
* add gitremote pull trigger

* add push gitremote trigger

* docs update
2022-10-18 01:46:27 +03:00
fc0d8387df extend triggers to on_start and on_stop methods
This commit also replaces old run method to new on_result
2022-09-26 01:22:54 +03:00
e0b0c3caeb add minimal install step to workflow 2022-09-17 14:52:08 +03:00
61969dd682 make sqlite import consistent 2022-09-17 14:32:21 +03:00
e441d93a56 Release 2.2.2 2022-09-17 04:05:06 +03:00
664b6369bb skip architecture list patching in case if any architecture is set 2022-09-17 04:04:28 +03:00
4f6bd29ff4 Release 2.2.1 2022-09-14 04:49:08 +03:00
f6d9ea480a docs update 2022-09-14 04:48:11 +03:00
08f62842ba Release 2.2.0 2022-09-14 03:28:28 +03:00
1912790ccc Make optional dependencies trully optional (#67)
The issue appears when there is no boto, jinja and some other libraries
are not installed because the classes which use these libraries are
still being imported inside the package file. The fix removes those
imports from package root, because they should not be here, in fact,
content of report and upload packages must be imported only inside the
trigger class and only if they are actually required

This commit also adds setuptools as required dependency since it is used
for some parsers (previously it was provided dependency)
2022-09-11 01:44:06 +03:00
cf3c48ffeb patch architecture list in runtime (#66) 2022-08-09 15:18:20 +03:00
6633766cc3 frozen dataclasses 2022-07-26 14:40:28 +03:00
f73d1eb424 reduce docker image size a bit 2022-07-18 11:42:26 +03:00
155 changed files with 4886 additions and 3265 deletions

View File

@ -1 +1 @@
skips: ['B101', 'B105', 'B404'] skips: ['B101', 'B105', 'B106', 'B404']

View File

@ -7,6 +7,22 @@ on:
branches: [ master ] branches: [ master ]
jobs: jobs:
run-setup-minimal:
runs-on: ubuntu-latest
container:
image: archlinux:latest
volumes:
- ${{ github.workspace }}:/build
options: --privileged -w /build
steps:
- uses: actions/checkout@v2
- name: setup the minimal service in arch linux container
run: .github/workflows/setup.sh minimal
run-setup: run-setup:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -3,21 +3,25 @@
set -ex set -ex
[[ $1 = "minimal" ]] && MINIMAL_INSTALL=1
# install dependencies # install dependencies
echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf
# refresh the image # refresh the image
pacman --noconfirm -Syu pacman --noconfirm -Syu
# main dependencies # main dependencies
pacman --noconfirm -Sy base-devel devtools git pyalpm python-aur python-passlib python-srcinfo sudo pacman --noconfirm -Sy base-devel devtools git pyalpm python-aur python-passlib python-setuptools python-srcinfo sudo
# make dependencies # make dependencies
pacman --noconfirm -Sy python-build python-installer python-wheel pacman --noconfirm -Sy python-build python-installer python-wheel
# optional dependencies # optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then
# VCS support # VCS support
pacman --noconfirm -Sy breezy darcs mercurial subversion pacman --noconfirm -Sy breezy darcs mercurial subversion
# web server # web server
pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja pacman --noconfirm -Sy python-aioauth-client python-aiohttp python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
# additional features # additional features
pacman --noconfirm -Sy gnupg python-boto3 rsync pacman --noconfirm -Sy gnupg python-boto3 rsync
fi
# create fresh tarball # create fresh tarball
make VERSION=1.0.0 archlinux # well, it does not really matter which version we will put here make VERSION=1.0.0 archlinux # well, it does not really matter which version we will put here
@ -33,14 +37,17 @@ systemd-machine-id-setup
# special thing for the container, because /dev/log interface is not available there # special thing for the container, because /dev/log interface is not available there
sed -i "s/handlers = syslog_handler/handlers = console_handler/g" /etc/ahriman.ini.d/logging.ini sed -i "s/handlers = syslog_handler/handlers = console_handler/g" /etc/ahriman.ini.d/logging.ini
# initial setup command as root # initial setup command as root
ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "github" --web-port 8080 [[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "github" "${WEB_ARGS[@]}"
# enable services # enable services
systemctl enable ahriman-web@x86_64 systemctl enable ahriman-web@x86_64
systemctl enable ahriman@x86_64.timer systemctl enable ahriman@x86_64.timer
if [[ -z $MINIMAL_INSTALL ]]; then
# run web service (detached) # run web service (detached)
sudo -u ahriman -- ahriman -a x86_64 web & sudo -u ahriman -- ahriman -a x86_64 web &
WEBPID=$! WEB_PID=$!
sleep 15s # wait for the web service activation sleep 15s # wait for the web service activation
fi
# add the first package # add the first package
# the build itself does not really work in the container # the build itself does not really work in the container
sudo -u ahriman -- ahriman package-add --now yay sudo -u ahriman -- ahriman package-add --now yay
@ -49,4 +56,4 @@ sudo -u ahriman -- ahriman package-add --now yay
# run package check # run package check
sudo -u ahriman -- ahriman repo-update sudo -u ahriman -- ahriman repo-update
# stop web service lol # stop web service lol
kill $WEBPID [[ -z $WEB_PID ]] || kill $WEB_PID

View File

@ -4,7 +4,7 @@
set -ex set -ex
# install dependencies # install dependencies
pacman --noconfirm -Syu base-devel python-pip python-tox pacman --noconfirm -Syu base-devel python-pip python-setuptools python-tox
# run test and check targets # run test and check targets
make check tests make check tests

View File

@ -82,7 +82,8 @@ disable=raw-checker-failed,
fixme, fixme,
too-many-arguments, too-many-arguments,
duplicate-code, duplicate-code,
cyclic-import cyclic-import,
confusing-with-statement,
# Enable the message, report, category or checker with the given id(s). You can # Enable the message, report, category or checker with the given id(s). You can

View File

@ -5,7 +5,7 @@ formats: all
build: build:
os: ubuntu-20.04 os: ubuntu-20.04
tools: tools:
python: "3.9" python: "3.10"
sphinx: sphinx:
builder: html builder: html

View File

@ -1,4 +1,4 @@
FROM archlinux:base-devel FROM archlinux:base
# image configuration # image configuration
ENV AHRIMAN_ARCHITECTURE="x86_64" ENV AHRIMAN_ARCHITECTURE="x86_64"
@ -13,24 +13,22 @@ ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"
ENV AHRIMAN_USER="ahriman" ENV AHRIMAN_USER="ahriman"
# install environment # install environment
## install git which is required for AUR interaction and go for yay ## install minimal required packages
RUN pacman --noconfirm -Syu git go RUN pacman --noconfirm -Syu binutils fakeroot git make sudo
## create build user ## create build user
RUN useradd -m -d /home/build -s /usr/bin/nologin build && \ RUN useradd -m -d /home/build -s /usr/bin/nologin build && \
echo "build ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/build echo "build ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/build
## install AUR helper COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
RUN YAY_DIR="$(runuser -u build -- mktemp -d)" && \
git clone https://aur.archlinux.org/yay.git "$YAY_DIR" && \
cd "$YAY_DIR" && \
runuser -u build -- makepkg --noconfirm --install && \
cd - && rm -r "$YAY_DIR"
## install package dependencies ## install package dependencies
RUN runuser -u build -- yay --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-srcinfo && \ ## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
runuser -u build -- yay --noconfirm -Sy python-build python-installer python-wheel && \ RUN pacman --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-setuptools python-srcinfo && \
runuser -u build -- yay --noconfirm -Sy breezy darcs mercurial python-aioauth-client python-aiohttp \ pacman --noconfirm -Sy python-build python-installer python-wheel && \
python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security \ pacman --noconfirm -Sy breezy mercurial python-aiohttp python-boto3 python-cryptography python-jinja rsync subversion && \
python-aiohttp-session python-boto3 python-cryptography python-jinja \ runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-jinja2 python-aiohttp-debugtoolbar \
rsync subversion python-aiohttp-session python-aiohttp-security
# cleanup unused
RUN find "/var/cache/pacman/pkg" -type f -delete
# install ahriman # install ahriman
## copy tree ## copy tree
@ -41,7 +39,7 @@ RUN cd "/home/build/ahriman" && \
cp ./*-src.tar.xz "package/archlinux" && \ cp ./*-src.tar.xz "package/archlinux" && \
cd "package/archlinux" && \ cd "package/archlinux" && \
runuser -u build -- makepkg --noconfirm --install --skipchecksums && \ runuser -u build -- makepkg --noconfirm --install --skipchecksums && \
cd - && rm -r "/home/build/ahriman" cd / && rm -r "/home/build/ahriman"
VOLUME ["/var/lib/ahriman"] VOLUME ["/var/lib/ahriman"]

View File

@ -56,4 +56,4 @@ version:
ifndef VERSION ifndef VERSION
$(error VERSION is required, but not set) $(error VERSION is required, but not set)
endif endif
sed -i '/__version__ = .*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py sed -i 's/^__version__ = .*/__version__ = "$(VERSION)"/' src/ahriman/version.py

View File

@ -30,6 +30,6 @@ For installation details kindly refer to the [documentation](https://ahriman.rea
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/latest/configuration.html). Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/latest/configuration.html).
The application provides reasonable defaults which allow to use it out-of-box, though additional steps (like configuring build toolchain and sudoers) is recommended and can be easily achieved by following install instructions. The application provides reasonable defaults which allow to use it out-of-box; however additional steps (like configuring build toolchain and sudoers) are recommended and can be easily achieved by following install instructions.
## [FAQ](https://ahriman.readthedocs.io/en/latest/faq.html) ## [FAQ](https://ahriman.readthedocs.io/en/latest/faq.html)

View File

@ -33,7 +33,7 @@ fi
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" repo-setup "${AHRIMAN_SETUP_ARGS[@]}" ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" repo-setup "${AHRIMAN_SETUP_ARGS[@]}"
# refresh database # refresh database
runuser -u build -- yay --noconfirm -Syy &> /dev/null pacman -Syy &> /dev/null
# create machine-id which is required by build tools # create machine-id which is required by build tools
systemd-machine-id-setup &> /dev/null systemd-machine-id-setup &> /dev/null

12
docker/install-aur-package.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
set -e
for PACKAGE in "$@"; do
BUILD_DIR="$(mktemp -d)"
git clone https://aur.archlinux.org/"$PACKAGE".git "$BUILD_DIR"
cd "$BUILD_DIR"
makepkg --noconfirm --install --rmdeps --syncdeps
cd /
rm -r "$BUILD_DIR"
done

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 537 KiB

After

Width:  |  Height:  |  Size: 537 KiB

View File

@ -3,7 +3,7 @@
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
.B ahriman .B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ... [-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
.SH DESCRIPTION .SH DESCRIPTION
ArcH linux ReposItory MANager ArcH linux ReposItory MANager
@ -71,7 +71,7 @@ remove package status
update package status update package status
.TP .TP
\fBahriman\fR \fI\,patch-add\/\fR \fBahriman\fR \fI\,patch-add\/\fR
add patch set add patch for PKGBUILD function
.TP .TP
\fBahriman\fR \fI\,patch-list\/\fR \fBahriman\fR \fI\,patch-list\/\fR
list patch sets list patch sets
@ -79,6 +79,9 @@ list patch sets
\fBahriman\fR \fI\,patch-remove\/\fR \fBahriman\fR \fI\,patch-remove\/\fR
remove patch set remove patch set
.TP .TP
\fBahriman\fR \fI\,patch-set-add\/\fR
add patch set
.TP
\fBahriman\fR \fI\,repo-backup\/\fR \fBahriman\fR \fI\,repo-backup\/\fR
backup repository data backup repository data
.TP .TP
@ -284,21 +287,25 @@ set status for specified packages. If no packages supplied, service status will
new status new status
.SH COMMAND \fI\,'ahriman patch-add'\/\fR .SH COMMAND \fI\,'ahriman patch-add'\/\fR
usage: ahriman patch-add [-h] [-t TRACK] package usage: ahriman patch-add [-h] [-p PATCH] package variable
create or update source patches create or update patched PKGBUILD function or variable
.TP .TP
\fBpackage\fR \fBpackage\fR
path to directory with changed files for patch addition/update package base
.TP
\fBvariable\fR
PKGBUILD variable or function name. If variable is a function, it must end with ()
.SH OPTIONS \fI\,'ahriman patch-add'\/\fR .SH OPTIONS \fI\,'ahriman patch-add'\/\fR
.TP .TP
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR \fB\-p\fR \fI\,PATCH\/\fR, \fB\-\-patch\fR \fI\,PATCH\/\fR
files which has to be tracked path to file which contains function or variable value. If not set, the value will be read from stdin
.SH COMMAND \fI\,'ahriman patch-list'\/\fR .SH COMMAND \fI\,'ahriman patch-list'\/\fR
usage: ahriman patch-list [-h] [-e] [package] usage: ahriman patch-list [-h] [-e] [-v VARIABLE] [package]
list available patches for the package list available patches for the package
@ -311,8 +318,12 @@ package base
\fB\-e\fR, \fB\-\-exit\-code\fR \fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty return non\-zero exit status if result is empty
.TP
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
if set, show only patches for specified PKGBUILD variables
.SH COMMAND \fI\,'ahriman patch-remove'\/\fR .SH COMMAND \fI\,'ahriman patch-remove'\/\fR
usage: ahriman patch-remove [-h] package usage: ahriman patch-remove [-h] [-v VARIABLE] package
remove patches for the package remove patches for the package
@ -320,6 +331,26 @@ remove patches for the package
\fBpackage\fR \fBpackage\fR
package base package base
.SH OPTIONS \fI\,'ahriman patch-remove'\/\fR
.TP
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
should be used for single\-function patches in case if you wold like to remove only specified PKGBUILD variables. In case
if not set, it will remove all patches related to the package
.SH COMMAND \fI\,'ahriman patch-set-add'\/\fR
usage: ahriman patch-set-add [-h] [-t TRACK] package
create or update source patches
.TP
\fBpackage\fR
path to directory with changed files for patch addition/update
.SH OPTIONS \fI\,'ahriman patch-set-add'\/\fR
.TP
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
files which has to be tracked
.SH COMMAND \fI\,'ahriman repo-backup'\/\fR .SH COMMAND \fI\,'ahriman repo-backup'\/\fR
usage: ahriman repo-backup [-h] path usage: ahriman repo-backup [-h] path
@ -391,7 +422,7 @@ just perform check for packages without rebuild process itself
.TP .TP
\fB\-\-from\-database\fR \fB\-\-from\-database\fR
read packages from database instead of filesystem. This feature in particular is required in case if you would like to read packages from database instead of filesystem. This feature in particular is required in case if you would like to
restore repository from another repository instance. Note however that in order to restore packages you need to have restore repository from another repository instance. Note, however, that in order to restore packages you need to have
original ahriman instance run with web service and have run repo\-update at least once. original ahriman instance run with web service and have run repo\-update at least once.
.TP .TP

View File

@ -28,6 +28,14 @@ ahriman.core.database.migrations.m002\_user\_access module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.database.migrations.m003\_patch\_variables module
--------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m003_patch_variables
:members:
:no-undoc-members:
:show-inheritance:
Module contents Module contents
--------------- ---------------

View File

@ -36,6 +36,14 @@ ahriman.core.formatters.package\_printer module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.formatters.patch\_printer module
---------------------------------------------
.. automodule:: ahriman.core.formatters.patch_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.printer module ahriman.core.formatters.printer module
-------------------------------------- --------------------------------------

View File

@ -0,0 +1,29 @@
ahriman.core.gitremote package
==============================
Submodules
----------
ahriman.core.gitremote.remote\_pull\_trigger module
---------------------------------------------------
.. automodule:: ahriman.core.gitremote.remote_pull_trigger
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.gitremote.remote\_push\_trigger module
---------------------------------------------------
.. automodule:: ahriman.core.gitremote.remote_push_trigger
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.gitremote
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -12,6 +12,7 @@ Subpackages
ahriman.core.build_tools ahriman.core.build_tools
ahriman.core.database ahriman.core.database
ahriman.core.formatters ahriman.core.formatters
ahriman.core.gitremote
ahriman.core.report ahriman.core.report
ahriman.core.repository ahriman.core.repository
ahriman.core.sign ahriman.core.sign
@ -38,6 +39,14 @@ ahriman.core.exceptions module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.lazy\_logging module
---------------------------------
.. automodule:: ahriman.core.lazy_logging
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.spawn module ahriman.core.spawn module
------------------------- -------------------------

View File

@ -92,6 +92,14 @@ ahriman.models.package\_source module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.pkgbuild\_patch module
-------------------------------------
.. automodule:: ahriman.models.pkgbuild_patch
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.property module ahriman.models.property module
------------------------------ ------------------------------

View File

@ -35,12 +35,13 @@ This package contains everything which is required for any time of application r
* ``ahriman.core.build_tools`` is a package which provides wrapper for ``devtools`` commands. * ``ahriman.core.build_tools`` is a package which provides wrapper for ``devtools`` commands.
* ``ahriman.core.database`` is everything including data and schema migrations for database. * ``ahriman.core.database`` is everything including data and schema migrations for database.
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers. * ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* ``ahriman.core.report`` is a package with reporting classes. Usually it must be called by ``ahriman.core.report.Report.load`` method. * ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation. * ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation.
* ``ahriman.core.sign`` package provides sign feature (only gpg calls are available). * ``ahriman.core.sign`` package provides sign feature (only gpg calls are available).
* ``ahriman.core.status`` contains helpers and watcher class which are required for web application. Reporter must be initialized by using ``ahriman.core.status.client.Client.load`` method. * ``ahriman.core.status`` contains helpers and watcher class which are required for web application. Reporter must be initialized by using ``ahriman.core.status.client.Client.load`` method.
* ``ahriman.core.triggers`` package contains base trigger classes. Classes from this package must be imported in order to implement user extensions. In fact, ``ahriman.core.report`` and ``ahriman.core.upload`` use this package. * ``ahriman.core.triggers`` package contains base trigger classes. Classes from this package must be imported in order to implement user extensions. In fact, ``ahriman.core.report`` and ``ahriman.core.upload`` use this package.
* ``ahriman.core.upload`` package provides sync feature, must be called by ``ahriman.core.upload.Upload.load`` method. * ``ahriman.core.upload`` package provides sync feature, should not be called directly.
This package also provides some generic functions and classes which may be used by other packages: This package also provides some generic functions and classes which may be used by other packages:
@ -202,12 +203,14 @@ In order to configure users there are special commands.
Triggers Triggers
^^^^^^^^ ^^^^^^^^
Triggers are extensions which can be used in order to perform any actions after the update process. The package provides two default extensions - one is report generation and another one is remote upload feature. Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit. The package provides two default extensions - one is report generation and another one is remote upload feature.
The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join remain part by ``.`` and interpret as module path, import module and extract attribute from it. The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join remain part by ``.`` and interpret as module path, import module and extract attribute from it.
The loaded triggers will be called with ``ahriman.models.result.Result`` and ``List[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs. The loaded triggers will be called with ``ahriman.models.result.Result`` and ``List[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs.
In addition triggers can implement ``on_start`` and ``on_stop`` actions which will be called on the application start and right before the application exit. The ``on_start`` action is usually being called from handlers directly in order to make sure that no trigger will be run when it is not required (e.g. on user management). As soon as ``on_start`` action is called, the additional flag will be set; ``ahriman.core.triggers.TriggerLoader`` class implements ``__del__`` method in which, if the flag is set, the ``on_stop`` actions will be called.
For more details how to deal with the triggers, refer to :doc:`documentation <triggers>` and modules descriptions. For more details how to deal with the triggers, refer to :doc:`documentation <triggers>` and modules descriptions.
Remote synchronization Remote synchronization

View File

@ -75,6 +75,18 @@ Settings for signing packages or repository. Group name can refer to architectur
* ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled. * ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled.
* ``key_*`` settings - PGP key which will be used for specific packages, string, optional. For example, if there is ``key_yay`` option the specified key will be used for yay package and default key for others. * ``key_*`` settings - PGP key which will be used for specific packages, string, optional. For example, if there is ``key_yay`` option the specified key will be used for yay package and default key for others.
``gitremote`` group
-------------------
Remote git source synchronization settings. Unlike ``Upload`` triggers those triggers are used for PKGBUILD synchronization - e.g. fetch from remote repository PKGBUILDs before updating process or pulling updated PKGBUILDs to the remote repository.
Both urls support authorization; to do so you'd need to prefix the url with authorization part, e.g. ``https://key:token@github.com/arcan1s/ahriman.git``. It is highly recommended to use application tokens instead of your user authorization details.
* ``pull_url`` - url of the remote repository from which PKGBUILDs can be pulled before build process, string, required.
* ``pull_branch`` - branch of the remote repository from which PKGBUILDs can be pulled before build process, string, optional, default is ``master``.
* ``push_url`` - url of the remote repository to which PKGBUILDs should be pushed after build process, string, required.
* ``push_branch`` - branch of the remote repository to which PKGBUILDs should be pushed after build process, string, optional, default is ``master``.
``report`` group ``report`` group
---------------- ----------------
@ -138,6 +150,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
* ``link_path`` - prefix for HTML links, string, required. * ``link_path`` - prefix for HTML links, string, required.
* ``template_path`` - path to Jinja2 template, string, required. * ``template_path`` - path to Jinja2 template, string, required.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``. * ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
``upload`` group ``upload`` group
---------------- ----------------
@ -167,6 +180,7 @@ This feature requires Github key creation (see below). Section name must be eith
#. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support). #. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support).
* ``repository`` - Github repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme). * ``repository`` - Github repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``username`` - Github authorization user, string, required. Basically the same as ``owner``. * ``username`` - Github authorization user, string, required. Basically the same as ``owner``.
``rsync`` type ``rsync`` type

View File

@ -112,14 +112,63 @@ TL;DR
Before using this command you will need to create local directory, put ``PKGBUILD`` there and generate ``.SRCINFO`` by using ``makepkg --printsrcinfo > .SRCINFO`` command. These packages will be stored locally and *will be ignored* during automatic update; in order to update the package you will need to run ``package-add`` command again. Before using this command you will need to create local directory, put ``PKGBUILD`` there and generate ``.SRCINFO`` by using ``makepkg --printsrcinfo > .SRCINFO`` command. These packages will be stored locally and *will be ignored* during automatic update; in order to update the package you will need to run ``package-add`` command again.
Err, I have remote repository with PKGBUILDs and would like to get versions from there automatically
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For that purpose you could use ``RemotePullTrigger`` trigger. To do so you will need:
#.
Append ``triggers`` option in ``build`` section with the following line:
.. code-block:: ini
[build]
triggers = ahriman.core.gitremote.RemotePullTrigger
#.
Configure trigger as following:
.. code-block:: ini
[gitremote]
pull_url = https://github.com/username/repository
During the next application run it will fetch repository from the specified url and will try to find packages there which can be used as local sources.
I would like to push PKGBUILDs to the remote repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For that purpose you'd need to use another trigger called ``RemotePushTrigger``. Configure it as following:
#.
Append ``triggers`` option in ``build`` section with the trigger name:
.. code-block:: ini
[build]
triggers = ahriman.core.gitremote.RemotePushTrigger
#.
Configure trigger as following:
.. code-block:: ini
[gitremote]
push_url = https://github.com/username/repository
Unlike ``RemotePullTrigger`` trigger, the ``RemotePushTrigger`` more likely will require authorization. It is highly recommended to use application tokens for that instead of using your password (e.g. for Github you can generate tokens `here <https://github.com/settings/tokens>`_ with scope ``public_repo``). Authorization can be supplied by using authorization part of the url, e.g. ``https://key:token@github.com/username/repository``.
But I just wanted to change PKGBUILD from AUR a bit! But I just wanted to change PKGBUILD from AUR a bit!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Well it is supported also. Well it is supported also. The recommended way is to patch specific function, e.g. by running ``sudo -u ahriman ahriman patch-add ahriman version``. This command will prompt for new value of the PKGBUILD variable ``version``. You can also write it to file and read from it ``sudo -u ahriman ahriman patch-add ahriman version version.patch``.
Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch:
#. Clone sources from AUR. #. Clone sources from AUR.
#. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches). #. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
#. Run ``sudo -u ahriman ahriman patch-add /path/to/local/directory/with/PKGBUILD``. #. Run ``sudo -u ahriman ahriman patch-set-add /path/to/local/directory/with/PKGBUILD``.
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management). The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
@ -240,7 +289,7 @@ The default action (in case if no arguments provided) is ``repo-update``. Basica
docker run -v /path/to/local/repo:/var/lib/ahriman -v /etc/ahriman.ini:/etc/ahriman.ini.d/10-overrides.ini arcan1s/ahriman:latest docker run -v /path/to/local/repo:/var/lib/ahriman -v /etc/ahriman.ini:/etc/ahriman.ini.d/10-overrides.ini arcan1s/ahriman:latest
By default, it runs ``repo-update``, but it can be overwritten to any other command you would like to, e.g.: The action can be specified during run, e.g.:
.. code-block:: shell .. code-block:: shell

View File

@ -1,12 +1,45 @@
Triggers Triggers
======== ========
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides two types of extensions - reporting and files uploading. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and implement ``run`` method The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides three types of extensions - reporting, files uploading and PKGBUILD syncronization. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and should implement at least one of the abstract methods:
* ``on_result`` - trigger action which will be called after build process, the build result and the list of repository packages will be supplied as arguments.
* ``on_start`` - trigger action which will be called right before the start of the application process.
* ``on_stop`` - action which will be called right before the exit.
Note, it isn't required to implement all of those methods (or even one of them), however, it is highly recommended to avoid trigger actions in ``__init__`` method as it will be run on any application start (e.g. even if you are just searching in AUR).
Built-in triggers
-----------------
For the configuration details and settings explanation kindly refer to the :doc:`documentation <configuration>`.
``ahriman.core.gitremote.RemotePullTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger will be called before any action (``on_start``) and pulls remote PKGBUILD repository locally; after that it copies found PKGBUILDs from the cloned repository to the local cache. It is useful in case if you have patched PGKBUILDs (or even missing in AUR) which you would like to use for package building and, technically, just simplifies the local package building.
In order to update those packages you would need to clone your repository separately, make changes in PKGBUILD (e.g. bump version and update checksums), commit them and push back. On the next ahriman's repository update, it will pull changes you commited and will perform package update.
``ahriman.core.gitremote.RemotePushTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger will be called right after build process (``on_result``). It will pick PKGBUILDs for the updated packages, pull them (together with any other files) and commit and push changes to remote repository. No real use cases, but the most of user repositories do it.
``ahriman.core.report.ReportTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Trigger which can be used for reporting. It implements ``on_result`` method and thus being called on each build update and generates report (e.g. html, telegram etc) according to the current settings.
``ahriman.core.upload.UploadTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger takes build result (``on_result``) and performs syncing of the local packages to the remote mirror (e.g. S3 or just by rsync).
Trigger example Trigger example
--------------- ---------------
Lets consider example of reporting trigger (e.g. `slack <https://slack.com/>`_, which provides easy HTTP API for integration triggers). Lets consider example of reporting trigger (e.g. `slack <https://slack.com/>`_, which provides easy HTTP API for integration triggers).gre
In order to post message to slack we will need a specific trigger url (something like ``https://hooks.slack.com/services/company_id/trigger_id``), channel (e.g. ``#archrepo``) and username (``repo-bot``). In order to post message to slack we will need a specific trigger url (something like ``https://hooks.slack.com/services/company_id/trigger_id``), channel (e.g. ``#archrepo``) and username (``repo-bot``).
@ -49,12 +82,12 @@ Obviously you can implement the specified method in class, but for guide purpose
self.channel = configuration.get("slack", "channel") self.channel = configuration.get("slack", "channel")
self.username = configuration.get("slack", "username") self.username = configuration.get("slack", "username")
def run(self, result, packages): def on_result(self, result, packages):
notify(result, self.slack_url, self.channel, self.username) notify(result, self.slack_url, self.channel, self.username)
Setup the trigger Setup the trigger
----------------- ^^^^^^^^^^^^^^^^^
First, put the trigger in any path it can be exported, e.g. by packing the resource into python package (which will lead to import path as ``package.slack_reporter.SlackReporter``) or just put file somewhere it can be accessed by application (e.g. ``/usr/local/lib/slack_reporter.py.SlackReporter``. First, put the trigger in any path it can be exported, e.g. by packing the resource into python package (which will lead to import path as ``package.slack_reporter.SlackReporter``) or just put file somewhere it can be accessed by application (e.g. ``/usr/local/lib/slack_reporter.py.SlackReporter``).
After that run application as usual and receive notification in your slack channel. After that run application as usual and receive notification in your slack channel.

View File

@ -1,13 +1,13 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.1.0 pkgver=2.2.2
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
url="https://github.com/arcan1s/ahriman" url="https://github.com/arcan1s/ahriman"
license=('GPL3') license=('GPL3')
depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo') depends=('devtools' 'git' 'pyalpm' 'python-inflection' 'python-passlib' 'python-requests' 'python-setuptools' 'python-srcinfo')
makedepends=('python-build' 'python-installer' 'python-wheel') makedepends=('python-build' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support' optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support' 'darcs: -darcs packages support'

View File

@ -20,7 +20,7 @@ archbuild_flags =
build_command = extra-x86_64-build build_command = extra-x86_64-build
ignore_packages = ignore_packages =
makechrootpkg_flags = makechrootpkg_flags =
makepkg_flags = --nocolor makepkg_flags = --nocolor --ignorearch
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
[repository] [repository]

View File

@ -27,7 +27,7 @@
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden> <button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-form" hidden>
<i class="bi bi-plus"></i> add <i class="bi bi-plus"></i> add
</button> </button>
<button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" disabled hidden> <button id="update-btn" class="btn btn-secondary" onclick="updatePackages()" hidden>
<i class="bi bi-play"></i> update <i class="bi bi-play"></i> update
</button> </button>
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden> <button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>

View File

@ -7,7 +7,6 @@
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
() => { () => {
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length); removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
updateButton.prop("disabled", !table.bootstrapTable("getSelections").length);
}); });
const architectureBadge = $("#badge-architecture"); const architectureBadge = $("#badge-architecture");
@ -16,8 +15,6 @@
const versionBadge = $("#badge-version"); const versionBadge = $("#badge-version");
function doPackageAction(uri, packages) { function doPackageAction(uri, packages) {
if (packages.length === 0)
return;
$.ajax({ $.ajax({
url: uri, url: uri,
data: JSON.stringify({packages: packages}), data: JSON.stringify({packages: packages}),

View File

@ -32,6 +32,7 @@ setup(
"inflection", "inflection",
"passlib", "passlib",
"requests", "requests",
"setuptools",
"srcinfo", "srcinfo",
], ],
setup_requires=[ setup_requires=[

View File

@ -82,6 +82,7 @@ def _parser() -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True) subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
_set_aur_search_parser(subparsers) _set_aur_search_parser(subparsers)
_set_daemon_parser(subparsers)
_set_help_parser(subparsers) _set_help_parser(subparsers)
_set_help_commands_unsafe_parser(subparsers) _set_help_commands_unsafe_parser(subparsers)
_set_key_import_parser(subparsers) _set_key_import_parser(subparsers)
@ -93,6 +94,7 @@ def _parser() -> argparse.ArgumentParser:
_set_patch_add_parser(subparsers) _set_patch_add_parser(subparsers)
_set_patch_list_parser(subparsers) _set_patch_list_parser(subparsers)
_set_patch_remove_parser(subparsers) _set_patch_remove_parser(subparsers)
_set_patch_set_add_parser(subparsers)
_set_repo_backup_parser(subparsers) _set_repo_backup_parser(subparsers)
_set_repo_check_parser(subparsers) _set_repo_check_parser(subparsers)
_set_repo_clean_parser(subparsers) _set_repo_clean_parser(subparsers)
@ -140,6 +142,28 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser return parser
def _set_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for daemon subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("daemon", help="run application as daemon",
description="start process which periodically will run update process",
formatter_class=_formatter)
parser.add_argument("-i", "--interval", help="interval between runs in seconds", type=int, default=60 * 60 * 12)
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
parser.add_argument("--no-local", help="do not check local packages for updates", action="store_true")
parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Daemon, dry_run=False, exit_code=False, package=[])
return parser
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for listing help subcommand add parser for listing help subcommand
@ -320,7 +344,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for new patch subcommand add parser for new single-function patch subcommand
Args: Args:
root(SubParserAction): subparsers for the commands root(SubParserAction): subparsers for the commands
@ -328,16 +352,18 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
Returns: Returns:
argparse.ArgumentParser: created argument parser argparse.ArgumentParser: created argument parser
""" """
parser = root.add_parser("patch-add", help="add patch set", description="create or update source patches", parser = root.add_parser("patch-add", help="add patch for PKGBUILD function",
epilog="In order to add a patch set for the package you will need to clone " description="create or update patched PKGBUILD function or variable",
"the AUR package manually, add required changes (e.g. external patches, " epilog="Unlike ``patch-set-add``, this function allows to patch only one PKGBUILD f"
"edit PKGBUILD) and run command, e.g. ``ahriman patch path/to/directory``. " "unction, e.g. typing ``ahriman patch-add ahriman version`` it will change the "
"By default it tracks *.patch and *.diff files, but this behavior can be changed " "``version`` inside PKGBUILD, typing ``ahriman patch-add ahriman build()`` "
"by using --track option", "it will change ``build()`` function inside PKGBUILD",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("package", help="path to directory with changed files for patch addition/update") parser.add_argument("package", help="package base")
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append", parser.add_argument("variable", help="PKGBUILD variable or function name. If variable is a function, "
default=["*.diff", "*.patch"]) "it must end with ()")
parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
"the value will be read from stdin", type=Path, nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True)
return parser return parser
@ -356,6 +382,8 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="list available patches for the package", formatter_class=_formatter) description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base", nargs="?") parser.add_argument("package", help="package base", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser return parser
@ -373,10 +401,39 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package", parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package",
formatter_class=_formatter) formatter_class=_formatter)
parser.add_argument("package", help="package base") parser.add_argument("package", help="package base")
parser.add_argument("-v", "--variable", help="should be used for single-function patches in case if you wold like "
"to remove only specified PKGBUILD variables. In case if not set, "
"it will remove all patches related to the package",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True) parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True)
return parser return parser
def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for new full-diff patch subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-set-add", help="add patch set", description="create or update source patches",
epilog="In order to add a patch set for the package you will need to clone "
"the AUR package manually, add required changes (e.g. external patches, "
"edit PKGBUILD) and run command, e.g. ``ahriman patch-set-add path/to/directory``. "
"By default it tracks *.patch and *.diff files, but this behavior can be changed "
"by using --track option",
formatter_class=_formatter)
parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path)
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True,
variable=None)
return parser
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for repository backup subcommand add parser for repository backup subcommand
@ -473,7 +530,7 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--from-database", parser.add_argument("--from-database",
help="read packages from database instead of filesystem. This feature in particular is " help="read packages from database instead of filesystem. This feature in particular is "
"required in case if you would like to restore repository from another repository " "required in case if you would like to restore repository from another repository "
"instance. Note however that in order to restore packages you need to have original " "instance. Note, however, that in order to restore packages you need to have original "
"ahriman instance run with web service and have run repo-update at least once.", "ahriman instance run with web service and have run repo-update at least once.",
action="store_true") action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")

View File

@ -49,15 +49,6 @@ class Application(ApplicationPackages, ApplicationRepository):
be used instead. be used instead.
""" """
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
"""
self.repository.process_triggers(result)
def _known_packages(self) -> Set[str]: def _known_packages(self) -> Set[str]:
""" """
load packages from repository and pacman repositories load packages from repository and pacman repositories
@ -73,3 +64,26 @@ class Application(ApplicationPackages, ApplicationRepository):
known_packages.update(properties.provides) known_packages.update(properties.provides)
known_packages.update(self.repository.pacman.all_packages()) known_packages.update(self.repository.pacman.all_packages())
return known_packages return known_packages
def on_result(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
"""
packages = self.repository.packages()
self.repository.triggers.on_result(result, packages)
def on_start(self) -> None:
"""
run triggers on start of the application
"""
self.repository.triggers.on_start()
def on_stop(self) -> None:
"""
run triggers on stop of the application. Note, however, that in most cases this method should not be called
directly as it will be called after on_start action
"""
self.repository.triggers.on_stop()

View File

@ -37,18 +37,6 @@ class ApplicationPackages(ApplicationProperties):
package control class package control class
""" """
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _known_packages(self) -> Set[str]: def _known_packages(self) -> Set[str]:
""" """
load packages from repository and pacman repositories load packages from repository and pacman repositories
@ -61,6 +49,18 @@ class ApplicationPackages(ApplicationProperties):
""" """
raise NotImplementedError raise NotImplementedError
def on_result(self, result: Result) -> None:
"""
generate report and sync to remote server
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _add_archive(self, source: str, *_: Any) -> None: def _add_archive(self, source: str, *_: Any) -> None:
""" """
add package from archive add package from archive
@ -86,8 +86,7 @@ class ApplicationPackages(ApplicationProperties):
self.database.build_queue_insert(package) self.database.build_queue_insert(package)
self.database.remote_update(package) self.database.remote_update(package)
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \ with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (local_dir := Path(dir_name)):
(local_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
Sources.load(local_dir, package, self.database.patches_get(package.base), self.repository.paths) Sources.load(local_dir, package, self.database.patches_get(package.base), self.repository.paths)
self._process_dependencies(local_dir, known_packages, without_dependencies) self._process_dependencies(local_dir, known_packages, without_dependencies)
@ -129,7 +128,7 @@ class ApplicationPackages(ApplicationProperties):
source(str): remote URL of the package archive source(str): remote URL of the package archive
""" """
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it? dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
response = requests.get(source, stream=True) response = requests.get(source, stream=True, timeout=None) # timeout=None to suppress pylint warns
response.raise_for_status() response.raise_for_status()
with dst.open("wb") as local_file: with dst.open("wb") as local_file:
@ -187,4 +186,4 @@ class ApplicationPackages(ApplicationProperties):
names(Iterable[str]): list of packages (either base or name) to remove names(Iterable[str]): list of packages (either base or name) to remove
""" """
self.repository.process_remove(names) self.repository.process_remove(names)
self._finalize(Result()) self.on_result(Result())

View File

@ -35,7 +35,7 @@ class ApplicationRepository(ApplicationProperties):
repository control class repository control class
""" """
def _finalize(self, result: Result) -> None: def on_result(self, result: Result) -> None:
""" """
generate report and sync to remote server generate report and sync to remote server
@ -89,7 +89,7 @@ class ApplicationRepository(ApplicationProperties):
self.update([]) self.update([])
# sign repository database if set # sign repository database if set
self.repository.sign.process_sign_repository(self.repository.repo.repo_path) self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
self._finalize(Result()) self.on_result(Result())
def unknown(self) -> List[str]: def unknown(self) -> List[str]:
""" """
@ -139,7 +139,7 @@ class ApplicationRepository(ApplicationProperties):
if not paths: if not paths:
return # don't need to process if no update supplied return # don't need to process if no update supplied
update_result = self.repository.process_update(paths) update_result = self.repository.process_update(paths)
self._finalize(result.merge(update_result)) self.on_result(result.merge(update_result))
# process built packages # process built packages
build_result = Result() build_result = Result()

View File

@ -22,6 +22,7 @@ from ahriman.application.handlers.handler import Handler
from ahriman.application.handlers.add import Add from ahriman.application.handlers.add import Add
from ahriman.application.handlers.backup import Backup from ahriman.application.handlers.backup import Backup
from ahriman.application.handlers.clean import Clean from ahriman.application.handlers.clean import Clean
from ahriman.application.handlers.daemon import Daemon
from ahriman.application.handlers.dump import Dump from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.help import Help from ahriman.application.handlers.help import Help
from ahriman.application.handlers.key_import import KeyImport from ahriman.application.handlers.key_import import KeyImport

View File

@ -45,6 +45,7 @@ class Add(Handler):
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
application = Application(architecture, configuration, no_report, unsafe) application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
application.add(args.package, args.source, args.without_dependencies) application.add(args.package, args.source, args.without_dependencies)
if not args.now: if not args.now:
return return

View File

@ -44,5 +44,6 @@ class Clean(Handler):
no_report(bool): force disable reporting no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
Application(architecture, configuration, no_report, unsafe).clean( application = Application(architecture, configuration, no_report, unsafe)
args.cache, args.chroot, args.manual, args.packages) application.on_start()
application.clean(args.cache, args.chroot, args.manual, args.packages)

View File

@ -0,0 +1,51 @@
#
# Copyright (c) 2021-2022 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
import threading
from typing import Type
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
class Daemon(Handler):
"""
daemon packages handler
"""
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
from ahriman.application.handlers import Update
Update.run(args, architecture, configuration, no_report, unsafe)
timer = threading.Timer(args.interval, Daemon.run, (args, architecture, configuration, no_report, unsafe))
timer.start()
timer.join()

View File

@ -46,5 +46,5 @@ class KeyImport(Handler):
no_report(bool): force disable reporting no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
Application(architecture, configuration, no_report, unsafe).repository.sign.key_import( application = Application(architecture, configuration, no_report, unsafe)
args.key_server, args.key) application.repository.sign.key_import(args.key_server, args.key)

View File

@ -18,17 +18,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional, Type from typing import List, Optional, Tuple, Type
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters import StringPrinter from ahriman.core.formatters import PatchPrinter
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class Patch(Handler): class Patch(Handler):
@ -50,52 +52,95 @@ class Patch(Handler):
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
application = Application(architecture, configuration, no_report, unsafe) application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
if args.action == Action.List: if args.action == Action.Update and args.variable is not None:
Patch.patch_set_list(application, args.package, args.exit_code) patch = Patch.patch_create_from_function(args.variable, args.patch)
Patch.patch_set_create(application, args.package, patch)
elif args.action == Action.Update and args.variable is None:
package_base, patch = Patch.patch_create_from_diff(args.package, args.track)
Patch.patch_set_create(application, package_base, patch)
elif args.action == Action.List:
Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
elif args.action == Action.Remove: elif args.action == Action.Remove:
Patch.patch_set_remove(application, args.package) Patch.patch_set_remove(application, args.package, args.variable)
elif args.action == Action.Update:
Patch.patch_set_create(application, Path(args.package), args.track)
@staticmethod @staticmethod
def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None: def patch_create_from_diff(sources_dir: Path, track: List[str]) -> Tuple[str, PkgbuildPatch]:
"""
create PKGBUILD plain diff patches from sources directory
Args:
sources_dir(Path): path to directory with the package sources
track(List[str]): track files which match the glob before creating the patch
Returns:
Tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
to current files
"""
package = Package.from_build(sources_dir)
patch = Sources.patch_create(sources_dir, *track)
return package.base, PkgbuildPatch(None, patch)
@staticmethod
def patch_create_from_function(variable: str, patch_path: Optional[Path]) -> PkgbuildPatch:
"""
create single-function patch set for the package base
Args:
variable(str): function or variable name inside PKGBUILD
patch_path(Path): optional path to patch content. If not set, it will be read from stdin
Returns:
PkgbuildPatch: created patch for the PKGBUILD function
"""
if patch_path is None:
print("Post new function or variable value below. Press Ctrl-D to finish:", file=sys.stderr)
patch = "".join(list(sys.stdin))
else:
patch = patch_path.read_text(encoding="utf8")
patch = patch.strip() # remove spaces around the patch
return PkgbuildPatch(variable, patch)
@staticmethod
def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None:
""" """
create patch set for the package base create patch set for the package base
Args: Args:
application(Application): application instance application(Application): application instance
sources_dir(Path): path to directory with the package sources package_base(str): package base
track(List[str]): track files which match the glob before creating the patch patch(PkgbuildPatch): patch descriptor
""" """
package = Package.from_build(sources_dir) application.database.patches_insert(package_base, patch)
patch = Sources.patch_create(sources_dir, *track)
application.database.patches_insert(package.base, patch)
@staticmethod @staticmethod
def patch_set_list(application: Application, package_base: Optional[str], exit_code: bool) -> None: def patch_set_list(application: Application, package_base: Optional[str], variables: List[str],
exit_code: bool) -> None:
""" """
list patches available for the package base list patches available for the package base
Args: Args:
application(Application): application instance application(Application): application instance
package_base(Optional[str]): package base package_base(Optional[str]): package base
variables(List[str]): extract patches only for specified PKGBUILD variables
exit_code(bool): exit with error on empty search result exit_code(bool): exit with error on empty search result
:
""" """
patches = application.database.patches_list(package_base) patches = application.database.patches_list(package_base, variables)
Patch.check_if_empty(exit_code, not patches) Patch.check_if_empty(exit_code, not patches)
for base, patch in patches.items(): for base, patch in patches.items():
content = base if package_base is None else patch PatchPrinter(base, patch).print(verbose=True, separator=" = ")
StringPrinter(content).print(verbose=True)
@staticmethod @staticmethod
def patch_set_remove(application: Application, package_base: str) -> None: def patch_set_remove(application: Application, package_base: str, variables: List[str]) -> None:
""" """
remove patch set for the package base remove patch set for the package base
Args: Args:
application(Application): application instance application(Application): application instance
package_base(str): package base package_base(str): package base
variables(List[str]): remove patches only for specified PKGBUILD variables
""" """
application.database.patches_remove(package_base) application.database.patches_remove(package_base, variables)

View File

@ -49,6 +49,8 @@ class Rebuild(Handler):
depends_on = set(args.depends_on) if args.depends_on else None depends_on = set(args.depends_on) if args.depends_on else None
application = Application(architecture, configuration, no_report, unsafe) application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
if args.from_database: if args.from_database:
updates = Rebuild.extract_packages(application) updates = Rebuild.extract_packages(application)
else: else:

View File

@ -44,4 +44,6 @@ class Remove(Handler):
no_report(bool): force disable reporting no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
Application(architecture, configuration, no_report, unsafe).remove(args.package) application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
application.remove(args.package)

View File

@ -46,6 +46,7 @@ class RemoveUnknown(Handler):
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
application = Application(architecture, configuration, no_report, unsafe) application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
unknown_packages = application.unknown() unknown_packages = application.unknown()
if args.dry_run: if args.dry_run:

View File

@ -133,7 +133,9 @@ class Setup(Handler):
repository(str): repository name repository(str): repository name
paths(RepositoryPaths): repository paths instance paths(RepositoryPaths): repository paths instance
""" """
configuration = Configuration() # allow_no_value=True is required because pacman uses boolean configuration in which just keys present
# (e.g. NoProgressBar) which will lead to exception
configuration = Configuration(allow_no_value=True)
# preserve case # preserve case
# stupid mypy thinks that it is impossible # stupid mypy thinks that it is impossible
configuration.optionxform = lambda key: key # type: ignore configuration.optionxform = lambda key: key # type: ignore

View File

@ -49,4 +49,5 @@ class Triggers(Handler):
if args.trigger: if args.trigger:
loader = application.repository.triggers loader = application.repository.triggers
loader.triggers = [loader.load_trigger(trigger) for trigger in args.trigger] loader.triggers = [loader.load_trigger(trigger) for trigger in args.trigger]
application.repository.process_triggers(Result()) application.on_start()
application.on_result(Result())

View File

@ -45,6 +45,7 @@ class Update(Handler):
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
""" """
application = Application(architecture, configuration, no_report, unsafe) application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
packages = application.updates(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs, packages = application.updates(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs,
Update.log_fn(application, args.dry_run)) Update.log_fn(application, args.dry_run))
Update.check_if_empty(args.exit_code, not packages) Update.check_if_empty(args.exit_code, not packages)

View File

@ -149,7 +149,7 @@ class Users(Handler):
Returns: Returns:
User: built user descriptor User: built user descriptor
""" """
user = User(args.username, args.password, args.role) password = args.password
if user.password is None: if password is None:
user.password = getpass.getpass() password = getpass.getpass()
return user return User(username=args.username, password=password, access=args.role)

View File

@ -36,11 +36,13 @@ class AUR(Remote):
DEFAULT_AUR_URL(str): (class attribute) default AUR url DEFAULT_AUR_URL(str): (class attribute) default AUR url
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
""" """
DEFAULT_AUR_URL = "https://aur.archlinux.org" DEFAULT_AUR_URL = "https://aur.archlinux.org"
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc" DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION = "5" DEFAULT_RPC_VERSION = "5"
DEFAULT_TIMEOUT = 30
@staticmethod @staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]: def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
@ -113,7 +115,7 @@ class AUR(Remote):
query[key] = value query[key] = value
try: try:
response = requests.get(self.DEFAULT_RPC_URL, params=query) response = requests.get(self.DEFAULT_RPC_URL, params=query, timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status() response.raise_for_status()
return self.parse_response(response.json()) return self.parse_response(response.json())
except requests.HTTPError as e: except requests.HTTPError as e:

View File

@ -36,11 +36,13 @@ class Official(Remote):
DEFAULT_ARCHLINUX_URL(str): (class attribute) default archlinux url DEFAULT_ARCHLINUX_URL(str): (class attribute) default archlinux url
DEFAULT_SEARCH_REPOSITORIES(List[str]): (class attribute) default list of repositories to search DEFAULT_SEARCH_REPOSITORIES(List[str]): (class attribute) default list of repositories to search
DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
""" """
DEFAULT_ARCHLINUX_URL = "https://archlinux.org" DEFAULT_ARCHLINUX_URL = "https://archlinux.org"
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib", "Community"] DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib", "Community"]
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json" DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
DEFAULT_TIMEOUT = 30
@staticmethod @staticmethod
def parse_response(response: Dict[str, Any]) -> List[AURPackage]: def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
@ -101,7 +103,10 @@ class Official(Remote):
List[AURPackage]: response parsed to package list List[AURPackage]: response parsed to package list
""" """
try: try:
response = requests.get(self.DEFAULT_RPC_URL, params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES}) response = requests.get(
self.DEFAULT_RPC_URL,
params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES},
timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status() response.raise_for_status()
return self.parse_response(response.json()) return self.parse_response(response.json())
except requests.HTTPError as e: except requests.HTTPError as e:

View File

@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import datetime
import shutil import shutil
from pathlib import Path from pathlib import Path
@ -25,6 +26,7 @@ from typing import List, Optional
from ahriman.core.lazy_logging import LazyLogging from ahriman.core.lazy_logging import LazyLogging
from ahriman.core.util import check_output, walk from ahriman.core.util import check_output, walk
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -42,6 +44,24 @@ class Sources(LazyLogging):
_check_output = check_output _check_output = check_output
@staticmethod
def extend_architectures(sources_dir: Path, architecture: str) -> List[PkgbuildPatch]:
"""
extend existing PKGBUILD with repository architecture
Args:
sources_dir(Path): local path to directory with source files
architecture(str): repository architecture
Returns:
List[PkgbuildPatch]: generated patch for PKGBUILD architectures if required
"""
architectures = Package.supported_architectures(sources_dir)
if "any" in architectures: # makepkg does not like when there is any other arch except for any
return []
architectures.add(architecture)
return [PkgbuildPatch("arch", list(architectures))]
@staticmethod @staticmethod
def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None: def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None:
""" """
@ -112,14 +132,14 @@ class Sources(LazyLogging):
exception=None, cwd=sources_dir, logger=instance.logger) exception=None, cwd=sources_dir, logger=instance.logger)
@staticmethod @staticmethod
def load(sources_dir: Path, package: Package, patch: Optional[str], paths: RepositoryPaths) -> None: def load(sources_dir: Path, package: Package, patches: List[PkgbuildPatch], paths: RepositoryPaths) -> None:
""" """
fetch sources from remote and apply patches fetch sources from remote and apply patches
Args: Args:
sources_dir(Path): local path to fetch sources_dir(Path): local path to fetch
package(Package): package definitions package(Package): package definitions
patch(Optional[str]): optional patch to be applied patches(List[PkgbuildPatch]): optional patch to be applied
paths(RepositoryPaths): repository paths instance paths(RepositoryPaths): repository paths instance
""" """
instance = Sources() instance = Sources()
@ -128,9 +148,8 @@ class Sources(LazyLogging):
shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True) shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
instance.fetch(sources_dir, package.remote) instance.fetch(sources_dir, package.remote)
if patch is None: patches.extend(instance.extend_architectures(sources_dir, paths.architecture))
instance.logger.info("no patches found") for patch in patches:
return
instance.patch_apply(sources_dir, patch) instance.patch_apply(sources_dir, patch)
@staticmethod @staticmethod
@ -150,6 +169,22 @@ class Sources(LazyLogging):
diff = instance.diff(sources_dir) diff = instance.diff(sources_dir)
return f"{diff}\n" # otherwise, patch will be broken return f"{diff}\n" # otherwise, patch will be broken
@staticmethod
def push(sources_dir: Path, remote: RemoteSource, *pattern: str) -> None:
"""
commit selected changes and push files to the remote repository
Args:
sources_dir(Path): local path to git repository
remote(RemoteSource): remote target, branch and url
*pattern(str): glob patterns
"""
instance = Sources()
instance.add(sources_dir, *pattern)
instance.commit(sources_dir)
Sources._check_output("git", "push", remote.git_url, remote.branch,
exception=None, cwd=sources_dir, logger=instance.logger)
def add(self, sources_dir: Path, *pattern: str) -> None: def add(self, sources_dir: Path, *pattern: str) -> None:
""" """
track found files via git track found files via git
@ -170,6 +205,20 @@ class Sources(LazyLogging):
*[str(fn.relative_to(sources_dir)) for fn in found_files], *[str(fn.relative_to(sources_dir)) for fn in found_files],
exception=None, cwd=sources_dir, logger=self.logger) exception=None, cwd=sources_dir, logger=self.logger)
def commit(self, sources_dir: Path, commit_message: Optional[str] = None) -> None:
"""
commit changes
Args:
sources_dir(Path): local path to git repository
commit_message(Optional[str]): optional commit message if any. If none set, message will be generated
according to the current timestamp
"""
if commit_message is None:
commit_message = f"Autogenerated commit at {datetime.datetime.utcnow()}"
Sources._check_output("git", "commit", "--all", "--message", commit_message,
exception=None, cwd=sources_dir, logger=self.logger)
def diff(self, sources_dir: Path) -> str: def diff(self, sources_dir: Path) -> str:
""" """
generate diff from the current version and write it to the output file generate diff from the current version and write it to the output file
@ -197,15 +246,18 @@ class Sources(LazyLogging):
dst = sources_dir / src.relative_to(pkgbuild_dir) dst = sources_dir / src.relative_to(pkgbuild_dir)
shutil.move(src, dst) shutil.move(src, dst)
def patch_apply(self, sources_dir: Path, patch: str) -> None: def patch_apply(self, sources_dir: Path, patch: PkgbuildPatch) -> None:
""" """
apply patches if any apply patches if any
Args: Args:
sources_dir(Path): local path to directory with git sources sources_dir(Path): local path to directory with git sources
patch(str): patch to be applied patch(PkgbuildPatch): patch to be applied
""" """
# create patch # create patch
self.logger.info("apply patch from database") self.logger.info("apply patch %s from database at %s", patch.key, sources_dir)
if patch.is_plain_diff:
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
exception=None, cwd=sources_dir, input_data=patch, logger=self.logger) exception=None, cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
else:
patch.write(sources_dir / "PKGBUILD")

View File

@ -70,11 +70,15 @@ class Configuration(configparser.RawConfigParser):
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"] ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini" SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
def __init__(self) -> None: def __init__(self, allow_no_value: bool = False) -> None:
""" """
default constructor. In the most cases must not be called directly default constructor. In the most cases must not be called directly
Args:
allow_no_value(bool): copies ``configparser.RawConfigParser`` behaviour. In case if it is set to ``True``,
the keys without values will be allowed
""" """
configparser.RawConfigParser.__init__(self, allow_no_value=True, converters={ configparser.RawConfigParser.__init__(self, allow_no_value=allow_no_value, converters={
"list": self.__convert_list, "list": self.__convert_list,
"path": self.__convert_path, "path": self.__convert_path,
}) })

View File

@ -83,7 +83,7 @@ class Migrations(LazyLogging):
module = import_module(f"{__name__}.{module_name}") module = import_module(f"{__name__}.{module_name}")
steps: List[str] = getattr(module, "steps", []) steps: List[str] = getattr(module, "steps", [])
self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps)) self.logger.debug("found migration %s at index %s with steps count %s", module_name, index, len(steps))
migrations.append(Migration(index, module_name, steps)) migrations.append(Migration(index=index, name=module_name, steps=steps))
return migrations return migrations
@ -97,7 +97,7 @@ class Migrations(LazyLogging):
migrations = self.migrations() migrations = self.migrations()
current_version = self.user_version() current_version = self.user_version()
expected_version = len(migrations) expected_version = len(migrations)
result = MigrationResult(current_version, expected_version) result = MigrationResult(old_version=current_version, new_version=expected_version)
if not result.is_outdated: if not result.is_outdated:
self.logger.info("no migrations required") self.logger.info("no migrations required")

View File

@ -0,0 +1,43 @@
#
# Copyright (c) 2021-2022 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/>.
#
__all__ = ["steps"]
steps = [
"""
alter table patches rename to patches_
""",
"""
create table patches (
package_base text not null,
variable text,
patch blob not null
)
""",
"""
create unique index patches_package_base_variable on patches (package_base, coalesce(variable, ''))
""",
"""
insert into patches (package_base, patch) select package_base, patch from patches_
""",
"""
drop table patches_
""",
]

View File

@ -58,7 +58,7 @@ class AuthOperations(Operations):
def run(connection: Connection) -> List[User]: def run(connection: Connection) -> List[User]:
return [ return [
User(cursor["username"], cursor["password"], UserAccess(cursor["access"])) User(username=cursor["username"], password=cursor["password"], access=UserAccess(cursor["access"]))
for cursor in connection.execute( for cursor in connection.execute(
""" """
select * from users select * from users

View File

@ -20,7 +20,6 @@
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from sqlite3 import Connection, Cursor
from typing import Any, Dict, Tuple, TypeVar, Callable from typing import Any, Dict, Tuple, TypeVar, Callable
from ahriman.core.lazy_logging import LazyLogging from ahriman.core.lazy_logging import LazyLogging
@ -46,7 +45,7 @@ class Operations(LazyLogging):
self.path = path self.path = path
@staticmethod @staticmethod
def factory(cursor: Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]: def factory(cursor: sqlite3.Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:
""" """
dictionary factory based on official documentation dictionary factory based on official documentation
@ -62,7 +61,7 @@ class Operations(LazyLogging):
result[column[0]] = row[index] result[column[0]] = row[index]
return result return result
def with_connection(self, query: Callable[[Connection], T], commit: bool = False) -> T: def with_connection(self, query: Callable[[sqlite3.Connection], T], commit: bool = False) -> T:
""" """
perform operation in connection perform operation in connection

View File

@ -154,7 +154,11 @@ class PackageOperations(Operations):
Dict[str, Package]: map of the package base to its descriptor (without packages themselves) Dict[str, Package]: map of the package base to its descriptor (without packages themselves)
""" """
return { return {
row["package_base"]: Package(row["package_base"], row["version"], RemoteSource.from_json(row), {}) row["package_base"]: Package(
base=row["package_base"],
version=row["version"],
remote=RemoteSource.from_json(row),
packages={})
for row in connection.execute("""select * from package_bases""") for row in connection.execute("""select * from package_bases""")
} }

View File

@ -17,10 +17,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from collections import defaultdict
from sqlite3 import Connection from sqlite3 import Connection
from typing import Dict, Optional from typing import Dict, List, Optional, Tuple
from ahriman.core.database.operations import Operations from ahriman.core.database.operations import Operations
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class PatchOperations(Operations): class PatchOperations(Operations):
@ -28,7 +31,7 @@ class PatchOperations(Operations):
operations for patches operations for patches
""" """
def patches_get(self, package_base: str) -> Optional[str]: def patches_get(self, package_base: str) -> List[PkgbuildPatch]:
""" """
retrieve patches for the package retrieve patches for the package
@ -36,62 +39,77 @@ class PatchOperations(Operations):
package_base(str): package base to search for patches package_base(str): package base to search for patches
Returns: Returns:
Optional[str]: plain text patch for the package List[PkgbuildPatch]: plain text patch for the package
""" """
return self.patches_list(package_base).get(package_base) return self.patches_list(package_base, []).get(package_base, [])
def patches_insert(self, package_base: str, patch: str) -> None: def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
""" """
insert or update patch in database insert or update patch in database
Args: Args:
package_base(str): package base to insert package_base(str): package base to insert
patch(str): patch content patch(PkgbuildPatch): patch content
""" """
def run(connection: Connection) -> None: def run(connection: Connection) -> None:
connection.execute( connection.execute(
""" """
insert into patches insert into patches
(package_base, patch) (package_base, variable, patch)
values values
(:package_base, :patch) (:package_base, :variable, :patch)
on conflict (package_base) do update set on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch patch = :patch
""", """,
{"package_base": package_base, "patch": patch}) {"package_base": package_base, "variable": patch.key, "patch": patch.value})
return self.with_connection(run, commit=True) return self.with_connection(run, commit=True)
def patches_list(self, package_base: Optional[str]) -> Dict[str, str]: def patches_list(self, package_base: Optional[str], variables: List[str]) -> Dict[str, List[PkgbuildPatch]]:
""" """
extract all patches extract all patches
Args: Args:
package_base(Optional[str]): optional filter by package base package_base(Optional[str]): optional filter by package base
variables(List[str]): extract patches only for specified PKGBUILD variables
Returns: Returns:
Dict[str, str]: map of package base to patch content Dict[str, List[PkgbuildPatch]]: map of package base to patch content
""" """
def run(connection: Connection) -> Dict[str, str]: def run(connection: Connection) -> List[Tuple[str, PkgbuildPatch]]:
return { return [
cursor["package_base"]: cursor["patch"] (cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"]))
for cursor in connection.execute( for cursor in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""", """select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base}) {"package_base": package_base})
} ]
return self.with_connection(run) # we could use itertools & operator but why?
patches: Dict[str, List[PkgbuildPatch]] = defaultdict(list)
for package, patch in self.with_connection(run):
if variables and patch.key not in variables:
continue
patches[package].append(patch)
return dict(patches)
def patches_remove(self, package_base: str) -> None: def patches_remove(self, package_base: str, variables: List[str]) -> None:
""" """
remove patch set remove patch set
Args: Args:
package_base(str): package base to clear patches package_base(str): package base to clear patches
variables(List[str]): remove patches only for specified PKGBUILD variables
""" """
def run_many(connection: Connection) -> None:
connection.executemany(
"""delete from patches where package_base = :package_base and variable = :variable""",
[{"package_base": package_base, "variable": variable} for variable in variables])
def run(connection: Connection) -> None: def run(connection: Connection) -> None:
connection.execute( connection.execute(
"""delete from patches where package_base = :package_base""", """delete from patches where package_base = :package_base""",
{"package_base": package_base}) {"package_base": package_base})
if variables:
return self.with_connection(run_many, commit=True)
return self.with_connection(run, commit=True) return self.with_connection(run, commit=True)

View File

@ -24,6 +24,7 @@ from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.core.formatters.build_printer import BuildPrinter from ahriman.core.formatters.build_printer import BuildPrinter
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
from ahriman.core.formatters.package_printer import PackagePrinter from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.status_printer import StatusPrinter from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter from ahriman.core.formatters.user_printer import UserPrinter

View File

@ -0,0 +1,56 @@
#
# Copyright (c) 2021-2022 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 typing import List
from ahriman.core.formatters import StringPrinter
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.property import Property
class PatchPrinter(StringPrinter):
"""
print content of the PKGBUILD patch
Attributes:
patches(List[PkgbuildPatch]): PKGBUILD patch object
"""
def __init__(self, package_base: str, patches: List[PkgbuildPatch]) -> None:
"""
default constructor
Args:
package_base(str): package base
patches(List[PkgbuildPatch]): PKGBUILD patch object
"""
StringPrinter.__init__(self, package_base)
self.patches = patches
def properties(self) -> List[Property]:
"""
convert content into printable data
Returns:
List[Property]: list of content properties
"""
return [
Property(patch.key or "Full source diff", patch.value, is_required=True)
for patch in self.patches
]

View File

@ -0,0 +1,21 @@
#
# copyright (c) 2021-2022 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 ahriman.core.gitremote.remote_pull_trigger import RemotePullTrigger
from ahriman.core.gitremote.remote_push_trigger import RemotePushTrigger

View File

@ -0,0 +1,86 @@
#
# copyright (c) 2021-2022 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 shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.core.util import walk
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
class RemotePullTrigger(Trigger):
"""
trigger for fetching PKGBUILDs from remote repository
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
repository_paths(RepositoryPaths): repository paths instance
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.remote_source = RemoteSource(
git_url=configuration.get("gitremote", "pull_url"),
web_url="",
path=".",
branch=configuration.get("gitremote", "pull_branch", fallback="master"),
source=PackageSource.Local,
)
self.repository_paths = configuration.repository_paths
def on_start(self) -> None:
"""
trigger action which will be called at the start of the application
"""
self.repo_clone()
def repo_clone(self) -> None:
"""
clone repository from remote source
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
self.repo_copy(clone_dir)
def repo_copy(self, clone_dir: Path) -> None:
"""
copy directories from cloned remote source to local cache
Args:
clone_dir(Path): path to temporary cloned directory
"""
for pkgbuild_path in filter(lambda path: path.name == "PKGBUILD", walk(clone_dir)):
cloned_pkgbuild_dir = pkgbuild_path.parent
package_base = cloned_pkgbuild_dir.name
local_pkgbuild_dir = self.repository_paths.cache_for(package_base)
shutil.copytree(cloned_pkgbuild_dir, local_pkgbuild_dir, dirs_exist_ok=True)
Sources.init(local_pkgbuild_dir) # initialized git repository is required for local sources

View File

@ -0,0 +1,110 @@
#
# copyright (c) 2021-2022 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 shutil
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Generator, Iterable
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
from ahriman.models.result import Result
class RemotePushTrigger(Trigger):
"""
trigger for syncing PKGBUILDs to remote repository
Attributes:
remote_source(RemoteSource): repository remote source (remote pull url and branch)
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, architecture, configuration)
self.remote_source = RemoteSource(
git_url=configuration.get("gitremote", "push_url"),
web_url="",
path=".",
branch=configuration.get("gitremote", "push_branch", fallback="master"),
source=PackageSource.Local,
)
@staticmethod
def package_update(package: Package, target_dir: Path) -> str:
"""
clone specified package and update its content in cloned PKGBUILD repository
Args:
package(Package): built package to update pkgbuild repository
target_dir(Path): path to the cloned PKGBUILD repository
Returns:
str: relative path to be added as new file
"""
# instead of iterating by directory we can simplify the process
# firstly, we need to remove old data to make sure that removed files are not tracked anymore...
package_target_dir = target_dir / package.base
shutil.rmtree(package_target_dir, ignore_errors=True)
# ...secondly, we copy whole tree...
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, package.remote)
shutil.copytree(clone_dir, package_target_dir)
# ...and last, but not least, we remove the dot-git directory...
shutil.rmtree(package_target_dir / ".git", ignore_errors=True)
# ...and finally return path to the copied directory
return package.base
@staticmethod
def packages_update(result: Result, target_dir: Path) -> Generator[str, None, None]:
"""
update all packages from the build result
Args:
result(Result): build result
target_dir(Path): path to the cloned PKGBUILD repository
Yields:
str: path to updated files
"""
for package in result.success:
yield RemotePushTrigger.package_update(package, target_dir)
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
"""
trigger action which will be called after build process with process result
Args:
result(Result): build result
packages(Iterable[Package]): list of all available packages
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
Sources.fetch(clone_dir, self.remote_source)
Sources.push(clone_dir, self.remote_source, *RemotePushTrigger.packages_update(result, clone_dir))

View File

@ -17,12 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from ahriman.core.report.report import Report
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.console import Console
from ahriman.core.report.email import Email
from ahriman.core.report.html import HTML
from ahriman.core.report.telegram import Telegram
from ahriman.core.report.report_trigger import ReportTrigger from ahriman.core.report.report_trigger import ReportTrigger

View File

@ -21,7 +21,7 @@ from typing import Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.formatters import BuildPrinter from ahriman.core.formatters import BuildPrinter
from ahriman.core.report import Report from ahriman.core.report.report import Report
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result

View File

@ -25,7 +25,8 @@ from email.mime.text import MIMEText
from typing import Dict, Iterable from typing import Dict, Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report import JinjaTemplate, Report from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.core.util import pretty_datetime from ahriman.core.util import pretty_datetime
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result

View File

@ -20,7 +20,8 @@
from typing import Iterable from typing import Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report import JinjaTemplate, Report from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result

View File

@ -84,16 +84,16 @@ class Report(LazyLogging):
section, provider_name = configuration.gettype(target, architecture) section, provider_name = configuration.gettype(target, architecture)
provider = ReportSettings.from_option(provider_name) provider = ReportSettings.from_option(provider_name)
if provider == ReportSettings.HTML: if provider == ReportSettings.HTML:
from ahriman.core.report import HTML from ahriman.core.report.html import HTML
return HTML(architecture, configuration, section) return HTML(architecture, configuration, section)
if provider == ReportSettings.Email: if provider == ReportSettings.Email:
from ahriman.core.report import Email from ahriman.core.report.email import Email
return Email(architecture, configuration, section) return Email(architecture, configuration, section)
if provider == ReportSettings.Console: if provider == ReportSettings.Console:
from ahriman.core.report import Console from ahriman.core.report.console import Console
return Console(architecture, configuration, section) return Console(architecture, configuration, section)
if provider == ReportSettings.Telegram: if provider == ReportSettings.Telegram:
from ahriman.core.report import Telegram from ahriman.core.report.telegram import Telegram
return Telegram(architecture, configuration, section) return Telegram(architecture, configuration, section)
return cls(architecture, configuration) # should never happen return cls(architecture, configuration) # should never happen

View File

@ -21,7 +21,7 @@ from typing import Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger from ahriman.core.triggers import Trigger
from ahriman.core.report import Report from ahriman.core.report.report import Report
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
@ -43,9 +43,9 @@ class ReportTrigger(Trigger):
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
Trigger.__init__(self, architecture, configuration) Trigger.__init__(self, architecture, configuration)
self.targets = configuration.getlist("report", "target") self.targets = configuration.getlist("report", "target", fallback=[])
def run(self, result: Result, packages: Iterable[Package]) -> None: def on_result(self, result: Result, packages: Iterable[Package]) -> None:
""" """
run trigger run trigger

View File

@ -23,7 +23,8 @@ import requests
from typing import Iterable from typing import Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report import JinjaTemplate, Report from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.core.util import exception_response_text from ahriman.core.util import exception_response_text
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
@ -40,6 +41,7 @@ class Telegram(Report, JinjaTemplate):
chat_id(str): chat id to post message, either string with @ or integer chat_id(str): chat id to post message, either string with @ or integer
template_path(Path): path to template for built packages template_path(Path): path to template for built packages
template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
timeout(int): HTTP request timeout in seconds
""" """
TELEGRAM_API_URL = "https://api.telegram.org" TELEGRAM_API_URL = "https://api.telegram.org"
@ -61,6 +63,7 @@ class Telegram(Report, JinjaTemplate):
self.chat_id = configuration.get(section, "chat_id") self.chat_id = configuration.get(section, "chat_id")
self.template_path = configuration.getpath(section, "template_path") self.template_path = configuration.getpath(section, "template_path")
self.template_type = configuration.get(section, "template_type", fallback="HTML") self.template_type = configuration.get(section, "template_type", fallback="HTML")
self.timeout = configuration.getint(section, "timeout", fallback=30)
def _send(self, text: str) -> None: def _send(self, text: str) -> None:
""" """
@ -72,7 +75,8 @@ class Telegram(Report, JinjaTemplate):
try: try:
response = requests.post( response = requests.post(
f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage", f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage",
data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type}) data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type},
timeout=self.timeout)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e: except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e)) self.logger.exception("could not perform request: %s", exception_response_text(e))

View File

@ -84,8 +84,7 @@ class Executor(Cleaner):
result = Result() result = Result()
for single in updates: for single in updates:
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \ with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (build_dir := Path(dir_name)):
(build_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
try: try:
build_single(single, build_dir) build_single(single, build_dir)
result.add_success(single) result.add_success(single)
@ -110,7 +109,7 @@ class Executor(Cleaner):
try: try:
self.paths.tree_clear(package_base) # remove all internal files self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base) self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base) self.database.patches_remove(package_base, [])
self.reporter.remove(package_base) # we only update status page in case of base removal self.reporter.remove(package_base) # we only update status page in case of base removal
except Exception: except Exception:
self.logger.exception("could not remove base %s", package_base) self.logger.exception("could not remove base %s", package_base)
@ -144,15 +143,6 @@ class Executor(Cleaner):
return self.repo.repo_path return self.repo.repo_path
def process_triggers(self, result: Result) -> None:
"""
process triggers setup by settings
Args:
result(Result): build result
"""
self.triggers.process(result, self.packages())
def process_update(self, packages: Iterable[Path]) -> Result: def process_update(self, packages: Iterable[Path]) -> Result:
""" """
sign packages, add them to repository and update repository database sign packages, add them to repository and update repository database

View File

@ -34,6 +34,7 @@ class GPG(LazyLogging):
gnupg wrapper gnupg wrapper
Attributes: Attributes:
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
architecture(str): repository architecture architecture(str): repository architecture
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
default_key(Optional[str]): default PGP key ID to use default_key(Optional[str]): default PGP key ID to use
@ -41,6 +42,7 @@ class GPG(LazyLogging):
""" """
_check_output = check_output _check_output = check_output
DEFAULT_TIMEOUT = 30
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
@ -95,7 +97,7 @@ class GPG(LazyLogging):
Tuple[Set[SignSettings], Optional[str]]: tuple of sign targets and default PGP key Tuple[Set[SignSettings], Optional[str]]: tuple of sign targets and default PGP key
""" """
targets: Set[SignSettings] = set() targets: Set[SignSettings] = set()
for option in configuration.getlist("sign", "target"): for option in configuration.getlist("sign", "target", fallback=[]):
target = SignSettings.from_option(option) target = SignSettings.from_option(option)
if target == SignSettings.Disabled: if target == SignSettings.Disabled:
continue continue
@ -120,7 +122,7 @@ class GPG(LazyLogging):
"op": "get", "op": "get",
"options": "mr", "options": "mr",
"search": key "search": key
}) }, timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status() response.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e)) self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e))
@ -179,7 +181,7 @@ class GPG(LazyLogging):
sign repository if required by configuration sign repository if required by configuration
Note: Note:
more likely you just want to pass ``repository_sign_args`` to repo wrapper More likely you just want to pass ``repository_sign_args`` to repo wrapper
Args: Args:
path(Path): path to repository database path(Path): path to repository database

View File

@ -86,10 +86,12 @@ class Spawn(Thread, LazyLogging):
packages(Iterable[str]): packages list to add packages(Iterable[str]): packages list to add
now(bool): build packages now now(bool): build packages now
""" """
if not packages:
return self.spawn_process("repo-update")
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now: if now:
kwargs["now"] = "" kwargs["now"] = ""
self.spawn_process("add", *packages, **kwargs) return self.spawn_process("package-add", *packages, **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None: def packages_remove(self, packages: Iterable[str]) -> None:
""" """
@ -98,7 +100,7 @@ class Spawn(Thread, LazyLogging):
Args: Args:
packages(Iterable[str]): packages list to remove packages(Iterable[str]): packages list to remove
""" """
self.spawn_process("remove", *packages) self.spawn_process("package-remove", *packages)
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None: def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
""" """

View File

@ -80,7 +80,7 @@ class Client:
Returns: Returns:
InternalStatus: current internal (web) service status InternalStatus: current internal (web) service status
""" """
return InternalStatus(BuildStatus()) return InternalStatus(status=BuildStatus())
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
""" """

View File

@ -189,7 +189,7 @@ class WebClient(Client, LazyLogging):
self.logger.exception("could not get web service status: %s", exception_response_text(e)) self.logger.exception("could not get web service status: %s", exception_response_text(e))
except Exception: except Exception:
self.logger.exception("could not get web service status") self.logger.exception("could not get web service status")
return InternalStatus(BuildStatus()) return InternalStatus(status=BuildStatus())
def remove(self, base: str) -> None: def remove(self, base: str) -> None:
""" """

View File

@ -72,8 +72,7 @@ class Leaf:
Returns: Returns:
Leaf: loaded class Leaf: loaded class
""" """
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, \ with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
(clone_dir := Path(dir_name)): # pylint: disable=confusing-with-statement
Sources.load(clone_dir, package, database.patches_get(package.base), paths) Sources.load(clone_dir, package, database.patches_get(package.base), paths)
dependencies = Package.dependencies(clone_dir) dependencies = Package.dependencies(clone_dir)
return cls(package, dependencies) return cls(package, dependencies)

View File

@ -37,7 +37,7 @@ class Trigger(LazyLogging):
This class must be used in order to create own extension. Basically idea is the following:: This class must be used in order to create own extension. Basically idea is the following::
>>> class CustomTrigger(Trigger): >>> class CustomTrigger(Trigger):
>>> def run(self, result: Result, packages: Iterable[Package]) -> None: >>> def on_result(self, result: Result, packages: Iterable[Package]) -> None:
>>> perform_some_action() >>> perform_some_action()
Having this class you can pass it to ``configuration`` and it will be run on action:: Having this class you can pass it to ``configuration`` and it will be run on action::
@ -48,7 +48,7 @@ class Trigger(LazyLogging):
>>> configuration.set_option("build", "triggers", "my.awesome.package.CustomTrigger") >>> configuration.set_option("build", "triggers", "my.awesome.package.CustomTrigger")
>>> >>>
>>> loader = TriggerLoader("x86_64", configuration) >>> loader = TriggerLoader("x86_64", configuration)
>>> loader.process(Result(), []) >>> loader.on_result(Result(), [])
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
@ -62,15 +62,23 @@ class Trigger(LazyLogging):
self.architecture = architecture self.architecture = architecture
self.configuration = configuration self.configuration = configuration
def run(self, result: Result, packages: Iterable[Package]) -> None: def on_result(self, result: Result, packages: Iterable[Package]) -> None:
""" """
run trigger trigger action which will be called after build process with process result
Args: Args:
result(Result): build result result(Result): build result
packages(Iterable[Package]): list of all available packages packages(Iterable[Package]): list of all available packages
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError if (run := getattr(self, "run", None)) is not None:
run(result, packages) # compatibility with old triggers
def on_start(self) -> None:
"""
trigger action which will be called at the start of the application
"""
def on_stop(self) -> None:
"""
trigger action which will be called before the stop of the application
"""

View File

@ -17,12 +17,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import contextlib
import importlib import importlib
import os import os
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Iterable from typing import Generator, Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InvalidExtension from ahriman.core.exceptions import InvalidExtension
@ -54,7 +55,7 @@ class TriggerLoader(LazyLogging):
After that you are free to run triggers:: After that you are free to run triggers::
>>> loader.process(Result(), []) >>> loader.on_result(Result(), [])
""" """
def __init__(self, architecture: str, configuration: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
@ -72,6 +73,31 @@ class TriggerLoader(LazyLogging):
self.load_trigger(trigger) self.load_trigger(trigger)
for trigger in configuration.getlist("build", "triggers") for trigger in configuration.getlist("build", "triggers")
] ]
self._on_stop_requested = False
def __del__(self) -> None:
"""
custom destructor object which calls on_stop in case if it was requested
"""
if not self._on_stop_requested:
return
self.on_stop()
@contextlib.contextmanager
def __execute_trigger(self, trigger: Trigger) -> Generator[None, None, None]:
"""
decorator for calling triggers
Args:
trigger(Trigger): trigger instance to be called
"""
trigger_name = type(trigger).__name__
try:
self.logger.info("executing extension %s", trigger_name)
yield
except Exception:
self.logger.exception("got exception while run trigger %s", trigger_name)
def _load_module_from_file(self, module_path: str, implementation: str) -> ModuleType: def _load_module_from_file(self, module_path: str, implementation: str) -> ModuleType:
""" """
@ -149,18 +175,34 @@ class TriggerLoader(LazyLogging):
return trigger return trigger
def process(self, result: Result, packages: Iterable[Package]) -> None: def on_result(self, result: Result, packages: Iterable[Package]) -> None:
""" """
run remote sync run trigger with result of application run
Args: Args:
result(Result): build result result(Result): build result
packages(Iterable[Package]): list of all available packages packages(Iterable[Package]): list of all available packages
""" """
self.logger.debug("executing triggers on result")
for trigger in self.triggers: for trigger in self.triggers:
trigger_name = type(trigger).__name__ with self.__execute_trigger(trigger):
try: trigger.on_result(result, packages)
self.logger.info("executing extension %s", trigger_name)
trigger.run(result, packages) def on_start(self) -> None:
except Exception: """
self.logger.exception("got exception while run trigger %s", trigger_name) run triggers on load
"""
self.logger.debug("executing triggers on start")
self._on_stop_requested = True
for trigger in self.triggers:
with self.__execute_trigger(trigger):
trigger.on_start()
def on_stop(self) -> None:
"""
run triggers before the application exit
"""
self.logger.debug("executing triggers on stop")
for trigger in self.triggers:
with self.__execute_trigger(trigger):
trigger.on_stop()

View File

@ -17,11 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from ahriman.core.upload.upload import Upload
from ahriman.core.upload.http_upload import HttpUpload
from ahriman.core.upload.github import Github
from ahriman.core.upload.rsync import Rsync
from ahriman.core.upload.s3 import S3
from ahriman.core.upload.upload_trigger import UploadTrigger from ahriman.core.upload.upload_trigger import UploadTrigger

View File

@ -24,7 +24,7 @@ from pathlib import Path
from typing import Any, Dict, Iterable, Optional from typing import Any, Dict, Iterable, Optional
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.upload import HttpUpload from ahriman.core.upload.http_upload import HttpUpload
from ahriman.core.util import walk from ahriman.core.util import walk
from ahriman.models.package import Package from ahriman.models.package import Package

View File

@ -24,7 +24,7 @@ from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.upload import Upload from ahriman.core.upload.upload import Upload
from ahriman.core.util import exception_response_text from ahriman.core.util import exception_response_text
@ -33,7 +33,8 @@ class HttpUpload(Upload):
helper for the http based uploads helper for the http based uploads
Attributes: Attributes:
auth(Tuple[str, str]): HTTP auth object auth(Optional[Tuple[str, str]]): HTTP auth object if set
timeout(int): HTTP request timeout in seconds
""" """
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
@ -46,9 +47,10 @@ class HttpUpload(Upload):
section(str): configuration section name section(str): configuration section name
""" """
Upload.__init__(self, architecture, configuration) Upload.__init__(self, architecture, configuration)
password = configuration.get(section, "password") password = configuration.get(section, "password", fallback=None)
username = configuration.get(section, "username") username = configuration.get(section, "username", fallback=None)
self.auth = (password, username) self.auth = (password, username) if password and username else None
self.timeout = configuration.getint(section, "timeout", fallback=30)
@staticmethod @staticmethod
def calculate_hash(path: Path) -> str: def calculate_hash(path: Path) -> str:
@ -108,7 +110,7 @@ class HttpUpload(Upload):
requests.Response: request response object requests.Response: request response object
""" """
try: try:
response = requests.request(method, url, auth=self.auth, **kwargs) response = requests.request(method, url, auth=self.auth, timeout=self.timeout, **kwargs)
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e: except requests.HTTPError as e:
self.logger.exception("could not perform %s request to %s: %s", method, url, exception_response_text(e)) self.logger.exception("could not perform %s request to %s: %s", method, url, exception_response_text(e))

View File

@ -21,7 +21,7 @@ from pathlib import Path
from typing import Iterable from typing import Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.upload import Upload from ahriman.core.upload.upload import Upload
from ahriman.core.util import check_output from ahriman.core.util import check_output
from ahriman.models.package import Package from ahriman.models.package import Package

View File

@ -25,7 +25,7 @@ from pathlib import Path
from typing import Any, Dict, Iterable from typing import Any, Dict, Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.upload import Upload from ahriman.core.upload.upload import Upload
from ahriman.core.util import walk from ahriman.core.util import walk
from ahriman.models.package import Package from ahriman.models.package import Package

View File

@ -83,13 +83,13 @@ class Upload(LazyLogging):
section, provider_name = configuration.gettype(target, architecture) section, provider_name = configuration.gettype(target, architecture)
provider = UploadSettings.from_option(provider_name) provider = UploadSettings.from_option(provider_name)
if provider == UploadSettings.Rsync: if provider == UploadSettings.Rsync:
from ahriman.core.upload import Rsync from ahriman.core.upload.rsync import Rsync
return Rsync(architecture, configuration, section) return Rsync(architecture, configuration, section)
if provider == UploadSettings.S3: if provider == UploadSettings.S3:
from ahriman.core.upload import S3 from ahriman.core.upload.s3 import S3
return S3(architecture, configuration, section) return S3(architecture, configuration, section)
if provider == UploadSettings.Github: if provider == UploadSettings.Github:
from ahriman.core.upload import Github from ahriman.core.upload.github import Github
return Github(architecture, configuration, section) return Github(architecture, configuration, section)
return cls(architecture, configuration) # should never happen return cls(architecture, configuration) # should never happen

View File

@ -21,7 +21,7 @@ from typing import Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.triggers import Trigger from ahriman.core.triggers import Trigger
from ahriman.core.upload import Upload from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
@ -43,9 +43,9 @@ class UploadTrigger(Trigger):
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
Trigger.__init__(self, architecture, configuration) Trigger.__init__(self, architecture, configuration)
self.targets = configuration.getlist("upload", "target") self.targets = configuration.getlist("upload", "target", fallback=[])
def run(self, result: Result, packages: Iterable[Package]) -> None: def on_result(self, result: Result, packages: Iterable[Package]) -> None:
""" """
run trigger run trigger

View File

@ -29,7 +29,7 @@ from typing import Any, Callable, Dict, List, Optional, Type
from ahriman.core.util import filter_json, full_version from ahriman.core.util import filter_json, full_version
@dataclass @dataclass(frozen=True, kw_only=True)
class AURPackage: class AURPackage:
""" """
AUR package descriptor AUR package descriptor

View File

@ -47,7 +47,7 @@ class BuildStatusEnum(str, Enum):
Success = "success" Success = "success"
@dataclass @dataclass(frozen=True)
class BuildStatus: class BuildStatus:
""" """
build status holder build status holder
@ -64,7 +64,7 @@ class BuildStatus:
""" """
convert status to enum type convert status to enum type
""" """
self.status = BuildStatusEnum(self.status) object.__setattr__(self, "status", BuildStatusEnum(self.status))
@classmethod @classmethod
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus: def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:

View File

@ -27,7 +27,7 @@ from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package
@dataclass @dataclass(frozen=True, kw_only=True)
class Counters: class Counters:
""" """
package counters package counters

View File

@ -26,7 +26,7 @@ from ahriman.models.build_status import BuildStatus
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
@dataclass @dataclass(frozen=True, kw_only=True)
class InternalStatus: class InternalStatus:
""" """
internal server status internal server status

View File

@ -21,7 +21,7 @@ from dataclasses import dataclass
from typing import List from typing import List
@dataclass @dataclass(frozen=True, kw_only=True)
class Migration: class Migration:
""" """
migration implementation migration implementation

View File

@ -22,7 +22,7 @@ from dataclasses import dataclass
from ahriman.core.exceptions import MigrationError from ahriman.core.exceptions import MigrationError
@dataclass @dataclass(frozen=True, kw_only=True)
class MigrationResult: class MigrationResult:
""" """
migration result implementation model migration result implementation model

View File

@ -38,7 +38,7 @@ from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@dataclass @dataclass(kw_only=True)
class Package(LazyLogging): class Package(LazyLogging):
""" """
package properties representation package properties representation
@ -147,7 +147,7 @@ class Package(LazyLogging):
""" """
package = pacman.handle.load_pkg(str(path)) package = pacman.handle.load_pkg(str(path))
description = PackageDescription.from_package(package, path) description = PackageDescription.from_package(package, path)
return cls(package.base, package.version, remote, {package.name: description}) return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description})
@classmethod @classmethod
def from_aur(cls: Type[Package], name: str, pacman: Pacman) -> Package: def from_aur(cls: Type[Package], name: str, pacman: Pacman) -> Package:
@ -163,7 +163,11 @@ class Package(LazyLogging):
""" """
package = AUR.info(name, pacman=pacman) package = AUR.info(name, pacman=pacman)
remote = RemoteSource.from_source(PackageSource.AUR, package.package_base, package.repository) remote = RemoteSource.from_source(PackageSource.AUR, package.package_base, package.repository)
return cls(package.package_base, package.version, remote, {package.name: PackageDescription()}) return cls(
base=package.package_base,
version=package.version,
remote=remote,
packages={package.name: PackageDescription()})
@classmethod @classmethod
def from_build(cls: Type[Package], path: Path) -> Package: def from_build(cls: Type[Package], path: Path) -> Package:
@ -186,7 +190,7 @@ class Package(LazyLogging):
packages = {key: PackageDescription() for key in srcinfo["packages"]} packages = {key: PackageDescription() for key in srcinfo["packages"]}
version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
return cls(srcinfo["pkgbase"], version, None, packages) return cls(base=srcinfo["pkgbase"], version=version, remote=None, packages=packages)
@classmethod @classmethod
def from_json(cls: Type[Package], dump: Dict[str, Any]) -> Package: def from_json(cls: Type[Package], dump: Dict[str, Any]) -> Package:
@ -204,11 +208,7 @@ class Package(LazyLogging):
for key, value in dump.get("packages", {}).items() for key, value in dump.get("packages", {}).items()
} }
remote = dump.get("remote", {}) remote = dump.get("remote", {})
return cls( return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages)
base=dump["base"],
version=dump["version"],
remote=RemoteSource.from_json(remote),
packages=packages)
@classmethod @classmethod
def from_official(cls: Type[Package], name: str, pacman: Pacman, use_syncdb: bool = True) -> Package: def from_official(cls: Type[Package], name: str, pacman: Pacman, use_syncdb: bool = True) -> Package:
@ -225,7 +225,11 @@ class Package(LazyLogging):
""" """
package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name, pacman=pacman) package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name, pacman=pacman)
remote = RemoteSource.from_source(PackageSource.Repository, package.package_base, package.repository) remote = RemoteSource.from_source(PackageSource.Repository, package.package_base, package.repository)
return cls(package.package_base, package.version, remote, {package.name: PackageDescription()}) return cls(
base=package.package_base,
version=package.version,
remote=remote,
packages={package.name: PackageDescription()})
@staticmethod @staticmethod
def dependencies(path: Path) -> Set[str]: def dependencies(path: Path) -> Set[str]:
@ -263,6 +267,26 @@ class Package(LazyLogging):
packages = set(srcinfo["packages"].keys()) packages = set(srcinfo["packages"].keys())
return (depends | makedepends) - packages return (depends | makedepends) - packages
@staticmethod
def supported_architectures(path: Path) -> Set[str]:
"""
load supported architectures from package sources
Args:
path(Path): path to package sources directory
Returns:
Set[str]: list of package supported architectures
Raises:
InvalidPackageInfo: if there are parsing errors
"""
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo", exception=None, cwd=path)
srcinfo, errors = parse_srcinfo(srcinfo_source)
if errors:
raise InvalidPackageInfo(errors)
return set(srcinfo.get("arch", []))
def actual_version(self, paths: RepositoryPaths) -> str: def actual_version(self, paths: RepositoryPaths) -> str:
""" """
additional method to handle VCS package versions additional method to handle VCS package versions
@ -281,7 +305,7 @@ class Package(LazyLogging):
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
Sources.load(paths.cache_for(self.base), self, None, paths) Sources.load(paths.cache_for(self.base), self, [], paths)
try: try:
# update pkgver first # update pkgver first

View File

@ -27,7 +27,7 @@ from typing import Any, Dict, List, Optional, Type
from ahriman.core.util import filter_json from ahriman.core.util import filter_json
@dataclass @dataclass(kw_only=True)
class PackageDescription: class PackageDescription:
""" """
package specific properties package specific properties

View File

@ -0,0 +1,111 @@
#
# Copyright (c) 2021-2022 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 shlex
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Union
@dataclass(frozen=True)
class PkgbuildPatch:
"""
wrapper for patching PKBGUILDs
Attributes:
key(Optional[str]): name of the property in PKGBUILD, e.g. version, url etc. If not set, patch will be
considered as full PKGBUILD diffs
value(Union[str, List[str]]): value of the stored PKGBUILD property. It must be either string or list of string
values
unsafe(bool): if set, value will be not quoted, might break PKGBUILD
"""
key: Optional[str]
value: Union[str, List[str]]
unsafe: bool = field(default=False, kw_only=True)
def __post_init__(self) -> None:
"""
remove empty key
"""
object.__setattr__(self, "key", self.key or None)
@property
def is_function(self) -> bool:
"""
parse key and define whether it function or not
Returns:
bool: True in case if key ends with parentheses and False otherwise
"""
return self.key is not None and self.key.endswith("()")
@property
def is_plain_diff(self) -> bool:
"""
check if patch is full diff one or just single-variable patch
Returns:
bool: True in case key set and False otherwise
"""
return self.key is None
def quote(self, value: str) -> str:
"""
quote value according to the unsafe flag
Args:
value(str): value to be quoted
Returns:
str: quoted string in case if unsafe is False and as is otherwise
"""
return value if self.unsafe else shlex.quote(value)
def serialize(self) -> str:
"""
serialize key-value pair into PKBGBUILD string. List values will be put inside parentheses. All string
values (including the ones inside list values) will be put inside quotes, no shell variables expanding supported
at the moment
Returns:
str: serialized key-value pair, print-friendly
"""
if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value))
return f"""{self.key}=({value})"""
if self.is_plain_diff: # no additional logic for plain diffs
return self.value
# we suppose that function values are only supported in string-like values
if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here
return f"""{self.key}={self.quote(self.value)}"""
def write(self, pkgbuild_path: Path) -> None:
"""
write serialized value into PKGBUILD by specified path
Args:
pkgbuild_path(Path): path to PKGBUILD file
"""
with pkgbuild_path.open("a") as pkgbuild:
pkgbuild.write("\n") # in case if file ends without new line we are appending it at the end
pkgbuild.write(self.serialize())
pkgbuild.write("\n") # append new line after the values

View File

@ -17,11 +17,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Any from typing import Any
@dataclass @dataclass(frozen=True)
class Property: class Property:
""" """
holder of object properties descriptor holder of object properties descriptor
@ -34,4 +34,4 @@ class Property:
name: str name: str
value: Any value: Any
is_required: bool = False is_required: bool = field(default=False, kw_only=True)

View File

@ -27,7 +27,7 @@ from ahriman.core.util import filter_json
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
@dataclass @dataclass(frozen=True, kw_only=True)
class RemoteSource: class RemoteSource:
""" """
remote package source properties remote package source properties
@ -50,7 +50,7 @@ class RemoteSource:
""" """
convert source to enum type convert source to enum type
""" """
self.source = PackageSource(self.source) object.__setattr__(self, "source", PackageSource(self.source))
@property @property
def pkgbuild_dir(self) -> Path: def pkgbuild_dir(self) -> Path:

View File

@ -29,7 +29,7 @@ from typing import Set, Tuple, Type
from ahriman.core.exceptions import InvalidPath from ahriman.core.exceptions import InvalidPath
@dataclass @dataclass(frozen=True)
class RepositoryPaths: class RepositoryPaths:
""" """
repository paths holder. For the most operations with paths you want to use this object repository paths holder. For the most operations with paths you want to use this object

View File

@ -19,7 +19,7 @@
# #
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, replace
from typing import Optional, Type from typing import Optional, Type
from passlib.pwd import genword as generate_password # type: ignore from passlib.pwd import genword as generate_password # type: ignore
from passlib.handlers.sha2_crypt import sha512_crypt # type: ignore from passlib.handlers.sha2_crypt import sha512_crypt # type: ignore
@ -27,7 +27,7 @@ from passlib.handlers.sha2_crypt import sha512_crypt # type: ignore
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@dataclass @dataclass(frozen=True, kw_only=True)
class User: class User:
""" """
authorized web user model authorized web user model
@ -82,7 +82,7 @@ class User:
""" """
if username is None or password is None: if username is None or password is None:
return None return None
return cls(username, password, access) return cls(username=username, password=password, access=access)
@staticmethod @staticmethod
def generate_password(length: int) -> str: def generate_password(length: int) -> str:
@ -130,7 +130,7 @@ class User:
# when we do not store any password here # when we do not store any password here
return self return self
password_hash: str = self._HASHER.hash(self.password + salt) password_hash: str = self._HASHER.hash(self.password + salt)
return User(self.username, password_hash, self.access) return replace(self, password=password_hash)
def verify_access(self, required: UserAccess) -> bool: def verify_access(self, required: UserAccess) -> bool:
""" """

View File

@ -25,7 +25,7 @@ from dataclasses import dataclass
from typing import Optional, Type from typing import Optional, Type
@dataclass @dataclass(frozen=True)
class UserIdentity: class UserIdentity:
""" """
user identity used inside web service user identity used inside web service

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.1.0" __version__ = "2.2.2"

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPFound from aiohttp.web import HTTPFound
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -47,11 +47,8 @@ class AddView(BaseView):
HTTPBadRequest: if bad data is supplied HTTPBadRequest: if bad data is supplied
HTTPFound: in case of success response HTTPFound: in case of success response
""" """
try:
data = await self.extract_data(["packages"]) data = await self.extract_data(["packages"])
packages = data["packages"] packages = data.get("packages", [])
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=True) self.spawner.packages_add(packages, now=True)

View File

@ -5,16 +5,6 @@ from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
def test_finalize(application: Application, mocker: MockerFixture) -> None:
"""
must report and sync at the last
"""
triggers_mock = mocker.patch("ahriman.core.repository.Repository.process_triggers")
application._finalize(Result())
triggers_mock.assert_called_once_with(Result())
def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must return not empty list of known packages must return not empty list of known packages
@ -23,3 +13,34 @@ def test_known_packages(application: Application, package_ahriman: Package, mock
packages = application._known_packages() packages = application._known_packages()
assert len(packages) > 1 assert len(packages) > 1
assert package_ahriman.base in packages assert package_ahriman.base in packages
def test_on_result(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must call on_result trigger function
"""
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.on_result")
application.on_result(Result())
triggers_mock.assert_called_once_with(Result(), [package_ahriman])
def test_on_start(application: Application, mocker: MockerFixture) -> None:
"""
must call on_start trigger function
"""
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.on_start")
application.on_start()
triggers_mock.assert_called_once_with()
def test_on_stop(application: Application, mocker: MockerFixture) -> None:
"""
must call on_stop trigger function
"""
triggers_mock = mocker.patch("ahriman.core.triggers.TriggerLoader.on_stop")
application.on_stop()
triggers_mock.assert_called_once_with()

View File

@ -11,12 +11,12 @@ from ahriman.models.package_source import PackageSource
from ahriman.models.result import Result from ahriman.models.result import Result
def test_finalize(application_packages: ApplicationPackages) -> None: def test_on_result(application_packages: ApplicationPackages) -> None:
""" """
must raise NotImplemented for missing finalize method must raise NotImplemented for missing finalize method
""" """
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
application_packages._finalize([]) application_packages.on_result(Result())
def test_known_packages(application_packages: ApplicationPackages) -> None: def test_known_packages(application_packages: ApplicationPackages) -> None:
@ -111,7 +111,7 @@ def test_add_remote(application_packages: ApplicationPackages, package_descripti
application_packages._add_remote(url) application_packages._add_remote(url)
open_mock.assert_called_once_with("wb") open_mock.assert_called_once_with("wb")
request_mock.assert_called_once_with(url, stream=True) request_mock.assert_called_once_with(url, stream=True, timeout=None)
response_mock.raise_for_status.assert_called_once_with() response_mock.raise_for_status.assert_called_once_with()
@ -242,8 +242,8 @@ def test_remove(application_packages: ApplicationPackages, mocker: MockerFixture
must remove package must remove package
""" """
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove") executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
finalize_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages._finalize") on_result_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages.on_result")
application_packages.remove([]) application_packages.remove([])
executor_mock.assert_called_once_with([]) executor_mock.assert_called_once_with([])
finalize_mock.assert_called_once_with(Result()) on_result_mock.assert_called_once_with(Result())

View File

@ -9,12 +9,12 @@ from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
def test_finalize(application_repository: ApplicationRepository) -> None: def test_on_result(application_repository: ApplicationRepository) -> None:
""" """
must raise NotImplemented for missing finalize method must raise NotImplemented for missing finalize method
""" """
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
application_repository._finalize(Result()) application_repository.on_result(Result())
def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_clean_cache(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -63,8 +63,8 @@ def test_sign(application_repository: ApplicationRepository, package_ahriman: Pa
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
update_mock = mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update") update_mock = mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update")
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository") sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository")
finalize_mock = mocker.patch( on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository._finalize") "ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.sign([]) application_repository.sign([])
copy_mock.assert_has_calls([ copy_mock.assert_has_calls([
@ -73,7 +73,7 @@ def test_sign(application_repository: ApplicationRepository, package_ahriman: Pa
]) ])
update_mock.assert_called_once_with([]) update_mock.assert_called_once_with([])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
finalize_mock.assert_called_once_with(Result()) on_result_mock.assert_called_once_with(Result())
def test_sign_skip(application_repository: ApplicationRepository, package_ahriman: Package, def test_sign_skip(application_repository: ApplicationRepository, package_ahriman: Package,
@ -84,7 +84,7 @@ def test_sign_skip(application_repository: ApplicationRepository, package_ahrima
package_ahriman.packages[package_ahriman.base].filename = None package_ahriman.packages[package_ahriman.base].filename = None
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update") mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update")
mocker.patch("ahriman.application.application.application_repository.ApplicationRepository._finalize") mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.sign([]) application_repository.sign([])
@ -99,8 +99,8 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
update_mock = mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update") update_mock = mocker.patch("ahriman.application.application.application_repository.ApplicationRepository.update")
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository") sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository")
finalize_mock = mocker.patch( on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository._finalize") "ahriman.application.application.application_repository.ApplicationRepository.on_result")
filename = package_ahriman.packages[package_ahriman.base].filepath filename = package_ahriman.packages[package_ahriman.base].filepath
application_repository.sign([package_ahriman.base]) application_repository.sign([package_ahriman.base])
@ -109,7 +109,7 @@ def test_sign_specific(application_repository: ApplicationRepository, package_ah
application_repository.repository.paths.packages / filename.name) application_repository.repository.paths.packages / filename.name)
update_mock.assert_called_once_with([]) update_mock.assert_called_once_with([])
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path) sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
finalize_mock.assert_called_once_with(Result()) on_result_mock.assert_called_once_with(Result())
def test_unknown_no_aur(application_repository: ApplicationRepository, package_ahriman: Package, def test_unknown_no_aur(application_repository: ApplicationRepository, package_ahriman: Package,
@ -163,13 +163,13 @@ def test_update(application_repository: ApplicationRepository, package_ahriman:
mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths) mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=paths)
build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=result) build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=result)
update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update", return_value=result) update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update", return_value=result)
finalize_mock = mocker.patch( on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository._finalize") "ahriman.application.application.application_repository.ApplicationRepository.on_result")
application_repository.update([package_ahriman]) application_repository.update([package_ahriman])
build_mock.assert_called_once_with([package_ahriman]) build_mock.assert_called_once_with([package_ahriman])
update_mock.assert_has_calls([mock.call(paths), mock.call(paths)]) update_mock.assert_has_calls([mock.call(paths), mock.call(paths)])
finalize_mock.assert_has_calls([mock.call(result), mock.call(result)]) on_result_mock.assert_has_calls([mock.call(result), mock.call(result)])
def test_update_empty(application_repository: ApplicationRepository, package_ahriman: Package, def test_update_empty(application_repository: ApplicationRepository, package_ahriman: Package,

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