mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-27 22:31:43 +00:00
Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
91de1c2b8a | |||
32a4a82603 | |||
e8a10c1bb5 | |||
d480eb7bc3 | |||
8b0f9bfd78 | |||
a2639f8dbb | |||
65ba590ace | |||
fcb130e226 | |||
ae99fe4535 | |||
ec23e3f912 | |||
d3ea81d234 | |||
09b0f2914d | |||
7351e20104 | |||
dfd87c502f | |||
0b9ab09879 | |||
47c54f0b40 | |||
a2f2fa0354 | |||
4d68080c05 | |||
eb16ef12f3 | |||
e10e362dae | |||
e59fdd1ccc | |||
22d92e3b4e | |||
56b77a84a6 | |||
a5a99ec0b8 | |||
04bbabe898 | |||
4521c2adde | |||
5c5e54228f | |||
6514924b2d | |||
16aa977fa8 | |||
6e377e7261 | |||
4502931c39 | |||
fcb167b1a3 | |||
72b26603bf | |||
ab8ca16981 | |||
7c4f84fbc7 | |||
3b6b2efcb1 | |||
9f99dd3ff2 | |||
bee97df87f | |||
6becd01803 | |||
db195391e4 | |||
59f2992559 |
21
.github/workflows/run-setup.yml
vendored
Normal file
21
.github/workflows/run-setup.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: setup
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
run-setup:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: setup the service in arch linux container
|
||||
run: |
|
||||
docker run \
|
||||
-v ${{ github.workspace }}:/build -w /build \
|
||||
archlinux:latest \
|
||||
.github/workflows/setup.sh
|
7
.github/workflows/run-tests.yml
vendored
7
.github/workflows/run-tests.yml
vendored
@ -18,9 +18,4 @@ jobs:
|
||||
docker run \
|
||||
-v ${{ github.workspace }}:/build -w /build \
|
||||
archlinux:latest \
|
||||
/bin/bash -c "pacman --noconfirm -Syu base-devel python-argparse-manpage python-pip && \
|
||||
pip install -e .[web] && \
|
||||
pip install -e .[check] && \
|
||||
pip install -e .[s3] && \
|
||||
pip install -e .[test] && \
|
||||
make check tests"
|
||||
.github/workflows/tests.sh
|
||||
|
49
.github/workflows/setup.sh
vendored
Executable file
49
.github/workflows/setup.sh
vendored
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Install the package and run main install commands
|
||||
|
||||
set -ex
|
||||
|
||||
# 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
|
||||
# make dependencies
|
||||
pacman --noconfirm -Sy python-pip
|
||||
# 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
|
||||
|
||||
# create fresh tarball
|
||||
make VERSION=1.0.0 archlinux # well, it does not really matter which version we will put here
|
||||
# run makepkg
|
||||
mv ahriman-*-src.tar.xz package/archlinux
|
||||
chmod +777 package/archlinux # because fuck you that's why
|
||||
cd package/archlinux
|
||||
sudo -u nobody makepkg -cf --skipchecksums --noconfirm
|
||||
pacman --noconfirm -U ahriman-1.0.0-1-any.pkg.tar.zst
|
||||
|
||||
# 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
|
||||
sudo -u ahriman ahriman -a x86_64 init
|
||||
ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "github" --web-port 8080
|
||||
# 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
|
||||
# add the first package
|
||||
# the build itself does not really work in the container because it requires procfs
|
||||
sudo -u ahriman ahriman package-add yay
|
||||
# run package check
|
||||
sudo -u ahriman ahriman repo-update
|
||||
# stop web service lol
|
||||
kill $WEBPID
|
16
.github/workflows/tests.sh
vendored
Executable file
16
.github/workflows/tests.sh
vendored
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Install dependencies and run test in container
|
||||
|
||||
set -ex
|
||||
|
||||
# install dependencies
|
||||
pacman --noconfirm -Syu base-devel python-pip
|
||||
|
||||
# install python packages
|
||||
pip install -e .[web]
|
||||
pip install -e .[check]
|
||||
pip install -e .[s3]
|
||||
pip install -e .[test]
|
||||
|
||||
# run test and check targets
|
||||
make check tests
|
2
Makefile
2
Makefile
@ -3,7 +3,7 @@
|
||||
|
||||
PROJECT := ahriman
|
||||
|
||||
FILES := AUTHORS COPYING README.md docs package src setup.cfg setup.py
|
||||
FILES := AUTHORS COPYING README.md docs package src setup.cfg setup.py web.png
|
||||
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
|
||||
IGNORE_FILES := package/archlinux src/.mypy_cache
|
||||
|
||||
|
56
README.md
56
README.md
@ -1,6 +1,7 @@
|
||||
# ArcH Linux ReposItory MANager
|
||||
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/run-tests.yml)
|
||||
[](https://github.com/arcan1s/ahriman/actions/workflows/run-setup.yml)
|
||||
[](https://www.codefactor.io/repository/github/arcan1s/ahriman)
|
||||
|
||||
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
|
||||
@ -11,62 +12,19 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
|
||||
* Multi-architecture support.
|
||||
* VCS packages support.
|
||||
* Sign support with gpg (repository, package, per package settings).
|
||||
* Synchronization to remote services (rsync, s3) and report generation (html).
|
||||
* Synchronization to remote services (rsync, s3 and github) and report generation (html).
|
||||
* Dependency manager.
|
||||
* Ability to patch AUR packages and even create package from local PKGBUILDs.
|
||||
* Repository status interface with optional authorization and control options:
|
||||
|
||||

|
||||
|
||||
## Installation and run
|
||||
|
||||
For installation details please refer to the [documentation](docs/setup.md). For command help, `--help` subcommand must be used, e.g.:
|
||||
|
||||
```shell
|
||||
$ ahriman --help
|
||||
usage: ahriman [-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-log] [--no-report] [--unsafe] [-v]
|
||||
{add,check,clean,config,create-user,init,key-import,rebuild,remove,remove-unknown,report,search,setup,sign,status,status-update,sync,update,web} ...
|
||||
|
||||
ArcH Linux ReposItory MANager
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-a ARCHITECTURE, --architecture ARCHITECTURE
|
||||
target architectures (can be used multiple times) (default: None)
|
||||
-c CONFIGURATION, --configuration CONFIGURATION
|
||||
configuration path (default: /etc/ahriman.ini)
|
||||
--force force run, remove file lock (default: False)
|
||||
-l LOCK, --lock LOCK lock file (default: /tmp/ahriman.lock)
|
||||
--no-log redirect all log messages to stderr (default: False)
|
||||
--no-report force disable reporting to web service (default: False)
|
||||
--unsafe allow to run ahriman as non-ahriman user (default: False)
|
||||
-v, --version show program's version number and exit
|
||||
|
||||
command:
|
||||
{add,check,clean,config,create-user,init,key-import,rebuild,remove,remove-unknown,report,search,setup,sign,status,status-update,sync,update,web}
|
||||
command to run
|
||||
add add package
|
||||
check check for updates
|
||||
clean clean local caches
|
||||
config dump configuration
|
||||
create-user create user for web services
|
||||
init create repository tree
|
||||
key-import import PGP key
|
||||
rebuild rebuild repository
|
||||
remove remove package
|
||||
remove-unknown remove unknown packages
|
||||
report generate report
|
||||
search search for package
|
||||
setup initial service configuration
|
||||
sign sign packages
|
||||
status get package status
|
||||
status-update update package status
|
||||
sync sync repository
|
||||
update update packages
|
||||
web start web server
|
||||
```
|
||||
|
||||
Subcommands have own help message as well.
|
||||
For installation details please refer to the [documentation](docs/setup.md). For command help, `--help` subcommand must be used. Subcommands have own help message as well. The package also provides a [man page](docs/ahriman.1).
|
||||
|
||||
## Configuration
|
||||
|
||||
Every available option is described in the [documentation](docs/configuration.md).
|
||||
|
||||
## [FAQ](docs/faq.md)
|
||||
|
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 398 KiB |
765
docs/ahriman.1
765
docs/ahriman.1
@ -3,7 +3,7 @@
|
||||
ahriman
|
||||
.SH SYNOPSIS
|
||||
.B ahriman
|
||||
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-log] [--no-report] [--unsafe] [-v] {add,check,clean,config,init,key-import,rebuild,remove,remove-unknown,report,search,setup,sign,status,status-update,sync,update,user,web} ...
|
||||
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,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-check,check,repo-clean,clean,repo-config,config,repo-init,init,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-setup,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-update,update,user-add,user-remove,web} ...
|
||||
.SH DESCRIPTION
|
||||
ArcH Linux ReposItory MANager
|
||||
.SH OPTIONS
|
||||
@ -24,17 +24,17 @@ force run, remove file lock
|
||||
\fB\-l\fR \fI\,LOCK\/\fR, \fB\-\-lock\fR \fI\,LOCK\/\fR
|
||||
lock file
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-log\fR
|
||||
redirect all log messages to stderr
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-report\fR
|
||||
force disable reporting to web service
|
||||
|
||||
.TP
|
||||
\fB\-q\fR, \fB\-\-quiet\fR
|
||||
force disable any logging
|
||||
|
||||
.TP
|
||||
\fB\-\-unsafe\fR
|
||||
allow to run ahriman as non\-ahriman user
|
||||
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
|
||||
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-version\fR
|
||||
@ -43,140 +43,124 @@ show program's version number and exit
|
||||
.SS
|
||||
\fBSub-commands\fR
|
||||
.TP
|
||||
\fBahriman\fR \fI\,add\/\fR
|
||||
add package
|
||||
.TP
|
||||
\fBahriman\fR \fI\,check\/\fR
|
||||
check for updates
|
||||
.TP
|
||||
\fBahriman\fR \fI\,clean\/\fR
|
||||
clean local caches
|
||||
.TP
|
||||
\fBahriman\fR \fI\,config\/\fR
|
||||
dump configuration
|
||||
.TP
|
||||
\fBahriman\fR \fI\,init\/\fR
|
||||
create repository tree
|
||||
\fBahriman\fR \fI\,aur-search\/\fR
|
||||
search for package
|
||||
.TP
|
||||
\fBahriman\fR \fI\,key-import\/\fR
|
||||
import PGP key
|
||||
.TP
|
||||
\fBahriman\fR \fI\,rebuild\/\fR
|
||||
rebuild repository
|
||||
\fBahriman\fR \fI\,package-add\/\fR
|
||||
add package
|
||||
.TP
|
||||
\fBahriman\fR \fI\,remove\/\fR
|
||||
\fBahriman\fR \fI\,package-remove\/\fR
|
||||
remove package
|
||||
.TP
|
||||
\fBahriman\fR \fI\,remove-unknown\/\fR
|
||||
remove unknown packages
|
||||
.TP
|
||||
\fBahriman\fR \fI\,report\/\fR
|
||||
generate report
|
||||
.TP
|
||||
\fBahriman\fR \fI\,search\/\fR
|
||||
search for package
|
||||
.TP
|
||||
\fBahriman\fR \fI\,setup\/\fR
|
||||
initial service configuration
|
||||
.TP
|
||||
\fBahriman\fR \fI\,sign\/\fR
|
||||
sign packages
|
||||
.TP
|
||||
\fBahriman\fR \fI\,status\/\fR
|
||||
\fBahriman\fR \fI\,package-status\/\fR
|
||||
get package status
|
||||
.TP
|
||||
\fBahriman\fR \fI\,status-update\/\fR
|
||||
\fBahriman\fR \fI\,package-status-remove\/\fR
|
||||
remove package status
|
||||
.TP
|
||||
\fBahriman\fR \fI\,package-status-update\/\fR
|
||||
update package status
|
||||
.TP
|
||||
\fBahriman\fR \fI\,sync\/\fR
|
||||
\fBahriman\fR \fI\,patch-add\/\fR
|
||||
add patch set
|
||||
.TP
|
||||
\fBahriman\fR \fI\,patch-list\/\fR
|
||||
list patch sets
|
||||
.TP
|
||||
\fBahriman\fR \fI\,patch-remove\/\fR
|
||||
remove patch set
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-check\/\fR
|
||||
check for updates
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-clean\/\fR
|
||||
clean local caches
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-config\/\fR
|
||||
dump configuration
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-init\/\fR
|
||||
create repository tree
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-rebuild\/\fR
|
||||
rebuild repository
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-remove-unknown\/\fR
|
||||
remove unknown packages
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-report\/\fR
|
||||
generate report
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-setup\/\fR
|
||||
initial service configuration
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-sign\/\fR
|
||||
sign packages
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-status-update\/\fR
|
||||
update repository status
|
||||
.TP
|
||||
\fBahriman\fR \fI\,repo-sync\/\fR
|
||||
sync repository
|
||||
.TP
|
||||
\fBahriman\fR \fI\,update\/\fR
|
||||
\fBahriman\fR \fI\,repo-update\/\fR
|
||||
update packages
|
||||
.TP
|
||||
\fBahriman\fR \fI\,user\/\fR
|
||||
manage users for web services
|
||||
\fBahriman\fR \fI\,user-add\/\fR
|
||||
create or update user
|
||||
.TP
|
||||
\fBahriman\fR \fI\,user-remove\/\fR
|
||||
remove user
|
||||
.TP
|
||||
\fBahriman\fR \fI\,web\/\fR
|
||||
start web server
|
||||
.SH OPTIONS 'ahriman add'
|
||||
usage: ahriman add [-h] [--now] [--source {PackageSource.Auto,PackageSource.Archive,PackageSource.Directory,PackageSource.AUR}] [--without-dependencies] package [package ...]
|
||||
web server
|
||||
.SH OPTIONS 'ahriman aur-search'
|
||||
usage: ahriman aur-search [-h] [-i]
|
||||
[--sort-by {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}]
|
||||
search [search ...]
|
||||
|
||||
add package
|
||||
search for package in AUR using API
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
package base/name or archive path
|
||||
\fBsearch\fR
|
||||
search terms, can be specified multiple times, result will match all terms
|
||||
|
||||
.TP
|
||||
\fB\-\-now\fR
|
||||
run update function after
|
||||
\fB\-i\fR, \fB\-\-info\fR
|
||||
show additional package information
|
||||
|
||||
.TP
|
||||
\fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.Directory,PackageSource.AUR}
|
||||
package source
|
||||
\fB\-\-sort\-by\fR {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}
|
||||
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
|
||||
by name
|
||||
|
||||
.SH OPTIONS 'ahriman search'
|
||||
usage: ahriman aur-search [-h] [-i]
|
||||
[--sort-by {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}]
|
||||
search [search ...]
|
||||
|
||||
search for package in AUR using API
|
||||
|
||||
.TP
|
||||
\fB\-\-without\-dependencies\fR
|
||||
do not add dependencies
|
||||
|
||||
.SH OPTIONS 'ahriman check'
|
||||
usage: ahriman check [-h] [--no-vcs] [package ...]
|
||||
|
||||
check for updates. Same as update \-\-dry\-run \-\-no\-manual
|
||||
\fBsearch\fR
|
||||
search terms, can be specified multiple times, result will match all terms
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
filter check by package base
|
||||
\fB\-i\fR, \fB\-\-info\fR
|
||||
show additional package information
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-vcs\fR
|
||||
do not check VCS packages
|
||||
|
||||
.SH OPTIONS 'ahriman clean'
|
||||
usage: ahriman clean [-h] [--no-build] [--no-cache] [--no-chroot] [--no-manual] [--no-packages]
|
||||
|
||||
clear local caches
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-build\fR
|
||||
do not clear directory with package sources
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-cache\fR
|
||||
do not clear directory with package caches
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-chroot\fR
|
||||
do not clear build chroot
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-manual\fR
|
||||
do not clear directory with manually added packages
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-packages\fR
|
||||
do not clear directory with built packages
|
||||
|
||||
.SH OPTIONS 'ahriman config'
|
||||
usage: ahriman config [-h]
|
||||
|
||||
dump configuration for specified architecture
|
||||
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman init'
|
||||
usage: ahriman init [-h]
|
||||
|
||||
create empty repository tree. Optional command for auto architecture support
|
||||
|
||||
|
||||
\fB\-\-sort\-by\fR {category_id,description,first_submitted,id,last_modified,license,maintainer,name,num_votes,out_of_date,package_base,package_base_id,url,url_path,version}
|
||||
sort field by this field. In case if two packages have the same value of the specified field, they will be always sorted
|
||||
by name
|
||||
|
||||
.SH OPTIONS 'ahriman key-import'
|
||||
usage: ahriman key-import [-h] [--key-server KEY_SERVER] key
|
||||
|
||||
import PGP key from public sources to repository user
|
||||
import PGP key from public sources to the repository user
|
||||
|
||||
.TP
|
||||
\fBkey\fR
|
||||
@ -186,59 +170,403 @@ PGP key to import from public server
|
||||
\fB\-\-key\-server\fR \fI\,KEY_SERVER\/\fR
|
||||
key server for key import
|
||||
|
||||
.SH OPTIONS 'ahriman rebuild'
|
||||
usage: ahriman rebuild [-h] [--depends-on DEPENDS_ON]
|
||||
|
||||
rebuild whole repository
|
||||
.SH OPTIONS 'ahriman package-add'
|
||||
usage: ahriman package-add [-h] [-n]
|
||||
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}]
|
||||
[--without-dependencies]
|
||||
package [package ...]
|
||||
|
||||
add existing or new package to the build queue
|
||||
|
||||
.TP
|
||||
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
|
||||
only rebuild packages that depend on specified package
|
||||
\fBpackage\fR
|
||||
package source (base name, path to local files, remote URL)
|
||||
|
||||
.SH OPTIONS 'ahriman remove'
|
||||
usage: ahriman remove [-h] package [package ...]
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-now\fR
|
||||
run update function after
|
||||
|
||||
remove package
|
||||
.TP
|
||||
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}
|
||||
explicitly specify the package source for this command
|
||||
|
||||
.TP
|
||||
\fB\-\-without\-dependencies\fR
|
||||
do not add dependencies
|
||||
|
||||
.SH OPTIONS 'ahriman add'
|
||||
usage: ahriman package-add [-h] [-n]
|
||||
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}]
|
||||
[--without-dependencies]
|
||||
package [package ...]
|
||||
|
||||
add existing or new package to the build queue
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
package source (base name, path to local files, remote URL)
|
||||
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-now\fR
|
||||
run update function after
|
||||
|
||||
.TP
|
||||
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}
|
||||
explicitly specify the package source for this command
|
||||
|
||||
.TP
|
||||
\fB\-\-without\-dependencies\fR
|
||||
do not add dependencies
|
||||
|
||||
.SH OPTIONS 'ahriman package-update'
|
||||
usage: ahriman package-add [-h] [-n]
|
||||
[-s {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}]
|
||||
[--without-dependencies]
|
||||
package [package ...]
|
||||
|
||||
add existing or new package to the build queue
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
package source (base name, path to local files, remote URL)
|
||||
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-now\fR
|
||||
run update function after
|
||||
|
||||
.TP
|
||||
\fB\-s\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}, \fB\-\-source\fR {PackageSource.Auto,PackageSource.Archive,PackageSource.AUR,PackageSource.Directory,PackageSource.Local,PackageSource.Remote}
|
||||
explicitly specify the package source for this command
|
||||
|
||||
.TP
|
||||
\fB\-\-without\-dependencies\fR
|
||||
do not add dependencies
|
||||
|
||||
.SH OPTIONS 'ahriman package-remove'
|
||||
usage: ahriman package-remove [-h] package [package ...]
|
||||
|
||||
remove package from the repository
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
package name or base
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman remove-unknown'
|
||||
usage: ahriman remove-unknown [-h] [--dry-run]
|
||||
.SH OPTIONS 'ahriman remove'
|
||||
usage: ahriman package-remove [-h] package [package ...]
|
||||
|
||||
remove packages which are missing in AUR
|
||||
remove package from the repository
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
package name or base
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman package-status'
|
||||
usage: ahriman package-status [-h] [--ahriman] [-i]
|
||||
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
|
||||
[package ...]
|
||||
|
||||
request status of the package
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
filter status by package base
|
||||
|
||||
.TP
|
||||
\fB\-\-ahriman\fR
|
||||
get service status itself
|
||||
|
||||
.TP
|
||||
\fB\-i\fR, \fB\-\-info\fR
|
||||
show additional package information
|
||||
|
||||
.TP
|
||||
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
|
||||
filter packages by status
|
||||
|
||||
.SH OPTIONS 'ahriman status'
|
||||
usage: ahriman package-status [-h] [--ahriman] [-i]
|
||||
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
|
||||
[package ...]
|
||||
|
||||
request status of the package
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
filter status by package base
|
||||
|
||||
.TP
|
||||
\fB\-\-ahriman\fR
|
||||
get service status itself
|
||||
|
||||
.TP
|
||||
\fB\-i\fR, \fB\-\-info\fR
|
||||
show additional package information
|
||||
|
||||
.TP
|
||||
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
|
||||
filter packages by status
|
||||
|
||||
.SH OPTIONS 'ahriman package-status-remove'
|
||||
usage: ahriman package-status-remove [-h] package [package ...]
|
||||
|
||||
remove the package from the status page
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
remove specified packages
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman package-status-update'
|
||||
usage: ahriman package-status-update [-h]
|
||||
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
|
||||
[package ...]
|
||||
|
||||
update package status on the status page
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
set status for specified packages. If no packages supplied, service status will be updated
|
||||
|
||||
.TP
|
||||
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
|
||||
new status
|
||||
|
||||
.SH OPTIONS 'ahriman status-update'
|
||||
usage: ahriman package-status-update [-h]
|
||||
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
|
||||
[package ...]
|
||||
|
||||
update package status on the status page
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
set status for specified packages. If no packages supplied, service status will be updated
|
||||
|
||||
.TP
|
||||
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
|
||||
new status
|
||||
|
||||
.SH OPTIONS 'ahriman patch-add'
|
||||
usage: ahriman patch-add [-h] [-t TRACK] package
|
||||
|
||||
create or update source patches
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
path to directory with changed files for patch addition/update
|
||||
|
||||
.TP
|
||||
\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
|
||||
files which has to be tracked
|
||||
|
||||
.SH OPTIONS 'ahriman patch-list'
|
||||
usage: ahriman patch-list [-h] package
|
||||
|
||||
list available patches for the package
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
package base
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman patch-remove'
|
||||
usage: ahriman patch-remove [-h] package
|
||||
|
||||
remove patches for the package
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
package base
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman repo-check'
|
||||
usage: ahriman repo-check [-h] [--no-vcs] [package ...]
|
||||
|
||||
check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
filter check by package base
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-vcs\fR
|
||||
do not check VCS packages
|
||||
|
||||
.SH OPTIONS 'ahriman check'
|
||||
usage: ahriman repo-check [-h] [--no-vcs] [package ...]
|
||||
|
||||
check for packages updates. Same as update \-\-dry\-run \-\-no\-manual
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
filter check by package base
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-vcs\fR
|
||||
do not check VCS packages
|
||||
|
||||
.SH OPTIONS 'ahriman repo-clean'
|
||||
usage: ahriman repo-clean [-h] [--build] [--cache] [--chroot] [--manual] [--packages] [--patches]
|
||||
|
||||
remove local caches
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-build\fR
|
||||
clear directory with package sources
|
||||
|
||||
.TP
|
||||
\fB\-\-cache\fR
|
||||
clear directory with package caches
|
||||
|
||||
.TP
|
||||
\fB\-\-chroot\fR
|
||||
clear build chroot
|
||||
|
||||
.TP
|
||||
\fB\-\-manual\fR
|
||||
clear directory with manually added packages
|
||||
|
||||
.TP
|
||||
\fB\-\-packages\fR
|
||||
clear directory with built packages
|
||||
|
||||
.TP
|
||||
\fB\-\-patches\fR
|
||||
clear directory with patches
|
||||
|
||||
.SH OPTIONS 'ahriman clean'
|
||||
usage: ahriman repo-clean [-h] [--build] [--cache] [--chroot] [--manual] [--packages] [--patches]
|
||||
|
||||
remove local caches
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-build\fR
|
||||
clear directory with package sources
|
||||
|
||||
.TP
|
||||
\fB\-\-cache\fR
|
||||
clear directory with package caches
|
||||
|
||||
.TP
|
||||
\fB\-\-chroot\fR
|
||||
clear build chroot
|
||||
|
||||
.TP
|
||||
\fB\-\-manual\fR
|
||||
clear directory with manually added packages
|
||||
|
||||
.TP
|
||||
\fB\-\-packages\fR
|
||||
clear directory with built packages
|
||||
|
||||
.TP
|
||||
\fB\-\-patches\fR
|
||||
clear directory with patches
|
||||
|
||||
.SH OPTIONS 'ahriman repo-config'
|
||||
usage: ahriman repo-config [-h]
|
||||
|
||||
dump configuration for the specified architecture
|
||||
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman config'
|
||||
usage: ahriman repo-config [-h]
|
||||
|
||||
dump configuration for the specified architecture
|
||||
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman repo-init'
|
||||
usage: ahriman repo-init [-h]
|
||||
|
||||
create empty repository tree. Optional command for auto architecture support
|
||||
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman init'
|
||||
usage: ahriman repo-init [-h]
|
||||
|
||||
create empty repository tree. Optional command for auto architecture support
|
||||
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman repo-rebuild'
|
||||
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON]
|
||||
|
||||
force rebuild whole repository
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
|
||||
only rebuild packages that depend on specified package
|
||||
|
||||
.SH OPTIONS 'ahriman rebuild'
|
||||
usage: ahriman repo-rebuild [-h] [--depends-on DEPENDS_ON]
|
||||
|
||||
force rebuild whole repository
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-depends\-on\fR \fI\,DEPENDS_ON\/\fR
|
||||
only rebuild packages that depend on specified package
|
||||
|
||||
.SH OPTIONS 'ahriman repo-remove-unknown'
|
||||
usage: ahriman repo-remove-unknown [-h] [--dry-run] [-i]
|
||||
|
||||
remove packages which are missing in AUR and do not have local PKGBUILDs
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-dry\-run\fR
|
||||
just perform check for packages without removal
|
||||
|
||||
.SH OPTIONS 'ahriman report'
|
||||
usage: ahriman report [-h] [target ...]
|
||||
.TP
|
||||
\fB\-i\fR, \fB\-\-info\fR
|
||||
show additional package information
|
||||
|
||||
generate report
|
||||
.SH OPTIONS 'ahriman remove-unknown'
|
||||
usage: ahriman repo-remove-unknown [-h] [--dry-run] [-i]
|
||||
|
||||
remove packages which are missing in AUR and do not have local PKGBUILDs
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-dry\-run\fR
|
||||
just perform check for packages without removal
|
||||
|
||||
.TP
|
||||
\fB\-i\fR, \fB\-\-info\fR
|
||||
show additional package information
|
||||
|
||||
.SH OPTIONS 'ahriman repo-report'
|
||||
usage: ahriman repo-report [-h] [target ...]
|
||||
|
||||
generate repository report according to current settings
|
||||
|
||||
.TP
|
||||
\fBtarget\fR
|
||||
target to generate report
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman search'
|
||||
usage: ahriman search [-h] search [search ...]
|
||||
.SH OPTIONS 'ahriman report'
|
||||
usage: ahriman repo-report [-h] [target ...]
|
||||
|
||||
search for package in AUR using API
|
||||
generate repository report according to current settings
|
||||
|
||||
.TP
|
||||
\fBsearch\fR
|
||||
search terms, can be specified multiple times
|
||||
\fBtarget\fR
|
||||
target to generate report
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman setup'
|
||||
usage: ahriman setup [-h] [--build-command BUILD_COMMAND] [--from-configuration FROM_CONFIGURATION] [--no-multilib] --packager PACKAGER --repository REPOSITORY [--sign-key SIGN_KEY]
|
||||
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
|
||||
.SH OPTIONS 'ahriman repo-setup'
|
||||
usage: ahriman repo-setup [-h] [--build-command BUILD_COMMAND] [--from-configuration FROM_CONFIGURATION] [--no-multilib]
|
||||
--packager PACKAGER --repository REPOSITORY [--sign-key SIGN_KEY]
|
||||
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
|
||||
|
||||
create initial service configuration, requires root
|
||||
|
||||
@ -275,64 +603,101 @@ sign options
|
||||
\fB\-\-web\-port\fR \fI\,WEB_PORT\/\fR
|
||||
port of the web service
|
||||
|
||||
.SH OPTIONS 'ahriman sign'
|
||||
usage: ahriman sign [-h] [package ...]
|
||||
.SH OPTIONS 'ahriman setup'
|
||||
usage: ahriman repo-setup [-h] [--build-command BUILD_COMMAND] [--from-configuration FROM_CONFIGURATION] [--no-multilib]
|
||||
--packager PACKAGER --repository REPOSITORY [--sign-key SIGN_KEY]
|
||||
[--sign-target {SignSettings.Packages,SignSettings.Repository}] [--web-port WEB_PORT]
|
||||
|
||||
(re\-)sign packages and repository database
|
||||
create initial service configuration, requires root
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-build\-command\fR \fI\,BUILD_COMMAND\/\fR
|
||||
build command prefix
|
||||
|
||||
.TP
|
||||
\fB\-\-from\-configuration\fR \fI\,FROM_CONFIGURATION\/\fR
|
||||
path to default devtools pacman configuration
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-multilib\fR
|
||||
do not add multilib repository
|
||||
|
||||
.TP
|
||||
\fB\-\-packager\fR \fI\,PACKAGER\/\fR
|
||||
packager name and email
|
||||
|
||||
.TP
|
||||
\fB\-\-repository\fR \fI\,REPOSITORY\/\fR
|
||||
repository name
|
||||
|
||||
.TP
|
||||
\fB\-\-sign\-key\fR \fI\,SIGN_KEY\/\fR
|
||||
sign key id
|
||||
|
||||
.TP
|
||||
\fB\-\-sign\-target\fR {SignSettings.Packages,SignSettings.Repository}
|
||||
sign options
|
||||
|
||||
.TP
|
||||
\fB\-\-web\-port\fR \fI\,WEB_PORT\/\fR
|
||||
port of the web service
|
||||
|
||||
.SH OPTIONS 'ahriman repo-sign'
|
||||
usage: ahriman repo-sign [-h] [package ...]
|
||||
|
||||
(re\-)sign packages and repository database according to current settings
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
sign only specified packages
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman status'
|
||||
usage: ahriman status [-h] [--ahriman] [--status {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}] [package ...]
|
||||
.SH OPTIONS 'ahriman sign'
|
||||
usage: ahriman repo-sign [-h] [package ...]
|
||||
|
||||
request status of the package
|
||||
(re\-)sign packages and repository database according to current settings
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
filter status by package base
|
||||
sign only specified packages
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman repo-status-update'
|
||||
usage: ahriman repo-status-update [-h]
|
||||
[-s {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}]
|
||||
|
||||
update repository status on the status page
|
||||
|
||||
|
||||
.TP
|
||||
\fB\-\-ahriman\fR
|
||||
get service status itself
|
||||
|
||||
.TP
|
||||
\fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
|
||||
filter packages by status
|
||||
|
||||
.SH OPTIONS 'ahriman status-update'
|
||||
usage: ahriman status-update [-h] [--status {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}] [--remove] [package ...]
|
||||
|
||||
request status of the package
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
set status for specified packages. If no packages supplied, service status will be updated
|
||||
|
||||
.TP
|
||||
\fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
|
||||
\fB\-s\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}, \fB\-\-status\fR {BuildStatusEnum.Unknown,BuildStatusEnum.Pending,BuildStatusEnum.Building,BuildStatusEnum.Failed,BuildStatusEnum.Success}
|
||||
new status
|
||||
|
||||
.TP
|
||||
\fB\-\-remove\fR
|
||||
remove package status page
|
||||
.SH OPTIONS 'ahriman repo-sync'
|
||||
usage: ahriman repo-sync [-h] [target ...]
|
||||
|
||||
.SH OPTIONS 'ahriman sync'
|
||||
usage: ahriman sync [-h] [target ...]
|
||||
|
||||
sync packages to remote server
|
||||
sync repository files to remote server according to current settings
|
||||
|
||||
.TP
|
||||
\fBtarget\fR
|
||||
target to sync
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman update'
|
||||
usage: ahriman update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
|
||||
.SH OPTIONS 'ahriman sync'
|
||||
usage: ahriman repo-sync [-h] [target ...]
|
||||
|
||||
run updates
|
||||
sync repository files to remote server according to current settings
|
||||
|
||||
.TP
|
||||
\fBtarget\fR
|
||||
target to sync
|
||||
|
||||
|
||||
.SH OPTIONS 'ahriman repo-update'
|
||||
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
|
||||
|
||||
check for packages updates and run build process if requested
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
@ -354,10 +719,37 @@ do not include manual updates
|
||||
\fB\-\-no\-vcs\fR
|
||||
do not check VCS packages
|
||||
|
||||
.SH OPTIONS 'ahriman user'
|
||||
usage: ahriman user [-h] [--as-service] [-a {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [--no-reload] [-p PASSWORD] [-r] [--secure] username
|
||||
.SH OPTIONS 'ahriman update'
|
||||
usage: ahriman repo-update [-h] [--dry-run] [--no-aur] [--no-manual] [--no-vcs] [package ...]
|
||||
|
||||
manage users for web services with password and role. In case if password was not entered it will be asked interactively
|
||||
check for packages updates and run build process if requested
|
||||
|
||||
.TP
|
||||
\fBpackage\fR
|
||||
filter check by package base
|
||||
|
||||
.TP
|
||||
\fB\-\-dry\-run\fR
|
||||
just perform check for updates, same as check command
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-aur\fR
|
||||
do not check for AUR updates. Implies \-\-no\-vcs
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-manual\fR
|
||||
do not include manual updates
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-vcs\fR
|
||||
do not check VCS packages
|
||||
|
||||
.SH OPTIONS 'ahriman user-add'
|
||||
usage: ahriman user-add [-h] [--as-service] [--no-reload] [-p PASSWORD]
|
||||
[-r {UserAccess.Safe,UserAccess.Read,UserAccess.Write}] [-s]
|
||||
username
|
||||
|
||||
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
|
||||
|
||||
.TP
|
||||
\fBusername\fR
|
||||
@ -368,23 +760,37 @@ username for web service
|
||||
add user as service user
|
||||
|
||||
.TP
|
||||
\fB\-a\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}, \fB\-\-access\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}
|
||||
\fB\-\-no\-reload\fR
|
||||
do not reload authentication module
|
||||
|
||||
.TP
|
||||
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
|
||||
user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2
|
||||
authorization type.
|
||||
|
||||
.TP
|
||||
\fB\-r\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}, \fB\-\-role\fR {UserAccess.Safe,UserAccess.Read,UserAccess.Write}
|
||||
user access level
|
||||
|
||||
.TP
|
||||
\fB\-s\fR, \fB\-\-secure\fR
|
||||
set file permissions to user\-only
|
||||
|
||||
.SH OPTIONS 'ahriman user-remove'
|
||||
usage: ahriman user-remove [-h] [--no-reload] [-s] username
|
||||
|
||||
remove user from the user mapping and update the configuration
|
||||
|
||||
.TP
|
||||
\fBusername\fR
|
||||
username for web service
|
||||
|
||||
.TP
|
||||
\fB\-\-no\-reload\fR
|
||||
do not reload authentication module
|
||||
|
||||
.TP
|
||||
\fB\-p\fR \fI\,PASSWORD\/\fR, \fB\-\-password\fR \fI\,PASSWORD\/\fR
|
||||
user password
|
||||
|
||||
.TP
|
||||
\fB\-r\fR, \fB\-\-remove\fR
|
||||
remove user from configuration
|
||||
|
||||
.TP
|
||||
\fB\-\-secure\fR
|
||||
\fB\-s\fR, \fB\-\-secure\fR
|
||||
set file permissions to user\-only
|
||||
|
||||
.SH OPTIONS 'ahriman web'
|
||||
@ -392,6 +798,11 @@ usage: ahriman web [-h]
|
||||
|
||||
start web server
|
||||
|
||||
|
||||
|
||||
.SH COMMENTS
|
||||
Argument list can also be read from file by using @ prefix.
|
||||
|
||||
.SH AUTHORS
|
||||
.B ahriman
|
||||
was written by ahriman team <>.
|
||||
|
@ -12,7 +12,13 @@ Full dependency diagram:
|
||||
|
||||
## `ahriman.application` package
|
||||
|
||||
This package contains application (aka executable) related classes and everything for that. It also contains package called `ahriman.application.handlers` in which all available subcommands are described as separated classes derived from base `ahriman.application.handlers.handler.Handler` class. `ahriman.application.ahriman` contains only command line parses and executes specified `Handler` on success, `ahriman.application.application.Application` is a god class which provides interfaces for all repository related actions. `ahriman.application.lock.Lock` is additional class which provides file-based lock and also performs some common checks.
|
||||
This package contains application (aka executable) related classes and everything for that. It also contains package called `ahriman.application.handlers` in which all available subcommands are described as separated classes derived from base `ahriman.application.handlers.handler.Handler` class.
|
||||
|
||||
`ahriman.application.application.application.Application` (god class) is used for any interaction from parsers with repository, web etc. It is divided into multiple traits by functions (package related and repository related) in the same package.
|
||||
|
||||
`ahriman.application.formatters` package provides `Printer` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
|
||||
|
||||
`ahriman.application.ahriman` contains only command line parses and executes specified `Handler` on success, `ahriman.application.lock.Lock` is additional class which provides file-based lock and also performs some common checks.
|
||||
|
||||
## `ahriman.core` package
|
||||
|
||||
@ -67,8 +73,11 @@ Application is designed to run from `systemd` services and provides parametrized
|
||||
Idea is to copy package to the directory from which it will be handled at the next update run. Different variants are supported:
|
||||
|
||||
* If supplied argument is file then application moves the file to the directory with built packages. Same rule applies for directory, but in this case it copies every package-like file from the specified directory.
|
||||
* If supplied argument is directory and there is `PKGBUILD` file there it will be treated as local package. In this case it will queue this package to build and copy source files (`PKGBUILD` and `.SRCINFO`) to caches.
|
||||
* If supplied argument iis not file then application tries to lookup for the specified name in AUR and clones it into the directory with manual updates. This scenario can also handle package dependencies which are missing in repositories.
|
||||
|
||||
This logic can be overwritten by specifying the `source` parameter, which is partially useful if you would like to add package from AUR, but there is local directory cloned from AUR.
|
||||
|
||||
## Rebuild packages
|
||||
|
||||
Same as add function for every package in repository. Optional filter by reverse dependency can be supplied.
|
||||
@ -97,7 +106,7 @@ After any step any package data is being removed.
|
||||
|
||||
## Configuration
|
||||
|
||||
`ahriman.core.configuration.Configuration` class provides some additional methods (e.g. `getpath` and `getlist`) and also combines multiple files into single configuration dictionary using architecture overrides. It is recommended to read class related settings from the class, not outside.
|
||||
`ahriman.core.configuration.Configuration` class provides some additional methods (e.g. `getpath` and `getlist`) and also combines multiple files into single configuration dictionary using architecture overrides. It is the recommended way to deal with settings.
|
||||
|
||||
## Utils
|
||||
|
||||
@ -105,7 +114,7 @@ For every external command run (which is actually not recommended if possible) c
|
||||
|
||||
## Submodules
|
||||
|
||||
Some packages provide different behaviour depending on configuration settings. In this cases inheritance is used and recommended way to deal with them is to call class method `load` from base classes.
|
||||
Some packages provide different behaviour depending on configuration settings. In these cases inheritance is used and recommended way to deal with them is to call class method `load` from base classes.
|
||||
|
||||
## Authorization
|
||||
|
||||
@ -131,7 +140,17 @@ OAuth provider uses library definitions (`aioauth-client`) in order _authenticat
|
||||
|
||||
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature is used by service reporting (aka robots).
|
||||
|
||||
In order to configure users there is special command.
|
||||
In order to configure users there are special commands.
|
||||
|
||||
## Remote synchronization
|
||||
|
||||
There are several supported synchronization providers, currently they are `rsync`, `s3`, `github`.
|
||||
|
||||
`rsync` provider does not have any specific logic except for running external rsync application with configured arguments. The service does not handle SSH configuration, thus it has to be configured before running application manually.
|
||||
|
||||
`s3` provider uses `boto3` package and implements sync feature. The files are stored in architecture directory (e.g. if bucket is `repository`, packages will be stored in `repository/x86_64` for the `x86_64` architecture), bucket must be created before any action and API key must have permissions to write to the bucket. No external configuration required. In order to upload only changed files the service compares calculated hashes with the Amazon ETags, used realization is described [here](https://teppen.io/2018/10/23/aws_s3_verify_etags/).
|
||||
|
||||
`github` provider authenticates through basic auth, API key with repository write permissions is required. There will be created a release with the name of the architecture in case if it does not exist; files will be uploaded to the release assets. It also stores array of files and their MD5 checksums in release body in order to upload only changed ones. According to the Github API in case if there is already uploaded asset with the same name (e.g. database files), asset will be removed first.
|
||||
|
||||
## Additional features
|
||||
|
||||
|
@ -1,6 +1,13 @@
|
||||
# ahriman configuration
|
||||
|
||||
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
|
||||
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build:x86_64` groups it will use the option from `build:x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
|
||||
|
||||
Some values have list of strings type. Those values will be read in the same way as shell does:
|
||||
|
||||
* By default, it splits value by spaces excluding empty elements.
|
||||
* In case if quotation mark (`"` or `'`) will be found, any spaces inside will be ignored.
|
||||
* In order to use quotation mark inside value it is required to put it to another quotation mark, e.g. `wor"'"d "with quote"` will be parsed as `["wor'd", "with quote"]` and vice versa.
|
||||
* Unclosed quotation mark is not allowed and will rise an exception.
|
||||
|
||||
## `settings` group
|
||||
|
||||
@ -38,11 +45,11 @@ Authorization mapping. Group name must refer to user access level, i.e. it shoul
|
||||
Key is always username (case-insensitive), option value depends on authorization provider:
|
||||
|
||||
* `OAuth` - by default requires only usernames and ignores values. But in case of direct login method call (via POST request) it will act as `Mapping` authorization method.
|
||||
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `create-user` subcommand.
|
||||
* `Mapping` (default) - reads salted password hashes from values, uses SHA512 in order to hash passwords. Password can be set by using `user-add` subcommand.
|
||||
|
||||
## `build:*` groups
|
||||
|
||||
Build related configuration. Group name must refer to architecture, e.g. it should be `build:x86_64` for x86_64 architecture.
|
||||
Build related configuration. Group name can refer to architecture, e.g. `build:x86_64` can be used for x86_64 architecture specific settings.
|
||||
|
||||
* `archbuild_flags` - additional flags passed to `archbuild` command, space separated list of strings, optional.
|
||||
* `build_command` - default build command, string, required.
|
||||
@ -59,7 +66,7 @@ Base repository settings.
|
||||
|
||||
## `sign:*` groups
|
||||
|
||||
Settings for signing packages or repository. Group name must refer to architecture, e.g. it should be `sign:x86_64` for x86_64 architecture.
|
||||
Settings for signing packages or repository. Group name can refer to architecture, e.g. `sign:x86_64` can be used for x86_64 architecture specific settings.
|
||||
|
||||
* `target` - configuration flag to enable signing, space separated list of strings, required. Allowed values are `package` (sign each package separately), `repository` (sign repository database file).
|
||||
* `key` - default PGP key, string, required. This key will also be used for database signing if enabled.
|
||||
@ -69,12 +76,19 @@ Settings for signing packages or repository. Group name must refer to architectu
|
||||
|
||||
Report generation settings.
|
||||
|
||||
* `target` - list of reports to be generated, space separated list of strings, required. Allowed values are `html`, `email`.
|
||||
* `target` - list of reports to be generated, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `email` must point to one of `email` of `email:x86_64` (the one with architecture has higher priority).
|
||||
|
||||
### `email:*` groups
|
||||
Type will be read from several ways:
|
||||
|
||||
Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_64 architecture.
|
||||
* In case if `type` option set inside the section, it will be used.
|
||||
* Otherwise, it will look for type from section name removing architecture name.
|
||||
* And finally, it will use section name as type.
|
||||
|
||||
### `email` type
|
||||
|
||||
Section name must be either `email` (plus optional architecture name, e.g. `email:x86_64`) or random name with `type` set.
|
||||
|
||||
* `type` - type of the report, string, optional, must be set to `email` if exists.
|
||||
* `full_template_path` - path to Jinja2 template for full package description index, string, optional.
|
||||
* `homepage` - link to homepage, string, optional.
|
||||
* `host` - SMTP host for sending emails, string, required.
|
||||
@ -88,10 +102,11 @@ Group name must refer to architecture, e.g. it should be `email:x86_64` for x86_
|
||||
* `template_path` - path to Jinja2 template, string, required.
|
||||
* `user` - SMTP user to authenticate, string, optional.
|
||||
|
||||
### `html:*` groups
|
||||
### `html` type
|
||||
|
||||
Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_64 architecture.
|
||||
Section name must be either `html` (plus optional architecture name, e.g. `html:x86_64`) or random name with `type` set.
|
||||
|
||||
* `type` - type of the report, string, optional, must be set to `html` if exists.
|
||||
* `path` - path to html report file, string, required.
|
||||
* `homepage` - link to homepage, string, optional.
|
||||
* `link_path` - prefix for HTML links, string, required.
|
||||
@ -101,19 +116,41 @@ Group name must refer to architecture, e.g. it should be `html:x86_64` for x86_6
|
||||
|
||||
Remote synchronization settings.
|
||||
|
||||
* `target` - list of synchronizations to be used, space separated list of strings, required. Allowed values are `rsync`, `s3`.
|
||||
* `target` - list of synchronizations to be used, space separated list of strings, required. It must point to valid section (or to section with architecture), e.g. `somerandomname` must point to existing section, `github` must point to one of `github` of `github:x86_64` (with architecture it has higher priority).
|
||||
|
||||
### `rsync:*` groups
|
||||
Type will be read from several ways:
|
||||
|
||||
Group name must refer to architecture, e.g. it should be `rsync:x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
|
||||
* In case if `type` option set inside the section, it will be used.
|
||||
* Otherwise, it will look for type from section name removing architecture name.
|
||||
* And finally, it will use section name as type.
|
||||
|
||||
### `github` type
|
||||
|
||||
This feature requires Github key creation (see below). Section name must be either `github` (plus optional architecture name, e.g. `github:x86_64`) or random name with `type` set.
|
||||
|
||||
* `type` - type of the upload, string, optional, must be set to `github` if exists.
|
||||
* `owner` - Github repository owner, string, required.
|
||||
* `password` - created Github API key. In order to create it do the following:
|
||||
1. Go to [settings page](https://github.com/settings/profile).
|
||||
2. Switch to [developers settings](https://github.com/settings/apps).
|
||||
3. Switch to [personal access tokens](https://github.com/settings/tokens).
|
||||
4. 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).
|
||||
* `username` - Github authorization user, string, required. Basically the same as `owner`.
|
||||
|
||||
### `rsync` type
|
||||
|
||||
Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`. Section name must be either `rsync` (plus optional architecture name, e.g. `rsync:x86_64`) or random name with `type` set.
|
||||
|
||||
* `type` - type of the upload, string, optional, must be set to `rsync` if exists.
|
||||
* `command` - rsync command to run, space separated list of string, required.
|
||||
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
|
||||
* `remote` - remote server to rsync (e.g. `1.2.3.4:path/to/sync`), string, required.
|
||||
|
||||
### `s3:*` groups
|
||||
### `s3` type
|
||||
|
||||
Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64 architecture.
|
||||
Requires `boto3` library to be installed. Section name must be either `s3` (plus optional architecture name, e.g. `s3:x86_64`) or random name with `type` set.
|
||||
|
||||
* `type` - type of the upload, string, optional, must be set to `github` if exists.
|
||||
* `access_key` - AWS access key ID, string, required.
|
||||
* `bucket` - bucket name (e.g. `bucket`), string, required.
|
||||
* `chunk_size` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
|
||||
@ -122,7 +159,7 @@ Group name must refer to architecture, e.g. it should be `s3:x86_64` for x86_64
|
||||
|
||||
## `web:*` groups
|
||||
|
||||
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name must refer to architecture, e.g. it should be `web:x86_64` for x86_64 architecture.
|
||||
Web server settings. If any of `host`/`port` is not set, web integration will be disabled. Group name can refer to architecture, e.g. `web:x86_64` can be used for x86_64 architecture specific settings. This feature requires `aiohttp` libraries to be installed.
|
||||
|
||||
* `address` - optional address in form `proto://host:port` (`port` can be omitted in case of default `proto` ports), will be used instead of `http://{host}:{port}` in case if set, string, optional. This option is required in case if `OAuth` provider is used.
|
||||
* `debug` - enable debug toolbar, boolean, optional, default `no`.
|
||||
|
479
docs/faq.md
Normal file
479
docs/faq.md
Normal file
@ -0,0 +1,479 @@
|
||||
# FAQ
|
||||
|
||||
## General topics
|
||||
|
||||
### What is the purpose of the project?
|
||||
|
||||
This project has been created in order to maintain self-hosted Arch Linux user repository without manual intervention - checking for updates and building packages.
|
||||
|
||||
### How do I install it?
|
||||
|
||||
TL;DR
|
||||
|
||||
```shell
|
||||
yay -S ahriman
|
||||
sudo -u ahriman ahriman -a x86_64 init
|
||||
sudo ahriman -a x86_64 repo-setup --packager "ahriman bot <ahriman@example.com>" --repository "repository"
|
||||
systemctl enable --now ahriman@x86_64.timer
|
||||
```
|
||||
|
||||
#### Long answer
|
||||
|
||||
The idea is to install the package as usual, create working directory tree, create configuration for `sudo` and `devtools`. Detailed description of the setup instruction can be found [here](setup.md).
|
||||
|
||||
### What does "architecture specific" mean? / How to configure for different architectures?
|
||||
|
||||
Some sections can be configured per architecture. The service will merge architecture specific values into common settings. In order to specify settings for specific architecture you must point it in section name.
|
||||
|
||||
For example, the section
|
||||
|
||||
```ini
|
||||
[build]
|
||||
build_command = extra-x86_64-build
|
||||
```
|
||||
|
||||
states that default build command is `extra-x86_64-build`. But if there is section
|
||||
|
||||
```ini
|
||||
[build:i686]
|
||||
build_command = extra-i686-build
|
||||
```
|
||||
|
||||
the `extra-i686-build` command will be used for `i686` architecture.
|
||||
|
||||
### How to use reporter/upload settings?
|
||||
|
||||
Normally you probably like to generate only one report for the specific type, e.g. only one email report. In order to do it you will need to have the following configuration:
|
||||
|
||||
```ini
|
||||
[report]
|
||||
target = email
|
||||
|
||||
[email]
|
||||
...
|
||||
```
|
||||
|
||||
or in case of multiple architectures and _different_ reporting settings:
|
||||
|
||||
```ini
|
||||
[report]
|
||||
target = email
|
||||
|
||||
[email:i686]
|
||||
...
|
||||
|
||||
[email:x86_64]
|
||||
...
|
||||
```
|
||||
|
||||
But for some cases you would like to have multiple different reports with the same type (e.g. sending different templates to different addresses). For these cases you will need to specify section name in target and type in section, e.g. the following configuration can be used:
|
||||
|
||||
```ini
|
||||
[report]
|
||||
target = email_1 email_2
|
||||
|
||||
[email_1]
|
||||
type = email
|
||||
...
|
||||
|
||||
[email_2]
|
||||
type = email
|
||||
...
|
||||
```
|
||||
|
||||
### Okay, I've installed ahriman, how do I add new package?
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman package-add ahriman --now
|
||||
```
|
||||
|
||||
`--now` flag is totally optional and just run `repo-update` subcommand after the registering the new package, Thus the extended flow is the following:
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman package-add ahriman
|
||||
sudo -u ahriman ahriman repo-update
|
||||
```
|
||||
|
||||
### AUR is fine, but I would like to create package from local PKGBUILD
|
||||
|
||||
TL;DR
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman package-add /path/to/local/directory/with/PKGBUILD --now
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### But I just wanted to change PKGBUILD from AUR a bit!
|
||||
|
||||
Well it is supported also.
|
||||
|
||||
1. Clone sources from AUR.
|
||||
2. Make changes you would like to (e.g. edit `PKGBUILD`, add external patches).
|
||||
3. Run `sudo -u ahriman ahriman patch-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).
|
||||
|
||||
### Package build fails because it cannot validate PGP signature of source files
|
||||
|
||||
TL;DR
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman key-import ...
|
||||
```
|
||||
|
||||
### How do I check if there are new commits for VCS packages?
|
||||
|
||||
Normally the service handles VCS packages correctly, but it requires additional dependencies:
|
||||
|
||||
```shell
|
||||
pacman -S breezy darcs mercurial subversion
|
||||
```
|
||||
|
||||
### I would like to remove package because it is no longer needed/moved to official repositories
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman package-remove ahriman
|
||||
```
|
||||
|
||||
Also, there is command `repo-remove-unknown` which checks packages in AUR and local storage and removes ones which have been removed.
|
||||
|
||||
Remove commands also remove any package files (patches, caches etc).
|
||||
|
||||
### There is new major release of %library-name%, how do I rebuild packages?
|
||||
|
||||
TL;DR
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman repo-rebuild --depends-on python
|
||||
```
|
||||
|
||||
You can even rebuild the whole repository (which is particular useful in case if you would like to change packager) if you do not supply `--depends-on` option.
|
||||
|
||||
However, note that you do not need to rebuild repository in case if you just changed signing option, just use `repo-sign` command instead.
|
||||
|
||||
### Hmm, I have packages built, but how can I use it?
|
||||
|
||||
Add the following lines to your `pacman.conf`:
|
||||
|
||||
```ini
|
||||
[repository]
|
||||
Server = file:///var/lib/ahriman/repository/x86_64
|
||||
```
|
||||
|
||||
(You might need to add `SigLevel` option according to the pacman documentation.)
|
||||
|
||||
|
||||
### I would like to serve the repository
|
||||
|
||||
Easy. For example, nginx configuration (without SSL) will look like:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name repo.example.com;
|
||||
|
||||
location / {
|
||||
autoindex on;
|
||||
root /var/lib/ahriman/repository;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example of the status page configuration is the following (status service is using 8080 port):
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name builds.example.com;
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarder-Proto $scheme;
|
||||
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Remote synchronization
|
||||
|
||||
### Wait I would like to use the repository from another server
|
||||
|
||||
There are several choices:
|
||||
|
||||
1. Easy and cheap, just share your local files through the internet, e.g. for `nginx`:
|
||||
|
||||
```
|
||||
server {
|
||||
location /x86_64 {
|
||||
root /var/lib/ahriman/repository/x86_64;
|
||||
autoindex on;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. You can also upload your packages using `rsync` to any available server. In order to use it you would need to configure ahriman first:
|
||||
|
||||
```ini
|
||||
[upload]
|
||||
target = rsync
|
||||
|
||||
[rsync]
|
||||
remote = 192.168.0.1:/srv/repo
|
||||
```
|
||||
|
||||
After that just add `/srv/repo` to the `pacman.conf` as usual. You can also upload to S3 (e.g. `Server = https://s3.eu-central-1.amazonaws.com/repository/x86_64`) or to Github (e.g. `Server = https://github.com/ahriman/repository/releases/download/x86_64`).
|
||||
|
||||
### How do I configure S3?
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```shell
|
||||
pacman -S python-boto3
|
||||
```
|
||||
|
||||
3. Create a bucket.
|
||||
4. Create user with write access to the bucket:
|
||||
|
||||
```
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "ListObjectsInBucket",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::repository"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sid": "AllObjectActions",
|
||||
"Effect": "Allow",
|
||||
"Action": "s3:*Object",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::repository/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
5. Create an API key for the user and store it.
|
||||
6. Configure the service as following:
|
||||
|
||||
```ini
|
||||
[upload]
|
||||
target = s3
|
||||
|
||||
[s3]
|
||||
access_key = ...
|
||||
bucket = repository
|
||||
region = eu-central-1
|
||||
secret_key = ...
|
||||
```
|
||||
|
||||
### How do I configure Github?
|
||||
|
||||
1. Create a repository.
|
||||
2. [Create API key](https://github.com/settings/tokens) with scope `public_repo`.
|
||||
3. Configure the service as following:
|
||||
|
||||
```ini
|
||||
[upload]
|
||||
target = github
|
||||
|
||||
[github]
|
||||
owner = ahriman
|
||||
password = ...
|
||||
repository = repository
|
||||
username = ahriman
|
||||
```
|
||||
|
||||
## Reporting
|
||||
|
||||
### I would like to get report to email
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```shell
|
||||
yay -S python-jinja
|
||||
```
|
||||
|
||||
2. Configure the service:
|
||||
|
||||
```ini
|
||||
[report]
|
||||
target = email
|
||||
|
||||
[email]
|
||||
host = smtp.example.com
|
||||
link_path = http://example.com/x86_64
|
||||
password = ...
|
||||
port = 465
|
||||
receivers = me@example.com
|
||||
sender = me@example.com
|
||||
user = me@example.com
|
||||
```
|
||||
|
||||
### I'm using synchronization to S3 and would like to generate index page
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```shell
|
||||
yay -S python-jinja
|
||||
```
|
||||
|
||||
2. Configure the service:
|
||||
|
||||
```ini
|
||||
[report]
|
||||
target = html
|
||||
|
||||
[html]
|
||||
path = /var/lib/ahriman/repository/x86_64/index.html
|
||||
link_path = http://example.com/x86_64
|
||||
```
|
||||
|
||||
After these steps `index.html` file will be automatically synced to S3
|
||||
|
||||
## Web service
|
||||
|
||||
### Readme mentions web interface, how do I use it?
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```shell
|
||||
yay -S python-aiohttp python-aiohttp-jinja2
|
||||
```
|
||||
|
||||
2. Configure service:
|
||||
|
||||
```ini
|
||||
[web]
|
||||
port = 8080
|
||||
```
|
||||
|
||||
3. Start the web service `systemctl enable --now ahriman-web@x86_64`.
|
||||
|
||||
### I would like to limit user access to the status page
|
||||
|
||||
1. Install dependencies 😊:
|
||||
|
||||
```shell
|
||||
yay -S python-aiohttp-security python-aiohttp-session python-cryptography
|
||||
```
|
||||
|
||||
2. Configure the service to enable authorization:
|
||||
|
||||
```ini
|
||||
[auth]
|
||||
target = configuration
|
||||
```
|
||||
|
||||
3. Create user for the service:
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman user-add --as-service -r write api
|
||||
```
|
||||
|
||||
This command will ask for the password, just type it in stdin; _do not_ leave the field blank, user will not be able to authorize.
|
||||
|
||||
4. Create end-user `sudo -u ahriman ahriman user-add -r write my-first-user` with password.
|
||||
5. Restart web service `systemctl restart ahriman-web@x86_64`.
|
||||
|
||||
### I would like to use OAuth
|
||||
|
||||
1. Create OAuth web application, download its `client_id` and `client_secret`.
|
||||
2. Guess what? Install dependencies:
|
||||
|
||||
```shell
|
||||
yay -S python-aiohttp-security python-aiohttp-session python-cryptography python-aioauth-client
|
||||
```
|
||||
|
||||
3. Configure the service:
|
||||
|
||||
```ini
|
||||
[auth]
|
||||
target = oauth
|
||||
client_id = ...
|
||||
client_secret = ...
|
||||
|
||||
[web]
|
||||
address = https://example.com
|
||||
```
|
||||
|
||||
Configure `oauth_provider` and `oauth_scopes` in case if you would like to use different from Google provider. Scope must grant access to user email. `web.address` is required to make callback URL available from internet.
|
||||
|
||||
4. Create service user:
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman user-add --as-service -r write api
|
||||
```
|
||||
|
||||
5. Create end-user `sudo -u ahriman ahriman user-add -r write my-first-user`. When it will ask for the password leave it blank.
|
||||
6. Restart web service `systemctl restart ahriman-web@x86_64`.
|
||||
|
||||
## Other topics
|
||||
|
||||
### How does it differ from %another-manager%?
|
||||
|
||||
Short answer - I do not know.
|
||||
|
||||
#### [archrepo2](https://github.com/lilydjwg/archrepo2)
|
||||
|
||||
Don't know, haven't tried it. But it lacks of documentation at least.
|
||||
|
||||
* Web interface.
|
||||
* No synchronization and reporting.
|
||||
* `archrepo2` actively uses direct shell calls and `yaourt` components.
|
||||
* It has constantly running process instead of timer process (it is not pro or con).
|
||||
|
||||
#### [repoctl](https://github.com/cassava/repoctl)
|
||||
|
||||
* Web interface.
|
||||
* No reporting.
|
||||
* Local packages and patches support.
|
||||
* Some actions are not fully automated (e.g. package update still requires manual intervention for the build itself).
|
||||
* `repoctl` has better AUR interaction features. With colors!
|
||||
* `repoctl` has much easier configuration and even completion.
|
||||
* `repoctl` is able to store old packages.
|
||||
* Ability to host repository from same command vs external services (e.g. nginx) in `ahriman`.
|
||||
|
||||
#### [repo-scripts](https://github.com/arcan1s/repo-scripts)
|
||||
|
||||
Though originally I've created ahriman by trying to improve the project, it still lacks a lot of features:
|
||||
|
||||
* Web interface.
|
||||
* Better reporting with template support.
|
||||
* Synchronization features (there was only `rsync` based).
|
||||
* Local packages and patches support.
|
||||
* No dependency management.
|
||||
* And so on.
|
||||
|
||||
`repo-scripts` also have bad architecture and bad quality code and uses out-of-dated `yaourt` and `package-query`.
|
||||
|
||||
### I would like to check service logs
|
||||
|
||||
By default, the service writes logs to `/dev/log` which can be accessed by using `journalctl` command (logs are written to the journal of the user under which command is run).
|
||||
|
||||
You can also edit configuration and forward logs to `stderr`, just change `handlers` value, e.g.:
|
||||
|
||||
```shell
|
||||
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.
|
||||
|
||||
### Html customization
|
||||
|
||||
It is possible to customize html templates. In order to do so, create files somewhere (refer to Jinja2 documentation and the service source code for available parameters) and put `template_path` to configuration pointing to this directory.
|
||||
|
||||
### I did not find my question
|
||||
|
||||
[Create an issue](https://github.com/arcan1s/ahriman/issues) with type **Question**.
|
@ -2,59 +2,66 @@
|
||||
|
||||
1. Install package as usual.
|
||||
2. Change settings if required, see [configuration reference](configuration.md) for more details.
|
||||
3. Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
|
||||
3. TL;DR
|
||||
|
||||
```shell
|
||||
echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
|
||||
```
|
||||
```shell
|
||||
sudo -u ahriman ahriman -a x86_64 repo-init
|
||||
sudo ahriman -a x86_64 repo-setup ...
|
||||
```
|
||||
|
||||
`repo-init` subcommand is required to create the repository tree with correct rights. `repo-setup` literally does the following steps:
|
||||
|
||||
4. Configure build tools (it is required for correct dependency management system):
|
||||
1. Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
|
||||
|
||||
1. Create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`).
|
||||
2. Create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`).
|
||||
3. Change configuration file, add your own repository, add multilib repository etc;
|
||||
4. Set `build_command` option to point to your command.
|
||||
5. Configure `/etc/sudoers.d/ahriman` to allow running command without a password.
|
||||
```shell
|
||||
echo 'PACKAGER="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
|
||||
```
|
||||
|
||||
```shell
|
||||
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build
|
||||
cp /usr/share/devtools/pacman-{extra,ahriman}.conf
|
||||
2. Configure build tools (it is required for correct dependency management system):
|
||||
|
||||
echo '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
1. Create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`).
|
||||
2. Create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`).
|
||||
3. Change configuration file, add your own repository, add multilib repository etc;
|
||||
4. Set `build_command` option to point to your command.
|
||||
5. Configure `/etc/sudoers.d/ahriman` to allow running command without a password.
|
||||
|
||||
echo '[aur-clone]' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'Server = file:///var/lib/ahriman/repository/$arch' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
```shell
|
||||
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build
|
||||
cp /usr/share/devtools/pacman-{extra,ahriman}.conf
|
||||
|
||||
echo '[build]' | tee -a /etc/ahriman.ini.d/build.ini
|
||||
echo 'build_command = ahriman-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini
|
||||
echo '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
|
||||
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
|
||||
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
|
||||
chmod 400 /etc/sudoers.d/ahriman
|
||||
```
|
||||
echo '[aur-clone]' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
echo 'Server = file:///var/lib/ahriman/repository/$arch' | tee -a /usr/share/devtools/pacman-ahriman.conf
|
||||
|
||||
5. Start and enable `ahriman@.timer` via `systemctl`:
|
||||
echo '[build]' | tee -a /etc/ahriman.ini.d/build.ini
|
||||
echo 'build_command = ahriman-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini
|
||||
|
||||
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
|
||||
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
|
||||
chmod 400 /etc/sudoers.d/ahriman
|
||||
```
|
||||
|
||||
4. Start and enable `ahriman@.timer` via `systemctl`:
|
||||
|
||||
```shell
|
||||
systemctl enable --now ahriman@x86_64.timer
|
||||
```
|
||||
|
||||
6. Start and enable status page:
|
||||
5. Start and enable status page:
|
||||
|
||||
```shell
|
||||
systemctl enable --now ahriman-web@x86_64
|
||||
```
|
||||
|
||||
7. Add packages by using `ahriman add {package}` command:
|
||||
6. Add packages by using `ahriman package-add {package}` command:
|
||||
|
||||
```shell
|
||||
sudo -u ahriman ahriman -a x86_64 add yay --now
|
||||
sudo -u ahriman ahriman -a x86_64 package-add ahriman --now
|
||||
```
|
||||
|
||||
Note that initial service configuration can be done by running `ahriman setup` with specific arguments.
|
||||
|
||||
## User creation
|
||||
|
||||
`create-user` subcommand is recommended for new user creation.
|
||||
`user-add` subcommand is recommended for new user creation.
|
@ -1,7 +1,7 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=1.4.0
|
||||
pkgver=1.6.2
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH Linux ReposItory MANager"
|
||||
arch=('any')
|
||||
@ -11,7 +11,6 @@ depends=('devtools' 'git' 'pyalpm' 'python-aur' 'python-passlib' 'python-srcinfo
|
||||
makedepends=('python-pip')
|
||||
optdepends=('breezy: -bzr packages support'
|
||||
'darcs: -darcs packages support'
|
||||
'gnupg: package and repository sign'
|
||||
'mercurial: -hg packages support'
|
||||
'python-aioauth-client: web server with OAuth2 authorization'
|
||||
'python-aiohttp: web server'
|
||||
|
@ -1 +1,2 @@
|
||||
d /var/lib/ahriman 0755 ahriman ahriman
|
||||
d /var/log/ahriman 0755 ahriman ahriman
|
@ -1,5 +1,5 @@
|
||||
[loggers]
|
||||
keys = root,builder,build_details,http
|
||||
keys = root,build_details,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] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[formatter_syslog_format]
|
||||
format = [%(levelname)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||
format = [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
|
||||
datefmt =
|
||||
|
||||
[logger_root]
|
||||
@ -32,12 +32,6 @@ level = DEBUG
|
||||
handlers = syslog_handler
|
||||
qualname = root
|
||||
|
||||
[logger_builder]
|
||||
level = DEBUG
|
||||
handlers = syslog_handler
|
||||
qualname = builder
|
||||
propagate = 0
|
||||
|
||||
[logger_build_details]
|
||||
level = DEBUG
|
||||
handlers = syslog_handler
|
||||
@ -49,3 +43,32 @@ level = DEBUG
|
||||
handlers = syslog_handler
|
||||
qualname = http
|
||||
propagate = 0
|
||||
|
||||
[logger_stderr]
|
||||
level = DEBUG
|
||||
handlers = console_handler
|
||||
qualname = stderr
|
||||
|
||||
[logger_boto3]
|
||||
level = INFO
|
||||
handlers = syslog_handler
|
||||
qualname = boto3
|
||||
propagate = 0
|
||||
|
||||
[logger_botocore]
|
||||
level = INFO
|
||||
handlers = syslog_handler
|
||||
qualname = botocore
|
||||
propagate = 0
|
||||
|
||||
[logger_nose]
|
||||
level = INFO
|
||||
handlers = syslog_handler
|
||||
qualname = nose
|
||||
propagate = 0
|
||||
|
||||
[logger_s3transfer]
|
||||
level = INFO
|
||||
handlers = syslog_handler
|
||||
qualname = s3transfer
|
||||
propagate = 0
|
||||
|
@ -27,13 +27,13 @@
|
||||
<div id="toolbar">
|
||||
{% if not auth.enabled or auth.username is not none %}
|
||||
<button id="add" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addForm">
|
||||
<i class="fa fa-plus"></i> Add
|
||||
<i class="fa fa-plus"></i> add
|
||||
</button>
|
||||
<button id="update" class="btn btn-secondary" onclick="updatePackages()" disabled>
|
||||
<i class="fa fa-play"></i> Update
|
||||
<i class="fa fa-play"></i> update
|
||||
</button>
|
||||
<button id="remove" class="btn btn-danger" onclick="removePackages()" disabled>
|
||||
<i class="fa fa-trash"></i> Remove
|
||||
<i class="fa fa-trash"></i> remove
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -3,25 +3,25 @@
|
||||
<div class="modal-content">
|
||||
<form action="/user-api/v1/login" method="post">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Login</h4>
|
||||
<h4 class="modal-title">login</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="username" class="col-sm-2 col-form-label">Username</label>
|
||||
<label for="username" class="col-sm-2 col-form-label">username</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="username" type="text" class="form-control" placeholder="enter username" name="username" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="password" class="col-sm-2 col-form-label">Password</label>
|
||||
<label for="password" class="col-sm-2 col-form-label">password</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary">Login</button>
|
||||
<button class="btn btn-primary">login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -2,12 +2,12 @@
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add new packages</h4>
|
||||
<h4 class="modal-title">add new packages</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label for="package" class="col-sm-2 col-form-label">Package</label>
|
||||
<label for="package" class="col-sm-2 col-form-label">package</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="package" type="text" list="knownPackages" class="form-control" placeholder="AUR package" name="package" required>
|
||||
<datalist id="knownPackages"></datalist>
|
||||
@ -15,9 +15,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" data-bs-dismiss="modal" onclick="requestPackages()">Request</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPackages()">Add</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">close</button>
|
||||
<button type="button" class="btn btn-success" data-bs-dismiss="modal" onclick="requestPackages()">request</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPackages()">add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -27,7 +27,7 @@
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger">
|
||||
<h4 class="modal-title">Failed</h4>
|
||||
<h4 class="modal-title">failed</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@ -35,7 +35,7 @@
|
||||
<p id="errorDetails"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -45,7 +45,7 @@
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-success">
|
||||
<h4 class="modal-title">Success</h4>
|
||||
<h4 class="modal-title">success</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@ -53,7 +53,7 @@
|
||||
<ul id="successDetails"></ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -82,7 +82,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
{% if homepage is not none %}
|
||||
<li><a class="nav-link" href="{{ homepage }}" title="homepage">Homepage</a></li>
|
||||
<li><a class="nav-link" href="{{ homepage }}" title="homepage">homepage</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</footer>
|
||||
|
@ -10,3 +10,14 @@
|
||||
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.18.3/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script>
|
||||
$("#packages").bootstrapTable({
|
||||
formatClearSearch: function () {
|
||||
return "Clear search";
|
||||
},
|
||||
formatSearch: function () {
|
||||
return "search";
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
2
setup.py
2
setup.py
@ -17,7 +17,7 @@ setup(
|
||||
|
||||
description="ArcH Linux ReposItory MANager",
|
||||
|
||||
author="arcanis",
|
||||
author="ahriman team",
|
||||
author_email="",
|
||||
url="https://github.com/arcan1s/ahriman",
|
||||
|
||||
|
@ -25,6 +25,7 @@ from pathlib import Path
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.application import handlers
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.sign_settings import SignSettings
|
||||
@ -35,125 +36,81 @@ from ahriman.models.user_access import UserAccess
|
||||
SubParserAction = argparse._SubParsersAction # pylint: disable=protected-access
|
||||
|
||||
|
||||
def _formatter(prog: str) -> argparse.HelpFormatter:
|
||||
"""
|
||||
formatter for the help message
|
||||
:param prog: application name
|
||||
:return: formatter used by default
|
||||
"""
|
||||
return argparse.ArgumentDefaultsHelpFormatter(prog, width=120)
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
"""
|
||||
command line parser generator
|
||||
:return: command line parser for the application
|
||||
"""
|
||||
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH Linux ReposItory MANager",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
epilog="Argument list can also be read from file by using @ prefix.",
|
||||
fromfile_prefix_chars="@", formatter_class=_formatter)
|
||||
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
|
||||
action="append")
|
||||
parser.add_argument("-c", "--configuration", help="configuration path", type=Path, default=Path("/etc/ahriman.ini"))
|
||||
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--lock",
|
||||
help="lock file",
|
||||
type=Path,
|
||||
default=Path(tempfile.gettempdir()) / "ahriman.lock")
|
||||
parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
|
||||
parser.add_argument("-l", "--lock", help="lock file", type=Path,
|
||||
default=Path(tempfile.gettempdir()) / "ahriman.lock")
|
||||
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
|
||||
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
|
||||
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
|
||||
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
|
||||
action="store_true")
|
||||
parser.add_argument("-v", "--version", action="version", version=version.__version__)
|
||||
|
||||
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
|
||||
|
||||
_set_add_parser(subparsers)
|
||||
_set_check_parser(subparsers)
|
||||
_set_clean_parser(subparsers)
|
||||
_set_config_parser(subparsers)
|
||||
_set_init_parser(subparsers)
|
||||
_set_aur_search_parser(subparsers)
|
||||
_set_key_import_parser(subparsers)
|
||||
_set_rebuild_parser(subparsers)
|
||||
_set_remove_parser(subparsers)
|
||||
_set_remove_unknown_parser(subparsers)
|
||||
_set_report_parser(subparsers)
|
||||
_set_search_parser(subparsers)
|
||||
_set_setup_parser(subparsers)
|
||||
_set_sign_parser(subparsers)
|
||||
_set_status_parser(subparsers)
|
||||
_set_status_update_parser(subparsers)
|
||||
_set_sync_parser(subparsers)
|
||||
_set_update_parser(subparsers)
|
||||
_set_user_parser(subparsers)
|
||||
_set_package_add_parser(subparsers)
|
||||
_set_package_remove_parser(subparsers)
|
||||
_set_package_status_parser(subparsers)
|
||||
_set_package_status_remove_parser(subparsers)
|
||||
_set_package_status_update_parser(subparsers)
|
||||
_set_patch_add_parser(subparsers)
|
||||
_set_patch_list_parser(subparsers)
|
||||
_set_patch_remove_parser(subparsers)
|
||||
_set_repo_check_parser(subparsers)
|
||||
_set_repo_clean_parser(subparsers)
|
||||
_set_repo_config_parser(subparsers)
|
||||
_set_repo_init_parser(subparsers)
|
||||
_set_repo_rebuild_parser(subparsers)
|
||||
_set_repo_remove_unknown_parser(subparsers)
|
||||
_set_repo_report_parser(subparsers)
|
||||
_set_repo_setup_parser(subparsers)
|
||||
_set_repo_sign_parser(subparsers)
|
||||
_set_repo_status_update_parser(subparsers)
|
||||
_set_repo_sync_parser(subparsers)
|
||||
_set_repo_update_parser(subparsers)
|
||||
_set_user_add_parser(subparsers)
|
||||
_set_user_remove_parser(subparsers)
|
||||
_set_web_parser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for add subcommand
|
||||
add parser for AUR search subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("add", help="add package", description="add package",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("package", help="package base/name or archive path", nargs="+")
|
||||
parser.add_argument("--now", help="run update function after", action="store_true")
|
||||
parser.add_argument("--source", help="package source", choices=PackageSource, type=PackageSource,
|
||||
default=PackageSource.Auto)
|
||||
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Add, architecture=[])
|
||||
return parser
|
||||
|
||||
|
||||
def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for check subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("check", help="check for updates",
|
||||
description="check for updates. Same as update --dry-run --no-manual",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Update, architecture=[], no_aur=False, no_manual=True, dry_run=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for clean subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("clean", help="clean local caches", description="clear local caches",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true")
|
||||
parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true")
|
||||
parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
|
||||
parser.add_argument("--no-manual", help="do not clear directory with manually added packages", action="store_true")
|
||||
parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Clean, architecture=[], no_log=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for config subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("config", help="dump configuration",
|
||||
description="dump configuration for specified architecture",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.set_defaults(handler=handlers.Dump, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for init subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("init", help="create repository tree",
|
||||
description="create empty repository tree. Optional command for auto architecture support",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.set_defaults(handler=handlers.Init, no_report=True)
|
||||
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
|
||||
description="search for package in AUR using API", formatter_class=_formatter)
|
||||
parser.add_argument("search", help="search terms, can be specified multiple times, result will match all terms",
|
||||
nargs="+")
|
||||
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
|
||||
parser.add_argument("--sort-by", help="sort field by this field. In case if two packages have the same value of "
|
||||
"the specified field, they will be always sorted by name",
|
||||
default="name", choices=sorted(handlers.Search.SORT_FIELDS))
|
||||
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, no_report=True, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -164,88 +121,275 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("key-import", help="import PGP key",
|
||||
description="import PGP key from public sources to repository user",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
description="import PGP key from public sources to the repository user",
|
||||
epilog="By default ahriman runs build process with package sources validation "
|
||||
"(in case if signature and keys are available in PKGBUILD). This process will "
|
||||
"fail in case if key is not known for build user. This subcommand can be used "
|
||||
"in order to import the PGP key to user keychain.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("--key-server", help="key server for key import", default="pgp.mit.edu")
|
||||
parser.add_argument("key", help="PGP key to import from public server")
|
||||
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for rebuild subcommand
|
||||
add parser for package addition subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("rebuild", help="rebuild repository", description="rebuild whole repository",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
|
||||
parser.set_defaults(handler=handlers.Rebuild, architecture=[])
|
||||
parser = root.add_parser("package-add", aliases=["add", "package-update"], help="add package",
|
||||
description="add existing or new package to the build queue",
|
||||
epilog="This subcommand should be used for new package addition. It also supports flag "
|
||||
"--now in case if you would like to build the package immediately. "
|
||||
"You can add new package from one of supported sources: "
|
||||
"1) if it is already built package you can specify the path to the archive; "
|
||||
"2) you can also add built packages from the directory (e.g. during the migration "
|
||||
"from another repository source); "
|
||||
"3) it is also possible to add package from local PKGBUILD, but in this case it "
|
||||
"will be ignored during the next automatic updates; "
|
||||
"4) ahriman supports downloading archives from remote (e.g. HTTP) sources; "
|
||||
"5) and finally you can add package from AUR.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="package source (base name, path to local files, remote URL)", nargs="+")
|
||||
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
||||
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
||||
type=PackageSource, choices=PackageSource, default=PackageSource.Auto)
|
||||
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Add)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for remove subcommand
|
||||
add parser for package removal subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("remove", help="remove package", description="remove package",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("package-remove", aliases=["remove"], help="remove package",
|
||||
description="remove package from the repository", formatter_class=_formatter)
|
||||
parser.add_argument("package", help="package name or base", nargs="+")
|
||||
parser.set_defaults(handler=handlers.Remove, architecture=[])
|
||||
parser.set_defaults(handler=handlers.Remove)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package status subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-status", aliases=["status"], help="get package status",
|
||||
description="request status of the package",
|
||||
epilog="This feature requests package status from the web interface if it is available.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="filter status by package base", nargs="*")
|
||||
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
|
||||
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
|
||||
parser.add_argument("-s", "--status", help="filter packages by status",
|
||||
type=BuildStatusEnum, choices=BuildStatusEnum)
|
||||
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_package_status_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package status remove subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-status-remove", help="remove package status",
|
||||
description="remove the package from the status page",
|
||||
epilog="Please note that this subcommand does not remove the package itself, it just "
|
||||
"clears the status page.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="remove specified packages", nargs="+")
|
||||
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Remove, lock=None, no_report=True, quiet=True,
|
||||
unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_package_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package status update subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-status-update", aliases=["status-update"], help="update package status",
|
||||
description="update package status on the status page", formatter_class=_formatter)
|
||||
parser.add_argument("package", help="set status for specified packages. "
|
||||
"If no packages supplied, service status will be updated",
|
||||
nargs="*")
|
||||
parser.add_argument("-s", "--status", help="new status",
|
||||
type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
|
||||
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, quiet=True,
|
||||
unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for new patch subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: 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",
|
||||
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.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for list patches subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("patch-list", help="list patch sets",
|
||||
description="list available patches for the package", formatter_class=_formatter)
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for remove patches subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
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.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for repository check subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("repo-check", aliases=["check"], help="check for updates",
|
||||
description="check for packages updates. Same as update --dry-run --no-manual",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Update, dry_run=True, no_aur=False, no_manual=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for repository clean subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("repo-clean", aliases=["clean"], help="clean local caches",
|
||||
description="remove local caches",
|
||||
epilog="The subcommand clears every temporary directories (builds, caches etc). Normally "
|
||||
"you should not run this command manually. Also in case if you are going to clear "
|
||||
"the chroot directories you will need root privileges.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("--build", help="clear directory with package sources", action="store_true")
|
||||
parser.add_argument("--cache", help="clear directory with package caches", action="store_true")
|
||||
parser.add_argument("--chroot", help="clear build chroot", action="store_true")
|
||||
parser.add_argument("--manual", help="clear directory with manually added packages", action="store_true")
|
||||
parser.add_argument("--packages", help="clear directory with built packages", action="store_true")
|
||||
parser.add_argument("--patches", help="clear directory with patches", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Clean, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for config subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("repo-config", aliases=["config"], help="dump configuration",
|
||||
description="dump configuration for the specified architecture",
|
||||
formatter_class=_formatter)
|
||||
parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_init_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for repository init subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("repo-init", aliases=["init"], help="create repository tree",
|
||||
description="create empty repository tree. Optional command for auto architecture support",
|
||||
formatter_class=_formatter)
|
||||
parser.set_defaults(handler=handlers.Init, no_report=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for repository rebuild subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository",
|
||||
description="force rebuild whole repository", formatter_class=_formatter)
|
||||
parser.add_argument("--depends-on", help="only rebuild packages that depend on specified package", action="append")
|
||||
parser.set_defaults(handler=handlers.Rebuild)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for remove unknown packages subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("remove-unknown", help="remove unknown packages",
|
||||
description="remove packages which are missing in AUR",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("repo-remove-unknown", aliases=["remove-unknown"], help="remove unknown packages",
|
||||
description="remove packages which are missing in AUR and do not have local PKGBUILDs",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
|
||||
parser.set_defaults(handler=handlers.RemoveUnknown, architecture=[])
|
||||
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
|
||||
parser.set_defaults(handler=handlers.RemoveUnknown)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for report subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("report", help="generate report", description="generate report",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("repo-report", aliases=["report"], help="generate report",
|
||||
description="generate repository report according to current settings",
|
||||
epilog="Create and/or update repository report as configured.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("target", help="target to generate report", nargs="*")
|
||||
parser.set_defaults(handler=handlers.Report, architecture=[])
|
||||
parser.set_defaults(handler=handlers.Report)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for search subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("search", help="search for package", description="search for package in AUR using API")
|
||||
parser.add_argument("search", help="search terms, can be specified multiple times", nargs="+")
|
||||
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for setup subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("setup", help="initial service configuration",
|
||||
parser = root.add_parser("repo-setup", aliases=["setup"], help="initial service configuration",
|
||||
description="create initial service configuration, requires root",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
epilog="Create _minimal_ configuration for the service according to provided options.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("--build-command", help="build command prefix", default="ahriman")
|
||||
parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
|
||||
type=Path, default=Path("/usr/share/devtools/pacman-extra.conf"))
|
||||
@ -253,115 +397,113 @@ def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("--packager", help="packager name and email", required=True)
|
||||
parser.add_argument("--repository", help="repository name", required=True)
|
||||
parser.add_argument("--sign-key", help="sign key id")
|
||||
parser.add_argument("--sign-target", help="sign options", type=SignSettings.from_option,
|
||||
choices=SignSettings, action="append")
|
||||
parser.add_argument("--sign-target", help="sign options", action="append",
|
||||
type=SignSettings.from_option, choices=SignSettings)
|
||||
parser.add_argument("--web-port", help="port of the web service", type=int)
|
||||
parser.set_defaults(handler=handlers.Setup, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for sign subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("sign", help="sign packages", description="(re-)sign packages and repository database",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("repo-sign", aliases=["sign"], help="sign packages",
|
||||
description="(re-)sign packages and repository database according to current settings",
|
||||
epilog="Sign repository and/or packages as configured.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="sign only specified packages", nargs="*")
|
||||
parser.set_defaults(handler=handlers.Sign, architecture=[])
|
||||
parser.set_defaults(handler=handlers.Sign)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for status subcommand
|
||||
add parser for repository status update subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("status", help="get package status", description="request status of the package",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
|
||||
parser.add_argument("--status", help="filter packages by status", choices=BuildStatusEnum, type=BuildStatusEnum)
|
||||
parser.add_argument("package", help="filter status by package base", nargs="*")
|
||||
parser.set_defaults(handler=handlers.Status, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
parser = root.add_parser("repo-status-update", help="update repository status",
|
||||
description="update repository status on the status page", formatter_class=_formatter)
|
||||
parser.add_argument("-s", "--status", help="new status",
|
||||
type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
|
||||
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, package=[],
|
||||
quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for status update subcommand
|
||||
add parser for repository sync subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("status-update", help="update package status", description="request status of the package",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument(
|
||||
"package",
|
||||
help="set status for specified packages. If no packages supplied, service status will be updated",
|
||||
nargs="*")
|
||||
parser.add_argument("--status", help="new status", choices=BuildStatusEnum,
|
||||
type=BuildStatusEnum, default=BuildStatusEnum.Success)
|
||||
parser.add_argument("--remove", help="remove package status page", action="store_true")
|
||||
parser.set_defaults(handler=handlers.StatusUpdate, lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for sync subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("sync", help="sync repository", description="sync packages to remote server",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("repo-sync", aliases=["sync"], help="sync repository",
|
||||
description="sync repository files to remote server according to current settings",
|
||||
epilog="Synchronize the repository to remote services as configured.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("target", help="target to sync", nargs="*")
|
||||
parser.set_defaults(handler=handlers.Sync, architecture=[])
|
||||
parser.set_defaults(handler=handlers.Sync)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for update subcommand
|
||||
add parser for repository update subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("update", help="update packages", description="run updates",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("repo-update", aliases=["update"], help="update packages",
|
||||
description="check for packages updates and run build process if requested",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
|
||||
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", 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.Update, architecture=[])
|
||||
parser.set_defaults(handler=handlers.Update)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_user_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for create user subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser(
|
||||
"user",
|
||||
help="manage users for web services",
|
||||
description="manage users for web services with password and role. In case if password was not entered it will be asked interactively",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("user-add", help="create or update user",
|
||||
description="update user for web services with the given password and role. "
|
||||
"In case if password was not entered it will be asked interactively",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("username", help="username for web service")
|
||||
parser.add_argument("--as-service", help="add user as service user", action="store_true")
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--access",
|
||||
help="user access level",
|
||||
type=UserAccess,
|
||||
choices=UserAccess,
|
||||
default=UserAccess.Read)
|
||||
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
|
||||
parser.add_argument("-p", "--password", help="user password")
|
||||
parser.add_argument("-r", "--remove", help="remove user from configuration", action="store_true")
|
||||
parser.add_argument("--secure", help="set file permissions to user-only", action="store_true")
|
||||
parser.set_defaults(handler=handlers.User, architecture=[""], lock=None, no_log=True, no_report=True, unsafe=True)
|
||||
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
|
||||
"which is in particular must be used for OAuth2 authorization type.")
|
||||
parser.add_argument("-r", "--role", help="user access level",
|
||||
type=UserAccess, choices=UserAccess, default=UserAccess.Read)
|
||||
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
|
||||
parser.set_defaults(handler=handlers.User, action=Action.Update, architecture=[""], lock=None, no_report=True,
|
||||
quiet=True, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for user removal subcommand
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("user-remove", help="remove user",
|
||||
description="remove user from the user mapping and update the configuration",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("username", help="username for web service")
|
||||
parser.add_argument("--no-reload", help="do not reload authentication module", action="store_true")
|
||||
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
|
||||
parser.set_defaults(handler=handlers.User, action=Action.Remove, architecture=[""], lock=None, no_report=True, # nosec
|
||||
password="", quiet=True, role=UserAccess.Read, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
@ -371,8 +513,7 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
:param root: subparsers for the commands
|
||||
:return: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("web", help="start web server", description="start web server",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = root.add_parser("web", help="web server", description="start web server", formatter_class=_formatter)
|
||||
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True, parser=_parser)
|
||||
return parser
|
||||
|
||||
|
20
src/ahriman/application/application/__init__.py
Normal file
20
src/ahriman/application/application/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright (c) 2021 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.application.application.application import Application
|
51
src/ahriman/application/application/application.py
Normal file
51
src/ahriman/application/application/application.py
Normal file
@ -0,0 +1,51 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 Iterable, Set
|
||||
|
||||
from ahriman.application.application.packages import Packages
|
||||
from ahriman.application.application.repository import Repository
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Application(Packages, Repository):
|
||||
"""
|
||||
base application class
|
||||
"""
|
||||
|
||||
def _finalize(self, built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
generate report and sync to remote server
|
||||
"""
|
||||
self.report([], built_packages)
|
||||
self.sync([], built_packages)
|
||||
|
||||
def _known_packages(self) -> Set[str]:
|
||||
"""
|
||||
load packages from repository and pacman repositories
|
||||
:return: list of known packages
|
||||
"""
|
||||
known_packages: Set[str] = set()
|
||||
# local set
|
||||
for base in self.repository.packages():
|
||||
for package, properties in base.packages.items():
|
||||
known_packages.add(package)
|
||||
known_packages.update(properties.provides)
|
||||
known_packages.update(self.repository.pacman.all_packages())
|
||||
return known_packages
|
146
src/ahriman/application/application/packages.py
Normal file
146
src/ahriman/application/application/packages.py
Normal file
@ -0,0 +1,146 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 requests
|
||||
import shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Set
|
||||
|
||||
from ahriman.application.application.properties import Properties
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
|
||||
|
||||
class Packages(Properties):
|
||||
"""
|
||||
package control class
|
||||
"""
|
||||
|
||||
def _finalize(self, built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
generate report and sync to remote server
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _known_packages(self) -> Set[str]:
|
||||
"""
|
||||
load packages from repository and pacman repositories
|
||||
:return: list of known packages
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _add_archive(self, source: str, *_: Any) -> None:
|
||||
"""
|
||||
add package from archive
|
||||
:param source: path to package archive
|
||||
"""
|
||||
local_path = Path(source)
|
||||
dst = self.repository.paths.packages / local_path.name
|
||||
shutil.copy(local_path, dst)
|
||||
|
||||
def _add_aur(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
|
||||
"""
|
||||
add package from AUR
|
||||
:param source: package base name
|
||||
:param known_packages: list of packages which are known by the service
|
||||
:param without_dependencies: if set, dependency check will be disabled
|
||||
"""
|
||||
aur_url = self.configuration.get("alpm", "aur_url")
|
||||
package = Package.load(source, PackageSource.AUR, self.repository.pacman, aur_url)
|
||||
local_path = self.repository.paths.manual_for(package.base)
|
||||
|
||||
Sources.load(local_path, package.git_url, self.repository.paths.patches_for(package.base))
|
||||
self._process_dependencies(local_path, known_packages, without_dependencies)
|
||||
|
||||
def _add_directory(self, source: str, *_: Any) -> None:
|
||||
"""
|
||||
add packages from directory
|
||||
:param source: path to local directory
|
||||
"""
|
||||
local_path = Path(source)
|
||||
for full_path in filter(package_like, local_path.iterdir()):
|
||||
self._add_archive(str(full_path))
|
||||
|
||||
def _add_local(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
|
||||
"""
|
||||
add package from local PKGBUILDs
|
||||
:param source: path to directory with local source files
|
||||
:param known_packages: list of packages which are known by the service
|
||||
:param without_dependencies: if set, dependency check will be disabled
|
||||
"""
|
||||
aur_url = self.configuration.get("alpm", "aur_url")
|
||||
package = Package.load(source, PackageSource.Local, self.repository.pacman, aur_url)
|
||||
cache_dir = self.repository.paths.cache_for(package.base)
|
||||
shutil.copytree(Path(source), cache_dir) # copy package to store in caches
|
||||
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
|
||||
|
||||
dst = self.repository.paths.manual_for(package.base)
|
||||
shutil.copytree(cache_dir, dst) # copy package for the build
|
||||
self._process_dependencies(dst, known_packages, without_dependencies)
|
||||
|
||||
def _add_remote(self, source: str, *_: Any) -> None:
|
||||
"""
|
||||
add package from remote sources (e.g. HTTP)
|
||||
:param remote_url: remote URL to the package archive
|
||||
"""
|
||||
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
|
||||
response = requests.get(source, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
with dst.open("wb") as local_file:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
local_file.write(chunk)
|
||||
|
||||
def _process_dependencies(self, local_path: Path, known_packages: Set[str], without_dependencies: bool) -> None:
|
||||
"""
|
||||
process package dependencies
|
||||
:param local_path: path to local package sources (i.e. cloned AUR repository)
|
||||
:param known_packages: list of packages which are known by the service
|
||||
:param without_dependencies: if set, dependency check will be disabled
|
||||
"""
|
||||
if without_dependencies:
|
||||
return
|
||||
|
||||
dependencies = Package.dependencies(local_path)
|
||||
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
|
||||
|
||||
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:
|
||||
"""
|
||||
add packages for the next build
|
||||
:param names: list of package bases to add
|
||||
:param source: package source to add
|
||||
:param without_dependencies: if set, dependency check will be disabled
|
||||
"""
|
||||
known_packages = self._known_packages() # speedup dependencies processing
|
||||
|
||||
for name in names:
|
||||
resolved_source = source.resolve(name)
|
||||
fn = getattr(self, f"_add_{resolved_source.value}")
|
||||
fn(name, known_packages, without_dependencies)
|
||||
|
||||
def remove(self, names: Iterable[str]) -> None:
|
||||
"""
|
||||
remove packages from repository
|
||||
:param names: list of packages (either base or name) to remove
|
||||
"""
|
||||
self.repository.process_remove(names)
|
||||
self._finalize([])
|
45
src/ahriman/application/application/properties.py
Normal file
45
src/ahriman/application/application/properties.py
Normal file
@ -0,0 +1,45 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
|
||||
|
||||
class Properties:
|
||||
"""
|
||||
application base properties class
|
||||
:ivar architecture: repository architecture
|
||||
:ivar configuration: configuration instance
|
||||
:ivar logger: application logger
|
||||
:ivar repository: repository instance
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.configuration = configuration
|
||||
self.architecture = architecture
|
||||
self.repository = Repository(architecture, configuration, no_report)
|
@ -17,155 +17,52 @@
|
||||
# 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 shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable, List, Set
|
||||
from typing import Callable, Iterable, List
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository.repository import Repository
|
||||
from ahriman.application.application.properties import Properties
|
||||
from ahriman.application.formatters.update_printer import UpdatePrinter
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.tree import Tree
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
|
||||
|
||||
class Application:
|
||||
class Repository(Properties):
|
||||
"""
|
||||
base application class
|
||||
:ivar architecture: repository architecture
|
||||
:ivar configuration: configuration instance
|
||||
:ivar logger: application logger
|
||||
:ivar repository: repository instance
|
||||
repository control class
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration, no_report: bool) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
self.logger = logging.getLogger("root")
|
||||
self.configuration = configuration
|
||||
self.architecture = architecture
|
||||
self.repository = Repository(architecture, configuration, no_report)
|
||||
|
||||
def _finalize(self, built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
generate report and sync to remote server
|
||||
"""
|
||||
self.report([], built_packages)
|
||||
self.sync([], built_packages)
|
||||
raise NotImplementedError
|
||||
|
||||
def _known_packages(self) -> Set[str]:
|
||||
"""
|
||||
load packages from repository and pacman repositories
|
||||
:return: list of known packages
|
||||
"""
|
||||
known_packages: Set[str] = set()
|
||||
# local set
|
||||
for base in self.repository.packages():
|
||||
for package, properties in base.packages.items():
|
||||
known_packages.add(package)
|
||||
known_packages.update(properties.provides)
|
||||
known_packages.update(self.repository.pacman.all_packages())
|
||||
return known_packages
|
||||
|
||||
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
|
||||
log_fn: Callable[[str], None]) -> List[Package]:
|
||||
"""
|
||||
get list of packages to run update process
|
||||
:param filter_packages: do not check every package just specified in the list
|
||||
:param no_aur: do not check for aur updates
|
||||
:param no_manual: do not check for manual updates
|
||||
:param no_vcs: do not check VCS packages
|
||||
:param log_fn: logger function to log updates
|
||||
:return: list of out-of-dated packages
|
||||
"""
|
||||
updates = []
|
||||
|
||||
if not no_aur:
|
||||
updates.extend(self.repository.updates_aur(filter_packages, no_vcs))
|
||||
if not no_manual:
|
||||
updates.extend(self.repository.updates_manual())
|
||||
|
||||
for package in updates:
|
||||
log_fn(f"{package.base} = {package.version}")
|
||||
|
||||
return updates
|
||||
|
||||
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:
|
||||
"""
|
||||
add packages for the next build
|
||||
:param names: list of package bases to add
|
||||
:param source: package source to add
|
||||
:param without_dependencies: if set, dependency check will be disabled
|
||||
"""
|
||||
known_packages = self._known_packages()
|
||||
|
||||
def add_directory(path: Path) -> None:
|
||||
for full_path in filter(package_like, path.iterdir()):
|
||||
add_archive(full_path)
|
||||
|
||||
def add_manual(src: str) -> Path:
|
||||
package = Package.load(src, self.repository.pacman, self.configuration.get("alpm", "aur_url"))
|
||||
path = self.repository.paths.manual / package.base
|
||||
Task.fetch(path, package.git_url)
|
||||
return path
|
||||
|
||||
def add_archive(src: Path) -> None:
|
||||
dst = self.repository.paths.packages / src.name
|
||||
shutil.move(src, dst)
|
||||
|
||||
def process_dependencies(path: Path) -> None:
|
||||
if without_dependencies:
|
||||
return
|
||||
dependencies = Package.dependencies(path)
|
||||
self.add(dependencies.difference(known_packages), PackageSource.AUR, without_dependencies)
|
||||
|
||||
def process_single(src: str) -> None:
|
||||
resolved_source = source.resolve(src)
|
||||
if resolved_source == PackageSource.Directory:
|
||||
add_directory(Path(src))
|
||||
elif resolved_source == PackageSource.Archive:
|
||||
add_archive(Path(src))
|
||||
else:
|
||||
path = add_manual(src)
|
||||
process_dependencies(path)
|
||||
|
||||
for name in names:
|
||||
process_single(name)
|
||||
|
||||
def clean(self, no_build: bool, no_cache: bool, no_chroot: bool, no_manual: bool, no_packages: bool) -> None:
|
||||
def clean(self, build: bool, cache: bool, chroot: bool, manual: bool, packages: bool, patches: bool) -> None:
|
||||
"""
|
||||
run all clean methods. Warning: some functions might not be available under non-root
|
||||
:param no_build: do not clear directory with package sources
|
||||
:param no_cache: do not clear directory with package caches
|
||||
:param no_chroot: do not clear build chroot
|
||||
:param no_manual: do not clear directory with manually added packages
|
||||
:param no_packages: do not clear directory with built packages
|
||||
:param build: clear directory with package sources
|
||||
:param cache: clear directory with package caches
|
||||
:param chroot: clear build chroot
|
||||
:param manual: clear directory with manually added packages
|
||||
:param packages: clear directory with built packages
|
||||
:param patches: clear directory with patches
|
||||
"""
|
||||
if not no_build:
|
||||
if build:
|
||||
self.repository.clear_build()
|
||||
if not no_cache:
|
||||
if cache:
|
||||
self.repository.clear_cache()
|
||||
if not no_chroot:
|
||||
if chroot:
|
||||
self.repository.clear_chroot()
|
||||
if not no_manual:
|
||||
if manual:
|
||||
self.repository.clear_manual()
|
||||
if not no_packages:
|
||||
if packages:
|
||||
self.repository.clear_packages()
|
||||
|
||||
def remove(self, names: Iterable[str]) -> None:
|
||||
"""
|
||||
remove packages from repository
|
||||
:param names: list of packages (either base or name) to remove
|
||||
"""
|
||||
self.repository.process_remove(names)
|
||||
self._finalize([])
|
||||
if patches:
|
||||
self.repository.clear_patches()
|
||||
|
||||
def report(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
@ -196,7 +93,7 @@ class Application:
|
||||
# run generic update function
|
||||
self.update([])
|
||||
# sign repository database if set
|
||||
self.repository.sign.sign_repository(self.repository.repo.repo_path)
|
||||
self.repository.sign.process_sign_repository(self.repository.repo.repo_path)
|
||||
self._finalize([])
|
||||
|
||||
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
|
||||
@ -213,13 +110,22 @@ class Application:
|
||||
get packages which were not found in AUR
|
||||
:return: unknown package list
|
||||
"""
|
||||
packages = []
|
||||
for base in self.repository.packages():
|
||||
def has_aur(package_base: str, aur_url: str) -> bool:
|
||||
try:
|
||||
_ = Package.from_aur(base.base, base.aur_url)
|
||||
_ = Package.from_aur(package_base, aur_url)
|
||||
except Exception:
|
||||
packages.append(base)
|
||||
return packages
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_local(package_base: str) -> bool:
|
||||
cache_dir = self.repository.paths.cache_for(package_base)
|
||||
return cache_dir.is_dir() and not Sources.has_remotes(cache_dir)
|
||||
|
||||
return [
|
||||
package
|
||||
for package in self.repository.packages()
|
||||
if not has_aur(package.base, package.aur_url) and not has_local(package.base)
|
||||
]
|
||||
|
||||
def update(self, updates: Iterable[Package]) -> None:
|
||||
"""
|
||||
@ -229,7 +135,10 @@ class Application:
|
||||
def process_update(paths: Iterable[Path]) -> None:
|
||||
if not paths:
|
||||
return # don't need to process if no update supplied
|
||||
updated = [Package.load(path, self.repository.pacman, self.repository.aur_url) for path in paths]
|
||||
updated = [
|
||||
Package.load(str(path), PackageSource.Archive, self.repository.pacman, self.repository.aur_url)
|
||||
for path in paths
|
||||
]
|
||||
self.repository.process_update(paths)
|
||||
self._finalize(updated)
|
||||
|
||||
@ -238,8 +147,33 @@ class Application:
|
||||
process_update(packages)
|
||||
|
||||
# process manual packages
|
||||
tree = Tree.load(updates)
|
||||
tree = Tree.load(updates, self.repository.paths)
|
||||
for num, level in enumerate(tree.levels()):
|
||||
self.logger.info("processing level #%i %s", num, [package.base for package in level])
|
||||
packages = self.repository.process_build(level)
|
||||
process_update(packages)
|
||||
|
||||
def updates(self, filter_packages: Iterable[str], no_aur: bool, no_manual: bool, no_vcs: bool,
|
||||
log_fn: Callable[[str], None]) -> List[Package]:
|
||||
"""
|
||||
get list of packages to run update process
|
||||
:param filter_packages: do not check every package just specified in the list
|
||||
:param no_aur: do not check for aur updates
|
||||
:param no_manual: do not check for manual updates
|
||||
:param no_vcs: do not check VCS packages
|
||||
:param log_fn: logger function to log updates
|
||||
:return: list of out-of-dated packages
|
||||
"""
|
||||
updates = []
|
||||
|
||||
if not no_aur:
|
||||
updates.extend(self.repository.updates_aur(filter_packages, no_vcs))
|
||||
if not no_manual:
|
||||
updates.extend(self.repository.updates_manual())
|
||||
|
||||
local_versions = {package.base: package.version for package in self.repository.packages()}
|
||||
for package in updates:
|
||||
UpdatePrinter(package, local_versions.get(package.base)).print(
|
||||
verbose=True, log_fn=log_fn, separator=" -> ")
|
||||
|
||||
return updates
|
19
src/ahriman/application/formatters/__init__.py
Normal file
19
src/ahriman/application/formatters/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
#
|
||||
# Copyright (c) 2021 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/>.
|
||||
#
|
62
src/ahriman/application/formatters/aur_printer.py
Normal file
62
src/ahriman/application/formatters/aur_printer.py
Normal file
@ -0,0 +1,62 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 aur # type: ignore
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from ahriman.application.formatters.printer import Printer
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class AurPrinter(Printer):
|
||||
"""
|
||||
print content of the AUR package
|
||||
"""
|
||||
|
||||
def __init__(self, package: aur.Package) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param package: AUR package description
|
||||
"""
|
||||
self.content = package
|
||||
|
||||
def properties(self) -> List[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
:return: list of content properties
|
||||
"""
|
||||
return [
|
||||
Property("Package base", self.content.package_base),
|
||||
Property("Description", self.content.description, is_required=True),
|
||||
Property("Upstream URL", self.content.url),
|
||||
Property("Licenses", self.content.license), # it should be actually a list
|
||||
Property("Maintainer", self.content.maintainer or ""), # I think it is optional
|
||||
Property("First submitted", pretty_datetime(self.content.first_submitted)),
|
||||
Property("Last updated", pretty_datetime(self.content.last_modified)),
|
||||
# more fields coming https://github.com/cdown/aur/pull/29
|
||||
]
|
||||
|
||||
def title(self) -> Optional[str]:
|
||||
"""
|
||||
generate entry title from content
|
||||
:return: content title if it can be generated and None otherwise
|
||||
"""
|
||||
return f"{self.content.name} {self.content.version} ({self.content.num_votes})"
|
55
src/ahriman/application/formatters/configuration_printer.py
Normal file
55
src/ahriman/application/formatters/configuration_printer.py
Normal file
@ -0,0 +1,55 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 Dict, List, Optional
|
||||
|
||||
from ahriman.application.formatters.printer import Printer
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class ConfigurationPrinter(Printer):
|
||||
"""
|
||||
print content of the configuration section
|
||||
"""
|
||||
|
||||
def __init__(self, section: str, values: Dict[str, str]) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param section: section name
|
||||
:param values: configuration values dictionary
|
||||
"""
|
||||
self.section = section
|
||||
self.content = values
|
||||
|
||||
def properties(self) -> List[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
:return: list of content properties
|
||||
"""
|
||||
return [
|
||||
Property(key, value, is_required=True)
|
||||
for key, value in sorted(self.content.items())
|
||||
]
|
||||
|
||||
def title(self) -> Optional[str]:
|
||||
"""
|
||||
generate entry title from content
|
||||
:return: content title if it can be generated and None otherwise
|
||||
"""
|
||||
return f"[{self.section}]"
|
60
src/ahriman/application/formatters/package_printer.py
Normal file
60
src/ahriman/application/formatters/package_printer.py
Normal file
@ -0,0 +1,60 @@
|
||||
#
|
||||
# Copyright (c) 2021 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, Optional
|
||||
|
||||
from ahriman.application.formatters.printer import Printer
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class PackagePrinter(Printer):
|
||||
"""
|
||||
print content of the internal package object
|
||||
"""
|
||||
|
||||
def __init__(self, package: Package, status: BuildStatus) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param package: package description
|
||||
:param status: build status
|
||||
"""
|
||||
self.content = package
|
||||
self.status = status
|
||||
|
||||
def properties(self) -> List[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
:return: list of content properties
|
||||
"""
|
||||
return [
|
||||
Property("Version", self.content.version, is_required=True),
|
||||
Property("Groups", " ".join(self.content.groups)),
|
||||
Property("Licenses", " ".join(self.content.licenses)),
|
||||
Property("Depends", " ".join(self.content.depends)),
|
||||
Property("Status", self.status.pretty_print(), is_required=True),
|
||||
]
|
||||
|
||||
def title(self) -> Optional[str]:
|
||||
"""
|
||||
generate entry title from content
|
||||
:return: content title if it can be generated and None otherwise
|
||||
"""
|
||||
return self.content.pretty_print()
|
55
src/ahriman/application/formatters/printer.py
Normal file
55
src/ahriman/application/formatters/printer.py
Normal file
@ -0,0 +1,55 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 Callable, List, Optional
|
||||
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class Printer:
|
||||
"""
|
||||
base class for formatters
|
||||
"""
|
||||
|
||||
def print(self, verbose: bool, log_fn: Callable[[str], None] = print, separator: str = ": ") -> None:
|
||||
"""
|
||||
print content
|
||||
:param verbose: print all fields
|
||||
:param log_fn: logger function to log data
|
||||
:param separator: separator for property name and property value
|
||||
"""
|
||||
if (title := self.title()) is not None:
|
||||
log_fn(title)
|
||||
for prop in self.properties():
|
||||
if not verbose and not prop.is_required:
|
||||
continue
|
||||
log_fn(f"\t{prop.name}{separator}{prop.value}")
|
||||
|
||||
def properties(self) -> List[Property]: # pylint: disable=no-self-use
|
||||
"""
|
||||
convert content into printable data
|
||||
:return: list of content properties
|
||||
"""
|
||||
return []
|
||||
|
||||
def title(self) -> Optional[str]:
|
||||
"""
|
||||
generate entry title from content
|
||||
:return: content title if it can be generated and None otherwise
|
||||
"""
|
51
src/ahriman/application/formatters/status_printer.py
Normal file
51
src/ahriman/application/formatters/status_printer.py
Normal file
@ -0,0 +1,51 @@
|
||||
#
|
||||
# Copyright (c) 2021 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, Optional
|
||||
|
||||
from ahriman.application.formatters.printer import Printer
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class StatusPrinter(Printer):
|
||||
"""
|
||||
print content of the status object
|
||||
"""
|
||||
|
||||
def __init__(self, status: BuildStatus) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param status: build status
|
||||
"""
|
||||
self.content = status
|
||||
|
||||
def properties(self) -> List[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
:return: list of content properties
|
||||
"""
|
||||
return []
|
||||
|
||||
def title(self) -> Optional[str]:
|
||||
"""
|
||||
generate entry title from content
|
||||
:return: content title if it can be generated and None otherwise
|
||||
"""
|
||||
return self.content.pretty_print()
|
53
src/ahriman/application/formatters/update_printer.py
Normal file
53
src/ahriman/application/formatters/update_printer.py
Normal file
@ -0,0 +1,53 @@
|
||||
#
|
||||
# Copyright (c) 2021 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, Optional
|
||||
|
||||
from ahriman.application.formatters.printer import Printer
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class UpdatePrinter(Printer):
|
||||
"""
|
||||
print content of the package update
|
||||
"""
|
||||
|
||||
def __init__(self, remote: Package, local_version: Optional[str]) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param remote: remote (new) package object
|
||||
:param local_version: local version of the package if any
|
||||
"""
|
||||
self.content = remote
|
||||
self.local_version = local_version or "N/A"
|
||||
|
||||
def properties(self) -> List[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
:return: list of content properties
|
||||
"""
|
||||
return [Property(self.local_version, self.content.version, is_required=True)]
|
||||
|
||||
def title(self) -> Optional[str]:
|
||||
"""
|
||||
generate entry title from content
|
||||
:return: content title if it can be generated and None otherwise
|
||||
"""
|
||||
return self.content.base
|
@ -24,6 +24,7 @@ from ahriman.application.handlers.clean import Clean
|
||||
from ahriman.application.handlers.dump import Dump
|
||||
from ahriman.application.handlers.init import Init
|
||||
from ahriman.application.handlers.key_import import KeyImport
|
||||
from ahriman.application.handlers.patch import Patch
|
||||
from ahriman.application.handlers.rebuild import Rebuild
|
||||
from ahriman.application.handlers.remove import Remove
|
||||
from ahriman.application.handlers.remove_unknown import RemoveUnknown
|
||||
|
@ -46,5 +46,5 @@ class Add(Handler):
|
||||
if not args.now:
|
||||
return
|
||||
|
||||
packages = application.get_updates(args.package, True, False, True, application.logger.info)
|
||||
packages = application.updates(args.package, True, False, True, application.logger.info)
|
||||
application.update(packages)
|
||||
|
@ -41,5 +41,5 @@ class Clean(Handler):
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
Application(architecture, configuration, no_report).clean(args.no_build, args.no_cache, args.no_chroot,
|
||||
args.no_manual, args.no_packages)
|
||||
Application(architecture, configuration, no_report).clean(
|
||||
args.build, args.cache, args.chroot, args.manual, args.packages, args.patches)
|
||||
|
@ -21,6 +21,7 @@ import argparse
|
||||
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
@ -30,7 +31,7 @@ class Dump(Handler):
|
||||
dump configuration handler
|
||||
"""
|
||||
|
||||
_print = print
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
@ -44,7 +45,4 @@ class Dump(Handler):
|
||||
"""
|
||||
dump = configuration.dump()
|
||||
for section, values in sorted(dump.items()):
|
||||
Dump._print(f"[{section}]")
|
||||
for key, value in sorted(values.items()):
|
||||
Dump._print(f"{key} = {value}")
|
||||
Dump._print()
|
||||
ConfigurationPrinter(section, values).print(verbose=False, separator=" = ")
|
||||
|
@ -34,11 +34,37 @@ from ahriman.models.repository_paths import RepositoryPaths
|
||||
class Handler:
|
||||
"""
|
||||
base handler class for command callbacks
|
||||
:cvar ALLOW_AUTO_ARCHITECTURE_RUN: allow to define architecture from existing repositories
|
||||
:cvar ALLOW_MULTI_ARCHITECTURE_RUN: allow to run with multiple architectures
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = True
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = True
|
||||
|
||||
@classmethod
|
||||
def architectures_extract(cls: Type[Handler], args: argparse.Namespace) -> Set[str]:
|
||||
"""
|
||||
get known architectures
|
||||
:param args: command line args
|
||||
:return: list of architectures for which tree is created
|
||||
"""
|
||||
if not cls.ALLOW_AUTO_ARCHITECTURE_RUN and args.architecture is None:
|
||||
# for some parsers (e.g. config) we need to run with specific architecture
|
||||
# for those cases architecture must be set explicitly
|
||||
raise MissingArchitecture(args.command)
|
||||
if args.architecture: # architecture is specified explicitly
|
||||
return set(args.architecture)
|
||||
|
||||
config = Configuration()
|
||||
config.load(args.configuration)
|
||||
# wtf???
|
||||
root = config.getpath("repository", "root") # pylint: disable=assignment-from-no-return
|
||||
architectures = RepositoryPaths.known_architectures(root)
|
||||
|
||||
if not architectures: # well we did not find anything
|
||||
raise MissingArchitecture(args.command)
|
||||
return architectures
|
||||
|
||||
@classmethod
|
||||
def call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
|
||||
"""
|
||||
@ -48,12 +74,13 @@ class Handler:
|
||||
:return: True on success, False otherwise
|
||||
"""
|
||||
try:
|
||||
configuration = Configuration.from_path(args.configuration, architecture, not args.no_log)
|
||||
configuration = Configuration.from_path(args.configuration, architecture, args.quiet)
|
||||
with Lock(args, architecture, configuration):
|
||||
cls.run(args, architecture, configuration, args.no_report)
|
||||
return True
|
||||
except Exception:
|
||||
logging.getLogger("root").exception("process exception")
|
||||
# we are basically always want to print error to stderr instead of default logger
|
||||
logging.getLogger("stderr").exception("process exception")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
@ -63,7 +90,7 @@ class Handler:
|
||||
:param args: command line args
|
||||
:return: 0 on success, 1 otherwise
|
||||
"""
|
||||
architectures = cls.extract_architectures(args)
|
||||
architectures = cls.architectures_extract(args)
|
||||
|
||||
# actually we do not have to spawn another process if it is single-process application, do we?
|
||||
if len(architectures) > 1:
|
||||
@ -78,28 +105,6 @@ class Handler:
|
||||
|
||||
return 0 if all(result) else 1
|
||||
|
||||
@classmethod
|
||||
def extract_architectures(cls: Type[Handler], args: argparse.Namespace) -> Set[str]:
|
||||
"""
|
||||
get known architectures
|
||||
:param args: command line args
|
||||
:return: list of architectures for which tree is created
|
||||
"""
|
||||
if args.architecture is None:
|
||||
raise MissingArchitecture(args.command)
|
||||
if args.architecture:
|
||||
return set(args.architecture)
|
||||
|
||||
config = Configuration()
|
||||
config.load(args.configuration)
|
||||
# wtf???
|
||||
root = config.getpath("repository", "root") # pylint: disable=assignment-from-no-return
|
||||
architectures = RepositoryPaths.known_architectures(root)
|
||||
|
||||
if not architectures:
|
||||
raise MissingArchitecture(args.command)
|
||||
return architectures
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
|
@ -31,6 +31,8 @@ class Init(Handler):
|
||||
repository init handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
|
@ -31,6 +31,8 @@ class KeyImport(Handler):
|
||||
key import packages handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
@ -41,4 +43,4 @@ class KeyImport(Handler):
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
Application(architecture, configuration, no_report).repository.sign.import_key(args.key_server, args.key)
|
||||
Application(architecture, configuration, no_report).repository.sign.key_import(args.key_server, args.key)
|
||||
|
99
src/ahriman/application/handlers/patch.py
Normal file
99
src/ahriman/application/handlers/patch.py
Normal file
@ -0,0 +1,99 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 shutil
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
|
||||
|
||||
class Patch(Handler):
|
||||
"""
|
||||
patch control handler
|
||||
"""
|
||||
|
||||
_print = print
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
:param args: command line args
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
application = Application(architecture, configuration, no_report)
|
||||
|
||||
if args.action == Action.List:
|
||||
Patch.patch_set_list(application, args.package)
|
||||
elif args.action == Action.Remove:
|
||||
Patch.patch_set_remove(application, args.package)
|
||||
elif args.action == Action.Update:
|
||||
Patch.patch_set_create(application, args.package, args.track)
|
||||
|
||||
@staticmethod
|
||||
def patch_set_create(application: Application, sources_dir: str, track: List[str]) -> None:
|
||||
"""
|
||||
create patch set for the package base
|
||||
:param application: application instance
|
||||
:param sources_dir: path to directory with the package sources
|
||||
:param track: track files which match the glob before creating the patch
|
||||
"""
|
||||
package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman,
|
||||
application.repository.aur_url)
|
||||
patch_dir = application.repository.paths.patches_for(package.base)
|
||||
|
||||
Patch.patch_set_remove(application, package.base) # remove old patches
|
||||
patch_dir.mkdir(mode=0o755, parents=True)
|
||||
|
||||
Sources.patch_create(Path(sources_dir), patch_dir / "00-main.patch", *track)
|
||||
|
||||
@staticmethod
|
||||
def patch_set_list(application: Application, package_base: str) -> None:
|
||||
"""
|
||||
list patches available for the package base
|
||||
:param application: application instance
|
||||
:param package_base: package base
|
||||
"""
|
||||
patch_dir = application.repository.paths.patches_for(package_base)
|
||||
if not patch_dir.is_dir():
|
||||
return
|
||||
for patch_path in sorted(patch_dir.glob("*.patch")):
|
||||
Patch._print(patch_path.name)
|
||||
|
||||
@staticmethod
|
||||
def patch_set_remove(application: Application, package_base: str) -> None:
|
||||
"""
|
||||
remove patch set for the package base
|
||||
:param application: application instance
|
||||
:param package_base: package base
|
||||
"""
|
||||
patch_dir = application.repository.paths.patches_for(package_base)
|
||||
shutil.rmtree(patch_dir, ignore_errors=True)
|
@ -22,9 +22,10 @@ import argparse
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.formatters.package_printer import PackagePrinter
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
|
||||
|
||||
class RemoveUnknown(Handler):
|
||||
@ -46,16 +47,7 @@ class RemoveUnknown(Handler):
|
||||
unknown_packages = application.unknown()
|
||||
if args.dry_run:
|
||||
for package in unknown_packages:
|
||||
RemoveUnknown.log_fn(package)
|
||||
PackagePrinter(package, BuildStatus()).print(args.info)
|
||||
return
|
||||
|
||||
application.remove(package.base for package in unknown_packages)
|
||||
|
||||
@staticmethod
|
||||
def log_fn(package: Package) -> None:
|
||||
"""
|
||||
log package information
|
||||
:param package: package object to log
|
||||
"""
|
||||
print(f"=> {package.base} {package.version}")
|
||||
print(f" {package.web_url}")
|
||||
|
@ -20,10 +20,13 @@
|
||||
import argparse
|
||||
import aur # type: ignore
|
||||
|
||||
from typing import Callable, Type
|
||||
from typing import Callable, Iterable, List, Tuple, Type
|
||||
|
||||
from ahriman.application.formatters.aur_printer import AurPrinter
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
from ahriman.core.util import aur_search
|
||||
|
||||
|
||||
class Search(Handler):
|
||||
@ -31,6 +34,9 @@ class Search(Handler):
|
||||
packages search handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
|
||||
SORT_FIELDS = set(aur.Package._fields) # later we will have to remove some fields from here (lists)
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
@ -41,20 +47,22 @@ class Search(Handler):
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
search = " ".join(args.search)
|
||||
packages = aur.search(search)
|
||||
|
||||
# it actually always should return string
|
||||
# explicit cast to string just to avoid mypy warning for untyped library
|
||||
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
|
||||
for package in sorted(packages, key=comparator):
|
||||
Search.log_fn(package)
|
||||
packages_list = aur_search(*args.search)
|
||||
for package in Search.sort(packages_list, args.sort_by):
|
||||
AurPrinter(package).print(args.info)
|
||||
|
||||
@staticmethod
|
||||
def log_fn(package: aur.Package) -> None:
|
||||
def sort(packages: Iterable[aur.Package], sort_by: str) -> List[aur.Package]:
|
||||
"""
|
||||
log package information
|
||||
:param package: package object as from AUR
|
||||
sort package list by specified field
|
||||
:param packages: packages list to sort
|
||||
:param sort_by: AUR package field name to sort by
|
||||
:return: sorted list for packages
|
||||
"""
|
||||
print(f"=> {package.package_base} {package.version}")
|
||||
print(f" {package.description}")
|
||||
if sort_by not in Search.SORT_FIELDS:
|
||||
raise InvalidOption(sort_by)
|
||||
# always sort by package name at the last
|
||||
# well technically it is not a string, but we can deal with it
|
||||
comparator: Callable[[aur.Package], Tuple[str, str]] =\
|
||||
lambda package: (getattr(package, sort_by), package.name)
|
||||
return sorted(packages, key=comparator)
|
||||
|
@ -37,6 +37,8 @@ class Setup(Handler):
|
||||
:cvar SUDOERS_PATH: path to sudoers.d include configuration
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
|
||||
ARCHBUILD_COMMAND_PATH = Path("/usr/bin/archbuild")
|
||||
BIN_DIR_PATH = Path("/usr/local/bin")
|
||||
MIRRORLIST_PATH = Path("/etc/pacman.d/mirrorlist")
|
||||
@ -53,12 +55,12 @@ class Setup(Handler):
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
application = Application(architecture, configuration, no_report)
|
||||
Setup.create_makepkg_configuration(args.packager, application.repository.paths)
|
||||
Setup.create_executable(args.build_command, architecture)
|
||||
Setup.create_devtools_configuration(args.build_command, architecture, args.from_configuration,
|
||||
Setup.configuration_create_makepkg(args.packager, application.repository.paths)
|
||||
Setup.executable_create(args.build_command, architecture)
|
||||
Setup.configuration_create_devtools(args.build_command, architecture, args.from_configuration,
|
||||
args.no_multilib, args.repository, application.repository.paths)
|
||||
Setup.create_ahriman_configuration(args, architecture, args.repository, configuration.include)
|
||||
Setup.create_sudo_configuration(args.build_command, architecture)
|
||||
Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include)
|
||||
Setup.configuration_create_sudo(args.build_command, architecture)
|
||||
|
||||
@staticmethod
|
||||
def build_command(prefix: str, architecture: str) -> Path:
|
||||
@ -71,7 +73,7 @@ class Setup(Handler):
|
||||
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
|
||||
|
||||
@staticmethod
|
||||
def create_ahriman_configuration(args: argparse.Namespace, architecture: str, repository: str,
|
||||
def configuration_create_ahriman(args: argparse.Namespace, architecture: str, repository: str,
|
||||
include_path: Path) -> None:
|
||||
"""
|
||||
create service specific configuration
|
||||
@ -100,7 +102,7 @@ class Setup(Handler):
|
||||
configuration.write(ahriman_configuration)
|
||||
|
||||
@staticmethod
|
||||
def create_devtools_configuration(prefix: str, architecture: str, source: Path,
|
||||
def configuration_create_devtools(prefix: str, architecture: str, source: Path,
|
||||
no_multilib: bool, repository: str, paths: RepositoryPaths) -> None:
|
||||
"""
|
||||
create configuration for devtools based on `source` configuration
|
||||
@ -136,7 +138,7 @@ class Setup(Handler):
|
||||
configuration.write(devtools_configuration)
|
||||
|
||||
@staticmethod
|
||||
def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None:
|
||||
def configuration_create_makepkg(packager: str, paths: RepositoryPaths) -> None:
|
||||
"""
|
||||
create configuration for makepkg
|
||||
:param packager: packager identifier (e.g. name, email)
|
||||
@ -145,7 +147,7 @@ class Setup(Handler):
|
||||
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n")
|
||||
|
||||
@staticmethod
|
||||
def create_sudo_configuration(prefix: str, architecture: str) -> None:
|
||||
def configuration_create_sudo(prefix: str, architecture: str) -> None:
|
||||
"""
|
||||
create configuration to run build command with sudo without password
|
||||
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||
@ -156,7 +158,7 @@ class Setup(Handler):
|
||||
Setup.SUDOERS_PATH.chmod(0o400) # security!
|
||||
|
||||
@staticmethod
|
||||
def create_executable(prefix: str, architecture: str) -> None:
|
||||
def executable_create(prefix: str, architecture: str) -> None:
|
||||
"""
|
||||
create executable for the service
|
||||
:param prefix: command prefix in {prefix}-{architecture}-build
|
||||
|
@ -22,6 +22,8 @@ import argparse
|
||||
from typing import Callable, Iterable, Tuple, Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.formatters.package_printer import PackagePrinter
|
||||
from ahriman.application.formatters.status_printer import StatusPrinter
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
@ -33,6 +35,8 @@ class Status(Handler):
|
||||
package status handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
@ -47,8 +51,7 @@ class Status(Handler):
|
||||
client = Application(architecture, configuration, no_report=False).repository.reporter
|
||||
if args.ahriman:
|
||||
ahriman = client.get_self()
|
||||
print(ahriman.pretty_print())
|
||||
print()
|
||||
StatusPrinter(ahriman).print(args.info)
|
||||
if args.package:
|
||||
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
|
||||
[client.get(base) for base in args.package],
|
||||
@ -60,6 +63,4 @@ class Status(Handler):
|
||||
filter_fn: Callable[[Tuple[Package, BuildStatus]], bool] =\
|
||||
lambda item: args.status is None or item[1].status == args.status
|
||||
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
|
||||
print(package.pretty_print())
|
||||
print(f"\t{package.version}")
|
||||
print(f"\t{package_status.pretty_print()}")
|
||||
PackagePrinter(package, package_status).print(args.info)
|
||||
|
@ -19,12 +19,12 @@
|
||||
#
|
||||
import argparse
|
||||
|
||||
from typing import Callable, Type
|
||||
from typing import Type
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import InvalidCommand
|
||||
from ahriman.models.action import Action
|
||||
|
||||
|
||||
class StatusUpdate(Handler):
|
||||
@ -32,6 +32,8 @@ class StatusUpdate(Handler):
|
||||
status update handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
@ -44,13 +46,14 @@ class StatusUpdate(Handler):
|
||||
"""
|
||||
# we are using reporter here
|
||||
client = Application(architecture, configuration, no_report=False).repository.reporter
|
||||
callback: Callable[[str], None] = lambda p: client.remove(p) if args.remove else client.update(p, args.status)
|
||||
if args.package:
|
||||
|
||||
if args.action == Action.Update and args.package:
|
||||
# update packages statuses
|
||||
for package in args.package:
|
||||
callback(package)
|
||||
elif args.remove:
|
||||
raise InvalidCommand("Remove option is supplied, but no packages set")
|
||||
else:
|
||||
client.update(package, args.status)
|
||||
elif args.action == Action.Update:
|
||||
# update service status
|
||||
client.update_self(args.status)
|
||||
elif args.action == Action.Remove:
|
||||
for package in args.package:
|
||||
client.remove(package)
|
||||
|
@ -42,8 +42,8 @@ class Update(Handler):
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
application = Application(architecture, configuration, no_report)
|
||||
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
|
||||
Update.log_fn(application, args.dry_run))
|
||||
packages = application.updates(args.package, args.no_aur, args.no_manual, args.no_vcs,
|
||||
Update.log_fn(application, args.dry_run))
|
||||
if args.dry_run:
|
||||
return
|
||||
|
||||
|
@ -26,6 +26,7 @@ from typing import Type
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.user import User as MUser
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
@ -35,6 +36,8 @@ class User(Handler):
|
||||
user management handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
|
||||
|
||||
@classmethod
|
||||
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
|
||||
configuration: Configuration, no_report: bool) -> None:
|
||||
@ -46,33 +49,20 @@ class User(Handler):
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
salt = User.get_salt(configuration)
|
||||
user = User.create_user(args)
|
||||
auth_configuration = User.get_auth_configuration(configuration.include)
|
||||
user = User.user_create(args)
|
||||
auth_configuration = User.configuration_get(configuration.include)
|
||||
|
||||
User.clear_user(auth_configuration, user)
|
||||
if not args.remove:
|
||||
User.create_configuration(auth_configuration, user, salt, args.as_service)
|
||||
User.write_configuration(auth_configuration, args.secure)
|
||||
User.user_clear(auth_configuration, user)
|
||||
if args.action == Action.Update:
|
||||
User.configuration_create(auth_configuration, user, salt, args.as_service)
|
||||
User.configuration_write(auth_configuration, args.secure)
|
||||
|
||||
if not args.no_reload:
|
||||
client = Application(architecture, configuration, no_report=False).repository.reporter
|
||||
client.reload_auth()
|
||||
|
||||
@staticmethod
|
||||
def clear_user(configuration: Configuration, user: MUser) -> None:
|
||||
"""
|
||||
remove user user from configuration file in case if it exists
|
||||
:param configuration: configuration instance
|
||||
:param user: user descriptor
|
||||
"""
|
||||
for role in UserAccess:
|
||||
section = Configuration.section_name("auth", role.value)
|
||||
if not configuration.has_option(section, user.username):
|
||||
continue
|
||||
configuration.remove_option(section, user.username)
|
||||
|
||||
@staticmethod
|
||||
def create_configuration(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None:
|
||||
def configuration_create(configuration: Configuration, user: MUser, salt: str, as_service_user: bool) -> None:
|
||||
"""
|
||||
put new user to configuration
|
||||
:param configuration: configuration instance
|
||||
@ -89,19 +79,7 @@ class User(Handler):
|
||||
configuration.set_option("web", "password", user.password)
|
||||
|
||||
@staticmethod
|
||||
def create_user(args: argparse.Namespace) -> MUser:
|
||||
"""
|
||||
create user descriptor from arguments
|
||||
:param args: command line args
|
||||
:return: built user descriptor
|
||||
"""
|
||||
user = MUser(args.username, args.password, args.access)
|
||||
if user.password is None:
|
||||
user.password = getpass.getpass()
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def get_auth_configuration(include_path: Path) -> Configuration:
|
||||
def configuration_get(include_path: Path) -> Configuration:
|
||||
"""
|
||||
create configuration instance
|
||||
:param include_path: path to directory with configuration includes
|
||||
@ -114,20 +92,7 @@ class User(Handler):
|
||||
return configuration
|
||||
|
||||
@staticmethod
|
||||
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
|
||||
"""
|
||||
get salt from configuration or create new string
|
||||
:param configuration: configuration instance
|
||||
:param salt_length: salt length
|
||||
:return: current salt
|
||||
"""
|
||||
salt = configuration.get("auth", "salt", fallback=None)
|
||||
if salt:
|
||||
return salt
|
||||
return MUser.generate_password(salt_length)
|
||||
|
||||
@staticmethod
|
||||
def write_configuration(configuration: Configuration, secure: bool) -> None:
|
||||
def configuration_write(configuration: Configuration, secure: bool) -> None:
|
||||
"""
|
||||
write configuration file
|
||||
:param configuration: configuration instance
|
||||
@ -139,3 +104,40 @@ class User(Handler):
|
||||
configuration.write(ahriman_configuration)
|
||||
if secure:
|
||||
configuration.path.chmod(0o600)
|
||||
|
||||
@staticmethod
|
||||
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
|
||||
"""
|
||||
get salt from configuration or create new string
|
||||
:param configuration: configuration instance
|
||||
:param salt_length: salt length
|
||||
:return: current salt
|
||||
"""
|
||||
if salt := configuration.get("auth", "salt", fallback=None):
|
||||
return salt
|
||||
return MUser.generate_password(salt_length)
|
||||
|
||||
@staticmethod
|
||||
def user_clear(configuration: Configuration, user: MUser) -> None:
|
||||
"""
|
||||
remove user user from configuration file in case if it exists
|
||||
:param configuration: configuration instance
|
||||
:param user: user descriptor
|
||||
"""
|
||||
for role in UserAccess:
|
||||
section = Configuration.section_name("auth", role.value)
|
||||
if not configuration.has_option(section, user.username):
|
||||
continue
|
||||
configuration.remove_option(section, user.username)
|
||||
|
||||
@staticmethod
|
||||
def user_create(args: argparse.Namespace) -> MUser:
|
||||
"""
|
||||
create user descriptor from arguments
|
||||
:param args: command line args
|
||||
:return: built user descriptor
|
||||
"""
|
||||
user = MUser(args.username, args.password, args.role)
|
||||
if user.password is None:
|
||||
user.password = getpass.getpass()
|
||||
return user
|
||||
|
@ -31,6 +31,7 @@ class Web(Handler):
|
||||
web server handler
|
||||
"""
|
||||
|
||||
ALLOW_AUTO_ARCHITECTURE_RUN = False
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
|
||||
|
||||
@classmethod
|
||||
|
@ -21,7 +21,6 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
@ -29,8 +28,9 @@ from typing import Literal, Optional, Type
|
||||
|
||||
from ahriman import version
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
|
||||
from ahriman.core.exceptions import DuplicateRun
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import check_user
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
|
||||
|
||||
@ -105,10 +105,7 @@ class Lock:
|
||||
"""
|
||||
if self.unsafe:
|
||||
return
|
||||
current_uid = os.getuid()
|
||||
root_uid = self.root.stat().st_uid
|
||||
if current_uid != root_uid:
|
||||
raise UnsafeRun(current_uid, root_uid)
|
||||
check_user(self.root)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""
|
||||
|
@ -104,7 +104,6 @@ class OAuth(Mapping):
|
||||
access_token, _ = await client.get_access_token(code, redirect_uri=self.redirect_uri)
|
||||
client.access_token = access_token
|
||||
|
||||
print(f"HEEELOOOO {client}")
|
||||
user, _ = await client.user_info()
|
||||
username: str = user.email # type: ignore
|
||||
return username
|
||||
|
150
src/ahriman/core/build_tools/sources.py
Normal file
150
src/ahriman/core/build_tools/sources.py
Normal file
@ -0,0 +1,150 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from ahriman.core.util import check_output
|
||||
|
||||
|
||||
class Sources:
|
||||
"""
|
||||
helper to download package sources (PKGBUILD etc)
|
||||
:cvar logger: class logger
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("build_details")
|
||||
|
||||
_branch = "master" # in case if BLM would like to change it
|
||||
_check_output = check_output
|
||||
|
||||
@staticmethod
|
||||
def add(sources_dir: Path, *pattern: str) -> None:
|
||||
"""
|
||||
track found files via git
|
||||
:param sources_dir: local path to git repository
|
||||
:param pattern: 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))
|
||||
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, patch_path: Path) -> None:
|
||||
"""
|
||||
generate diff from the current version and write it to the output file
|
||||
:param sources_dir: local path to git repository
|
||||
:param patch_path: path to result patch
|
||||
"""
|
||||
patch = Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
patch_path.write_text(patch)
|
||||
|
||||
@staticmethod
|
||||
def fetch(sources_dir: Path, remote: str) -> None:
|
||||
"""
|
||||
either clone repository or update it to origin/`branch`
|
||||
:param sources_dir: local path to fetch
|
||||
:param remote: remote target (from where to fetch)
|
||||
"""
|
||||
# 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):
|
||||
# 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)
|
||||
return
|
||||
|
||||
if is_initialized_git:
|
||||
Sources.logger.info("update HEAD to remote at %s", sources_dir)
|
||||
Sources._check_output("git", "fetch", "origin", Sources._branch,
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
else:
|
||||
Sources.logger.info("clone remote %s to %s", remote, sources_dir)
|
||||
Sources._check_output("git", "clone", remote, str(sources_dir), exception=None, logger=Sources.logger)
|
||||
# and now force reset to our branch
|
||||
Sources._check_output("git", "checkout", "--force", Sources._branch,
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
Sources._check_output("git", "reset", "--hard", f"origin/{Sources._branch}",
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@staticmethod
|
||||
def has_remotes(sources_dir: Path) -> bool:
|
||||
"""
|
||||
check if there are remotes for the repository
|
||||
:param sources_dir: local path to git repository
|
||||
:return: 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)
|
||||
return bool(remotes)
|
||||
|
||||
@staticmethod
|
||||
def init(sources_dir: Path) -> None:
|
||||
"""
|
||||
create empty git repository at the specified path
|
||||
:param sources_dir: local path to sources
|
||||
"""
|
||||
Sources._check_output("git", "init", "--initial-branch", Sources._branch,
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@staticmethod
|
||||
def load(sources_dir: Path, remote: str, patch_dir: Path) -> None:
|
||||
"""
|
||||
fetch sources from remote and apply patches
|
||||
:param sources_dir: local path to fetch
|
||||
:param remote: remote target (from where to fetch)
|
||||
:param patch_dir: path to directory with package patches
|
||||
"""
|
||||
Sources.fetch(sources_dir, remote)
|
||||
Sources.patch_apply(sources_dir, patch_dir)
|
||||
|
||||
@staticmethod
|
||||
def patch_apply(sources_dir: Path, patch_dir: Path) -> None:
|
||||
"""
|
||||
apply patches if any
|
||||
:param sources_dir: local path to directory with git sources
|
||||
:param patch_dir: path to directory with package patches
|
||||
"""
|
||||
# check if even there are patches
|
||||
if not patch_dir.is_dir():
|
||||
return # no patches provided
|
||||
# find everything that looks like patch and sort it
|
||||
patches = sorted(patch_dir.glob("*.patch"))
|
||||
Sources.logger.info("found %s patches", patches)
|
||||
for patch in patches:
|
||||
Sources.logger.info("apply patch %s", patch.name)
|
||||
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace", str(patch),
|
||||
exception=None, cwd=sources_dir, logger=Sources.logger)
|
||||
|
||||
@staticmethod
|
||||
def patch_create(sources_dir: Path, patch_path: Path, *pattern: str) -> None:
|
||||
"""
|
||||
create patch set for the specified local path
|
||||
:param sources_dir: local path to git repository
|
||||
:param patch_path: path to result patch
|
||||
:param pattern: glob patterns
|
||||
"""
|
||||
Sources.add(sources_dir, *pattern)
|
||||
Sources.diff(sources_dir, patch_path)
|
@ -23,6 +23,7 @@ import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import BuildFailed
|
||||
from ahriman.core.util import check_output
|
||||
@ -48,7 +49,7 @@ class Task:
|
||||
:param configuration: configuration instance
|
||||
:param paths: repository paths instance
|
||||
"""
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.logger = logging.getLogger("root")
|
||||
self.build_logger = logging.getLogger("build_details")
|
||||
self.package = package
|
||||
self.paths = paths
|
||||
@ -58,38 +59,6 @@ class Task:
|
||||
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
|
||||
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
|
||||
|
||||
@property
|
||||
def cache_path(self) -> Path:
|
||||
"""
|
||||
:return: path to cached packages
|
||||
"""
|
||||
return self.paths.cache / self.package.base
|
||||
|
||||
@property
|
||||
def git_path(self) -> Path:
|
||||
"""
|
||||
:return: path to clone package from git
|
||||
"""
|
||||
return self.paths.sources / self.package.base
|
||||
|
||||
@staticmethod
|
||||
def fetch(local: Path, remote: str, branch: str = "master") -> None:
|
||||
"""
|
||||
either clone repository or update it to origin/`branch`
|
||||
:param local: local path to fetch
|
||||
:param remote: remote target (from where to fetch)
|
||||
:param branch: branch name to checkout, master by default
|
||||
"""
|
||||
logger = logging.getLogger("build_details")
|
||||
# local directory exists and there is .git directory
|
||||
if (local / ".git").is_dir():
|
||||
Task._check_output("git", "fetch", "origin", branch, exception=None, cwd=local, logger=logger)
|
||||
else:
|
||||
Task._check_output("git", "clone", remote, str(local), exception=None, logger=logger)
|
||||
# and now force reset to our branch
|
||||
Task._check_output("git", "checkout", "--force", branch, exception=None, cwd=local, logger=logger)
|
||||
Task._check_output("git", "reset", "--hard", f"origin/{branch}", exception=None, cwd=local, logger=logger)
|
||||
|
||||
def build(self) -> List[Path]:
|
||||
"""
|
||||
run package build
|
||||
@ -104,13 +73,13 @@ class Task:
|
||||
Task._check_output(
|
||||
*command,
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=self.git_path,
|
||||
cwd=self.paths.sources_for(self.package.base),
|
||||
logger=self.build_logger)
|
||||
|
||||
# well it is not actually correct, but we can deal with it
|
||||
packages = Task._check_output("makepkg", "--packagelist",
|
||||
exception=BuildFailed(self.package.base),
|
||||
cwd=self.git_path,
|
||||
cwd=self.paths.sources_for(self.package.base),
|
||||
logger=self.build_logger).splitlines()
|
||||
return [Path(package) for package in packages]
|
||||
|
||||
@ -119,8 +88,8 @@ class Task:
|
||||
fetch package from git
|
||||
:param path: optional local path to fetch. If not set default path will be used
|
||||
"""
|
||||
git_path = path or self.git_path
|
||||
if self.cache_path.is_dir():
|
||||
git_path = path or self.paths.sources_for(self.package.base)
|
||||
if self.paths.cache_for(self.package.base).is_dir():
|
||||
# no need to clone whole repository, just copy from cache first
|
||||
shutil.copytree(self.cache_path, git_path)
|
||||
return self.fetch(git_path, self.package.git_url)
|
||||
shutil.copytree(self.paths.cache_for(self.package.base), git_path)
|
||||
Sources.load(git_path, self.package.git_url, self.paths.patches_for(self.package.base))
|
||||
|
@ -24,7 +24,7 @@ import logging
|
||||
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
|
||||
|
||||
from ahriman.core.exceptions import InitializeException
|
||||
|
||||
@ -39,17 +39,17 @@ class Configuration(configparser.RawConfigParser):
|
||||
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
|
||||
"""
|
||||
|
||||
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
|
||||
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
|
||||
DEFAULT_LOG_LEVEL = logging.DEBUG
|
||||
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "sign", "web"]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
default constructor. In the most cases must not be called directly
|
||||
"""
|
||||
configparser.RawConfigParser.__init__(self, allow_no_value=True, converters={
|
||||
"list": lambda value: value.split(),
|
||||
"list": self.__convert_list,
|
||||
"path": self.__convert_path,
|
||||
})
|
||||
self.architecture: Optional[str] = None
|
||||
@ -70,20 +70,46 @@ class Configuration(configparser.RawConfigParser):
|
||||
return self.getpath("settings", "logging")
|
||||
|
||||
@classmethod
|
||||
def from_path(cls: Type[Configuration], path: Path, architecture: str, logfile: bool) -> Configuration:
|
||||
def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration:
|
||||
"""
|
||||
constructor with full object initialization
|
||||
:param path: path to root configuration file
|
||||
:param architecture: repository architecture
|
||||
:param logfile: use log file to output messages
|
||||
:param quiet: force disable any log messages
|
||||
:return: configuration instance
|
||||
"""
|
||||
config = cls()
|
||||
config.load(path)
|
||||
config.merge_sections(architecture)
|
||||
config.load_logging(logfile)
|
||||
config.load_logging(quiet)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def __convert_list(value: str) -> List[str]:
|
||||
"""
|
||||
convert string value to list of strings
|
||||
:param value: string configuration value
|
||||
:return: list of string from the parsed string
|
||||
"""
|
||||
def generator() -> Generator[str, None, None]:
|
||||
quote_mark = None
|
||||
word = ""
|
||||
for char in value:
|
||||
if char in ("'", "\"") and quote_mark is None: # quoted part started, store quote and do nothing
|
||||
quote_mark = char
|
||||
elif char == quote_mark: # quoted part ended, reset quotation
|
||||
quote_mark = None
|
||||
elif char == " " and quote_mark is None: # found space outside of the quotation, yield the word
|
||||
yield word
|
||||
word = ""
|
||||
else: # append character to the buffer
|
||||
word += char
|
||||
if quote_mark: # there is unmatched quote
|
||||
raise ValueError(f"unmatched quote in {value}")
|
||||
yield word # sequence done, return whatever we found
|
||||
|
||||
return [word for word in generator() if word]
|
||||
|
||||
@staticmethod
|
||||
def section_name(section: str, suffix: str) -> str:
|
||||
"""
|
||||
@ -121,6 +147,26 @@ class Configuration(configparser.RawConfigParser):
|
||||
|
||||
def getpath(self, *args: Any, **kwargs: Any) -> Path: ...
|
||||
|
||||
def gettype(self, section: str, architecture: str) -> Tuple[str, str]:
|
||||
"""
|
||||
get type variable with fallback to old logic
|
||||
Despite the fact that it has same semantics as other get* methods, but it has different argument list
|
||||
:param section: section name
|
||||
:param architecture: repository architecture
|
||||
:return: section name and found type name
|
||||
"""
|
||||
group_type = self.get(section, "type", fallback=None) # new-style logic
|
||||
if group_type is not None:
|
||||
return section, group_type
|
||||
# okay lets check for the section with architecture name
|
||||
full_section = self.section_name(section, architecture)
|
||||
if self.has_section(full_section):
|
||||
return full_section, section
|
||||
# okay lets just use section as type
|
||||
if not self.has_section(section):
|
||||
raise configparser.NoSectionError(section)
|
||||
return section, section
|
||||
|
||||
def load(self, path: Path) -> None:
|
||||
"""
|
||||
fully load configuration
|
||||
@ -142,27 +188,20 @@ class Configuration(configparser.RawConfigParser):
|
||||
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
|
||||
pass
|
||||
|
||||
def load_logging(self, logfile: bool) -> None:
|
||||
def load_logging(self, quiet: bool) -> None:
|
||||
"""
|
||||
setup logging settings from configuration
|
||||
:param logfile: use log file to output messages
|
||||
:param quiet: force disable any log messages
|
||||
"""
|
||||
def file_logger() -> None:
|
||||
try:
|
||||
path = self.logging_path
|
||||
fileConfig(path)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
console_logger()
|
||||
logging.exception("could not create logfile, fallback to stderr")
|
||||
|
||||
def console_logger() -> None:
|
||||
try:
|
||||
path = self.logging_path
|
||||
fileConfig(path)
|
||||
except Exception:
|
||||
logging.basicConfig(filename=None, format=self.DEFAULT_LOG_FORMAT,
|
||||
level=self.DEFAULT_LOG_LEVEL)
|
||||
|
||||
if logfile:
|
||||
file_logger()
|
||||
else:
|
||||
console_logger()
|
||||
logging.exception("could not load logging from configuration, fallback to stderr")
|
||||
if quiet:
|
||||
logging.disable(logging.WARNING) # only print errors here
|
||||
|
||||
def merge_sections(self, architecture: str) -> None:
|
||||
"""
|
||||
@ -191,6 +230,8 @@ class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
if self.path is None or self.architecture is None:
|
||||
raise InitializeException("Configuration path and/or architecture are not set")
|
||||
for section in self.sections(): # clear current content
|
||||
self.remove_section(section)
|
||||
self.load(self.path)
|
||||
self.merge_sections(self.architecture)
|
||||
|
||||
|
@ -42,7 +42,8 @@ class DuplicateRun(RuntimeError):
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
RuntimeError.__init__(self, "Another application instance is run")
|
||||
RuntimeError.__init__(
|
||||
self, "Another application instance is run. This error can be suppressed by using --force flag.")
|
||||
|
||||
|
||||
class DuplicateUser(ValueError):
|
||||
@ -71,19 +72,6 @@ class InitializeException(RuntimeError):
|
||||
RuntimeError.__init__(self, f"Could not load service: {details}")
|
||||
|
||||
|
||||
class InvalidCommand(ValueError):
|
||||
"""
|
||||
exception raised on invalid command line options
|
||||
"""
|
||||
|
||||
def __init__(self, details: Any) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param details" error details
|
||||
"""
|
||||
ValueError.__init__(self, details)
|
||||
|
||||
|
||||
class InvalidOption(ValueError):
|
||||
"""
|
||||
exception which will be raised on configuration errors
|
||||
|
@ -45,27 +45,28 @@ class Email(Report, JinjaTemplate):
|
||||
:ivar user: username to authenticate via SMTP
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param section: settings section name
|
||||
"""
|
||||
Report.__init__(self, architecture, configuration)
|
||||
JinjaTemplate.__init__(self, "email", configuration)
|
||||
JinjaTemplate.__init__(self, section, configuration)
|
||||
|
||||
self.full_template_path = configuration.getpath("email", "full_template_path", fallback=None)
|
||||
self.template_path = configuration.getpath("email", "template_path")
|
||||
self.full_template_path = configuration.getpath(section, "full_template_path", fallback=None)
|
||||
self.template_path = configuration.getpath(section, "template_path")
|
||||
|
||||
# base smtp settings
|
||||
self.host = configuration.get("email", "host")
|
||||
self.no_empty_report = configuration.getboolean("email", "no_empty_report", fallback=True)
|
||||
self.password = configuration.get("email", "password", fallback=None)
|
||||
self.port = configuration.getint("email", "port")
|
||||
self.receivers = configuration.getlist("email", "receivers")
|
||||
self.sender = configuration.get("email", "sender")
|
||||
self.ssl = SmtpSSLSettings.from_option(configuration.get("email", "ssl", fallback="disabled"))
|
||||
self.user = configuration.get("email", "user", fallback=None)
|
||||
self.host = configuration.get(section, "host")
|
||||
self.no_empty_report = configuration.getboolean(section, "no_empty_report", fallback=True)
|
||||
self.password = configuration.get(section, "password", fallback=None)
|
||||
self.port = configuration.getint(section, "port")
|
||||
self.receivers = configuration.getlist(section, "receivers")
|
||||
self.sender = configuration.get(section, "sender")
|
||||
self.ssl = SmtpSSLSettings.from_option(configuration.get(section, "ssl", fallback="disabled"))
|
||||
self.user = configuration.get(section, "user", fallback=None)
|
||||
|
||||
def _send(self, text: str, attachment: Dict[str, str]) -> None:
|
||||
"""
|
||||
|
@ -31,17 +31,18 @@ class HTML(Report, JinjaTemplate):
|
||||
:ivar report_path: output path to html report
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param section: settings section name
|
||||
"""
|
||||
Report.__init__(self, architecture, configuration)
|
||||
JinjaTemplate.__init__(self, "html", configuration)
|
||||
JinjaTemplate.__init__(self, section, configuration)
|
||||
|
||||
self.report_path = configuration.getpath("html", "path")
|
||||
self.template_path = configuration.getpath("html", "template_path")
|
||||
self.report_path = configuration.getpath(section, "path")
|
||||
self.template_path = configuration.getpath(section, "template_path")
|
||||
|
||||
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
|
@ -43,7 +43,7 @@ class Report:
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
|
||||
@ -53,16 +53,17 @@ class Report:
|
||||
load client from settings
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param target: target to generate report (e.g. html)
|
||||
:param target: target to generate report aka section name (e.g. html)
|
||||
:return: client according to current settings
|
||||
"""
|
||||
provider = ReportSettings.from_option(target)
|
||||
section, provider_name = configuration.gettype(target, architecture)
|
||||
provider = ReportSettings.from_option(provider_name)
|
||||
if provider == ReportSettings.HTML:
|
||||
from ahriman.core.report.html import HTML
|
||||
return HTML(architecture, configuration)
|
||||
return HTML(architecture, configuration, section)
|
||||
if provider == ReportSettings.Email:
|
||||
from ahriman.core.report.email import Email
|
||||
return Email(architecture, configuration)
|
||||
return Email(architecture, configuration, section)
|
||||
return cls(architecture, configuration) # should never happen
|
||||
|
||||
def generate(self, packages: Iterable[Package], built_packages: Iterable[Package]) -> None:
|
||||
|
@ -17,3 +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.repository.repository import Repository
|
||||
|
@ -76,3 +76,11 @@ class Cleaner(Properties):
|
||||
self.logger.info("clear built packages directory")
|
||||
for package in self.packages_built():
|
||||
package.unlink()
|
||||
|
||||
def clear_patches(self) -> None:
|
||||
"""
|
||||
clear directory with patches
|
||||
"""
|
||||
self.logger.info("clear patches directory")
|
||||
for package in self.paths.patches.iterdir():
|
||||
shutil.rmtree(package)
|
||||
|
@ -27,6 +27,7 @@ from ahriman.core.report.report import Report
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.upload.upload import Upload
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
|
||||
|
||||
class Executor(Cleaner):
|
||||
@ -72,9 +73,16 @@ class Executor(Cleaner):
|
||||
:param packages: list of package names or bases to remove
|
||||
:return: path to repository database
|
||||
"""
|
||||
def remove_single(package: str, fn: Path) -> None:
|
||||
def remove_base(package_base: str) -> None:
|
||||
try:
|
||||
self.repo.remove(package, fn)
|
||||
self.paths.tree_clear(package_base) # remove all internal files
|
||||
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)
|
||||
|
||||
def remove_package(package: str, fn: Path) -> None:
|
||||
try:
|
||||
self.repo.remove(package, fn) # remove the package itself
|
||||
except Exception:
|
||||
self.logger.exception("could not remove %s", package)
|
||||
|
||||
@ -86,7 +94,7 @@ class Executor(Cleaner):
|
||||
for package, properties in local.packages.items()
|
||||
if properties.filename is not None
|
||||
}
|
||||
self.reporter.remove(local.base) # we only update status page in case of base removal
|
||||
remove_base(local.base)
|
||||
elif requested.intersection(local.packages.keys()):
|
||||
to_remove = {
|
||||
package: Path(properties.filename)
|
||||
@ -95,8 +103,9 @@ class Executor(Cleaner):
|
||||
}
|
||||
else:
|
||||
to_remove = {}
|
||||
|
||||
for package, filename in to_remove.items():
|
||||
remove_single(package, filename)
|
||||
remove_package(package, filename)
|
||||
|
||||
return self.repo.repo_path
|
||||
|
||||
@ -130,24 +139,24 @@ class Executor(Cleaner):
|
||||
:param packages: list of filenames to run
|
||||
:return: path to repository database
|
||||
"""
|
||||
def update_single(fn: Optional[str], base: str) -> None:
|
||||
if fn is None:
|
||||
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
|
||||
full_path = self.paths.packages / fn
|
||||
files = self.sign.sign_package(full_path, base)
|
||||
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
|
||||
shutil.move(src, dst)
|
||||
package_path = self.paths.repository / fn
|
||||
package_path = self.paths.repository / name
|
||||
self.repo.add(package_path)
|
||||
|
||||
# we are iterating over bases, not single packages
|
||||
updates: Dict[str, Package] = {}
|
||||
for filename in packages:
|
||||
try:
|
||||
local = Package.load(filename, self.pacman, self.aur_url)
|
||||
local = Package.load(str(filename), PackageSource.Archive, self.pacman, self.aur_url)
|
||||
updates.setdefault(local.base, local).packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception("could not load package from %s", filename)
|
||||
|
@ -22,8 +22,10 @@ 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.exceptions import UnsafeRun
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.util import check_user
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
@ -50,7 +52,7 @@ class Properties:
|
||||
:param configuration: configuration instance
|
||||
:param no_report: force disable reporting
|
||||
"""
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
|
||||
@ -58,7 +60,11 @@ class Properties:
|
||||
self.name = configuration.get("repository", "name")
|
||||
|
||||
self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
|
||||
self.paths.create_tree()
|
||||
try:
|
||||
check_user(self.paths.root)
|
||||
self.paths.tree_create()
|
||||
except UnsafeRun:
|
||||
self.logger.warning("root owner differs from the current user, skipping tree creation")
|
||||
|
||||
self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
|
||||
self.pacman = Pacman(configuration)
|
||||
|
@ -24,6 +24,7 @@ from ahriman.core.repository.executor import Executor
|
||||
from ahriman.core.repository.update_handler import UpdateHandler
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
|
||||
|
||||
class Repository(Executor, UpdateHandler):
|
||||
@ -39,7 +40,7 @@ class Repository(Executor, UpdateHandler):
|
||||
result: Dict[str, Package] = {}
|
||||
for full_path in filter(package_like, self.paths.repository.iterdir()):
|
||||
try:
|
||||
local = Package.load(full_path, self.pacman, self.aur_url)
|
||||
local = Package.load(str(full_path), PackageSource.Archive, self.pacman, self.aur_url)
|
||||
result.setdefault(local.base, local).packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception("could not load package from %s", full_path)
|
||||
|
@ -21,6 +21,7 @@ from typing import Iterable, List
|
||||
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
|
||||
|
||||
class UpdateHandler(Cleaner):
|
||||
@ -53,7 +54,7 @@ class UpdateHandler(Cleaner):
|
||||
continue
|
||||
|
||||
try:
|
||||
remote = Package.load(local.base, self.pacman, self.aur_url)
|
||||
remote = Package.load(local.base, PackageSource.AUR, self.pacman, self.aur_url)
|
||||
if local.is_outdated(remote, self.paths):
|
||||
self.reporter.set_pending(local.base)
|
||||
result.append(remote)
|
||||
@ -72,16 +73,16 @@ class UpdateHandler(Cleaner):
|
||||
result: List[Package] = []
|
||||
known_bases = {package.base for package in self.packages()}
|
||||
|
||||
for fn in self.paths.manual.iterdir():
|
||||
for dirname in self.paths.manual.iterdir():
|
||||
try:
|
||||
local = Package.load(fn, self.pacman, self.aur_url)
|
||||
local = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url)
|
||||
result.append(local)
|
||||
if local.base not in known_bases:
|
||||
self.reporter.set_unknown(local)
|
||||
else:
|
||||
self.reporter.set_pending(local.base)
|
||||
except Exception:
|
||||
self.logger.exception("could not add package from %s", fn)
|
||||
self.logger.exception("could not add package from %s", dirname)
|
||||
self.clear_manual()
|
||||
|
||||
return result
|
||||
|
@ -88,7 +88,7 @@ class GPG:
|
||||
default_key = configuration.get("sign", "key") if targets else None
|
||||
return targets, default_key
|
||||
|
||||
def download_key(self, server: str, key: str) -> str:
|
||||
def key_download(self, server: str, key: str) -> str:
|
||||
"""
|
||||
download key from public PGP server
|
||||
:param server: public PGP server which will be used to download the key
|
||||
@ -108,13 +108,13 @@ class GPG:
|
||||
raise
|
||||
return response.text
|
||||
|
||||
def import_key(self, server: str, key: str) -> None:
|
||||
def key_import(self, server: str, key: str) -> None:
|
||||
"""
|
||||
import key to current user and sign it locally
|
||||
:param server: public PGP server which will be used to download the key
|
||||
:param key: key ID to import
|
||||
"""
|
||||
key_body = self.download_key(server, key)
|
||||
key_body = self.key_download(server, key)
|
||||
GPG._check_output("gpg", "--import", input_data=key_body, exception=None, logger=self.logger)
|
||||
GPG._check_output("gpg", "--quick-lsign-key", key, exception=None, logger=self.logger)
|
||||
|
||||
@ -131,7 +131,7 @@ class GPG:
|
||||
logger=self.logger)
|
||||
return [path, path.parent / f"{path.name}.sig"]
|
||||
|
||||
def sign_package(self, path: Path, base: str) -> List[Path]:
|
||||
def process_sign_package(self, path: Path, base: str) -> List[Path]:
|
||||
"""
|
||||
sign package if required by configuration
|
||||
:param path: path to file to sign
|
||||
@ -146,7 +146,7 @@ class GPG:
|
||||
return [path]
|
||||
return self.process(path, key)
|
||||
|
||||
def sign_repository(self, path: Path) -> List[Path]:
|
||||
def process_sign_repository(self, path: Path) -> List[Path]:
|
||||
"""
|
||||
sign repository if required by configuration
|
||||
:note: more likely you just want to pass `repository_sign_args` to repo wrapper
|
||||
|
@ -25,7 +25,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import UnknownPackage
|
||||
from ahriman.core.repository.repository import Repository
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
|
||||
@ -126,11 +126,10 @@ class Watcher:
|
||||
"""
|
||||
for package in self.repository.packages():
|
||||
# get status of build or assign unknown
|
||||
current = self.known.get(package.base)
|
||||
if current is None:
|
||||
status = BuildStatus()
|
||||
else:
|
||||
if (current := self.known.get(package.base)) is not None:
|
||||
_, status = current
|
||||
else:
|
||||
status = BuildStatus()
|
||||
self.known[package.base] = (package, status)
|
||||
self._cache_load()
|
||||
|
||||
|
@ -111,7 +111,7 @@ class WebClient(Client):
|
||||
try:
|
||||
response = self.__session.post(self._login_url, json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not login as %s: %s", self.user, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not login as %s", self.user)
|
||||
@ -138,7 +138,7 @@ class WebClient(Client):
|
||||
try:
|
||||
response = self.__session.post(self._package_url(package.base), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not add %s: %s", package.base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not add %s", package.base)
|
||||
@ -158,7 +158,7 @@ class WebClient(Client):
|
||||
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
|
||||
for package in status_json
|
||||
]
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not get %s: %s", base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not get %s", base)
|
||||
@ -175,7 +175,7 @@ class WebClient(Client):
|
||||
|
||||
status_json = response.json()
|
||||
return InternalStatus.from_json(status_json)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
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")
|
||||
@ -192,7 +192,7 @@ class WebClient(Client):
|
||||
|
||||
status_json = response.json()
|
||||
return BuildStatus.from_json(status_json)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not get service status: %s", exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not get service status")
|
||||
@ -205,7 +205,7 @@ class WebClient(Client):
|
||||
try:
|
||||
response = self.__session.post(self._reload_auth_url)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not reload auth module: %s", exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not reload auth module")
|
||||
@ -218,7 +218,7 @@ class WebClient(Client):
|
||||
try:
|
||||
response = self.__session.delete(self._package_url(base))
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not delete %s: %s", base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not delete %s", base)
|
||||
@ -234,7 +234,7 @@ class WebClient(Client):
|
||||
try:
|
||||
response = self.__session.post(self._package_url(base), json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not update %s: %s", base, exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not update %s", base)
|
||||
@ -249,7 +249,7 @@ class WebClient(Client):
|
||||
try:
|
||||
response = self.__session.post(self._ahriman_url, json=payload)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
except requests.HTTPError as e:
|
||||
self.logger.exception("could not update service status: %s", exception_response_text(e))
|
||||
except Exception:
|
||||
self.logger.exception("could not update service status")
|
||||
|
@ -25,8 +25,9 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Set, Type
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Leaf:
|
||||
@ -53,15 +54,16 @@ class Leaf:
|
||||
return self.package.packages.keys()
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[Leaf], package: Package) -> Leaf:
|
||||
def load(cls: Type[Leaf], package: Package, paths: RepositoryPaths) -> Leaf:
|
||||
"""
|
||||
load leaf from package with dependencies
|
||||
:param package: package properties
|
||||
:param paths: repository paths instance
|
||||
:return: loaded class
|
||||
"""
|
||||
clone_dir = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
Task.fetch(clone_dir, package.git_url)
|
||||
Sources.load(clone_dir, package.git_url, paths.patches_for(package.base))
|
||||
dependencies = Package.dependencies(clone_dir)
|
||||
finally:
|
||||
shutil.rmtree(clone_dir, ignore_errors=True)
|
||||
@ -93,13 +95,14 @@ class Tree:
|
||||
self.leaves = leaves
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[Tree], packages: Iterable[Package]) -> Tree:
|
||||
def load(cls: Type[Tree], packages: Iterable[Package], paths: RepositoryPaths) -> Tree:
|
||||
"""
|
||||
load tree from packages
|
||||
:param packages: packages list
|
||||
:param paths: repository paths instance
|
||||
:return: loaded class
|
||||
"""
|
||||
return cls([Leaf.load(package) for package in packages])
|
||||
return cls([Leaf.load(package, paths) for package in packages])
|
||||
|
||||
def levels(self) -> List[List[Package]]:
|
||||
"""
|
||||
|
164
src/ahriman/core/upload/github.py
Normal file
164
src/ahriman/core/upload/github.py
Normal file
@ -0,0 +1,164 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 mimetypes
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.http_upload import HttpUpload
|
||||
from ahriman.core.util import walk
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class Github(HttpUpload):
|
||||
"""
|
||||
upload files to github releases
|
||||
:ivar gh_owner: github repository owner
|
||||
:ivar gh_repository: github repository name
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param section: settings section name
|
||||
"""
|
||||
HttpUpload.__init__(self, architecture, configuration, section)
|
||||
self.gh_owner = configuration.get(section, "owner")
|
||||
self.gh_repository = configuration.get(section, "repository")
|
||||
|
||||
def asset_remove(self, release: Dict[str, Any], name: str) -> None:
|
||||
"""
|
||||
remove asset from the release by name
|
||||
:param release: release object
|
||||
:param name: asset name
|
||||
"""
|
||||
try:
|
||||
asset = next(asset for asset in release["assets"] if asset["name"] == name)
|
||||
self._request("DELETE", asset["url"])
|
||||
except StopIteration:
|
||||
self.logger.info("no asset %s found in release %s", name, release["name"])
|
||||
|
||||
def asset_upload(self, release: Dict[str, Any], path: Path) -> None:
|
||||
"""
|
||||
upload asset to the release
|
||||
:param release: release object
|
||||
:param path: path to local file
|
||||
"""
|
||||
exists = any(path.name == asset["name"] for asset in release["assets"])
|
||||
if exists:
|
||||
self.asset_remove(release, path.name)
|
||||
(url, _) = release["upload_url"].split("{") # it is parametrized url
|
||||
(mime, _) = mimetypes.guess_type(path)
|
||||
headers = {"Content-Type": mime} if mime is not None else {"Content-Type": "application/octet-stream"}
|
||||
self._request("POST", url, params={"name": path.name}, data=path.open("rb"), headers=headers)
|
||||
|
||||
def get_local_files(self, path: Path) -> Dict[Path, str]:
|
||||
"""
|
||||
get all local files and their calculated checksums
|
||||
:param path: local path to sync
|
||||
:return: map of path objects to its checksum
|
||||
"""
|
||||
return {
|
||||
local_file: self.calculate_hash(local_file)
|
||||
for local_file in walk(path)
|
||||
}
|
||||
|
||||
def files_remove(self, release: Dict[str, Any], local_files: Dict[Path, str], remote_files: Dict[str, str]) -> None:
|
||||
"""
|
||||
remove files from github
|
||||
:param release: release object
|
||||
:param local_files: map of local file paths to its checksum
|
||||
:param remote_files: map of the remote files and its checksum
|
||||
"""
|
||||
local_filenames = {local_file.name for local_file in local_files}
|
||||
for remote_file in remote_files:
|
||||
if remote_file in local_filenames:
|
||||
continue
|
||||
self.asset_remove(release, remote_file)
|
||||
|
||||
def files_upload(self, release: Dict[str, Any], local_files: Dict[Path, str], remote_files: Dict[str, str]) -> None:
|
||||
"""
|
||||
upload files to github
|
||||
:param release: release object
|
||||
:param local_files: map of local file paths to its checksum
|
||||
:param remote_files: map of the remote files and its checksum
|
||||
"""
|
||||
for local_file, checksum in local_files.items():
|
||||
remote_checksum = remote_files.get(local_file.name)
|
||||
if remote_checksum == checksum:
|
||||
continue
|
||||
self.asset_upload(release, local_file)
|
||||
|
||||
def release_create(self) -> Dict[str, Any]:
|
||||
"""
|
||||
create empty release
|
||||
:return: github API release object for the new release
|
||||
"""
|
||||
response = self._request("POST", f"https://api.github.com/repos/{self.gh_owner}/{self.gh_repository}/releases",
|
||||
json={"tag_name": self.architecture, "name": self.architecture})
|
||||
release: Dict[str, Any] = response.json()
|
||||
return release
|
||||
|
||||
def release_get(self) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
get release object if any
|
||||
:return: github API release object if release found and None otherwise
|
||||
"""
|
||||
try:
|
||||
response = self._request(
|
||||
"GET",
|
||||
f"https://api.github.com/repos/{self.gh_owner}/{self.gh_repository}/releases/tags/{self.architecture}")
|
||||
release: Dict[str, Any] = response.json()
|
||||
return release
|
||||
except requests.HTTPError as e:
|
||||
status_code = e.response.status_code if e.response is not None else None
|
||||
if status_code == 404:
|
||||
return None
|
||||
raise
|
||||
|
||||
def release_update(self, release: Dict[str, Any], body: str) -> None:
|
||||
"""
|
||||
update release
|
||||
:param release: release object
|
||||
:param body: new release body
|
||||
"""
|
||||
self._request("POST", release["url"], json={"body": body})
|
||||
|
||||
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
sync data to remote server
|
||||
:param path: local path to sync
|
||||
:param built_packages: list of packages which has just been built
|
||||
"""
|
||||
release = self.release_get()
|
||||
if release is None:
|
||||
release = self.release_create()
|
||||
|
||||
body: str = release.get("body") or ""
|
||||
remote_files = self.get_hashes(body)
|
||||
local_files = self.get_local_files(path)
|
||||
|
||||
self.files_upload(release, local_files, remote_files)
|
||||
self.files_remove(release, local_files, remote_files)
|
||||
self.release_update(release, self.get_body(local_files))
|
96
src/ahriman/core/upload/http_upload.py
Normal file
96
src/ahriman/core/upload/http_upload.py
Normal file
@ -0,0 +1,96 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 hashlib
|
||||
import requests
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.upload import Upload
|
||||
from ahriman.core.util import exception_response_text
|
||||
|
||||
|
||||
class HttpUpload(Upload):
|
||||
"""
|
||||
helper for the http based uploads
|
||||
:ivar auth: HTTP auth object
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param section: configuration section name
|
||||
"""
|
||||
Upload.__init__(self, architecture, configuration)
|
||||
password = configuration.get(section, "password")
|
||||
username = configuration.get(section, "username")
|
||||
self.auth = (password, username)
|
||||
|
||||
@staticmethod
|
||||
def calculate_hash(path: Path) -> str:
|
||||
"""
|
||||
calculate file checksum
|
||||
:param path: path to local file
|
||||
:return: calculated checksum of the file
|
||||
"""
|
||||
with path.open("rb") as local_file:
|
||||
md5 = hashlib.md5(local_file.read()) # nosec
|
||||
return md5.hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def get_body(local_files: Dict[Path, str]) -> str:
|
||||
"""
|
||||
generate release body from the checksums as returned from HttpUpload.get_hashes method
|
||||
:param local_files: map of the paths to its checksum
|
||||
:return: body to be inserted into release
|
||||
"""
|
||||
return "\n".join(f"{file.name} {md5}" for file, md5 in sorted(local_files.items()))
|
||||
|
||||
@staticmethod
|
||||
def get_hashes(body: str) -> Dict[str, str]:
|
||||
"""
|
||||
get checksums of the content from the repository
|
||||
:param body: release string body object
|
||||
:return: map of the filename to its checksum as it is written in body
|
||||
"""
|
||||
files = {}
|
||||
for line in body.splitlines():
|
||||
file, md5 = line.split()
|
||||
files[file] = md5
|
||||
return files
|
||||
|
||||
def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
|
||||
"""
|
||||
request wrapper
|
||||
:param method: request method
|
||||
:param url: request url
|
||||
:param kwargs: request parameters to be passed as is
|
||||
:return: request response object
|
||||
"""
|
||||
try:
|
||||
response = requests.request(method, url, auth=self.auth, **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))
|
||||
raise
|
||||
return response
|
@ -35,15 +35,16 @@ class Rsync(Upload):
|
||||
|
||||
_check_output = check_output
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
:param section: settings section name
|
||||
"""
|
||||
Upload.__init__(self, architecture, configuration)
|
||||
self.command = configuration.getlist("rsync", "command")
|
||||
self.remote = configuration.get("rsync", "remote")
|
||||
self.command = configuration.getlist(section, "command")
|
||||
self.remote = configuration.get(section, "remote")
|
||||
|
||||
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
|
@ -22,10 +22,11 @@ import hashlib
|
||||
import mimetypes
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generator, Iterable
|
||||
from typing import Any, Dict, Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.upload.upload import Upload
|
||||
from ahriman.core.util import walk
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@ -36,15 +37,15 @@ class S3(Upload):
|
||||
:ivar chunk_size: chunk size for calculating checksums
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
"""
|
||||
Upload.__init__(self, architecture, configuration)
|
||||
self.bucket = self.get_bucket(configuration)
|
||||
self.chunk_size = configuration.getint("s3", "chunk_size", fallback=8 * 1024 * 1024)
|
||||
self.bucket = self.get_bucket(configuration, section)
|
||||
self.chunk_size = configuration.getint(section, "chunk_size", fallback=8 * 1024 * 1024)
|
||||
|
||||
@staticmethod
|
||||
def calculate_etag(path: Path, chunk_size: int) -> str:
|
||||
@ -69,20 +70,21 @@ class S3(Upload):
|
||||
return f"{checksum.hexdigest()}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def get_bucket(configuration: Configuration) -> Any:
|
||||
def get_bucket(configuration: Configuration, section: str) -> Any:
|
||||
"""
|
||||
create resource client from configuration
|
||||
:param configuration: configuration instance
|
||||
:param section: settings section name
|
||||
:return: amazon client
|
||||
"""
|
||||
client = boto3.resource(service_name="s3",
|
||||
region_name=configuration.get("s3", "region"),
|
||||
aws_access_key_id=configuration.get("s3", "access_key"),
|
||||
aws_secret_access_key=configuration.get("s3", "secret_key"))
|
||||
return client.Bucket(configuration.get("s3", "bucket"))
|
||||
region_name=configuration.get(section, "region"),
|
||||
aws_access_key_id=configuration.get(section, "access_key"),
|
||||
aws_secret_access_key=configuration.get(section, "secret_key"))
|
||||
return client.Bucket(configuration.get(section, "bucket"))
|
||||
|
||||
@staticmethod
|
||||
def remove_files(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||
def files_remove(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||
"""
|
||||
remove files which have been removed locally
|
||||
:param local_files: map of local path object to its checksum
|
||||
@ -93,19 +95,33 @@ class S3(Upload):
|
||||
continue
|
||||
remote_object.delete()
|
||||
|
||||
def files_upload(self, path: Path, local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||
"""
|
||||
upload changed files to s3
|
||||
:param path: local path to sync
|
||||
:param local_files: map of local path object to its checksum
|
||||
:param remote_objects: map of remote path object to the remote s3 object
|
||||
"""
|
||||
for local_file, checksum in local_files.items():
|
||||
remote_object = remote_objects.get(local_file)
|
||||
# 0 and -1 elements are " (double quote)
|
||||
remote_checksum = remote_object.e_tag[1:-1] if remote_object is not None else None
|
||||
if remote_checksum == checksum:
|
||||
continue
|
||||
|
||||
local_path = path / local_file
|
||||
remote_path = Path(self.architecture) / local_file
|
||||
(mime, _) = mimetypes.guess_type(local_path)
|
||||
extra_args = {"ContentType": mime} if mime is not None else None
|
||||
|
||||
self.bucket.upload_file(Filename=str(local_path), Key=str(remote_path), ExtraArgs=extra_args)
|
||||
|
||||
def get_local_files(self, path: Path) -> Dict[Path, str]:
|
||||
"""
|
||||
get all local files and their calculated checksums
|
||||
:param path: local path to sync
|
||||
:return: map of path object to its checksum
|
||||
"""
|
||||
# credits to https://stackoverflow.com/a/64915960
|
||||
def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||
for element in directory_path.iterdir():
|
||||
if element.is_dir():
|
||||
yield from walk(element)
|
||||
continue
|
||||
yield element
|
||||
return {
|
||||
local_file.relative_to(path): self.calculate_etag(local_file, self.chunk_size)
|
||||
for local_file in walk(path)
|
||||
@ -128,26 +144,5 @@ class S3(Upload):
|
||||
remote_objects = self.get_remote_objects()
|
||||
local_files = self.get_local_files(path)
|
||||
|
||||
self.upload_files(path, local_files, remote_objects)
|
||||
self.remove_files(local_files, remote_objects)
|
||||
|
||||
def upload_files(self, path: Path, local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
|
||||
"""
|
||||
upload changed files to s3
|
||||
:param path: local path to sync
|
||||
:param local_files: map of local path object to its checksum
|
||||
:param remote_objects: map of remote path object to the remote s3 object
|
||||
"""
|
||||
for local_file, checksum in local_files.items():
|
||||
remote_object = remote_objects.get(local_file)
|
||||
# 0 and -1 elements are " (double quote)
|
||||
remote_checksum = remote_object.e_tag[1:-1] if remote_object is not None else None
|
||||
if remote_checksum == checksum:
|
||||
continue
|
||||
|
||||
local_path = path / local_file
|
||||
remote_path = Path(self.architecture) / local_file
|
||||
(mime, _) = mimetypes.guess_type(local_path)
|
||||
extra_args = {"ContentType": mime} if mime is not None else None
|
||||
|
||||
self.bucket.upload_file(Filename=str(local_path), Key=str(remote_path), ExtraArgs=extra_args)
|
||||
self.files_upload(path, local_files, remote_objects)
|
||||
self.files_remove(local_files, remote_objects)
|
||||
|
@ -44,7 +44,7 @@ class Upload:
|
||||
:param architecture: repository architecture
|
||||
:param configuration: configuration instance
|
||||
"""
|
||||
self.logger = logging.getLogger("builder")
|
||||
self.logger = logging.getLogger("root")
|
||||
self.architecture = architecture
|
||||
self.config = configuration
|
||||
|
||||
@ -57,13 +57,17 @@ class Upload:
|
||||
:param target: target to run sync (e.g. s3)
|
||||
:return: client according to current settings
|
||||
"""
|
||||
provider = UploadSettings.from_option(target)
|
||||
section, provider_name = configuration.gettype(target, architecture)
|
||||
provider = UploadSettings.from_option(provider_name)
|
||||
if provider == UploadSettings.Rsync:
|
||||
from ahriman.core.upload.rsync import Rsync
|
||||
return Rsync(architecture, configuration)
|
||||
return Rsync(architecture, configuration, section)
|
||||
if provider == UploadSettings.S3:
|
||||
from ahriman.core.upload.s3 import S3
|
||||
return S3(architecture, configuration)
|
||||
return S3(architecture, configuration, section)
|
||||
if provider == UploadSettings.Github:
|
||||
from ahriman.core.upload.github import Github
|
||||
return Github(architecture, configuration, section)
|
||||
return cls(architecture, configuration) # should never happen
|
||||
|
||||
def run(self, path: Path, built_packages: Iterable[Package]) -> None:
|
||||
|
@ -17,15 +17,35 @@
|
||||
# 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 aur # type: ignore
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import requests
|
||||
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Dict, Generator, Iterable, List, Optional, Union
|
||||
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
from ahriman.core.exceptions import InvalidOption, UnsafeRun
|
||||
|
||||
|
||||
def aur_search(*terms: str) -> List[aur.Package]:
|
||||
"""
|
||||
search in AUR by using API with multiple words. This method is required in order to handle
|
||||
https://bugs.archlinux.org/task/49133. In addition short words will be dropped
|
||||
:param terms: search terms, e.g. "ahriman", "is", "cool"
|
||||
:return: list of packages each of them matches all search terms
|
||||
"""
|
||||
packages: Dict[str, aur.Package] = {}
|
||||
for term in filter(lambda word: len(word) > 3, terms):
|
||||
portion = aur.search(term)
|
||||
packages = {
|
||||
package.package_base: package
|
||||
for package in portion
|
||||
if package.package_base in packages or not packages
|
||||
}
|
||||
return list(packages.values())
|
||||
|
||||
|
||||
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
|
||||
@ -54,6 +74,19 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
|
||||
raise exception or e
|
||||
|
||||
|
||||
def check_user(root: Path) -> None:
|
||||
"""
|
||||
check if current user is the owner of the root
|
||||
:param root: root directory (i.e. ahriman home)
|
||||
"""
|
||||
if not root.exists():
|
||||
return # no directory found, skip check
|
||||
current_uid = os.getuid()
|
||||
root_uid = root.stat().st_uid
|
||||
if current_uid != root_uid:
|
||||
raise UnsafeRun(current_uid, root_uid)
|
||||
|
||||
|
||||
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
||||
"""
|
||||
safe response exception text generation
|
||||
@ -64,6 +97,16 @@ def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
filter json object by fields used for json-to-object conversion
|
||||
:param source: raw json object
|
||||
:param known_fields: list of fields which have to be known for the target object
|
||||
:return: json object without unknown and empty fields
|
||||
"""
|
||||
return {key: value for key, value in source.items() if key in known_fields and value is not None}
|
||||
|
||||
|
||||
def package_like(filename: Path) -> bool:
|
||||
"""
|
||||
check if file looks like package
|
||||
@ -74,13 +117,17 @@ def package_like(filename: Path) -> bool:
|
||||
return ".pkg." in name and not name.endswith(".sig")
|
||||
|
||||
|
||||
def pretty_datetime(timestamp: Optional[Union[float, int]]) -> str:
|
||||
def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
|
||||
"""
|
||||
convert datetime object to string
|
||||
:param timestamp: datetime to convert
|
||||
:return: pretty printable datetime as string
|
||||
"""
|
||||
return "" if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
if timestamp is None:
|
||||
return ""
|
||||
if isinstance(timestamp, (int, float)):
|
||||
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
|
||||
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def pretty_size(size: Optional[float], level: int = 0) -> str:
|
||||
@ -106,3 +153,17 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
|
||||
if size < 1024 or level >= 3:
|
||||
return f"{size:.1f} {str_level()}"
|
||||
return pretty_size(size / 1024, level + 1)
|
||||
|
||||
|
||||
def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||
"""
|
||||
list all file paths in given directory
|
||||
Credits to https://stackoverflow.com/a/64915960
|
||||
:param directory_path: root directory path
|
||||
:return: all found files in given directory with full path
|
||||
"""
|
||||
for element in directory_path.iterdir():
|
||||
if element.is_dir():
|
||||
yield from walk(element)
|
||||
continue
|
||||
yield element
|
||||
|
33
src/ahriman/models/action.py
Normal file
33
src/ahriman/models/action.py
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 enum import Enum
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
"""
|
||||
base action enumeration
|
||||
:cvar List: list available values
|
||||
:cvar Remove: remove everything from local storage
|
||||
:cvar Update: update local storage or add to
|
||||
"""
|
||||
|
||||
List = "list"
|
||||
Remove = "remove"
|
||||
Update = "update"
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
@ -33,9 +33,9 @@ class AuthSettings(Enum):
|
||||
:cvar OAuth: OAuth based provider
|
||||
"""
|
||||
|
||||
Disabled = auto()
|
||||
Configuration = auto()
|
||||
OAuth = auto()
|
||||
Disabled = "disabled"
|
||||
Configuration = "configuration"
|
||||
OAuth = "oauth2"
|
||||
|
||||
@classmethod
|
||||
def from_option(cls: Type[AuthSettings], value: str) -> AuthSettings:
|
||||
|
@ -21,10 +21,11 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
from dataclasses import dataclass, fields
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional, Type, Union
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.core.util import filter_json, pretty_datetime
|
||||
|
||||
|
||||
class BuildStatusEnum(Enum):
|
||||
@ -74,6 +75,7 @@ class BuildStatusEnum(Enum):
|
||||
return "secondary"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildStatus:
|
||||
"""
|
||||
build status holder
|
||||
@ -81,15 +83,14 @@ class BuildStatus:
|
||||
:ivar timestamp: build status update time
|
||||
"""
|
||||
|
||||
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
|
||||
timestamp: Optional[int] = None) -> None:
|
||||
status: BuildStatusEnum = BuildStatusEnum.Unknown
|
||||
timestamp: int = int(datetime.datetime.utcnow().timestamp())
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""
|
||||
default constructor
|
||||
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
|
||||
:param timestamp: build status timestamp. Current timestamp will be used if not set
|
||||
convert status to enum type
|
||||
"""
|
||||
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
|
||||
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
|
||||
self.status = BuildStatusEnum(self.status)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
|
||||
@ -98,7 +99,8 @@ class BuildStatus:
|
||||
:param dump: json dump body
|
||||
:return: status properties
|
||||
"""
|
||||
return cls(dump.get("status"), dump.get("timestamp"))
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
def pretty_print(self) -> str:
|
||||
"""
|
||||
@ -116,20 +118,3 @@ class BuildStatus:
|
||||
"status": self.status.value,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""
|
||||
compare object to other
|
||||
:param other: other object to compare
|
||||
:return: True in case if objects are equal
|
||||
"""
|
||||
if not isinstance(other, BuildStatus):
|
||||
return False
|
||||
return self.status == other.status and self.timestamp == other.timestamp
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
generate string representation of object
|
||||
:return: unique string representation
|
||||
"""
|
||||
return f"BuildStatus(status={self.status.value}, timestamp={self.timestamp})"
|
||||
|
@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, fields
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
from ahriman.core.util import filter_json
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
|
||||
@ -54,8 +55,7 @@ class Counters:
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
dump = {key: value for key, value in dump.items() if key in known_fields}
|
||||
return cls(**dump)
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
@classmethod
|
||||
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:
|
||||
|
@ -26,12 +26,13 @@ from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from pyalpm import vercmp # type: ignore
|
||||
from srcinfo.parse import parse_srcinfo # type: ignore
|
||||
from typing import Any, Dict, List, Optional, Set, Type, Union
|
||||
from typing import Any, Dict, List, Optional, Set, Type
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.exceptions import InvalidPackageInfo
|
||||
from ahriman.core.util import check_output
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
@ -164,21 +165,24 @@ class Package:
|
||||
packages=packages)
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[Package], path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
|
||||
def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package:
|
||||
"""
|
||||
package constructor from available sources
|
||||
:param path: one of path to sources directory, path to archive or package name/base
|
||||
:param package: one of path to sources directory, path to archive or package name/base
|
||||
:param source: source of the package required to define the load method
|
||||
:param pacman: alpm wrapper instance (required to load from archive)
|
||||
:param aur_url: AUR root url
|
||||
:return: package properties
|
||||
"""
|
||||
try:
|
||||
maybe_path = Path(path)
|
||||
if maybe_path.is_dir():
|
||||
return cls.from_build(maybe_path, aur_url)
|
||||
if maybe_path.is_file():
|
||||
return cls.from_archive(maybe_path, pacman, aur_url)
|
||||
return cls.from_aur(str(path), aur_url)
|
||||
resolved_source = source.resolve(package)
|
||||
if resolved_source == PackageSource.Archive:
|
||||
return cls.from_archive(Path(package), pacman, aur_url)
|
||||
if resolved_source == PackageSource.AUR:
|
||||
return cls.from_aur(package, aur_url)
|
||||
if resolved_source == PackageSource.Local:
|
||||
return cls.from_build(Path(package), aur_url)
|
||||
raise InvalidPackageInfo(f"Unsupported local package source {resolved_source}")
|
||||
except InvalidPackageInfo:
|
||||
raise
|
||||
except Exception as e:
|
||||
@ -231,22 +235,18 @@ class Package:
|
||||
if not self.is_vcs:
|
||||
return self.version
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
|
||||
clone_dir = paths.cache / self.base
|
||||
logger = logging.getLogger("build_details")
|
||||
Task.fetch(clone_dir, self.git_url)
|
||||
Sources.load(paths.cache_for(self.base), self.git_url, paths.patches_for(self.base))
|
||||
|
||||
try:
|
||||
# update pkgver first
|
||||
Package._check_output("makepkg", "--nodeps", "--nobuild", exception=None, cwd=clone_dir, logger=logger)
|
||||
Package._check_output("makepkg", "--nodeps", "--nobuild",
|
||||
exception=None, cwd=paths.cache_for(self.base), logger=logger)
|
||||
# generate new .SRCINFO and put it to parser
|
||||
srcinfo_source = Package._check_output(
|
||||
"makepkg",
|
||||
"--printsrcinfo",
|
||||
exception=None,
|
||||
cwd=clone_dir,
|
||||
logger=logger)
|
||||
srcinfo_source = Package._check_output("makepkg", "--printsrcinfo",
|
||||
exception=None, cwd=paths.cache_for(self.base), logger=logger)
|
||||
srcinfo, errors = parse_srcinfo(srcinfo_source)
|
||||
if errors:
|
||||
raise InvalidPackageInfo(errors)
|
||||
|
@ -24,6 +24,8 @@ from pathlib import Path
|
||||
from pyalpm import Package # type: ignore
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from ahriman.core.util import filter_json
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageDescription:
|
||||
@ -70,8 +72,7 @@ class PackageDescription:
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
dump = {key: value for key, value in dump.items() if key in known_fields}
|
||||
return cls(**dump)
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
@classmethod
|
||||
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
|
||||
|
@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman.core.util import package_like
|
||||
|
||||
@ -30,14 +31,18 @@ class PackageSource(Enum):
|
||||
package source for addition enumeration
|
||||
:cvar Auto: automatically determine type of the source
|
||||
:cvar Archive: source is a package archive
|
||||
:cvar Directory: source is a directory which contains packages
|
||||
:cvar AUR: source is an AUR package for which it should search
|
||||
:cvar Directory: source is a directory which contains packages
|
||||
:cvar Local: source is locally stored PKGBUILD
|
||||
:cvar Remote: source is remote (http, ftp etc) link
|
||||
"""
|
||||
|
||||
Auto = "auto"
|
||||
Archive = "archive"
|
||||
Directory = "directory"
|
||||
AUR = "aur"
|
||||
Directory = "directory"
|
||||
Local = "local"
|
||||
Remote = "remote"
|
||||
|
||||
def resolve(self, source: str) -> PackageSource:
|
||||
"""
|
||||
@ -47,9 +52,17 @@ class PackageSource(Enum):
|
||||
"""
|
||||
if self != PackageSource.Auto:
|
||||
return self
|
||||
maybe_path = Path(source)
|
||||
|
||||
maybe_url = urlparse(source) # handle file:// like paths
|
||||
maybe_path = Path(maybe_url.path)
|
||||
|
||||
if maybe_url.scheme and maybe_url.scheme not in ("data", "file") and package_like(maybe_path):
|
||||
return PackageSource.Remote
|
||||
if (maybe_path / "PKGBUILD").is_file():
|
||||
return PackageSource.Local
|
||||
if maybe_path.is_dir():
|
||||
return PackageSource.Directory
|
||||
if maybe_path.is_file() and package_like(maybe_path):
|
||||
return PackageSource.Archive
|
||||
|
||||
return PackageSource.AUR
|
||||
|
35
src/ahriman/models/property.py
Normal file
35
src/ahriman/models/property.py
Normal file
@ -0,0 +1,35 @@
|
||||
#
|
||||
# Copyright (c) 2021 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 dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Property:
|
||||
"""
|
||||
holder of object properties descriptor
|
||||
:ivar name: name of the property
|
||||
:ivar value: property value
|
||||
:ivar is_required: if set to True then this property is required
|
||||
"""
|
||||
|
||||
name: str
|
||||
value: Any
|
||||
is_required: bool = False
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
@ -33,9 +33,9 @@ class ReportSettings(Enum):
|
||||
:cvar Email: email report generation
|
||||
"""
|
||||
|
||||
Disabled = auto() # for testing purpose
|
||||
HTML = auto()
|
||||
Email = auto()
|
||||
Disabled = "disabled" # for testing purpose
|
||||
HTML = "html"
|
||||
Email = "email"
|
||||
|
||||
@classmethod
|
||||
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
|
||||
|
@ -19,6 +19,8 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Set, Type
|
||||
@ -64,6 +66,13 @@ class RepositoryPaths:
|
||||
"""
|
||||
return self.root / "packages" / self.architecture
|
||||
|
||||
@property
|
||||
def patches(self) -> Path:
|
||||
"""
|
||||
:return: directory for source patches
|
||||
"""
|
||||
return self.root / "patches"
|
||||
|
||||
@property
|
||||
def repository(self) -> Path:
|
||||
"""
|
||||
@ -92,13 +101,60 @@ class RepositoryPaths:
|
||||
if path.is_dir()
|
||||
}
|
||||
|
||||
def create_tree(self) -> None:
|
||||
def cache_for(self, package_base: str) -> Path:
|
||||
"""
|
||||
get path to cached PKGBUILD and package sources for the package base
|
||||
:param package_base: package base name
|
||||
:return: full path to directory for specified package base cache
|
||||
"""
|
||||
return self.cache / package_base
|
||||
|
||||
def manual_for(self, package_base: str) -> Path:
|
||||
"""
|
||||
get manual path for specific package base
|
||||
:param package_base: package base name
|
||||
:return: full path to directory for specified package base manual updates
|
||||
"""
|
||||
return self.manual / package_base
|
||||
|
||||
def patches_for(self, package_base: str) -> Path:
|
||||
"""
|
||||
get patches path for specific package base
|
||||
:param package_base: package base name
|
||||
:return: full path to directory for specified package base patches
|
||||
"""
|
||||
return self.patches / package_base
|
||||
|
||||
def sources_for(self, package_base: str) -> Path:
|
||||
"""
|
||||
get path to directory from where build will start for the package base
|
||||
:param package_base: package base name
|
||||
:return: full path to directory for specified package base sources
|
||||
"""
|
||||
return self.sources / package_base
|
||||
|
||||
def tree_clear(self, package_base: str) -> None:
|
||||
"""
|
||||
clear package specific files
|
||||
:param package_base: package base name
|
||||
"""
|
||||
for directory in (
|
||||
self.cache_for(package_base),
|
||||
self.manual_for(package_base),
|
||||
self.patches_for(package_base),
|
||||
self.sources_for(package_base)):
|
||||
shutil.rmtree(directory, ignore_errors=True)
|
||||
|
||||
def tree_create(self) -> None:
|
||||
"""
|
||||
create ahriman working tree
|
||||
"""
|
||||
self.cache.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.chroot.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.manual.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.packages.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.repository.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self.sources.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
for directory in (
|
||||
self.cache,
|
||||
self.chroot,
|
||||
self.manual,
|
||||
self.packages,
|
||||
self.patches,
|
||||
self.repository,
|
||||
self.sources):
|
||||
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
@ -32,8 +32,8 @@ class SignSettings(Enum):
|
||||
:cvar Repository: sign repository database file
|
||||
"""
|
||||
|
||||
Packages = auto()
|
||||
Repository = auto()
|
||||
Packages = "pacakges"
|
||||
Repository = "repository"
|
||||
|
||||
@classmethod
|
||||
def from_option(cls: Type[SignSettings], value: str) -> SignSettings:
|
||||
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
|
||||
@ -31,9 +31,9 @@ class SmtpSSLSettings(Enum):
|
||||
:cvar STARTTLS: use STARTTLS in normal SMTP client
|
||||
"""
|
||||
|
||||
Disabled = auto()
|
||||
SSL = auto()
|
||||
STARTTLS = auto()
|
||||
Disabled = "disabled"
|
||||
SSL = "ssl"
|
||||
STARTTLS = "starttls"
|
||||
|
||||
@classmethod
|
||||
def from_option(cls: Type[SmtpSSLSettings], value: str) -> SmtpSSLSettings:
|
||||
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from enum import Enum
|
||||
from typing import Type
|
||||
|
||||
from ahriman.core.exceptions import InvalidOption
|
||||
@ -31,11 +31,13 @@ class UploadSettings(Enum):
|
||||
:cvar Disabled: no sync will be performed, required for testing purpose
|
||||
:cvar Rsync: sync via rsync
|
||||
:cvar S3: sync to Amazon S3
|
||||
:cvar Github: sync to github releases page
|
||||
"""
|
||||
|
||||
Disabled = auto() # for testing purpose
|
||||
Rsync = auto()
|
||||
S3 = auto()
|
||||
Disabled = "disabled" # for testing purpose
|
||||
Rsync = "rsync"
|
||||
S3 = "s3"
|
||||
Github = "github"
|
||||
|
||||
@classmethod
|
||||
def from_option(cls: Type[UploadSettings], value: str) -> UploadSettings:
|
||||
@ -48,4 +50,6 @@ class UploadSettings(Enum):
|
||||
return cls.Rsync
|
||||
if value.lower() in ("s3",):
|
||||
return cls.S3
|
||||
if value.lower() in ("github",):
|
||||
return cls.Github
|
||||
raise InvalidOption(value)
|
||||
|
@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "1.4.0"
|
||||
__version__ = "1.6.2"
|
||||
|
@ -81,8 +81,7 @@ def auth_handler() -> MiddlewareType:
|
||||
"""
|
||||
@middleware
|
||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||
permission_method = getattr(handler, "get_permission", None)
|
||||
if permission_method is not None:
|
||||
if (permission_method := getattr(handler, "get_permission", None)) is not None:
|
||||
permission = await permission_method(request)
|
||||
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
|
||||
handler_instance = getattr(handler, "__self__", None)
|
||||
|
@ -18,8 +18,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import middleware, Request
|
||||
from aiohttp.web_exceptions import HTTPClientError
|
||||
from aiohttp.web_response import StreamResponse
|
||||
from aiohttp.web_exceptions import HTTPClientError, HTTPException, HTTPServerError
|
||||
from aiohttp.web_response import json_response, StreamResponse
|
||||
from logging import Logger
|
||||
|
||||
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
||||
@ -35,10 +35,15 @@ def exception_handler(logger: Logger) -> MiddlewareType:
|
||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||
try:
|
||||
return await handler(request)
|
||||
except HTTPClientError:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("exception during performing request to %s", request.path)
|
||||
raise
|
||||
except HTTPClientError as e:
|
||||
return json_response(data={"error": e.reason}, status=e.status_code)
|
||||
except HTTPServerError as e:
|
||||
logger.exception("server exception during performing request to %s", request.path)
|
||||
return json_response(data={"error": e.reason}, status=e.status_code)
|
||||
except HTTPException:
|
||||
raise # just raise 2xx and 3xx codes
|
||||
except Exception as e:
|
||||
logger.exception("unknown exception during performing request to %s", request.path)
|
||||
return json_response(data={"error": str(e)}, status=500)
|
||||
|
||||
return handle
|
||||
|
@ -17,7 +17,7 @@
|
||||
# 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 aiohttp.web import HTTPFound, Response, json_response
|
||||
from aiohttp.web import HTTPBadRequest, HTTPFound, Response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.base import BaseView
|
||||
@ -42,12 +42,11 @@ class AddView(BaseView):
|
||||
|
||||
:return: redirect to main page on success
|
||||
"""
|
||||
data = await self.extract_data(["packages"])
|
||||
|
||||
try:
|
||||
data = await self.extract_data(["packages"])
|
||||
packages = data["packages"]
|
||||
except Exception as e:
|
||||
return json_response(data=str(e), status=400)
|
||||
raise HTTPBadRequest(reason=str(e))
|
||||
|
||||
self.spawner.packages_add(packages, now=True)
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# 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 aiohttp.web import HTTPFound, Response, json_response
|
||||
from aiohttp.web import HTTPBadRequest, HTTPFound, Response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.base import BaseView
|
||||
@ -42,12 +42,11 @@ class RemoveView(BaseView):
|
||||
|
||||
:return: redirect to main page on success
|
||||
"""
|
||||
data = await self.extract_data(["packages"])
|
||||
|
||||
try:
|
||||
data = await self.extract_data(["packages"])
|
||||
packages = data["packages"]
|
||||
except Exception as e:
|
||||
return json_response(data=str(e), status=400)
|
||||
raise HTTPBadRequest(reason=str(e))
|
||||
|
||||
self.spawner.packages_remove(packages)
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# 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 aiohttp.web import HTTPFound, Response, json_response
|
||||
from aiohttp.web import HTTPBadRequest, HTTPFound, Response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.base import BaseView
|
||||
@ -42,12 +42,11 @@ class RequestView(BaseView):
|
||||
|
||||
:return: redirect to main page on success
|
||||
"""
|
||||
data = await self.extract_data(["packages"])
|
||||
|
||||
try:
|
||||
data = await self.extract_data(["packages"])
|
||||
packages = data["packages"]
|
||||
except Exception as e:
|
||||
return json_response(data=str(e), status=400)
|
||||
raise HTTPBadRequest(reason=str(e))
|
||||
|
||||
self.spawner.packages_add(packages, now=False)
|
||||
|
||||
|
@ -19,9 +19,10 @@
|
||||
#
|
||||
import aur # type: ignore
|
||||
|
||||
from aiohttp.web import Response, json_response
|
||||
from typing import Callable, Iterator
|
||||
from aiohttp.web import HTTPNotFound, Response, json_response
|
||||
from typing import Callable, List
|
||||
|
||||
from ahriman.core.util import aur_search
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
@ -43,12 +44,10 @@ class SearchView(BaseView):
|
||||
|
||||
:return: 200 with found package bases and descriptions sorted by base
|
||||
"""
|
||||
search: Iterator[str] = filter(lambda s: len(s) > 3, self.request.query.getall("for", default=[]))
|
||||
search_string = " ".join(search)
|
||||
|
||||
if not search_string:
|
||||
return json_response(data="Search string must not be empty", status=400)
|
||||
packages = aur.search(search_string)
|
||||
search: List[str] = self.request.query.getall("for", default=[])
|
||||
packages = aur_search(*search)
|
||||
if not packages:
|
||||
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
|
||||
|
||||
comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
|
||||
response = [
|
||||
|
@ -17,7 +17,7 @@
|
||||
# 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 aiohttp.web import HTTPNoContent, Response, json_response
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -53,12 +53,11 @@ class AhrimanView(BaseView):
|
||||
|
||||
:return: 204 on success
|
||||
"""
|
||||
data = await self.extract_data()
|
||||
|
||||
try:
|
||||
data = await self.extract_data()
|
||||
status = BuildStatusEnum(data["status"])
|
||||
except Exception as e:
|
||||
return json_response(data=str(e), status=400)
|
||||
raise HTTPBadRequest(reason=str(e))
|
||||
|
||||
self.service.update_self(status)
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# 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 aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackage
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
@ -88,11 +88,11 @@ class PackageView(BaseView):
|
||||
package = Package.from_json(data["package"]) if "package" in data else None
|
||||
status = BuildStatusEnum(data["status"])
|
||||
except Exception as e:
|
||||
return json_response(data=str(e), status=400)
|
||||
raise HTTPBadRequest(reason=str(e))
|
||||
|
||||
try:
|
||||
self.service.update(base, status, package)
|
||||
except UnknownPackage:
|
||||
return json_response(data=f"Package {base} is unknown, but no package body set", status=400)
|
||||
raise HTTPBadRequest(reason=f"Package {base} is unknown, but no package body set")
|
||||
|
||||
raise HTTPNoContent()
|
||||
|
@ -45,11 +45,11 @@ class LoginView(BaseView):
|
||||
"""
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
|
||||
code = self.request.query.getone("code", default=None)
|
||||
oauth_provider = self.validator
|
||||
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
|
||||
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
||||
|
||||
code = self.request.query.getone("code", default=None)
|
||||
if not code:
|
||||
raise HTTPFound(oauth_provider.get_oauth_url())
|
||||
|
||||
|
44
tests/ahriman/application/application/conftest.py
Normal file
44
tests/ahriman/application/application/conftest.py
Normal file
@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.application.packages import Packages
|
||||
from ahriman.application.application.properties import Properties
|
||||
from ahriman.application.application.repository import Repository
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application_packages(configuration: Configuration, mocker: MockerFixture) -> Packages:
|
||||
"""
|
||||
fixture for application with package functions
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||
return Packages("x86_64", configuration, no_report=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application_properties(configuration: Configuration, mocker: MockerFixture) -> Properties:
|
||||
"""
|
||||
fixture for application with properties only
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||
return Properties("x86_64", configuration, no_report=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application_repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
|
||||
"""
|
||||
fixture for application with repository functions
|
||||
:param configuration: configuration fixture
|
||||
:param mocker: mocker object
|
||||
:return: application test instance
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
|
||||
return Repository("x86_64", configuration, no_report=True)
|
26
tests/ahriman/application/application/test_application.py
Normal file
26
tests/ahriman/application/application/test_application.py
Normal file
@ -0,0 +1,26 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_finalize(application: Application, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must report and sync at the last
|
||||
"""
|
||||
report_mock = mocker.patch("ahriman.application.application.Application.report")
|
||||
sync_mock = mocker.patch("ahriman.application.application.Application.sync")
|
||||
|
||||
application._finalize([])
|
||||
report_mock.assert_called_once()
|
||||
sync_mock.assert_called_once()
|
||||
|
||||
|
||||
def test_known_packages(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return not empty list of known packages
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
|
||||
packages = application._known_packages()
|
||||
assert len(packages) > 1
|
||||
assert package_ahriman.base in packages
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user