mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-15 15:05:48 +00:00
Compare commits
27 Commits
2.0.0
...
d98211e5e5
Author | SHA1 | Date | |
---|---|---|---|
d98211e5e5 | |||
b97c8928e1 | |||
649df81aa5 | |||
ad7cdb7d95 | |||
4bb598d2eb | |||
f47be6cab0 | |||
342b3cb652 | |||
fc0d8387df | |||
e0b0c3caeb | |||
61969dd682 | |||
e441d93a56 | |||
664b6369bb | |||
4f6bd29ff4 | |||
f6d9ea480a | |||
08f62842ba | |||
1912790ccc | |||
cf3c48ffeb | |||
6633766cc3 | |||
f73d1eb424 | |||
87ab3683ad | |||
9c2f73af8c | |||
31f551bdf2 | |||
cbbe2b01e4 | |||
cd361a483d | |||
cee4fd4cce | |||
18daecaac7 | |||
03c298c762 |
@ -1 +1 @@
|
||||
skips: ['B101', 'B105', 'B404']
|
||||
skips: ['B101', 'B105', 'B106', 'B404']
|
16
.github/workflows/run-setup.yml
vendored
16
.github/workflows/run-setup.yml
vendored
@ -7,6 +7,22 @@ on:
|
||||
branches: [ master ]
|
||||
|
||||
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:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
33
.github/workflows/setup.sh
vendored
33
.github/workflows/setup.sh
vendored
@ -3,21 +3,25 @@
|
||||
|
||||
set -ex
|
||||
|
||||
[[ $1 = "minimal" ]] && MINIMAL_INSTALL=1
|
||||
|
||||
# install dependencies
|
||||
echo -e '[arcanisrepo]\nServer = http://repo.arcanis.me/$arch\nSigLevel = Never' | tee -a /etc/pacman.conf
|
||||
# refresh the image
|
||||
pacman --noconfirm -Syu
|
||||
# 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
|
||||
pacman --noconfirm -Sy python-build python-installer python-wheel
|
||||
# optional dependencies
|
||||
# VCS support
|
||||
pacman --noconfirm -Sy breezy darcs mercurial subversion
|
||||
# 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
|
||||
# additional features
|
||||
pacman --noconfirm -Sy gnupg python-boto3 rsync
|
||||
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||
# VCS support
|
||||
pacman --noconfirm -Sy breezy darcs mercurial subversion
|
||||
# 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
|
||||
# additional features
|
||||
pacman --noconfirm -Sy gnupg python-boto3 rsync
|
||||
fi
|
||||
|
||||
# create fresh tarball
|
||||
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
|
||||
sed -i "s/handlers = syslog_handler/handlers = console_handler/g" /etc/ahriman.ini.d/logging.ini
|
||||
# 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
|
||||
systemctl enable ahriman-web@x86_64
|
||||
systemctl enable ahriman@x86_64.timer
|
||||
# run web service (detached)
|
||||
sudo -u ahriman -- ahriman -a x86_64 web &
|
||||
WEBPID=$!
|
||||
sleep 15s # wait for the web service activation
|
||||
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||
# run web service (detached)
|
||||
sudo -u ahriman -- ahriman -a x86_64 web &
|
||||
WEB_PID=$!
|
||||
sleep 15s # wait for the web service activation
|
||||
fi
|
||||
# add the first package
|
||||
# the build itself does not really work in the container
|
||||
sudo -u ahriman -- ahriman package-add --now yay
|
||||
@ -49,4 +56,4 @@ sudo -u ahriman -- ahriman package-add --now yay
|
||||
# run package check
|
||||
sudo -u ahriman -- ahriman repo-update
|
||||
# stop web service lol
|
||||
kill $WEBPID
|
||||
[[ -z $WEB_PID ]] || kill $WEB_PID
|
||||
|
2
.github/workflows/tests.sh
vendored
2
.github/workflows/tests.sh
vendored
@ -4,7 +4,7 @@
|
||||
set -ex
|
||||
|
||||
# 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
|
||||
make check tests
|
||||
|
@ -82,7 +82,8 @@ disable=raw-checker-failed,
|
||||
fixme,
|
||||
too-many-arguments,
|
||||
duplicate-code,
|
||||
cyclic-import
|
||||
cyclic-import,
|
||||
confusing-with-statement,
|
||||
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
|
@ -5,7 +5,7 @@ formats: all
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.9"
|
||||
python: "3.10"
|
||||
|
||||
sphinx:
|
||||
builder: html
|
||||
|
@ -31,7 +31,7 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
* In case if class load requires some actions, it is recommended to create class method which can be used for class instantiating.
|
||||
* The code must follow the exception safety, unless it is explicitly asked by end user. It means that most exceptions must be handled and printed to log, no other actions must be done (e.g. raising another exception).
|
||||
* For the external command `ahriman.core.util.check_output` function must be used.
|
||||
* Every temporary file/directory must be removed at the end of processing, no matter what. The `ahriman.core.util.tmpdir` function provides wrapper for the directories.
|
||||
* Every temporary file/directory must be removed at the end of processing, no matter what. The ``tempfile`` module provides good ways to do it.
|
||||
* Import order must be the following:
|
||||
|
||||
```python
|
||||
@ -60,6 +60,7 @@ Again, the most checks can be performed by `make check` command, though some add
|
||||
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits.
|
||||
* No global variable allowed outside of `ahriman.version` module.
|
||||
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
||||
* If your class writes anything to log, the `ahriman.core.lazy_logging.LazyLogging` trait must be used.
|
||||
|
||||
### Other checks
|
||||
|
||||
|
30
Dockerfile
30
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM archlinux:base-devel
|
||||
FROM archlinux:base
|
||||
|
||||
# image configuration
|
||||
ENV AHRIMAN_ARCHITECTURE="x86_64"
|
||||
@ -13,24 +13,22 @@ ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"
|
||||
ENV AHRIMAN_USER="ahriman"
|
||||
|
||||
# install environment
|
||||
## install git which is required for AUR interaction and go for yay
|
||||
RUN pacman --noconfirm -Syu git go
|
||||
## install minimal required packages
|
||||
RUN pacman --noconfirm -Syu binutils fakeroot git make sudo
|
||||
## create build user
|
||||
RUN useradd -m -d /home/build -s /usr/bin/nologin build && \
|
||||
echo "build ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/build
|
||||
## install AUR helper
|
||||
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"
|
||||
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
||||
## install package dependencies
|
||||
RUN runuser -u build -- yay --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-srcinfo && \
|
||||
runuser -u build -- yay --noconfirm -Sy python-build python-installer python-wheel && \
|
||||
runuser -u build -- yay --noconfirm -Sy breezy darcs mercurial python-aioauth-client python-aiohttp \
|
||||
python-aiohttp-debugtoolbar python-aiohttp-jinja2 python-aiohttp-security \
|
||||
python-aiohttp-session python-boto3 python-cryptography python-jinja \
|
||||
rsync subversion
|
||||
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
||||
RUN pacman --noconfirm -Sy devtools git pyalpm python-inflection python-passlib python-requests python-setuptools python-srcinfo && \
|
||||
pacman --noconfirm -Sy python-build python-installer python-wheel && \
|
||||
pacman --noconfirm -Sy breezy mercurial python-aiohttp python-boto3 python-cryptography python-jinja rsync subversion && \
|
||||
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-jinja2 python-aiohttp-debugtoolbar \
|
||||
python-aiohttp-session python-aiohttp-security
|
||||
|
||||
# cleanup unused
|
||||
RUN find "/var/cache/pacman/pkg" -type f -delete
|
||||
|
||||
# install ahriman
|
||||
## copy tree
|
||||
@ -41,7 +39,7 @@ RUN cd "/home/build/ahriman" && \
|
||||
cp ./*-src.tar.xz "package/archlinux" && \
|
||||
cd "package/archlinux" && \
|
||||
runuser -u build -- makepkg --noconfirm --install --skipchecksums && \
|
||||
cd - && rm -r "/home/build/ahriman"
|
||||
cd / && rm -r "/home/build/ahriman"
|
||||
|
||||
VOLUME ["/var/lib/ahriman"]
|
||||
|
||||
|
2
Makefile
2
Makefile
@ -56,4 +56,4 @@ version:
|
||||
ifndef VERSION
|
||||
$(error VERSION is required, but not set)
|
||||
endif
|
||||
sed -i '/__version__ = .*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py
|
||||
sed -i 's/^__version__ = .*/__version__ = "$(VERSION)"/' src/ahriman/version.py
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml)
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/docker-image.yml)
|
||||
[](https://hub.docker.com/r/arcan1s/ahriman)
|
||||
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||
[](https://ahriman.readthedocs.io/?badge=latest)
|
||||
|
||||
@ -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).
|
||||
|
||||
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)
|
||||
|
@ -33,7 +33,7 @@ fi
|
||||
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" repo-setup "${AHRIMAN_SETUP_ARGS[@]}"
|
||||
|
||||
# refresh database
|
||||
runuser -u build -- yay --noconfirm -Syy &> /dev/null
|
||||
pacman -Syy &> /dev/null
|
||||
# create machine-id which is required by build tools
|
||||
systemd-machine-id-setup &> /dev/null
|
||||
|
||||
|
12
docker/install-aur-package.sh
Executable file
12
docker/install-aur-package.sh
Executable 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: 522 KiB After Width: | Height: | Size: 537 KiB |
@ -3,7 +3,7 @@
|
||||
ahriman
|
||||
.SH SYNOPSIS
|
||||
.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
|
||||
ArcH linux ReposItory MANager
|
||||
|
||||
@ -71,7 +71,7 @@ remove package status
|
||||
update package status
|
||||
.TP
|
||||
\fBahriman\fR \fI\,patch-add\/\fR
|
||||
add patch set
|
||||
add patch for PKGBUILD function
|
||||
.TP
|
||||
\fBahriman\fR \fI\,patch-list\/\fR
|
||||
list patch sets
|
||||
@ -79,6 +79,9 @@ list patch sets
|
||||
\fBahriman\fR \fI\,patch-remove\/\fR
|
||||
remove patch set
|
||||
.TP
|
||||
\fBahriman\fR \fI\,patch-set-add\/\fR
|
||||
add patch set
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-backup\/\fR
|
||||
backup repository data
|
||||
.TP
|
||||
@ -284,21 +287,25 @@ set status for specified packages. If no packages supplied, service status will
|
||||
new status
|
||||
|
||||
.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
|
||||
\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
|
||||
.TP
|
||||
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
|
||||
files which has to be tracked
|
||||
\fB\-p\fR \fI\,PATCH\/\fR, \fB\-\-patch\fR \fI\,PATCH\/\fR
|
||||
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
|
||||
usage: ahriman patch-list [-h] [-e] [package]
|
||||
usage: ahriman patch-list [-h] [-e] [-v VARIABLE] [package]
|
||||
|
||||
list available patches for the package
|
||||
|
||||
@ -311,8 +318,12 @@ package base
|
||||
\fB\-e\fR, \fB\-\-exit\-code\fR
|
||||
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
|
||||
usage: ahriman patch-remove [-h] package
|
||||
usage: ahriman patch-remove [-h] [-v VARIABLE] package
|
||||
|
||||
remove patches for the package
|
||||
|
||||
@ -320,6 +331,26 @@ remove patches for the package
|
||||
\fBpackage\fR
|
||||
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
|
||||
usage: ahriman repo-backup [-h] path
|
||||
|
||||
@ -391,7 +422,7 @@ just perform check for packages without rebuild process itself
|
||||
.TP
|
||||
\fB\-\-from\-database\fR
|
||||
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.
|
||||
|
||||
.TP
|
||||
|
@ -28,6 +28,14 @@ ahriman.core.database.migrations.m002\_user\_access module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
---------------
|
||||
|
||||
|
@ -36,6 +36,14 @@ ahriman.core.formatters.package\_printer module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
--------------------------------------
|
||||
|
||||
|
29
docs/ahriman.core.gitremote.rst
Normal file
29
docs/ahriman.core.gitremote.rst
Normal 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:
|
@ -12,6 +12,7 @@ Subpackages
|
||||
ahriman.core.build_tools
|
||||
ahriman.core.database
|
||||
ahriman.core.formatters
|
||||
ahriman.core.gitremote
|
||||
ahriman.core.report
|
||||
ahriman.core.repository
|
||||
ahriman.core.sign
|
||||
@ -38,6 +39,14 @@ ahriman.core.exceptions module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.lazy\_logging module
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.lazy_logging
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.spawn module
|
||||
-------------------------
|
||||
|
||||
|
@ -92,6 +92,14 @@ ahriman.models.package\_source module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.pkgbuild\_patch module
|
||||
-------------------------------------
|
||||
|
||||
.. automodule:: ahriman.models.pkgbuild_patch
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.property module
|
||||
------------------------------
|
||||
|
||||
|
@ -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.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.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.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.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:
|
||||
|
||||
@ -202,12 +203,14 @@ In order to configure users there are special commands.
|
||||
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 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.
|
||||
|
||||
Remote synchronization
|
||||
|
@ -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_*`` 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
|
||||
----------------
|
||||
|
||||
@ -138,6 +150,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
|
||||
* ``link_path`` - prefix for HTML links, 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``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
|
||||
|
||||
``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).
|
||||
|
||||
* ``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``.
|
||||
|
||||
``rsync`` type
|
||||
|
57
docs/faq.rst
57
docs/faq.rst
@ -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.
|
||||
|
||||
|
||||
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!
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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.
|
||||
#. 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).
|
||||
|
||||
@ -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
|
||||
|
||||
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
|
||||
|
||||
@ -692,7 +741,7 @@ You can also edit configuration and forward logs to ``stderr``, just change ``ha
|
||||
|
||||
sed -i 's/handlers = syslog_handler/handlers = console_handler/g' /etc/ahriman.ini.d/logging.ini
|
||||
|
||||
You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration <https://docs.python.org/3/library/logging.config.html>`_.
|
||||
You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration <https://docs.python.org/3/library/logging.config.html>`_. The application uses java concept to log messages, e.g. class ``Application`` imported from ``ahriman.application.application`` package will have logger called ``ahriman.application.application.Application``. In order to e.g. change logger name for whole application package it is possible to change values for ``ahriman.application`` package; thus editing ``ahriman`` logger configuration will change logging for whole application (unless there are overrides for another logger).
|
||||
|
||||
Html customization
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
@ -30,10 +30,3 @@ Contents
|
||||
advanced-usage
|
||||
triggers
|
||||
modules
|
||||
|
||||
Indices and tables
|
||||
------------------
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
@ -1,12 +1,45 @@
|
||||
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
|
||||
---------------
|
||||
|
||||
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``).
|
||||
|
||||
@ -49,12 +82,12 @@ Obviously you can implement the specified method in class, but for guide purpose
|
||||
self.channel = configuration.get("slack", "channel")
|
||||
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)
|
||||
|
||||
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.
|
||||
|
@ -1,13 +1,13 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=2.0.0
|
||||
pkgver=2.2.2
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
url="https://github.com/arcan1s/ahriman"
|
||||
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')
|
||||
optdepends=('breezy: -bzr packages support'
|
||||
'darcs: -darcs packages support'
|
||||
|
@ -20,7 +20,7 @@ archbuild_flags =
|
||||
build_command = extra-x86_64-build
|
||||
ignore_packages =
|
||||
makechrootpkg_flags =
|
||||
makepkg_flags = --nocolor
|
||||
makepkg_flags = --nocolor --ignorearch
|
||||
triggers = ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger
|
||||
|
||||
[repository]
|
||||
|
@ -1,5 +1,5 @@
|
||||
[loggers]
|
||||
keys = root,build_details,database,http,stderr,boto3,botocore,nose,s3transfer
|
||||
keys = root,http,stderr,boto3,botocore,nose,s3transfer
|
||||
|
||||
[handlers]
|
||||
keys = console_handler,syslog_handler
|
||||
@ -20,11 +20,11 @@ formatter = syslog_format
|
||||
args = ("/dev/log",)
|
||||
|
||||
[formatter_generic_format]
|
||||
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s
|
||||
format = [%(levelname)s %(asctime)s] [%(threadName)s] [%(name)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[formatter_syslog_format]
|
||||
format = [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||
format = [%(levelname)s] [%(threadName)s] [%(name)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[logger_root]
|
||||
@ -32,18 +32,6 @@ level = DEBUG
|
||||
handlers = syslog_handler
|
||||
qualname = root
|
||||
|
||||
[logger_build_details]
|
||||
level = DEBUG
|
||||
handlers = syslog_handler
|
||||
qualname = build_details
|
||||
propagate = 0
|
||||
|
||||
[logger_database]
|
||||
level = DEBUG
|
||||
handlers = syslog_handler
|
||||
qualname = database
|
||||
propagate = 0
|
||||
|
||||
[logger_http]
|
||||
level = DEBUG
|
||||
handlers = syslog_handler
|
||||
|
@ -27,7 +27,7 @@
|
||||
<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
|
||||
</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
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removePackages()" disabled hidden>
|
||||
|
@ -50,12 +50,12 @@
|
||||
});
|
||||
|
||||
function addPackages() {
|
||||
const packages = [packageInput.val()]
|
||||
const packages = [packageInput.val()];
|
||||
doPackageAction("/api/v1/service/add", packages);
|
||||
}
|
||||
|
||||
function requestPackages() {
|
||||
const packages = [packageInput.val()]
|
||||
const packages = [packageInput.val()];
|
||||
doPackageAction("/api/v1/service/request", packages);
|
||||
}
|
||||
</script>
|
||||
|
@ -7,7 +7,6 @@
|
||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table",
|
||||
() => {
|
||||
removeButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||
updateButton.prop("disabled", !table.bootstrapTable("getSelections").length);
|
||||
});
|
||||
|
||||
const architectureBadge = $("#badge-architecture");
|
||||
@ -16,8 +15,6 @@
|
||||
const versionBadge = $("#badge-version");
|
||||
|
||||
function doPackageAction(uri, packages) {
|
||||
if (packages.length === 0)
|
||||
return;
|
||||
$.ajax({
|
||||
url: uri,
|
||||
data: JSON.stringify({packages: packages}),
|
||||
@ -32,7 +29,7 @@
|
||||
showSuccess(details);
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => { showFailure(errorThrown); },
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getSelection() {
|
||||
@ -58,25 +55,30 @@
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const extractListProperties = (description, property) => {
|
||||
return Object.values(description.packages).map(pkg => {
|
||||
return pkg[property];
|
||||
}).reduce((left, right) => { return left.concat(right); }, []);
|
||||
return Object.values(description.packages)
|
||||
.map(pkg => { return pkg[property]; })
|
||||
.reduce((left, right) => { return left.concat(right); }, []);
|
||||
};
|
||||
const listToTable = data => {
|
||||
return Array.from(new Set(data))
|
||||
.sort()
|
||||
.map(entry => { return safe(entry); })
|
||||
.join("<br>");
|
||||
};
|
||||
const listToTable = data => { return Array.from(new Set(data)).sort().join("<br>"); };
|
||||
|
||||
const payload = response.map(description => {
|
||||
const package_base = description.package.base;
|
||||
const web_url = description.package.remote?.web_url;
|
||||
return {
|
||||
id: description.package.base,
|
||||
base: web_url ? `<a href="${web_url}" title="${package_base}">${package_base}</a>` : package_base,
|
||||
version: description.package.version,
|
||||
id: package_base,
|
||||
base: web_url ? `<a href="${safe(web_url)}" title="${safe(package_base)}">${safe(package_base)}</a>` : safe(package_base),
|
||||
version: safe(description.package.version),
|
||||
packages: listToTable(Object.keys(description.package.packages)),
|
||||
groups: listToTable(extractListProperties(description.package, "groups")),
|
||||
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
||||
timestamp: new Date(1000 * description.status.timestamp).toISOString(),
|
||||
status: description.status.status
|
||||
}
|
||||
status: description.status.status,
|
||||
};
|
||||
});
|
||||
|
||||
table.bootstrapTable("load", payload);
|
||||
@ -85,17 +87,17 @@
|
||||
hideControls(false);
|
||||
},
|
||||
error: (jqXHR, _, errorThrown) => {
|
||||
hideControls(true);
|
||||
if ((jqXHR.status === 401) || (jqXHR.status === 403)) {
|
||||
// authorization error
|
||||
const text = "In order to see statuses you must login first.";
|
||||
table.find("tr.unauthorized").remove();
|
||||
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${text}</td></tr>`);
|
||||
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
|
||||
table.bootstrapTable("hideLoading");
|
||||
} else {
|
||||
// other errors
|
||||
showFailure(errorThrown);
|
||||
}
|
||||
hideControls(true);
|
||||
},
|
||||
});
|
||||
|
||||
@ -129,6 +131,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
function safe(string) {
|
||||
return String(string)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function statusFormat(value) {
|
||||
const cellClass = status => {
|
||||
if (status === "pending") return "table-warning";
|
||||
@ -143,5 +153,5 @@
|
||||
$(() => {
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
})
|
||||
});
|
||||
</script>
|
@ -5,19 +5,8 @@
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script>
|
||||
$("#packages").bootstrapTable({
|
||||
formatClearSearch: function () {
|
||||
return "Clear search";
|
||||
},
|
||||
formatSearch: function () {
|
||||
return "search";
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet">
|
||||
|
||||
|
1
setup.py
1
setup.py
@ -32,6 +32,7 @@ setup(
|
||||
"inflection",
|
||||
"passlib",
|
||||
"requests",
|
||||
"setuptools",
|
||||
"srcinfo",
|
||||
],
|
||||
setup_requires=[
|
||||
|
@ -82,6 +82,7 @@ def _parser() -> argparse.ArgumentParser:
|
||||
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
|
||||
|
||||
_set_aur_search_parser(subparsers)
|
||||
_set_daemon_parser(subparsers)
|
||||
_set_help_parser(subparsers)
|
||||
_set_help_commands_unsafe_parser(subparsers)
|
||||
_set_key_import_parser(subparsers)
|
||||
@ -93,6 +94,7 @@ def _parser() -> argparse.ArgumentParser:
|
||||
_set_patch_add_parser(subparsers)
|
||||
_set_patch_list_parser(subparsers)
|
||||
_set_patch_remove_parser(subparsers)
|
||||
_set_patch_set_add_parser(subparsers)
|
||||
_set_repo_backup_parser(subparsers)
|
||||
_set_repo_check_parser(subparsers)
|
||||
_set_repo_clean_parser(subparsers)
|
||||
@ -140,6 +142,28 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
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:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
add parser for new patch subcommand
|
||||
add parser for new single-function patch subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
@ -328,16 +352,18 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("patch-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 path/to/directory``. "
|
||||
"By default it tracks *.patch and *.diff files, but this behavior can be changed "
|
||||
"by using --track option",
|
||||
parser = root.add_parser("patch-add", help="add patch for PKGBUILD function",
|
||||
description="create or update patched PKGBUILD function or variable",
|
||||
epilog="Unlike ``patch-set-add``, this function allows to patch only one PKGBUILD f"
|
||||
"unction, e.g. typing ``ahriman patch-add ahriman version`` it will change the "
|
||||
"``version`` inside PKGBUILD, typing ``ahriman patch-add ahriman build()`` "
|
||||
"it will change ``build()`` function inside PKGBUILD",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="path to directory with changed files for patch addition/update")
|
||||
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
|
||||
default=["*.diff", "*.patch"])
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.add_argument("variable", help="PKGBUILD variable or function name. If variable is a function, "
|
||||
"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)
|
||||
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)
|
||||
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("-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)
|
||||
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",
|
||||
formatter_class=_formatter)
|
||||
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)
|
||||
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:
|
||||
"""
|
||||
add parser for repository backup subcommand
|
||||
@ -473,7 +530,7 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("--from-database",
|
||||
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 "
|
||||
"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.",
|
||||
action="store_true")
|
||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
||||
|
@ -49,15 +49,6 @@ class Application(ApplicationPackages, ApplicationRepository):
|
||||
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]:
|
||||
"""
|
||||
load packages from repository and pacman repositories
|
||||
@ -73,3 +64,26 @@ class Application(ApplicationPackages, ApplicationRepository):
|
||||
known_packages.update(properties.provides)
|
||||
known_packages.update(self.repository.pacman.all_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()
|
||||
|
@ -21,11 +21,12 @@ import requests
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any, Iterable, Set
|
||||
|
||||
from ahriman.application.application.application_properties import ApplicationProperties
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.util import package_like, tmpdir
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.result import Result
|
||||
@ -36,18 +37,6 @@ class ApplicationPackages(ApplicationProperties):
|
||||
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]:
|
||||
"""
|
||||
load packages from repository and pacman repositories
|
||||
@ -60,6 +49,18 @@ class ApplicationPackages(ApplicationProperties):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
add package from archive
|
||||
@ -85,7 +86,7 @@ class ApplicationPackages(ApplicationProperties):
|
||||
self.database.build_queue_insert(package)
|
||||
self.database.remote_update(package)
|
||||
|
||||
with tmpdir() as local_dir:
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (local_dir := Path(dir_name)):
|
||||
Sources.load(local_dir, package, self.database.patches_get(package.base), self.repository.paths)
|
||||
self._process_dependencies(local_dir, known_packages, without_dependencies)
|
||||
|
||||
@ -127,7 +128,7 @@ class ApplicationPackages(ApplicationProperties):
|
||||
source(str): remote URL of the package archive
|
||||
"""
|
||||
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()
|
||||
|
||||
with dst.open("wb") as local_file:
|
||||
@ -185,4 +186,4 @@ class ApplicationPackages(ApplicationProperties):
|
||||
names(Iterable[str]): list of packages (either base or name) to remove
|
||||
"""
|
||||
self.repository.process_remove(names)
|
||||
self._finalize(Result())
|
||||
self.on_result(Result())
|
||||
|
@ -17,14 +17,13 @@
|
||||
# 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 logging
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.repository import Repository
|
||||
|
||||
|
||||
class ApplicationProperties:
|
||||
class ApplicationProperties(LazyLogging):
|
||||
"""
|
||||
application base properties class
|
||||
|
||||
@ -32,7 +31,6 @@ class ApplicationProperties:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
database(SQLite): database instance
|
||||
logger(logging.Logger): application logger
|
||||
repository(Repository): repository instance
|
||||
"""
|
||||
|
||||
@ -44,9 +42,8 @@ class ApplicationProperties:
|
||||
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
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.configuration = configuration
|
||||
self.architecture = architecture
|
||||
self.database = SQLite.load(configuration)
|
||||
|
@ -35,7 +35,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
repository control class
|
||||
"""
|
||||
|
||||
def _finalize(self, result: Result) -> None:
|
||||
def on_result(self, result: Result) -> None:
|
||||
"""
|
||||
generate report and sync to remote server
|
||||
|
||||
@ -89,7 +89,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
self.update([])
|
||||
# sign repository database if set
|
||||
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
|
||||
self._finalize(Result())
|
||||
self.on_result(Result())
|
||||
|
||||
def unknown(self) -> List[str]:
|
||||
"""
|
||||
@ -139,7 +139,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
if not paths:
|
||||
return # don't need to process if no update supplied
|
||||
update_result = self.repository.process_update(paths)
|
||||
self._finalize(result.merge(update_result))
|
||||
self.on_result(result.merge(update_result))
|
||||
|
||||
# process built packages
|
||||
build_result = Result()
|
||||
|
@ -22,6 +22,7 @@ from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.application.handlers.add import Add
|
||||
from ahriman.application.handlers.backup import Backup
|
||||
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.help import Help
|
||||
from ahriman.application.handlers.key_import import KeyImport
|
||||
|
@ -45,6 +45,7 @@ class Add(Handler):
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
application = Application(architecture, configuration, no_report, unsafe)
|
||||
application.on_start()
|
||||
application.add(args.package, args.source, args.without_dependencies)
|
||||
if not args.now:
|
||||
return
|
||||
|
@ -44,5 +44,6 @@ class Clean(Handler):
|
||||
no_report(bool): force disable reporting
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
Application(architecture, configuration, no_report, unsafe).clean(
|
||||
args.cache, args.chroot, args.manual, args.packages)
|
||||
application = Application(architecture, configuration, no_report, unsafe)
|
||||
application.on_start()
|
||||
application.clean(args.cache, args.chroot, args.manual, args.packages)
|
||||
|
51
src/ahriman/application/handlers/daemon.py
Normal file
51
src/ahriman/application/handlers/daemon.py
Normal 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()
|
@ -46,5 +46,5 @@ class KeyImport(Handler):
|
||||
no_report(bool): force disable reporting
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
Application(architecture, configuration, no_report, unsafe).repository.sign.key_import(
|
||||
args.key_server, args.key)
|
||||
application = Application(architecture, configuration, no_report, unsafe)
|
||||
application.repository.sign.key_import(args.key_server, args.key)
|
||||
|
@ -18,17 +18,19 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
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.handlers import Handler
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
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.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
class Patch(Handler):
|
||||
@ -50,52 +52,95 @@ class Patch(Handler):
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
application = Application(architecture, configuration, no_report, unsafe)
|
||||
application.on_start()
|
||||
|
||||
if args.action == Action.List:
|
||||
Patch.patch_set_list(application, args.package, args.exit_code)
|
||||
if args.action == Action.Update and args.variable is not None:
|
||||
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:
|
||||
Patch.patch_set_remove(application, args.package)
|
||||
elif args.action == Action.Update:
|
||||
Patch.patch_set_create(application, Path(args.package), args.track)
|
||||
Patch.patch_set_remove(application, args.package, args.variable)
|
||||
|
||||
@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
|
||||
|
||||
Args:
|
||||
application(Application): application instance
|
||||
sources_dir(Path): path to directory with the package sources
|
||||
track(List[str]): track files which match the glob before creating the patch
|
||||
package_base(str): package base
|
||||
patch(PkgbuildPatch): patch descriptor
|
||||
"""
|
||||
package = Package.from_build(sources_dir)
|
||||
patch = Sources.patch_create(sources_dir, *track)
|
||||
application.database.patches_insert(package.base, patch)
|
||||
application.database.patches_insert(package_base, patch)
|
||||
|
||||
@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
|
||||
|
||||
Args:
|
||||
application(Application): application instance
|
||||
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
|
||||
:
|
||||
"""
|
||||
patches = application.database.patches_list(package_base)
|
||||
patches = application.database.patches_list(package_base, variables)
|
||||
Patch.check_if_empty(exit_code, not patches)
|
||||
|
||||
for base, patch in patches.items():
|
||||
content = base if package_base is None else patch
|
||||
StringPrinter(content).print(verbose=True)
|
||||
PatchPrinter(base, patch).print(verbose=True, separator=" = ")
|
||||
|
||||
@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
|
||||
|
||||
Args:
|
||||
application(Application): application instance
|
||||
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)
|
||||
|
@ -49,10 +49,12 @@ class Rebuild(Handler):
|
||||
depends_on = set(args.depends_on) if args.depends_on else None
|
||||
|
||||
application = Application(architecture, configuration, no_report, unsafe)
|
||||
application.on_start()
|
||||
|
||||
if args.from_database:
|
||||
updates = Rebuild.extract_packages(application)
|
||||
else:
|
||||
updates = application.repository.packages_depends_on(depends_on)
|
||||
updates = application.repository.packages_depend_on(depends_on)
|
||||
|
||||
Rebuild.check_if_empty(args.exit_code, not updates)
|
||||
if args.dry_run:
|
||||
|
@ -44,4 +44,6 @@ class Remove(Handler):
|
||||
no_report(bool): force disable reporting
|
||||
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)
|
||||
|
@ -46,6 +46,7 @@ class RemoveUnknown(Handler):
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
application = Application(architecture, configuration, no_report, unsafe)
|
||||
application.on_start()
|
||||
unknown_packages = application.unknown()
|
||||
|
||||
if args.dry_run:
|
||||
|
@ -133,7 +133,9 @@ class Setup(Handler):
|
||||
repository(str): repository name
|
||||
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
|
||||
# stupid mypy thinks that it is impossible
|
||||
configuration.optionxform = lambda key: key # type: ignore
|
||||
|
@ -49,4 +49,5 @@ class Triggers(Handler):
|
||||
if args.trigger:
|
||||
loader = application.repository.triggers
|
||||
loader.triggers = [loader.load_trigger(trigger) for trigger in args.trigger]
|
||||
application.repository.process_triggers(Result())
|
||||
application.on_start()
|
||||
application.on_result(Result())
|
||||
|
@ -45,6 +45,7 @@ class Update(Handler):
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
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,
|
||||
Update.log_fn(application, args.dry_run))
|
||||
Update.check_if_empty(args.exit_code, not packages)
|
||||
|
@ -149,7 +149,7 @@ class Users(Handler):
|
||||
Returns:
|
||||
User: built user descriptor
|
||||
"""
|
||||
user = User(args.username, args.password, args.role)
|
||||
if user.password is None:
|
||||
user.password = getpass.getpass()
|
||||
return user
|
||||
password = args.password
|
||||
if password is None:
|
||||
password = getpass.getpass()
|
||||
return User(username=args.username, password=password, access=args.role)
|
||||
|
@ -20,7 +20,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
@ -29,12 +28,13 @@ from typing import Literal, Optional, Type
|
||||
from ahriman import version
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import DuplicateRun
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import check_user
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
|
||||
|
||||
class Lock:
|
||||
class Lock(LazyLogging):
|
||||
"""
|
||||
wrapper for application lock file
|
||||
|
||||
@ -115,10 +115,8 @@ class Lock:
|
||||
"""
|
||||
status = self.reporter.get_internal()
|
||||
if status.version is not None and status.version != version.__version__:
|
||||
logging.getLogger("root").warning(
|
||||
"status watcher version mismatch, our %s, their %s",
|
||||
version.__version__,
|
||||
status.version)
|
||||
self.logger.warning("status watcher version mismatch, our %s, their %s",
|
||||
version.__version__, status.version)
|
||||
|
||||
def check_user(self) -> None:
|
||||
"""
|
||||
|
@ -36,11 +36,13 @@ class AUR(Remote):
|
||||
DEFAULT_AUR_URL(str): (class attribute) default AUR url
|
||||
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
|
||||
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_RPC_URL = f"{DEFAULT_AUR_URL}/rpc"
|
||||
DEFAULT_RPC_VERSION = "5"
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
@staticmethod
|
||||
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
|
||||
@ -113,7 +115,7 @@ class AUR(Remote):
|
||||
query[key] = value
|
||||
|
||||
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()
|
||||
return self.parse_response(response.json())
|
||||
except requests.HTTPError as e:
|
||||
|
@ -36,11 +36,13 @@ class Official(Remote):
|
||||
DEFAULT_ARCHLINUX_URL(str): (class attribute) default archlinux url
|
||||
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_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
|
||||
"""
|
||||
|
||||
DEFAULT_ARCHLINUX_URL = "https://archlinux.org"
|
||||
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib", "Community"]
|
||||
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
@staticmethod
|
||||
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
|
||||
@ -101,7 +103,10 @@ class Official(Remote):
|
||||
List[AURPackage]: response parsed to package list
|
||||
"""
|
||||
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()
|
||||
return self.parse_response(response.json())
|
||||
except requests.HTTPError as e:
|
||||
|
@ -19,21 +19,17 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
|
||||
|
||||
class Remote:
|
||||
class Remote(LazyLogging):
|
||||
"""
|
||||
base class for remote package search
|
||||
|
||||
Attributes:
|
||||
logger(logging.Logger): class logger
|
||||
|
||||
Examples:
|
||||
These classes are designed to be used without instancing. In order to achieve it several class methods are
|
||||
provided: ``info``, ``multisearch`` and ``search``. Thus, the basic flow is the following::
|
||||
@ -47,12 +43,6 @@ class Remote:
|
||||
directly, whereas ``multisearch`` splits search one by one and finds intersection between search results.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
self.logger = logging.getLogger("build_details")
|
||||
|
||||
@classmethod
|
||||
def info(cls: Type[Remote], package_name: str, *, pacman: Pacman) -> AURPackage:
|
||||
"""
|
||||
|
@ -17,22 +17,20 @@
|
||||
# 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 logging
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Repo:
|
||||
class Repo(LazyLogging):
|
||||
"""
|
||||
repo-add and repo-remove wrapper
|
||||
|
||||
Attributes:
|
||||
logger(logging.Logger): class logger
|
||||
name(str): repository name
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
sign_args(List[str]): additional args which have to be used to sign repository archive
|
||||
@ -50,7 +48,6 @@ class Repo:
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
sign_args(List[str]): additional args which have to be used to sign repository archive
|
||||
"""
|
||||
self.logger = logging.getLogger("build_details")
|
||||
self.name = name
|
||||
self.paths = paths
|
||||
self.uid, _ = paths.root_owner
|
||||
|
@ -19,23 +19,21 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Optional, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.models.auth_settings import AuthSettings
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
|
||||
class Auth:
|
||||
class Auth(LazyLogging):
|
||||
"""
|
||||
helper to deal with user authorization
|
||||
|
||||
Attributes:
|
||||
enabled(bool): indicates if authorization is enabled
|
||||
logger(logging.Logger): class logger
|
||||
max_age(int): session age in seconds. It will be used for both client side and server side checks
|
||||
allow_read_only(bool): allow read only access to APIs
|
||||
"""
|
||||
@ -48,8 +46,6 @@ class Auth:
|
||||
configuration(Configuration): configuration instance
|
||||
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Disabled)
|
||||
"""
|
||||
self.logger = logging.getLogger("http")
|
||||
|
||||
self.allow_read_only = configuration.getboolean("auth", "allow_read_only")
|
||||
|
||||
self.enabled = provider.is_enabled
|
||||
|
@ -17,81 +17,50 @@
|
||||
# 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 logging
|
||||
import datetime
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.util import check_output, walk
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Sources:
|
||||
class Sources(LazyLogging):
|
||||
"""
|
||||
helper to download package sources (PKGBUILD etc)
|
||||
|
||||
Attributes:
|
||||
DEFAULT_BRANCH(str): (class attribute) default branch to process git repositories.
|
||||
Must be used only for local stored repositories, use RemoteSource descriptor instead for real packages
|
||||
logger(logging.Logger): (class attribute) class logger
|
||||
"""
|
||||
|
||||
DEFAULT_BRANCH = "master" # default fallback branch
|
||||
logger = logging.getLogger("build_details")
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
@staticmethod
|
||||
def _add(sources_dir: Path, *pattern: str) -> None:
|
||||
def extend_architectures(sources_dir: Path, architecture: str) -> List[PkgbuildPatch]:
|
||||
"""
|
||||
track found files via git
|
||||
extend existing PKGBUILD with repository architecture
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to git repository
|
||||
*pattern(str): glob patterns
|
||||
"""
|
||||
# glob directory to find files which match the specified patterns
|
||||
found_files: List[Path] = []
|
||||
for glob in pattern:
|
||||
found_files.extend(sources_dir.glob(glob))
|
||||
if not found_files:
|
||||
return # no additional files found
|
||||
Sources.logger.info("found matching files %s", found_files)
|
||||
# add them to index
|
||||
Sources._check_output("git", "add", "--intent-to-add",
|
||||
*[str(fn.relative_to(sources_dir)) for fn in found_files],
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@staticmethod
|
||||
def _diff(sources_dir: Path) -> str:
|
||||
"""
|
||||
generate diff from the current version and write it to the output file
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to git repository
|
||||
sources_dir(Path): local path to directory with source files
|
||||
architecture(str): repository architecture
|
||||
|
||||
Returns:
|
||||
str: patch as plain string
|
||||
List[PkgbuildPatch]: generated patch for PKGBUILD architectures if required
|
||||
"""
|
||||
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@staticmethod
|
||||
def _move(pkgbuild_dir: Path, sources_dir: Path) -> None:
|
||||
"""
|
||||
move content from pkgbuild_dir to sources_dir
|
||||
|
||||
Args:
|
||||
pkgbuild_dir(Path): path to directory with pkgbuild from which need to move
|
||||
sources_dir(Path): path to target directory
|
||||
"""
|
||||
if pkgbuild_dir == sources_dir:
|
||||
return # directories are the same, no need to move
|
||||
for src in walk(pkgbuild_dir):
|
||||
dst = sources_dir / src.relative_to(pkgbuild_dir)
|
||||
shutil.move(src, dst)
|
||||
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
|
||||
def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None:
|
||||
@ -102,37 +71,38 @@ class Sources:
|
||||
sources_dir(Path): local path to fetch
|
||||
remote(Optional[RemoteSource]): remote target (from where to fetch)
|
||||
"""
|
||||
instance = Sources()
|
||||
# local directory exists and there is .git directory
|
||||
is_initialized_git = (sources_dir / ".git").is_dir()
|
||||
if is_initialized_git and not Sources.has_remotes(sources_dir):
|
||||
if is_initialized_git and not instance.has_remotes(sources_dir):
|
||||
# there is git repository, but no remote configured so far
|
||||
Sources.logger.info("skip update at %s because there are no branches configured", sources_dir)
|
||||
instance.logger.info("skip update at %s because there are no branches configured", sources_dir)
|
||||
return
|
||||
|
||||
branch = remote.branch if remote is not None else Sources.DEFAULT_BRANCH
|
||||
branch = remote.branch if remote is not None else instance.DEFAULT_BRANCH
|
||||
if is_initialized_git:
|
||||
Sources.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch)
|
||||
instance.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch)
|
||||
Sources._check_output("git", "fetch", "origin", branch,
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
exception=None, cwd=sources_dir, logger=instance.logger)
|
||||
elif remote is not None:
|
||||
Sources.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch)
|
||||
instance.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch)
|
||||
Sources._check_output("git", "clone", "--branch", branch, "--single-branch",
|
||||
remote.git_url, str(sources_dir),
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
exception=None, cwd=sources_dir, logger=instance.logger)
|
||||
else:
|
||||
# it will cause an exception later
|
||||
Sources.logger.error("%s is not initialized, but no remote provided", sources_dir)
|
||||
instance.logger.error("%s is not initialized, but no remote provided", sources_dir)
|
||||
|
||||
# and now force reset to our branch
|
||||
Sources._check_output("git", "checkout", "--force", branch,
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
exception=None, cwd=sources_dir, logger=instance.logger)
|
||||
Sources._check_output("git", "reset", "--hard", f"origin/{branch}",
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
exception=None, cwd=sources_dir, logger=instance.logger)
|
||||
|
||||
# move content if required
|
||||
# we are using full path to source directory in order to make append possible
|
||||
pkgbuild_dir = remote.pkgbuild_dir if remote is not None else sources_dir.resolve()
|
||||
Sources._move((sources_dir / pkgbuild_dir).resolve(), sources_dir)
|
||||
instance.move((sources_dir / pkgbuild_dir).resolve(), sources_dir)
|
||||
|
||||
@staticmethod
|
||||
def has_remotes(sources_dir: Path) -> bool:
|
||||
@ -145,7 +115,8 @@ class Sources:
|
||||
Returns:
|
||||
bool: True in case if there is any remote and false otherwise
|
||||
"""
|
||||
remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
instance = Sources()
|
||||
remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=instance.logger)
|
||||
return bool(remotes)
|
||||
|
||||
@staticmethod
|
||||
@ -156,43 +127,30 @@ class Sources:
|
||||
Args:
|
||||
sources_dir(Path): local path to sources
|
||||
"""
|
||||
Sources._check_output("git", "init", "--initial-branch", Sources.DEFAULT_BRANCH,
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
instance = Sources()
|
||||
Sources._check_output("git", "init", "--initial-branch", instance.DEFAULT_BRANCH,
|
||||
exception=None, cwd=sources_dir, logger=instance.logger)
|
||||
|
||||
@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
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to fetch
|
||||
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
|
||||
"""
|
||||
instance = Sources()
|
||||
if (cache_dir := paths.cache_for(package.base)).is_dir() and cache_dir != sources_dir:
|
||||
# no need to clone whole repository, just copy from cache first
|
||||
shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
|
||||
Sources.fetch(sources_dir, package.remote)
|
||||
instance.fetch(sources_dir, package.remote)
|
||||
|
||||
if patch is None:
|
||||
Sources.logger.info("no patches found")
|
||||
return
|
||||
Sources.patch_apply(sources_dir, patch)
|
||||
|
||||
@staticmethod
|
||||
def patch_apply(sources_dir: Path, patch: str) -> None:
|
||||
"""
|
||||
apply patches if any
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to directory with git sources
|
||||
patch(str): patch to be applied
|
||||
"""
|
||||
# create patch
|
||||
Sources.logger.info("apply patch from database")
|
||||
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
|
||||
exception=None, cwd=sources_dir, input_data=patch, logger=Sources.logger)
|
||||
patches.extend(instance.extend_architectures(sources_dir, paths.architecture))
|
||||
for patch in patches:
|
||||
instance.patch_apply(sources_dir, patch)
|
||||
|
||||
@staticmethod
|
||||
def patch_create(sources_dir: Path, *pattern: str) -> str:
|
||||
@ -206,6 +164,100 @@ class Sources:
|
||||
Returns:
|
||||
str: patch as plain text
|
||||
"""
|
||||
Sources._add(sources_dir, *pattern)
|
||||
diff = Sources._diff(sources_dir)
|
||||
instance = Sources()
|
||||
instance.add(sources_dir, *pattern)
|
||||
diff = instance.diff(sources_dir)
|
||||
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:
|
||||
"""
|
||||
track found files via git
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to git repository
|
||||
*pattern(str): glob patterns
|
||||
"""
|
||||
# glob directory to find files which match the specified patterns
|
||||
found_files: List[Path] = []
|
||||
for glob in pattern:
|
||||
found_files.extend(sources_dir.glob(glob))
|
||||
if not found_files:
|
||||
return # no additional files found
|
||||
self.logger.info("found matching files %s", found_files)
|
||||
# add them to index
|
||||
Sources._check_output("git", "add", "--intent-to-add",
|
||||
*[str(fn.relative_to(sources_dir)) for fn in found_files],
|
||||
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:
|
||||
"""
|
||||
generate diff from the current version and write it to the output file
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to git repository
|
||||
|
||||
Returns:
|
||||
str: patch as plain string
|
||||
"""
|
||||
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=self.logger)
|
||||
|
||||
def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None:
|
||||
"""
|
||||
move content from pkgbuild_dir to sources_dir
|
||||
|
||||
Args:
|
||||
pkgbuild_dir(Path): path to directory with pkgbuild from which need to move
|
||||
sources_dir(Path): path to target directory
|
||||
"""
|
||||
del self
|
||||
if pkgbuild_dir == sources_dir:
|
||||
return # directories are the same, no need to move
|
||||
for src in walk(pkgbuild_dir):
|
||||
dst = sources_dir / src.relative_to(pkgbuild_dir)
|
||||
shutil.move(src, dst)
|
||||
|
||||
def patch_apply(self, sources_dir: Path, patch: PkgbuildPatch) -> None:
|
||||
"""
|
||||
apply patches if any
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to directory with git sources
|
||||
patch(PkgbuildPatch): patch to be applied
|
||||
"""
|
||||
# create patch
|
||||
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",
|
||||
exception=None, cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
||||
else:
|
||||
patch.write(sources_dir / "PKGBUILD")
|
||||
|
@ -17,8 +17,6 @@
|
||||
# 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 logging
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
@ -26,18 +24,17 @@ from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Task:
|
||||
class Task(LazyLogging):
|
||||
"""
|
||||
base package build task
|
||||
|
||||
Attributes:
|
||||
build_logger(logging.Logger): logger for build process
|
||||
logger(logging.Logger): class logger
|
||||
package(Package): package definitions
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
uid(int): uid of the repository owner user
|
||||
@ -54,8 +51,6 @@ class Task:
|
||||
configuration(Configuration): configuration instance
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.build_logger = logging.getLogger("build_details")
|
||||
self.package = package
|
||||
self.paths = paths
|
||||
self.uid, _ = paths.root_owner
|
||||
@ -85,14 +80,14 @@ class Task:
|
||||
*command,
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=sources_dir,
|
||||
logger=self.build_logger,
|
||||
logger=self.logger,
|
||||
user=self.uid)
|
||||
|
||||
# well it is not actually correct, but we can deal with it
|
||||
packages = Task._check_output("makepkg", "--packagelist",
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=sources_dir,
|
||||
logger=self.build_logger).splitlines()
|
||||
logger=self.logger).splitlines()
|
||||
return [Path(package) for package in packages]
|
||||
|
||||
def init(self, sources_dir: Path, database: SQLite) -> None:
|
||||
|
@ -70,11 +70,15 @@ class Configuration(configparser.RawConfigParser):
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
|
||||
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
|
||||
|
||||
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,
|
||||
"path": self.__convert_path,
|
||||
})
|
||||
|
@ -19,8 +19,6 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from pkgutil import iter_modules
|
||||
@ -29,11 +27,12 @@ from typing import List, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.data import migrate_data
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.models.migration import Migration
|
||||
from ahriman.models.migration_result import MigrationResult
|
||||
|
||||
|
||||
class Migrations:
|
||||
class Migrations(LazyLogging):
|
||||
"""
|
||||
simple migration wrapper for the sqlite
|
||||
idea comes from https://www.ash.dev/blog/simple-migration-system-in-sqlite/
|
||||
@ -41,7 +40,6 @@ class Migrations:
|
||||
Attributes:
|
||||
configuration(Configuration): configuration instance
|
||||
connection(Connection): database connection
|
||||
logger(logging.Logger): class logger
|
||||
"""
|
||||
|
||||
def __init__(self, connection: Connection, configuration: Configuration) -> None:
|
||||
@ -54,7 +52,6 @@ class Migrations:
|
||||
"""
|
||||
self.connection = connection
|
||||
self.configuration = configuration
|
||||
self.logger = logging.getLogger("database")
|
||||
|
||||
@classmethod
|
||||
def migrate(cls: Type[Migrations], connection: Connection, configuration: Configuration) -> MigrationResult:
|
||||
@ -86,7 +83,7 @@ class Migrations:
|
||||
module = import_module(f"{__name__}.{module_name}")
|
||||
steps: List[str] = getattr(module, "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
|
||||
|
||||
@ -100,7 +97,7 @@ class Migrations:
|
||||
migrations = self.migrations()
|
||||
current_version = self.user_version()
|
||||
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:
|
||||
self.logger.info("no migrations required")
|
||||
|
43
src/ahriman/core/database/migrations/m003_patch_variables.py
Normal file
43
src/ahriman/core/database/migrations/m003_patch_variables.py
Normal 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_
|
||||
""",
|
||||
]
|
@ -58,7 +58,7 @@ class AuthOperations(Operations):
|
||||
|
||||
def run(connection: Connection) -> List[User]:
|
||||
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(
|
||||
"""
|
||||
select * from users
|
||||
|
@ -17,23 +17,21 @@
|
||||
# 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 logging
|
||||
import sqlite3
|
||||
|
||||
from pathlib import Path
|
||||
from sqlite3 import Connection, Cursor
|
||||
from typing import Any, Dict, Tuple, TypeVar, Callable
|
||||
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Operations:
|
||||
class Operations(LazyLogging):
|
||||
"""
|
||||
base operation class
|
||||
|
||||
Attributes:
|
||||
logger(logging.Logger): class logger
|
||||
path(Path): path to the database file
|
||||
"""
|
||||
|
||||
@ -45,10 +43,9 @@ class Operations:
|
||||
path(Path): path to the database file
|
||||
"""
|
||||
self.path = path
|
||||
self.logger = logging.getLogger("database")
|
||||
|
||||
@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
|
||||
|
||||
@ -64,7 +61,7 @@ class Operations:
|
||||
result[column[0]] = row[index]
|
||||
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
|
||||
|
||||
|
@ -154,7 +154,11 @@ class PackageOperations(Operations):
|
||||
Dict[str, Package]: map of the package base to its descriptor (without packages themselves)
|
||||
"""
|
||||
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""")
|
||||
}
|
||||
|
||||
|
@ -17,10 +17,13 @@
|
||||
# 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 collections import defaultdict
|
||||
|
||||
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.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
class PatchOperations(Operations):
|
||||
@ -28,7 +31,7 @@ class PatchOperations(Operations):
|
||||
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
|
||||
|
||||
@ -36,62 +39,77 @@ class PatchOperations(Operations):
|
||||
package_base(str): package base to search for patches
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
package_base(str): package base to insert
|
||||
patch(str): patch content
|
||||
patch(PkgbuildPatch): patch content
|
||||
"""
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
insert into patches
|
||||
(package_base, patch)
|
||||
(package_base, variable, patch)
|
||||
values
|
||||
(:package_base, :patch)
|
||||
on conflict (package_base) do update set
|
||||
(:package_base, :variable, :patch)
|
||||
on conflict (package_base, coalesce(variable, '')) do update set
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
package_base(Optional[str]): optional filter by package base
|
||||
variables(List[str]): extract patches only for specified PKGBUILD variables
|
||||
|
||||
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]:
|
||||
return {
|
||||
cursor["package_base"]: cursor["patch"]
|
||||
def run(connection: Connection) -> List[Tuple[str, PkgbuildPatch]]:
|
||||
return [
|
||||
(cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"]))
|
||||
for cursor in connection.execute(
|
||||
"""select * from patches where :package_base is null or 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
|
||||
|
||||
Args:
|
||||
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:
|
||||
connection.execute(
|
||||
"""delete from patches where 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)
|
||||
|
@ -24,6 +24,7 @@ from ahriman.core.formatters.aur_printer import AurPrinter
|
||||
from ahriman.core.formatters.build_printer import BuildPrinter
|
||||
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
|
||||
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.update_printer import UpdatePrinter
|
||||
from ahriman.core.formatters.user_printer import UserPrinter
|
||||
|
56
src/ahriman/core/formatters/patch_printer.py
Normal file
56
src/ahriman/core/formatters/patch_printer.py
Normal 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
|
||||
]
|
21
src/ahriman/core/gitremote/__init__.py
Normal file
21
src/ahriman/core/gitremote/__init__.py
Normal 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
|
86
src/ahriman/core/gitremote/remote_pull_trigger.py
Normal file
86
src/ahriman/core/gitremote/remote_pull_trigger.py
Normal 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
|
110
src/ahriman/core/gitremote/remote_push_trigger.py
Normal file
110
src/ahriman/core/gitremote/remote_push_trigger.py
Normal 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))
|
64
src/ahriman/core/lazy_logging.py
Normal file
64
src/ahriman/core/lazy_logging.py
Normal file
@ -0,0 +1,64 @@
|
||||
#
|
||||
# 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 logging
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class LazyLogging:
|
||||
"""
|
||||
wrapper for the logger library inspired by scala lazy logging module
|
||||
|
||||
Attributes:
|
||||
logger(logging.Logger): class logger instance
|
||||
"""
|
||||
|
||||
logger: logging.Logger
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
"""
|
||||
logger extractor
|
||||
|
||||
Args:
|
||||
item(str) property name:
|
||||
|
||||
Returns:
|
||||
Any: attribute by its name
|
||||
|
||||
Raises:
|
||||
AttributeError: in case if no such attribute found
|
||||
"""
|
||||
if item == "logger":
|
||||
logger = logging.getLogger(self.logger_name)
|
||||
setattr(self, item, logger)
|
||||
return logger
|
||||
raise AttributeError(f"'{self.__class__.__qualname__}' object has no attribute '{item}'")
|
||||
|
||||
@property
|
||||
def logger_name(self) -> str:
|
||||
"""
|
||||
extract logger name for the class
|
||||
|
||||
Returns:
|
||||
str: logger name as combination of module name and class name
|
||||
"""
|
||||
clazz = self.__class__
|
||||
prefix = "" if clazz.__module__ is None else f"{clazz.__module__}."
|
||||
return f"{prefix}{self.__class__.__qualname__}"
|
@ -17,12 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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
|
||||
|
@ -21,7 +21,7 @@ from typing import Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
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.result import Result
|
||||
|
||||
|
@ -25,7 +25,8 @@ from email.mime.text import MIMEText
|
||||
from typing import Dict, Iterable
|
||||
|
||||
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.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
@ -20,7 +20,8 @@
|
||||
from typing import Iterable
|
||||
|
||||
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.result import Result
|
||||
|
||||
|
@ -19,25 +19,23 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Iterable, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import ReportFailed
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.report_settings import ReportSettings
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class Report:
|
||||
class Report(LazyLogging):
|
||||
"""
|
||||
base report generator
|
||||
|
||||
Attributes:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
logger(logging.Logger): class logger
|
||||
|
||||
Examples:
|
||||
``Report`` classes provide several method in order to operate with the report generation and additional class
|
||||
@ -67,7 +65,6 @@ class Report:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
|
||||
@ -87,16 +84,16 @@ class Report:
|
||||
section, provider_name = configuration.gettype(target, architecture)
|
||||
provider = ReportSettings.from_option(provider_name)
|
||||
if provider == ReportSettings.HTML:
|
||||
from ahriman.core.report import HTML
|
||||
from ahriman.core.report.html import HTML
|
||||
return HTML(architecture, configuration, section)
|
||||
if provider == ReportSettings.Email:
|
||||
from ahriman.core.report import Email
|
||||
from ahriman.core.report.email import Email
|
||||
return Email(architecture, configuration, section)
|
||||
if provider == ReportSettings.Console:
|
||||
from ahriman.core.report import Console
|
||||
from ahriman.core.report.console import Console
|
||||
return Console(architecture, configuration, section)
|
||||
if provider == ReportSettings.Telegram:
|
||||
from ahriman.core.report import Telegram
|
||||
from ahriman.core.report.telegram import Telegram
|
||||
return Telegram(architecture, configuration, section)
|
||||
return cls(architecture, configuration) # should never happen
|
||||
|
||||
|
@ -21,7 +21,7 @@ from typing import Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
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.result import Result
|
||||
|
||||
@ -43,9 +43,9 @@ class ReportTrigger(Trigger):
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
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
|
||||
|
||||
|
@ -23,7 +23,8 @@ import requests
|
||||
from typing import Iterable
|
||||
|
||||
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.models.package import Package
|
||||
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
|
||||
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
|
||||
timeout(int): HTTP request timeout in seconds
|
||||
"""
|
||||
|
||||
TELEGRAM_API_URL = "https://api.telegram.org"
|
||||
@ -61,6 +63,7 @@ class Telegram(Report, JinjaTemplate):
|
||||
self.chat_id = configuration.get(section, "chat_id")
|
||||
self.template_path = configuration.getpath(section, "template_path")
|
||||
self.template_type = configuration.get(section, "template_type", fallback="HTML")
|
||||
self.timeout = configuration.getint(section, "timeout", fallback=30)
|
||||
|
||||
def _send(self, text: str) -> None:
|
||||
"""
|
||||
@ -72,7 +75,8 @@ class Telegram(Report, JinjaTemplate):
|
||||
try:
|
||||
response = requests.post(
|
||||
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()
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not perform request: %s", exception_response_text(e))
|
||||
@ -94,8 +98,11 @@ class Telegram(Report, JinjaTemplate):
|
||||
text = self.make_html(result, self.template_path)
|
||||
# telegram content is limited by 4096 symbols, so we are going to split the message by new lines
|
||||
# to fit into this restriction
|
||||
if len(text) > self.TELEGRAM_MAX_CONTENT_LENGTH:
|
||||
while len(text) > self.TELEGRAM_MAX_CONTENT_LENGTH:
|
||||
position = text.rfind("\n", 0, self.TELEGRAM_MAX_CONTENT_LENGTH)
|
||||
if position == -1:
|
||||
# normally should not happen, but we allow templates editing
|
||||
raise ValueError("substring not found")
|
||||
portion, text = text[:position], text[position + 1:] # +1 to exclude newline we split
|
||||
self._send(portion)
|
||||
# send remaining (or full in case if size is less than max length) text
|
||||
|
@ -20,12 +20,14 @@
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Iterable, List, Optional, Set
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.util import tmpdir
|
||||
from ahriman.core.util import safe_filename
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
@ -82,7 +84,7 @@ class Executor(Cleaner):
|
||||
|
||||
result = Result()
|
||||
for single in updates:
|
||||
with tmpdir() as build_dir:
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (build_dir := Path(dir_name)):
|
||||
try:
|
||||
build_single(single, build_dir)
|
||||
result.add_success(single)
|
||||
@ -107,7 +109,7 @@ class Executor(Cleaner):
|
||||
try:
|
||||
self.paths.tree_clear(package_base) # remove all internal files
|
||||
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
|
||||
except Exception:
|
||||
self.logger.exception("could not remove base %s", package_base)
|
||||
@ -122,16 +124,16 @@ class Executor(Cleaner):
|
||||
for local in self.packages():
|
||||
if local.base in packages or all(package in requested for package in local.packages):
|
||||
to_remove = {
|
||||
package: Path(properties.filename)
|
||||
package: properties.filepath
|
||||
for package, properties in local.packages.items()
|
||||
if properties.filename is not None
|
||||
if properties.filepath is not None
|
||||
}
|
||||
remove_base(local.base)
|
||||
elif requested.intersection(local.packages.keys()):
|
||||
to_remove = {
|
||||
package: Path(properties.filename)
|
||||
package: properties.filepath
|
||||
for package, properties in local.packages.items()
|
||||
if package in requested and properties.filename is not None
|
||||
if package in requested and properties.filepath is not None
|
||||
}
|
||||
else:
|
||||
to_remove = {}
|
||||
@ -141,15 +143,6 @@ class Executor(Cleaner):
|
||||
|
||||
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:
|
||||
"""
|
||||
sign packages, add them to repository and update repository database
|
||||
@ -160,17 +153,25 @@ class Executor(Cleaner):
|
||||
Returns:
|
||||
Result: path to repository database
|
||||
"""
|
||||
def rename(archive: PackageDescription, base: str) -> None:
|
||||
if archive.filename is None:
|
||||
self.logger.warning("received empty package name for base %s", base)
|
||||
return # suppress type checking, it never can be none actually
|
||||
if (safe := safe_filename(archive.filename)) != archive.filename:
|
||||
shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe)
|
||||
archive.filename = safe
|
||||
|
||||
def update_single(name: Optional[str], base: str) -> None:
|
||||
if name is None:
|
||||
self.logger.warning("received empty package name for base %s", base)
|
||||
return # suppress type checking, it never can be none actually
|
||||
# in theory it might be NOT packages directory, but we suppose it is
|
||||
# in theory, it might be NOT packages directory, but we suppose it is
|
||||
full_path = self.paths.packages / name
|
||||
files = self.sign.process_sign_package(full_path, base)
|
||||
for src in files:
|
||||
dst = self.paths.repository / src.name
|
||||
dst = self.paths.repository / safe_filename(src.name)
|
||||
shutil.move(src, dst)
|
||||
package_path = self.paths.repository / name
|
||||
package_path = self.paths.repository / safe_filename(name)
|
||||
self.repo.add(package_path)
|
||||
|
||||
current_packages = self.packages()
|
||||
@ -181,6 +182,7 @@ class Executor(Cleaner):
|
||||
for local in updates:
|
||||
try:
|
||||
for description in local.packages.values():
|
||||
rename(description, local.base)
|
||||
update_single(description.filename, local.base)
|
||||
self.reporter.set_success(local)
|
||||
result.add_success(local)
|
||||
|
@ -99,7 +99,7 @@ class Repository(Executor, UpdateHandler):
|
||||
"""
|
||||
return list(filter(package_like, self.paths.packages.iterdir()))
|
||||
|
||||
def packages_depends_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
|
||||
def packages_depend_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
|
||||
"""
|
||||
extract list of packages which depends on specified package
|
||||
|
||||
|
@ -17,20 +17,19 @@
|
||||
# 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 logging
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.exceptions import UnsafeRun
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.triggers import TriggerLoader
|
||||
from ahriman.core.util import check_user
|
||||
|
||||
|
||||
class RepositoryProperties:
|
||||
class RepositoryProperties(LazyLogging):
|
||||
"""
|
||||
repository internal objects holder
|
||||
|
||||
@ -39,7 +38,6 @@ class RepositoryProperties:
|
||||
configuration(Configuration): configuration instance
|
||||
database(SQLite): database instance
|
||||
ignore_list(List[str]): package bases which will be ignored during auto updates
|
||||
logger(logging.Logger): class logger
|
||||
name(str): repository name
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
@ -61,7 +59,6 @@ class RepositoryProperties:
|
||||
no_report(bool): force disable reporting
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
self.database = database
|
||||
|
@ -17,7 +17,6 @@
|
||||
# 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 logging
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
@ -25,23 +24,25 @@ from typing import List, Optional, Set, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.util import check_output, exception_response_text
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
|
||||
|
||||
class GPG:
|
||||
class GPG(LazyLogging):
|
||||
"""
|
||||
gnupg wrapper
|
||||
|
||||
Attributes:
|
||||
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
default_key(Optional[str]): default PGP key ID to use
|
||||
logger(logging.Logger): class logger
|
||||
targets(Set[SignSettings]): list of targets to sign (repository, package etc)
|
||||
"""
|
||||
|
||||
_check_output = check_output
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
@ -51,7 +52,6 @@ class GPG:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("build_details")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
self.targets, self.default_key = self.sign_options(configuration)
|
||||
@ -97,7 +97,7 @@ class GPG:
|
||||
Tuple[Set[SignSettings], Optional[str]]: tuple of sign targets and default PGP key
|
||||
"""
|
||||
targets: Set[SignSettings] = set()
|
||||
for option in configuration.getlist("sign", "target"):
|
||||
for option in configuration.getlist("sign", "target", fallback=[]):
|
||||
target = SignSettings.from_option(option)
|
||||
if target == SignSettings.Disabled:
|
||||
continue
|
||||
@ -122,7 +122,7 @@ class GPG:
|
||||
"op": "get",
|
||||
"options": "mr",
|
||||
"search": key
|
||||
})
|
||||
}, timeout=self.DEFAULT_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e))
|
||||
@ -181,7 +181,7 @@ class GPG:
|
||||
sign repository if required by configuration
|
||||
|
||||
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:
|
||||
path(Path): path to repository database
|
||||
|
@ -20,7 +20,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from multiprocessing import Process, Queue
|
||||
@ -28,10 +27,11 @@ from threading import Lock, Thread
|
||||
from typing import Callable, Dict, Iterable, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.models.package_source import PackageSource
|
||||
|
||||
|
||||
class Spawn(Thread):
|
||||
class Spawn(Thread, LazyLogging):
|
||||
"""
|
||||
helper to spawn external ahriman process
|
||||
MUST NOT be used directly, the only one usage allowed is to spawn process from web services
|
||||
@ -40,7 +40,6 @@ class Spawn(Thread):
|
||||
active(Dict[str, Process]): map of active child processes required to avoid zombies
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
logger(logging.Logger): spawner logger
|
||||
queue(Queue[Tuple[str, bool]]): multiprocessing queue to read updates from processes
|
||||
"""
|
||||
|
||||
@ -57,7 +56,6 @@ class Spawn(Thread):
|
||||
self.architecture = architecture
|
||||
self.args_parser = args_parser
|
||||
self.configuration = configuration
|
||||
self.logger = logging.getLogger("http")
|
||||
|
||||
self.lock = Lock()
|
||||
self.active: Dict[str, Process] = {}
|
||||
@ -88,10 +86,12 @@ class Spawn(Thread):
|
||||
packages(Iterable[str]): packages list to add
|
||||
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
|
||||
if 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:
|
||||
"""
|
||||
@ -100,7 +100,7 @@ class Spawn(Thread):
|
||||
Args:
|
||||
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:
|
||||
"""
|
||||
|
@ -80,7 +80,7 @@ class Client:
|
||||
Returns:
|
||||
InternalStatus: current internal (web) service status
|
||||
"""
|
||||
return InternalStatus(BuildStatus())
|
||||
return InternalStatus(status=BuildStatus())
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
|
@ -17,19 +17,18 @@
|
||||
# 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 logging
|
||||
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.exceptions import UnknownPackage
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Watcher:
|
||||
class Watcher(LazyLogging):
|
||||
"""
|
||||
package status watcher
|
||||
|
||||
@ -38,7 +37,6 @@ class Watcher:
|
||||
database(SQLite): database instance
|
||||
known(Dict[str, Tuple[Package, BuildStatus]]): list of known packages. For the most cases ``packages`` should
|
||||
be used instead
|
||||
logger(logging.Logger): class logger
|
||||
repository(Repository): repository object
|
||||
status(BuildStatus): daemon status
|
||||
"""
|
||||
@ -52,8 +50,6 @@ class Watcher:
|
||||
configuration(Configuration): configuration instance
|
||||
database(SQLite): database instance
|
||||
"""
|
||||
self.logger = logging.getLogger("http")
|
||||
|
||||
self.architecture = architecture
|
||||
self.database = database
|
||||
self.repository = Repository(architecture, configuration, database, no_report=True, unsafe=False)
|
||||
|
@ -17,12 +17,12 @@
|
||||
# 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 logging
|
||||
import requests
|
||||
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import exception_response_text
|
||||
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
|
||||
@ -31,13 +31,12 @@ from ahriman.models.package import Package
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
||||
class WebClient(Client):
|
||||
class WebClient(Client, LazyLogging):
|
||||
"""
|
||||
build status reporter web client
|
||||
|
||||
Attributes:
|
||||
address(str): address of the web service
|
||||
logger(logging.Logger): class logger
|
||||
user(Optional[User]): web service user descriptor
|
||||
"""
|
||||
|
||||
@ -48,7 +47,6 @@ class WebClient(Client):
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("http")
|
||||
self.address = self.parse_address(configuration)
|
||||
self.user = User.from_option(
|
||||
configuration.get("web", "username", fallback=None),
|
||||
@ -191,7 +189,7 @@ class WebClient(Client):
|
||||
self.logger.exception("could not get web service status: %s", exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not get web service status")
|
||||
return InternalStatus(BuildStatus())
|
||||
return InternalStatus(status=BuildStatus())
|
||||
|
||||
def remove(self, base: str) -> None:
|
||||
"""
|
||||
|
@ -19,11 +19,12 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Iterable, List, Set, Type
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.util import tmpdir
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
@ -71,7 +72,7 @@ class Leaf:
|
||||
Returns:
|
||||
Leaf: loaded class
|
||||
"""
|
||||
with tmpdir() as clone_dir:
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name, (clone_dir := Path(dir_name)):
|
||||
Sources.load(clone_dir, package, database.patches_get(package.base), paths)
|
||||
dependencies = Package.dependencies(clone_dir)
|
||||
return cls(package, dependencies)
|
||||
|
@ -17,29 +17,27 @@
|
||||
# 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 logging
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class Trigger:
|
||||
class Trigger(LazyLogging):
|
||||
"""
|
||||
trigger base class
|
||||
|
||||
Attributes:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
logger(logging.Logger): application logger
|
||||
|
||||
Examples:
|
||||
This class must be used in order to create own extension. Basically idea is the following::
|
||||
|
||||
>>> 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()
|
||||
|
||||
Having this class you can pass it to ``configuration`` and it will be run on action::
|
||||
@ -50,7 +48,7 @@ class Trigger:
|
||||
>>> configuration.set_option("build", "triggers", "my.awesome.package.CustomTrigger")
|
||||
>>>
|
||||
>>> loader = TriggerLoader("x86_64", configuration)
|
||||
>>> loader.process(Result(), [])
|
||||
>>> loader.on_result(Result(), [])
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
@ -61,19 +59,26 @@ class Trigger:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
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:
|
||||
result(Result): build result
|
||||
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
|
||||
"""
|
||||
|
@ -17,29 +17,29 @@
|
||||
# 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 contextlib
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Iterable
|
||||
from typing import Generator, Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import InvalidExtension
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class TriggerLoader:
|
||||
class TriggerLoader(LazyLogging):
|
||||
"""
|
||||
trigger loader class
|
||||
|
||||
Attributes:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
logger(logging.Logger): application logger
|
||||
triggers(List[Trigger]): list of loaded triggers according to the configuration
|
||||
|
||||
Examples:
|
||||
@ -55,7 +55,7 @@ class TriggerLoader:
|
||||
|
||||
After that you are free to run triggers::
|
||||
|
||||
>>> loader.process(Result(), [])
|
||||
>>> loader.on_result(Result(), [])
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
@ -66,7 +66,6 @@ class TriggerLoader:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
|
||||
@ -74,6 +73,31 @@ class TriggerLoader:
|
||||
self.load_trigger(trigger)
|
||||
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:
|
||||
"""
|
||||
@ -151,18 +175,34 @@ class TriggerLoader:
|
||||
|
||||
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:
|
||||
result(Result): build result
|
||||
packages(Iterable[Package]): list of all available packages
|
||||
"""
|
||||
self.logger.debug("executing triggers on result")
|
||||
for trigger in self.triggers:
|
||||
trigger_name = type(trigger).__name__
|
||||
try:
|
||||
self.logger.info("executing extension %s", trigger_name)
|
||||
trigger.run(result, packages)
|
||||
except Exception:
|
||||
self.logger.exception("got exception while run trigger %s", trigger_name)
|
||||
with self.__execute_trigger(trigger):
|
||||
trigger.on_result(result, packages)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
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()
|
||||
|
@ -17,11 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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
|
||||
|
@ -24,7 +24,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
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.models.package import Package
|
||||
|
||||
|
@ -24,7 +24,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -33,7 +33,8 @@ class HttpUpload(Upload):
|
||||
helper for the http based uploads
|
||||
|
||||
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:
|
||||
@ -46,9 +47,10 @@ class HttpUpload(Upload):
|
||||
section(str): configuration section name
|
||||
"""
|
||||
Upload.__init__(self, architecture, configuration)
|
||||
password = configuration.get(section, "password")
|
||||
username = configuration.get(section, "username")
|
||||
self.auth = (password, username)
|
||||
password = configuration.get(section, "password", fallback=None)
|
||||
username = configuration.get(section, "username", fallback=None)
|
||||
self.auth = (password, username) if password and username else None
|
||||
self.timeout = configuration.getint(section, "timeout", fallback=30)
|
||||
|
||||
@staticmethod
|
||||
def calculate_hash(path: Path) -> str:
|
||||
@ -108,7 +110,7 @@ class HttpUpload(Upload):
|
||||
requests.Response: request response object
|
||||
"""
|
||||
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()
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not perform %s request to %s: %s", method, url, exception_response_text(e))
|
||||
|
@ -21,7 +21,7 @@ from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
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.models.package import Package
|
||||
|
||||
|
@ -25,7 +25,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterable
|
||||
|
||||
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.models.package import Package
|
||||
|
||||
|
@ -19,25 +19,23 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import SyncFailed
|
||||
from ahriman.core.lazy_logging import LazyLogging
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.upload_settings import UploadSettings
|
||||
|
||||
|
||||
class Upload:
|
||||
class Upload(LazyLogging):
|
||||
"""
|
||||
base remote sync class
|
||||
|
||||
Attributes:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
logger(logging.Logger): application logger
|
||||
|
||||
Examples:
|
||||
These classes provide the way to upload packages to remote sources as it is described in their implementations.
|
||||
@ -66,7 +64,6 @@ class Upload:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
|
||||
@ -86,13 +83,13 @@ class Upload:
|
||||
section, provider_name = configuration.gettype(target, architecture)
|
||||
provider = UploadSettings.from_option(provider_name)
|
||||
if provider == UploadSettings.Rsync:
|
||||
from ahriman.core.upload import Rsync
|
||||
from ahriman.core.upload.rsync import Rsync
|
||||
return Rsync(architecture, configuration, section)
|
||||
if provider == UploadSettings.S3:
|
||||
from ahriman.core.upload import S3
|
||||
from ahriman.core.upload.s3 import S3
|
||||
return S3(architecture, configuration, section)
|
||||
if provider == UploadSettings.Github:
|
||||
from ahriman.core.upload import Github
|
||||
from ahriman.core.upload.github import Github
|
||||
return Github(architecture, configuration, section)
|
||||
return cls(architecture, configuration) # should never happen
|
||||
|
||||
|
@ -21,7 +21,7 @@ from typing import Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
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.result import Result
|
||||
|
||||
@ -43,9 +43,9 @@ class UploadTrigger(Trigger):
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
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
|
||||
|
||||
|
@ -20,14 +20,11 @@
|
||||
import datetime
|
||||
import io
|
||||
import os
|
||||
from enum import Enum
|
||||
|
||||
import re
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union
|
||||
@ -37,7 +34,7 @@ from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
__all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "enum_values",
|
||||
"package_like", "pretty_datetime", "pretty_size", "tmpdir", "walk"]
|
||||
"package_like", "pretty_datetime", "pretty_size", "safe_filename", "walk"]
|
||||
|
||||
|
||||
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
|
||||
@ -273,26 +270,25 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
|
||||
return pretty_size(size / 1024, level + 1)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def tmpdir() -> Generator[Path, None, None]:
|
||||
def safe_filename(source: str) -> str:
|
||||
"""
|
||||
wrapper for tempfile to remove directory after all
|
||||
convert source string to its safe representation
|
||||
|
||||
Yields:
|
||||
Path: path to the created directory
|
||||
Args:
|
||||
source(str): string to convert
|
||||
|
||||
Examples:
|
||||
This function must be used only inside context manager as decorator states::
|
||||
|
||||
>>> with tmpdir() as path:
|
||||
>>> do_something(path)
|
||||
>>> raise Exception("Clear me after exception please")
|
||||
Returns:
|
||||
str: result string in which all unsafe characters are replaced by dash
|
||||
"""
|
||||
path = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
yield path
|
||||
finally:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
# RFC-3986 https://datatracker.ietf.org/doc/html/rfc3986 states that unreserved characters are
|
||||
# https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
|
||||
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
# however we would like to allow some gen-delims characters in filename, because those characters are used
|
||||
# as delimiter in other URI parts. The ones we allow are
|
||||
# ":" - used as separator in schema and userinfo
|
||||
# "[" and "]" - used for host part
|
||||
# "@" - used as separator between host and userinfo
|
||||
return re.sub(r"[^A-Za-z\d\-._~:\[\]@]", "-", source)
|
||||
|
||||
|
||||
def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||
|
@ -29,7 +29,7 @@ from typing import Any, Callable, Dict, List, Optional, Type
|
||||
from ahriman.core.util import filter_json, full_version
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AURPackage:
|
||||
"""
|
||||
AUR package descriptor
|
||||
|
@ -47,7 +47,7 @@ class BuildStatusEnum(str, Enum):
|
||||
Success = "success"
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class BuildStatus:
|
||||
"""
|
||||
build status holder
|
||||
@ -64,7 +64,7 @@ class BuildStatus:
|
||||
"""
|
||||
convert status to enum type
|
||||
"""
|
||||
self.status = BuildStatusEnum(self.status)
|
||||
object.__setattr__(self, "status", BuildStatusEnum(self.status))
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user