Compare commits

...

30 Commits

Author SHA1 Message Date
b050c409cf Release 2.12.2 2023-11-13 12:26:00 +02:00
d77cf7c4bb style: highligh __del__ methods in formatting 2023-11-13 12:16:30 +02:00
e03fcbfab5 fix: fix some security issues 2023-11-12 22:22:05 +02:00
62dd77317d feat: add separated switch for status reporting 2023-11-11 15:18:17 +02:00
95056cfbe7 feat: extend result class 2023-11-10 17:09:01 +02:00
2d31a415ce fix: pass packagers object insteaed of string during rebuild 2023-11-08 16:05:27 +02:00
5cbeec40f8 build: rename push action to release 2023-11-06 23:25:29 +02:00
4a644fc80f Release 2.12.1 2023-11-06 16:31:56 +02:00
d1aeeda975 fix: show unique upstream urls only 2023-11-06 16:29:18 +02:00
53aedbb9aa Release 2.12.0 2023-11-06 15:50:11 +02:00
c941226a9b feat: add upstream and aur urls to package info modal 2023-11-06 15:46:32 +02:00
69cbbfce65 feat: serve favicon in root 2023-11-06 15:46:32 +02:00
52d69fa9f6 build: remove spaces from names 2023-11-06 14:04:15 +02:00
279ac785a1 docs: correct crossreference in docs 2023-11-06 14:04:15 +02:00
f51b8e2358 refactor: replace enum with intenum and strenum 2023-11-05 13:26:11 +02:00
b116e6fa07 feat: allow to pass repository identifier to all database methods 2023-11-05 13:26:11 +02:00
7b667c8157 build: remove unused files from tarball 2023-11-05 13:26:11 +02:00
c51463d36b chore: add metthod definition order plugin to pylint
Also reorder some methods to fix errors
2023-11-05 13:26:11 +02:00
79947af013 feat: read old web sections 2023-11-04 13:34:53 +02:00
e6d6cb006b feat: improve small screens views
also fix case when invalid repository id has been sent
2023-11-01 16:38:53 +02:00
f553cc059a fix: reset logs window manually 2023-10-31 12:33:57 +02:00
c99d4f6f0f fix: correct month serialization 2023-10-31 10:13:25 +02:00
554827cc57 feat: add patch controls to web, review web, enrich info tab (#115)
* add ability to specify one-time patch on package addition

* support vars in interface
2023-10-29 23:41:20 +02:00
8524f1eb20 refactor: drop _check_output class attribute 2023-10-23 02:24:53 +03:00
4b6feb9ae6 refactor: add ability to pass anything as environment variable to build task 2023-10-23 02:24:41 +03:00
ad901067d8 fix: make toggle button works on small displays 2023-10-21 15:18:09 +03:00
4c20897e14 fix: write new line for telegram index 2023-10-21 03:56:48 +03:00
f8cbd07259 fix: do not automatically add unknown local packages
Instead of automatic package addition now it is required to add package
manually after clone. Less magic, plus would allow to use caches for
multi-repo setup (see #109)
2023-10-21 01:33:12 +03:00
16ad96d8c6 feat: add ability to run multiple commands on success 2023-10-19 04:34:49 +03:00
6bd1636bfa feat: allow to use single web instance for all repositories (#114)
* Allow to use single web instance for any repository

* some improvements

* drop includes from user home directory, introduce new variables to docker

The old solution didn't actually work as expected, because devtools
configuration belongs to filesystem (as well as sudo one), so it was
still required to run setup command.

In order to handle additional repositories, the POSTSETUP and PRESETUP
commands variables have been introduced. FAQ has been updated as well

* raise 404 in case if repository is unknown
2023-10-17 03:58:50 +03:00
240 changed files with 12991 additions and 8755 deletions

View File

@ -42,7 +42,7 @@ ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@exa
# validate configuration
ahriman service-config-validate --exit-code
# enable services
systemctl enable ahriman-web@x86_64-github
systemctl enable ahriman-web
systemctl enable ahriman@x86_64-github.timer
if [[ -z $MINIMAL_INSTALL ]]; then
# run web service (detached)

View File

@ -67,7 +67,7 @@ ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
init-hook='import sys; sys.path.append("pylint_plugins")'
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
@ -81,7 +81,8 @@ limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
load-plugins=pylint.extensions.docparams,
definition_order,
# Pickle collected data for later comparisons.
persistent=yes
@ -227,7 +228,7 @@ name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
no-docstring-rgx=
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.

View File

@ -26,7 +26,7 @@ In order to resolve all difficult cases the `autopep8` is used. You can perform
Again, the most checks can be performed by `make check` command, though some additional guidelines must be applied:
* Every class, every function (including private and protected), every attribute must be documented. The project follows [Google style documentation](https://google.github.io/styleguide/pyguide.html). The only exception is local functions.
* Correct way to document function, if section is empty, e.g. no notes or there are no args, it should be omitted:
* Correct way to document function (if a section is empty, e.g. no notes or there are no args, it should be omitted) is the following:
```python
def foo(argument: str, *, flag: bool = False) -> int:
@ -43,7 +43,8 @@ Again, the most checks can be performed by `make check` command, though some add
flag(bool, optional): a flag (Default value = False)
Returns:
int: result
int: result with
long description
Raises:
RuntimeError: a local function error occurs
@ -64,7 +65,7 @@ Again, the most checks can be performed by `make check` command, though some add
```python
class Clazz(BaseClazz):
"""
brand-new implementation of ``BaseClazz``
brand-new implementation of :class:`BaseClazz`
Attributes:
CLAZZ_ATTRIBUTE(int): (class attribute) a brand-new class attribute
@ -92,7 +93,7 @@ Again, the most checks can be performed by `make check` command, though some add
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typinng.Optional` (e.g. `str | None` instead of `Optional[str]`).
* `classmethod` should always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
* `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
* Recommended order of function definitions in class:
```python
@ -121,10 +122,12 @@ Again, the most checks can be performed by `make check` command, though some add
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
```
Methods inside one group should be ordered alphabetically, the only exception is `__init__` method (`__post__init__` for dataclasses) which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
For the most cases there is custom `pylint` plugin, which performs checks automatically.
* Abstract methods must raise `NotImplementedError` instead of using `abc.abstractmethod`. The reason behind this restriction is the fact that we have class/static abstract methods for those we need to define their attribute first making the code harder to read.
* For any path interactions `pathlib.Path` must be used.
* Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance.
@ -167,7 +170,7 @@ Again, the most checks can be performed by `make check` command, though some add
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
* Web API methods must be documented by using `aiohttp_apispec` library. Schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
* Web API methods must be documented by using `aiohttp_apispec` library. The schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):
```python
import aiohttp_apispec
@ -191,6 +194,7 @@ Again, the most checks can be performed by `make check` command, though some add
class Foo(BaseView):
POST_PERMISSION = ...
ROUTES = ...
@aiohttp_apispec.docs(
tags=["Tag"],
@ -202,6 +206,7 @@ Again, the most checks can be performed by `make check` command, though some add
400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method
401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented
403: {"description": "Access is forbidden", "schema": ErrorSchema}, # should be always presented
404: {"description": "Repository is unknown", "schema": ErrorSchema}, # include if BaseView.service() method is called
500: {"description": "Internal server error", "schema": ErrorSchema}, # should be always presented
},
security=[{"token": [POST_PERMISSION]}],
@ -215,7 +220,30 @@ Again, the most checks can be performed by `make check` command, though some add
* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced.
* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release.
* Enumerations (`Enum` classes) are allowed and recommended. However, it is recommended to use `StrEnum` class if there are from/to string conversions and `IntEnum` otherwise.
### Other checks
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
## Developers how to
### Run automated checks
```shell
make check tests
```
### Generate documentation templates
```shell
make specification
```
### Create release
```shell
make VERSION=x.y.z check tests release
```
The command above will also run checks first and will generate documentation, tags, etc., and will push them to GitHub. Other things will be handled by GitHub workflows automatically.

View File

@ -10,6 +10,8 @@ ENV AHRIMAN_OUTPUT=""
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PACMAN_MIRROR=""
ENV AHRIMAN_PORT=""
ENV AHRIMAN_POSTSETUP_COMMAND=""
ENV AHRIMAN_PRESETUP_COMMAND=""
ENV AHRIMAN_REPOSITORY="aur-clone"
ENV AHRIMAN_REPOSITORY_SERVER=""
ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"

View File

@ -1,9 +1,9 @@
.PHONY: archive archlinux check clean directory html push specification tests version
.PHONY: archive archlinux check clean directory html release specification tests version
.DEFAULT_GOAL := archlinux
PROJECT := ahriman
FILES := AUTHORS CONTRIBUTING.md COPYING Makefile README.md SECURITY.md docs package pyproject.toml src tox.ini web.png
FILES := AUTHORS CONTRIBUTING.md COPYING README.md SECURITY.md package pyproject.toml src web.png
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache
@ -37,7 +37,7 @@ html: specification
rm -rf docs/html
tox -e docs-html
push: specification archlinux
release: specification archlinux
git add package/archlinux/PKGBUILD src/ahriman/__init__.py docs/ahriman-architecture.svg package/share/man/man1/ahriman.1 package/share/bash-completion/completions/_ahriman package/share/zsh/site-functions/_ahriman
git commit -m "Release $(VERSION)"
git tag "$(VERSION)"

View File

@ -4,39 +4,39 @@
[![setup status](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml/badge.svg)](https://github.com/arcan1s/ahriman/actions/workflows/setup.yml)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/arcan1s/ahriman?label=Docker%20image)](https://hub.docker.com/r/arcan1s/ahriman)
[![CodeFactor](https://www.codefactor.io/repository/github/arcan1s/ahriman/badge)](https://www.codefactor.io/repository/github/arcan1s/ahriman)
[![Documentation Status](https://readthedocs.org/projects/ahriman/badge/?version=latest)](https://ahriman.readthedocs.io/?badge=latest)
[![Documentation Status](https://readthedocs.org/projects/ahriman/badge/?version=latest)](https://ahriman.readthedocs.io)
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
## Features
* Install-configure-forget manager for the very own repository.
* Multi-architecture support.
* Multi architecture and repository support.
* Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Various rebuild options with ability to automatically bump package version.
* Sign support with gpg (repository, package), multiple packagers support.
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Triggers for repository updates, e.g. synchronization to remote services (rsync, S3 and GitHub) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options:
![web interface](web.png)
## Installation and run
For installation details kindly refer to the [documentation](https://ahriman.readthedocs.io/en/latest/setup.html). For application commands it is possible to get information by using `--help`/`help` command or by using man page ([web version](https://ahriman.readthedocs.io/en/latest/command-line.html)).
For installation details kindly refer to the [documentation](https://ahriman.readthedocs.io/en/stable/setup.html). For application commands it is possible to get information by using `--help`/`help` command or by using man page ([web version](https://ahriman.readthedocs.io/en/stable/command-line.html)).
## Configuration
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/latest/configuration.html).
Every available option is described in the [documentation](https://ahriman.readthedocs.io/en/stable/configuration.html).
The application provides reasonable defaults which allow to use it out-of-box; however additional steps (like configuring build toolchain and sudoers) are recommended and can be easily achieved by following install instructions.
## [FAQ](https://ahriman.readthedocs.io/en/latest/faq.html)
## [FAQ](https://ahriman.readthedocs.io/en/stable/faq.html)
## Live demos
* [Build status page](https://ahriman-demo.arcanis.me). You can log in as `demo` user by using `demo` password. However, you will not be able to run tasks. [HTTP API documentation](https://ahriman-demo.arcanis.me/api-docs) is also available.
* [Repository index](http://repo.arcanis.me/arcanisrepo/x86_64/).
* [Repository index](https://repo.arcanis.me/arcanisrepo/x86_64/).
* [Telegram feed](https://t.me/arcanisrepo).

View File

@ -49,7 +49,10 @@ fi
if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then
AHRIMAN_SETUP_ARGS+=("--web-unix-socket" "$AHRIMAN_UNIX_SOCKET")
fi
[ -n "$AHRIMAN_PRESETUP_COMMAND" ] && eval "$AHRIMAN_PRESETUP_COMMAND"
ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-setup "${AHRIMAN_SETUP_ARGS[@]}"
[ -n "$AHRIMAN_POSTSETUP_COMMAND" ] && eval "$AHRIMAN_POSTSETUP_COMMAND"
# validate configuration if set
[ -n "$AHRIMAN_VALIDATE_CONFIGURATION" ] && ahriman "${AHRIMAN_DEFAULT_ARGS[@]}" service-config-validate --exit-code

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 839 KiB

After

Width:  |  Height:  |  Size: 993 KiB

View File

@ -100,6 +100,14 @@ ahriman.application.handlers.remove\_unknown module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.repositories module
------------------------------------------------
.. automodule:: ahriman.application.handlers.repositories
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.restore module
-------------------------------------------
@ -108,6 +116,14 @@ ahriman.application.handlers.restore module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.run module
---------------------------------------
.. automodule:: ahriman.application.handlers.run
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.search module
------------------------------------------

View File

@ -60,6 +60,14 @@ ahriman.core.formatters.printer module
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.repository\_printer module
--------------------------------------------------
.. automodule:: ahriman.core.formatters.repository_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.status\_printer module
----------------------------------------------

View File

@ -140,6 +140,14 @@ ahriman.models.pkgbuild\_patch module
:no-undoc-members:
:show-inheritance:
ahriman.models.process\_status module
-------------------------------------
.. automodule:: ahriman.models.process_status
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.property module
------------------------------

View File

@ -100,6 +100,14 @@ ahriman.web.schemas.package\_names\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.package\_patch\_schema module
-------------------------------------------------
.. automodule:: ahriman.web.schemas.package_patch_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.package\_properties\_schema module
------------------------------------------------------
@ -132,6 +140,22 @@ ahriman.web.schemas.pagination\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.patch\_name\_schema module
----------------------------------------------
.. automodule:: ahriman.web.schemas.patch_name_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.patch\_schema module
----------------------------------------
.. automodule:: ahriman.web.schemas.patch_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.pgp\_key\_id\_schema module
-----------------------------------------------
@ -172,6 +196,14 @@ ahriman.web.schemas.remote\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.repository\_id\_schema module
-------------------------------------------------
.. automodule:: ahriman.web.schemas.repository_id_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.search\_schema module
-----------------------------------------
@ -196,6 +228,14 @@ ahriman.web.schemas.update\_flags\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.versioned\_log\_schema module
-------------------------------------------------
.. automodule:: ahriman.web.schemas.versioned_log_schema
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -30,6 +30,14 @@ ahriman.web.views.index module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.static module
-------------------------------
.. automodule:: ahriman.web.views.static
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -28,6 +28,30 @@ ahriman.web.views.v1.status.packages module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.patch module
----------------------------------------
.. automodule:: ahriman.web.views.v1.status.patch
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.patches module
------------------------------------------
.. automodule:: ahriman.web.views.v1.status.patches
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.repositories module
-----------------------------------------------
.. automodule:: ahriman.web.views.v1.status.repositories
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.status module
-----------------------------------------

View File

@ -41,9 +41,8 @@ Base configuration settings.
* ``apply_migrations`` - perform migrations on application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations automatically.
* ``database`` - path to SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
``alpm:*`` groups
-----------------
@ -86,7 +85,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention.
* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation.
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``604800``.
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, integer, optional, default ``604800``.
``repository`` group
--------------------
@ -103,10 +102,21 @@ Settings for signing packages or repository. Group name can refer to architectur
* ``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.
``web:*`` groups
``status`` group
----------------
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.
Reporting to web service related settings. In most cases there is fallback to web section settings.
* ``enabled`` - enable reporting to web service, boolean, optional, default ``yes`` for backward compatibility.
* ``address`` - remote web service address with protocol, string, optional. In case of websocket, the ``http+unix`` scheme and url encoded address (e.g. ``%2Fvar%2Flib%2Fahriman`` for ``/var/lib/ahriman``) must be used, e.g. ``http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket``. In case if none set, it will be guessed from ``web`` section.
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
``web`` group
-------------
Web server settings. If any of ``host``/``port`` is not set, web integration will be disabled. 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``.
@ -116,15 +126,13 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
* ``host`` - host to bind, string, optional.
* ``index_url`` - full url of the repository index page, string, optional.
* ``max_body_size`` - max body size in bytes to be validated for archive upload, integer, optional. If not set, validation will be disabled.
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``port`` - port to bind, int, optional.
* ``port`` - port to bind, integer, optional.
* ``static_path`` - path to directory with static files, string, required.
* ``templates`` - path to templates directories, space separated list of strings, required.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, int, optional.
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
``keyring`` group
--------------------
@ -237,7 +245,7 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
* ``link_path`` - prefix for HTML links, string, required.
* ``no_empty_report`` - skip report generation for empty packages list, boolean, optional, default ``yes``.
* ``password`` - SMTP password to authenticate, string, optional.
* ``port`` - SMTP port for sending emails, int, required.
* ``port`` - SMTP port for sending emails, integer, required.
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
* ``sender`` - SMTP sender address, string, required.
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
@ -267,7 +275,7 @@ Section name must be either ``remote-call`` (plus optional architecture name, e.
* ``aur`` - check for AUR packages updates, boolean, optional, default ``no``.
* ``local`` - check for local packages updates, boolean, optional, default ``no``.
* ``manual`` - update manually built packages, boolean, optional, default ``no``.
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, int, optional, default ``-1``.
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, integer, optional, default ``-1``.
``telegram`` type
^^^^^^^^^^^^^^^^^
@ -282,7 +290,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
* ``template`` - Jinja2 template name, string, required.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
* ``templates`` - path to templates directories, space separated list of strings, required.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
``upload`` group
----------------
@ -312,7 +320,7 @@ This feature requires GitHub key creation (see below). Section name must be eith
#. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support).
* ``repository`` - GitHub repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
* ``use_full_release_name`` - if set to ``yes``, the release will contain both repository name and architecture, and only architecture otherwise, boolean, optional, default ``no`` (legacy behavior).
* ``username`` - GitHub authorization user, string, required. Basically the same as ``owner``.
@ -322,7 +330,7 @@ This feature requires GitHub key creation (see below). Section name must be eith
Section name must be either ``remote-service`` (plus optional architecture name, e.g. ``remote-service:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``remote-service`` if exists.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
``rsync`` type
^^^^^^^^^^^^^^
@ -341,7 +349,7 @@ Requires ``boto3`` library to be installed. Section name must be either ``s3`` (
* ``type`` - type of the upload, string, optional, must be set to ``s3`` 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.
* ``chunk_size`` - chunk size for calculating entity tags, integer, optional, default 8 * 1024 * 1024.
* ``object_path`` - path prefix for stored objects, string, optional. If none set, the prefix as in repository tree will be used.
* ``region`` - bucket region (e.g. ``eu-central-1``), string, required.
* ``secret_key`` - AWS secret access key, string, required.

View File

@ -200,6 +200,14 @@ Alternatively you can create full-diff patches, which are calculated by using ``
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).
It is also possible to create simple patch during package addition, e.g.:
.. code-block:: shell
sudo -u ahriman ahriman package-add ahriman --variable PKGEXT=.pkg.tar.xz
The ``--variable`` argument accepts variables in shell like format: quotation and lists are supported as usual, but functions are not. This feature is useful in particular in order to override specific makepkg variables during build.
How to build package from official repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -402,6 +410,8 @@ The following environment variables are supported:
* ``AHRIMAN_PACKAGER`` - packager name from which packages will be built, default is ``ahriman bot <ahriman@example.com>``.
* ``AHRIMAN_PACMAN_MIRROR`` - override pacman mirror server if set.
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty.
* ``AHRIMAN_POSTSETUP_COMMAND`` - if set, the command which will be called (as root) after the setup command, but before any other actions.
* ``AHRIMAN_PRESETUP_COMMAND`` - if set, the command which will be called (as root) right before the setup command.
* ``AHRIMAN_REPOSITORY`` - repository name, default is ``aur-clone``.
* ``AHRIMAN_REPOSITORY_SERVER`` - optional override for the repository url. Useful if you would like to download packages from remote instead of local filesystem.
* ``AHRIMAN_REPOSITORY_ROOT`` - repository root. Because of filesystem rights it is required to override default repository root. By default, it uses ``ahriman`` directory inside ahriman's home, which can be passed as mount volume.
@ -429,7 +439,7 @@ This command uses same rules as ``repo-update``, thus, e.g. requires ``--privile
Web service setup
^^^^^^^^^^^^^^^^^
Well for that you would need to have web container instance running forever; it can be achieved by the following command:
For that you would need to have web container instance running forever; it can be achieved by the following command:
.. code-block:: shell
@ -451,6 +461,20 @@ Otherwise, you would need to pass ``AHRIMAN_PORT`` and mount container network t
docker run --privileged --net=host -e AHRIMAN_PORT=8080 -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
Mutli-repository web service
""""""""""""""""""""""""""""
Idea is pretty same as to just run web service. However, it is required to run setup commands for each repository, except for one which is specified by ``AHRIMAN_REPOSITORY`` and ``AHRIMAN_ARCHITECTURE`` variables.
In order to create configuration for additional repositories, the ``AHRIMAN_POSTSETUP_COMMAND`` variable should be used, e.g.:
.. code-block:: shell
docker run --privileged -p 8080:8080 -e AHRIMAN_PORT=8080 -e AHRIMAN_UNIX_SOCKET=/var/lib/ahriman/ahriman/ahriman-web.sock -e AHRIMAN_POSTSETUP_COMMAND="ahriman --architecture x86_64 --repository aur-clone-v2 service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'" -v /path/to/local/repo:/var/lib/ahriman arcan1s/ahriman:latest
The command above will also create configuration for the repository named ``aur-clone-v2``.
Note, however, that the command above is only required in case if the service is going to be used to run subprocesses. Otherwise, everything else (web interface, status, etc) will be handled as usual.
Non-x86_64 architecture setup
-----------------------------
@ -845,12 +869,12 @@ Worker nodes configuration
.. code-block:: ini
[web]
address = master.example.com
[status]
address = https://master.example.com
username = worker-user
password = very-secure-password
As it has been mentioned above, ``web.address`` must be available for workers. In case if unix socket is used, it can be passed as ``web.unix_socket`` variable as usual. Optional ``web.username``/``web.password`` can be supplied in case if authentication was enabled on master node.
As it has been mentioned above, ``status.address`` must be available for workers. In case if unix socket is used, it can be passed in the same option as usual. Optional ``status.username``/``status.password`` can be supplied in case if authentication was enabled on master node.
#.
Each worker must call master node on success:
@ -934,7 +958,7 @@ The user ``worker-user`` has been created additionally. Worker node config (``wo
.. code-block:: ini
[web]
[status]
address = http://172.17.0.1:8080
username = worker-user
password = very-secure-password
@ -1073,7 +1097,7 @@ How to setup web service
port = 8080
#.
Start the web service ``systemctl enable --now ahriman-web@x86_64-aur-clone``.
Start the web service ``systemctl enable --now ahriman-web``.
How to enable basic authorization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -1118,7 +1142,7 @@ How to enable basic authorization
.. code-block:: ini
[web]
[status]
username = api
password = pa55w0rd
@ -1130,7 +1154,7 @@ How to enable basic authorization
sudo -u ahriman ahriman user-add -r full my-first-user
#.
Restart web service ``systemctl restart ahriman-web@x86_64-aur-clone``.
Restart web service ``systemctl restart ahriman-web``.
How to enable OAuth authorization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -1176,7 +1200,7 @@ How to enable OAuth authorization
When it will ask for the password leave it blank.
#.
Restart web service ``systemctl restart ahriman-web@x86_64-aur-clone``.
Restart web service ``systemctl restart ahriman-web``.
How to implement own interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -7,21 +7,21 @@ Features
--------
* Install-configure-forget manager for the very own repository.
* Multi-architecture support.
* Multi architecture and repository support.
* Dependency manager.
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Various rebuild options with ability to automatically bump package version.
* Sign support with gpg (repository, package), multiple packagers support.
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Triggers for repository updates, e.g. synchronization to remote services (rsync, S3 and GitHub) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options.
Live demos
----------
* `Build status page <https://ahriman-demo.arcanis.me>`_. You can login as ``demo`` user by using ``demo`` password. Note, however, you will not be able to run tasks. `HTTP API documentation <https://ahriman-demo.arcanis.me/api-docs>`_ is also available.
* `Repository index <http://repo.arcanis.me/x86_64/index.html>`_.
* `Repository index <https://repo.arcanis.me/x86_64/index.html>`_.
* `Telegram feed <https://t.me/arcanisrepo>`_.
Contents

View File

@ -1,7 +1,7 @@
Manual migrations
=================
Normally most of migrations are handled automatically after application start. However, some upgrades require manual interventions; this document describes them.
Normally the most of migrations are handled automatically after application start, however, some upgrades require manual interventions; this document describes them.
Upgrades to breakpoints
-----------------------
@ -13,8 +13,8 @@ This release includes major upgrade for the newest devtools and archlinux reposi
#. Upgrade to the latest major release of python (3.11) (required by other changes).
#. Upgrade devtools to the latest release.
#. Backup your settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default.
#. Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as you used before. This step can be done manually by moving ``devtools`` configuration (something like ``/usr/share/devtools/pacman-ahriman*.conf``) to new location ``/usr/share/devtools/pacman.conf.d/`` under name ``ahriman.conf``. After that make sure to remove any ``community`` mentions from configurations (e.g. ``/usr/share/devtools/pacman.conf.d/ahriman.conf``, ``/etc/ahriman.ini``) if there were any. The only thing which will change is ``devtools`` configuration.
#. Backup local settings, ``/etc/ahriman.ini.d/00-setup-overrides.ini`` by default.
#. Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as used before. This step can be done manually by moving ``devtools`` configuration (something like ``/usr/share/devtools/pacman-ahriman*.conf``) to new location ``/usr/share/devtools/pacman.conf.d/`` under name ``ahriman.conf``. After that make sure to remove any ``community`` mentions from configurations (e.g. ``/usr/share/devtools/pacman.conf.d/ahriman.conf``, ``/etc/ahriman.ini``) if there were any. The only thing which will change is ``devtools`` configuration.
#. Remove build chroot as it is incompatible, e.g. ``sudo ahriman service-clean --chroot``.
#. Run ``sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy`` in order to update local databases.
@ -27,7 +27,7 @@ This release includes paths migration. Unlike usual case, no automatic migration
* Path to pacman databases now includes repository name too, it has been changed from ``/var/lib/ahriman/pacman/x86_64`` to ``/var/lib/ahriman/pacman/aur-clone/x86_64``.
* Path to repository itself also includes repository name, from ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``.
In order to migrate to new filesystem tree the following actions are required:
In order to migrate to the new filesystem tree the following actions are required:
#.
Stop and disable all services, e.g. timer and web service:
@ -38,13 +38,13 @@ In order to migrate to new filesystem tree the following actions are required:
sudo systemctl disable --now ahriman-web@x86_64
#.
Create directory tree. It can be done by running ``ahriman service-tree-migrate`` subcommand. It performs copying between the old repository tree and the new one. Alternatively you can copy directories by hands.
Create directory tree. It can be done by running ``ahriman service-tree-migrate`` subcommand. It performs copying between the old repository tree and the new one. Alternatively directories can be copied by hands.
#.
Edit configuration in case if anything is pointing to the old path, e.g. HTML report generation, in the way in which it will be pointed to directory inside repository specific one, e.g. ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``.
Edit configuration in case if anything is pointing to the old path, e.g. HTML report generation, in the way in which it will point to the directory inside repository specific one, e.g. ``/var/lib/ahriman/repository/x86_64`` to ``/var/lib/ahriman/repository/aur-clone/x86_64``.
#.
Edit devtools pacman configuration (``/usr/share/devtools/pacman.conf.d/ahriman-x86_64.conf`` by default) replacing ``Server`` with path to your repository, e.g.:
Run setup command (i.e. ``ahriman service-setup``) again with the same arguments as used before. This step can be done manually by editing devtools pacman configuration (``/usr/share/devtools/pacman.conf.d/ahriman-x86_64.conf`` by default) replacing ``Server`` with path to the repository, e.g.:
.. code-block:: ini
@ -52,15 +52,15 @@ In order to migrate to new filesystem tree the following actions are required:
SigLevel = Optional TrustAll
Server = file:///var/lib/ahriman/repository/aur-clone/x86_64
Alternatively it can be done by running ``service-setup`` command again.
In case of manual interventions make sure to remove architecture reference from ``web`` sections (if any) to avoid ambiguity.
#.
Make sure to update remote synchronization services if any. Almost all of them rely on current repository tree by default, so you need to setup either redirects or configure to synchronize to the old locations (e.g. ``object_path`` option for S3 synchronization).
Make sure to update remote synchronization services if any. Almost all of them rely on current repository tree by default, so it is required to setup either redirects or configure to synchronize to the old locations (e.g. ``object_path`` option for S3 synchronization).
#.
Enable and start services again. Unit template parameter should include both repository architecture and name, dash separated, e.g. ``x86_64-aur-clone``:
Enable and start services again. Unit template parameter should include both repository architecture and name, dash separated, e.g. ``x86_64-aur-clone``, where ``x86_64`` is the repository architecture and ``aur-clone`` is the repository name:
.. code-block:: shell
sudo systemctl enable --now ahriman@x86_64-aur-clone.timer
sudo systemctl enable --now ahriman-web@x86_64-aur-clone
sudo systemctl enable --now ahriman-web

View File

@ -81,7 +81,7 @@ Initial setup
.. code-block:: shell
systemctl enable --now ahriman-web@x86_64-aur-clone
systemctl enable --now ahriman-web
#.
Add packages by using ``ahriman package-add {package}`` command:

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.11.0
pkgver=2.12.2
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
@ -46,7 +46,7 @@ package() {
python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl"
# thanks too PEP517, which we all wanted, you need to install data files manually nowadays
pushd package && find . -type f -exec install -Dm644 "{}" "$pkgdir/usr/{}" \; && popd
pushd package && find . \( -type f -or -type l \) -exec install -Dm644 "{}" "$pkgdir/usr/{}" \; && popd
# keep usr/share configs as reference and copy them to /etc
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"

View File

@ -13,30 +13,30 @@ post_upgrade() {
_2_9_0_1_changes() {
cat << EOF
It was found that you were upgrading from old-devtools package to the new one, which requires manual intervention:
It was found that there was an upgrade from old devtools package to the new one, which requires manual intervention:
* make sure that devtools are upgraded to the latest release;
* merge service configuration if required;
* run setup command (i.e. sudo ahriman service-setup) with the same arguments as you did before;
* remove build chroot: sudo rm -r /var/lib/ahriman/chroot/ahriman-x86_64/;
* update local databases: sudo -u ahriman ahriman update --no-aur --no-local --no-manual -yy;
* merge upstream configuration into local configuration (e.g. by using pacdiff);
* run setup command (i.e. ahriman service-setup) with the same arguments as did before;
* remove build chroot, e.g.: ahriman service-clean --chroot;
* update local databases: ahriman update --no-aur --no-local --no-manual -yy.
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html.
EOF
}
_2_12_0_1_changes() {
cat << EOF
Whereas old tree is still supported it is highly recommended to migrate to the new one:
Whereas old local tree is still supported it is highly recommended to migrate to the new one:
* stop and disable all services;
* run service-tree-migrate as ahriman user;
* edit configuration to avoid pointing to the old paths;
* update devtools pacman configuration (e.g. /usr/share/devtools/pacman.conf.d/ahriman-x86_64.conf) with new repository path;
* update synchronization services in order to support new paths (or setup redirects);
* enable web and timer services again by using x86_64-aur-clone suffix, where x86_64 is your architecture and
aur-clone is repository name;
* edit local configuration to avoid pointing to the old paths;
* run setup command (i.e. ahriman service-setup) with the same arguments as did before;
* update remote services in order to support new paths (or setup redirects) if any;
* enable web and timer services again by using x86_64-aur-clone suffix,
where x86_64 is the repository architecture and aur-clone is the repository name.
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html
For more information kindly refer to migration notes https://ahriman.readthedocs.io/en/stable/migration.html.
EOF
}

View File

@ -0,0 +1,12 @@
[Unit]
Description=ArcH linux ReposItory MANager web server
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ahriman web
User=ahriman
Group=ahriman
[Install]
WantedBy=multi-user.target

View File

@ -1,12 +0,0 @@
[Unit]
Description=ArcH linux ReposItory MANager web server (%i)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ahriman --repository-id "%I" web
User=ahriman
Group=ahriman
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1 @@
ahriman-web.service

View File

@ -3,7 +3,6 @@ include = ahriman.ini.d
logging = ahriman.ini.d/logging.ini
apply_migrations = yes
database = /var/lib/ahriman/ahriman.db
suppress_http_log_errors = yes
[alpm]
database = /var/lib/pacman
@ -62,6 +61,10 @@ ssl = disabled
template = repo-index.jinja2
templates = /usr/share/ahriman/templates
[status]
enabled = yes
suppress_http_log_errors = yes
[telegram]
template = telegram-index.jinja2
templates = /usr/share/ahriman/templates

View File

@ -6,10 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
<link rel="shortcut icon" href="/static/favicon.ico">
<script src="https://cdn.jsdelivr.net/npm/@stoplight/elements@7.13.7/web-components.min.js" integrity="sha384-aKMPitODat9Dqj3Mva9Rs9jS5Z3KPSW0sFlAOazuULJMFYhAfmORI5SlH9aWIst8" crossorigin="anonymous" type="application/javascript"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@stoplight/elements@7.13.7/styles.min.css" integrity="sha384-wPzTs1aFAoGq9gqp9NAs2YVTkFXcU2d6Bx11aKRFhVw2B7o1bCwaV9pGHTlUfD2+" crossorigin="anonymous" type="text/css">
</head>
<body>

View File

@ -1,12 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<title>{{ repository }}</title>
<title>ahriman</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
{% include "utils/style.jinja2" %}
{% include "user-style.jinja2" ignore missing %}
</head>
@ -16,7 +14,22 @@
{% include "utils/bootstrap-scripts.jinja2" %}
<div class="container">
<h1 id="badge-repository">ahriman</h1>
<nav class="navbar navbar-expand-lg">
<div class="navbar-brand"><img src="/static/logo.svg" width="30" height="30" alt=""> ahriman</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar-supported-content" aria-controls="repositories-navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="repositories-navbar-supported-content" class="collapse navbar-collapse">
<ul id="repositories" class="nav nav-tabs">
{% for repository in repositories %}
<li class="nav-item">
<a id="{{ repository.id }}-link" class="nav-link" href="#{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</a>
</li>
{% endfor %}
</ul>
</div>
</nav>
</div>
<div id="alert-placeholder" class="toast-container p3 top-0 start-50 translate-middle-x"></div>
@ -27,42 +40,43 @@
{% if not auth.enabled or auth.username is not none %}
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-box"></i> packages
<i class="bi bi-box"></i><span class="d-none d-sm-inline"> packages</span>
</button>
<ul class="dropdown-menu">
<li>
<button id="package-add-btn" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
<button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
<i class="bi bi-plus"></i> add
</button>
</li>
<li>
<button id="package-update-btn" class="btn dropdown-item" onclick="updatePackages()" hidden>
<button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden>
<i class="bi bi-play"></i> update
</button>
</li>
<li>
<button id="package-rebuild-btn" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
<button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
<i class="bi bi-arrow-clockwise"></i> rebuild
</button>
</li>
<li>
<button id="package-remove-btn" class="btn dropdown-item" onclick="removePackages()" disabled hidden>
<button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
</li>
</ul>
<button id="key-import-btn" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
<i class="bi bi-key"></i> import key
<button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
<i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span>
</button>
{% endif %}
<button type="button" class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i> reload
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
</button>
</div>
<table id="packages" class="table table-striped table-hover"
<table id="packages"
data-classes="table table-hover"
data-export-options='{"fileName": "packages"}'
data-filter-control="true"
data-filter-control-visible="false"
@ -87,13 +101,13 @@
<tr>
<th data-checkbox="true"></th>
<th data-sortable="true" data-switchable="false" data-field="base" data-filter-control="input" data-filter-control-placeholder="(any base)">package base</th>
<th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
<th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
<th data-sortable="true" data-field="packages" data-filter-control="input" data-filter-control-placeholder="(any package)">packages</th>
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
<th data-sortable="true" data-visible="false" data-field="packager" data-filter-control="select" data-filter-custom-search="filterContains" data-filter-control-placeholder="(any packager)">packager</th>
<th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th>
<th data-sortable="true" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th>
<th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th>
<th data-sortable="true" data-align="center" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th>
</tr>
</thead>
</table>
@ -117,7 +131,7 @@
{% if auth.enabled %}
<ul class="nav">
{% if auth.username is none %}
<li>{{ auth.control|safe }}</li>
<li>{{ auth.control | safe }}</li>
{% else %}
<li>
<form action="/api/v1/logout" method="post">

View File

@ -8,21 +8,21 @@
</div>
<div class="modal-body">
<div class="form-group row">
<label for="key-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
<div class="col-sm-10">
<input id="key-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
<label for="key-import-fingerprint-input" class="col-2 col-form-label">fingerprint</label>
<div class="col-10">
<input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
</div>
</div>
<div class="form-group row">
<label for="key-server-input" class="col-sm-2 col-form-label">key server</label>
<div class="col-sm-10">
<input id="key-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
<label for="key-import-server-input" class="col-2 col-form-label">key server</label>
<div class="col-10">
<input id="key-import-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<pre class="language-less"><samp id="key-body-input" class="pre-scrollable language-less"></samp><button id="key-copy-btn" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
<div class="col-2"></div>
<div class="col-10">
<pre class="language-less"><samp id="key-import-body-input" class="pre-scrollable language-less"></samp><button id="key-import-copy-button" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
</div>
</div>
@ -39,24 +39,24 @@
const keyImportModal = $("#key-import-modal");
const keyImportForm = $("#key-import-form");
keyImportModal.on("hidden.bs.modal", () => {
keyBodyInput.text("");
keyImportBodyInput.text("");
keyImportForm.trigger("reset");
});
const keyBodyInput = $("#key-body-input");
const keyCopyButton = $("#key-copy-btn");
const keyImportBodyInput = $("#key-import-body-input");
const keyImportCopyButton = $("#key-import-copy-button");
const keyFingerprintInput = $("#key-fingerprint-input");
const keyServerInput = $("#key-server-input");
const keyImportFingerprintInput = $("#key-import-fingerprint-input");
const keyImportServerInput = $("#key-import-server-input");
async function copyPgpKey() {
const logs = keyBodyInput.text();
await copyToClipboard(logs, keyCopyButton);
const logs = keyImportBodyInput.text();
await copyToClipboard(logs, keyImportCopyButton);
}
function fetchPgpKey() {
const key = keyFingerprintInput.val();
const server = keyServerInput.val();
const key = keyImportFingerprintInput.val();
const server = keyImportServerInput.val();
if (key && server) {
$.ajax({
@ -64,14 +64,14 @@
data: {"key": key, "server": server},
type: "GET",
dataType: "json",
success: response => { keyBodyInput.text(response.key); },
success: response => { keyImportBodyInput.text(response.key); },
});
}
}
function importPgpKey() {
const key = keyFingerprintInput.val();
const server = keyServerInput.val();
const key = keyImportFingerprintInput.val();
const server = keyImportServerInput.val();
if (key && server) {
$.ajax({

View File

@ -8,18 +8,18 @@
</div>
<div class="modal-body">
<div class="form-group row">
<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>
<label for="login-username" class="col-4 col-form-label">username</label>
<div class="col-8">
<input id="login-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>
<div class="col-sm-10">
<label for="login-password" class="col-4 col-form-label">password</label>
<div class="col-8">
<div class="input-group">
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
<input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="show-hide-password-btn" class="bi bi-eye"></i></button>
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="login-show-hide-password-button" class="bi bi-eye"></i></button>
</div>
</div>
</div>
@ -34,8 +34,8 @@
</div>
<script>
const passwordInput = $("#password");
const showHidePasswordButton = $("#show-hide-password-btn");
const passwordInput = $("#login-password");
const showHidePasswordButton = $("#login-show-hide-password-button");
function showPassword() {
if (passwordInput.attr("type") === "password") {

View File

@ -1,5 +1,5 @@
<div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="package-add-form" onsubmit="return false">
<div class="modal-header">
@ -8,12 +8,28 @@
</div>
<div class="modal-body">
<div class="form-group row">
<label for="package-input" class="col-sm-2 col-form-label">package</label>
<div class="col-sm-10">
<input id="package-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
<datalist id="known-packages-dlist"></datalist>
<label for="package-add-repository-input" class="col-3 col-form-label">repository</label>
<div class="col-9">
<select id="package-add-repository-input" class="form-control" required>
{% for repository in repositories %}
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group row">
<label for="package-add-input" class="col-3 col-form-label">package</label>
<div class="col-9">
<input id="package-add-input" type="text" list="package-add-known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" required>
<datalist id="package-add-known-packages-dlist"></datalist>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button id="package-add-variable-button" type="button" class="form-control btn btn-light rounded" onclick="packageAddVariableInputCreate()"><i class="bi bi-plus"></i> add environment variable </button>
</div>
</div>
<div id="package-add-variables-div" class="form-group row"></div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button>
@ -27,14 +43,21 @@
<script>
const packageAddModal = $("#package-add-modal");
const packageAddForm = $("#package-add-form");
packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); });
packageAddModal.on("shown.bs.modal", () => {
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageAddModal.on("hidden.bs.modal", () => {
packageAddVariablesDiv.empty();
packageAddForm.trigger("reset");
});
const packageInput = $("#package-input");
const knownPackagesList = $("#known-packages-dlist");
packageInput.keyup(() => {
clearTimeout(packageInput.data("timeout"));
packageInput.data("timeout", setTimeout($.proxy(() => {
const value = packageInput.val();
const packageAddInput = $("#package-add-input");
const packageAddRepositoryInput = $("#package-add-repository-input");
const packageAddKnownPackagesList = $("#package-add-known-packages-dlist");
packageAddInput.keyup(() => {
clearTimeout(packageAddInput.data("timeout"));
packageAddInput.data("timeout", setTimeout($.proxy(() => {
const value = packageAddInput.val();
if (value.length >= 3) {
$.ajax({
@ -49,30 +72,88 @@
option.innerText = `${pkg.package} (${pkg.description})`;
return option;
});
knownPackagesList.empty().append(options);
packageAddKnownPackagesList.empty().append(options);
},
});
}
}, this), 500));
});
function packagesAdd() {
const packages = packageInput.val();
const packageAddVariablesDiv = $("#package-add-variables-div");
function packageAddVariableInputCreate() {
const variableInput = document.createElement("div");
variableInput.classList.add("input-group");
variableInput.classList.add("package-add-variable");
const variableNameInput = document.createElement("input");
variableNameInput.type = "text";
variableNameInput.classList.add("form-control");
variableNameInput.classList.add("package-add-variable-name");
variableNameInput.placeholder = "name";
variableNameInput.ariaLabel = "variable name";
const variableSeparator = document.createElement("span");
variableSeparator.classList.add("input-group-text")
variableSeparator.textContent = "=";
const variableValueInput = document.createElement("input");
variableValueInput.type = "text";
variableValueInput.classList.add("form-control");
variableValueInput.classList.add("package-add-variable-value");
variableValueInput.placeholder = "value";
variableValueInput.ariaLabel = "variable value";
const variableButtonRemove = document.createElement("button");
variableButtonRemove.type = "button";
variableButtonRemove.classList.add("btn");
variableButtonRemove.classList.add("btn-outline-danger");
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
variableButtonRemove.onclick = _ => { return variableInput.remove(); };
// bring them together
variableInput.appendChild(variableNameInput);
variableInput.appendChild(variableSeparator);
variableInput.appendChild(variableValueInput);
variableInput.appendChild(variableButtonRemove);
packageAddVariablesDiv.append(variableInput);
}
function patchesParse() {
const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => {
const richElement = $(element);
return {
key: richElement.find(".package-add-variable-name").val(),
value: richElement.find(".package-add-variable-value").val(),
};
}).filter((_, patch) => patch.key).get();
return {patches: patches};
}
function packagesAdd(packages, patches, repository) {
packages = packages ?? packageAddInput.val();
patches = patches ?? patchesParse();
repository = repository ?? getRepositorySelector(packageAddRepositoryInput);
if (packages) {
packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been added`;
const onFailure = error => `Package addition failed: ${error}`;
doPackageAction("/api/v1/service/add", [packages], onSuccess, onFailure);
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches);
}
}
function packagesRequest() {
const packages = packageInput.val();
function packagesRequest(packages, patches) {
packages = packages ?? packageAddInput.val();
patches = patches ?? patchesParse();
const repository = getRepositorySelector(packageAddRepositoryInput);
if (packages) {
packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been requested`;
const onFailure = error => `Package request failed: ${error}`;
doPackageAction("/api/v1/service/request", [packages], onSuccess, onFailure);
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches);
}
}
</script>

View File

@ -6,11 +6,49 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="logs-copy-btn" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
<div class="form-group row mt-2">
<div class="col-4 col-lg-1" style="text-align: right">version</div>
<div id="package-info-version" class="col-8 col-lg-5"></div>
<div class="col-4 col-lg-1" style="text-align: right">packager</div>
<div id="package-info-packager" class="col-8 col-lg-5"></div>
</div>
<div class="form-group row mt-2">
<div class="col-4 col-lg-1" style="text-align: right">groups</div>
<div id="package-info-groups" class="col-8 col-lg-5"></div>
<div class="col-4 col-lg-1" style="text-align: right">licenses</div>
<div id="package-info-licenses" class="col-8 col-lg-5"></div>
</div>
<div class="form-group row mt-2">
<div class="col-4 col-lg-1" style="text-align: right">upstream</div>
<div id="package-info-upstream-url" class="col-8 col-lg-5"></div>
<div class="col-4 col-lg-1" style="text-align: right">AUR</div>
<div id="package-info-aur-url" class="col-8 col-lg-5"></div>
</div>
<div class="form-group row mt-2">
<div class="col-4 col-lg-1" style="text-align: right">packages</div>
<div id="package-info-packages" class="col-8 col-lg-5"></div>
<div class="col-4 col-lg-1" style="text-align: right">depends</div>
<div id="package-info-depends" class="col-8 col-lg-5"></div>
</div>
<hr class="col-12">
<h3>Environment variables</h3>
<div id="package-info-variables-div" class="form-group row"></div>
<hr class="col-12">
<h3>Build logs</h3>
<pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="package-info-logs-copy-button" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button>
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal" hidden><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal" hidden><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
</div>
</div>
</div>
@ -20,22 +58,110 @@
const packageInfoModal = $("#package-info-modal");
const packageInfoModalHeader = $("#package-info-modal-header");
const packageInfo = $("#package-info");
packageInfoModal.on("hidden.bs.modal", () => {
packageInfoAurUrl.empty();
packageInfoDepends.empty();
packageInfoGroups.empty();
packageInfoLicenses.empty();
packageInfoPackager.empty();
packageInfoPackages.empty();
packageInfoUpstreamUrl.empty();
packageInfoVersion.empty();
packageInfoVariablesDiv.empty();
packageInfoLogsInput.empty();
packageInfoModal.trigger("reset");
hideInfoControls(true);
});
const packageInfoLogsInput = $("#package-info-logs-input");
const packageInfoLogsCopyButton = $("#logs-copy-btn");
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
const packageInfoAurUrl = $("#package-info-aur-url");
const packageInfoDepends = $("#package-info-depends");
const packageInfoGroups = $("#package-info-groups");
const packageInfoLicenses = $("#package-info-licenses");
const packageInfoPackager = $("#package-info-packager");
const packageInfoPackages = $("#package-info-packages");
const packageInfoUpstreamUrl = $("#package-info-upstream-url");
const packageInfoVersion = $("#package-info-version");
const packageInfoVariablesDiv = $("#package-info-variables-div");
async function copyLogs() {
const logs = packageInfoLogsInput.text();
await copyToClipboard(logs, packageInfoLogsCopyButton);
}
function showLogs(packageBase) {
const isPackageBaseSet = packageBase !== undefined;
if (isPackageBaseSet)
packageInfoModal.data("package", packageBase); // set package base as currently used
else
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
function hideInfoControls(hidden) {
packageInfoRemoveButton.attr("hidden", hidden);
packageInfoUpdateButton.attr("hidden", hidden);
}
function insertVariable(packageBase, variable) {
const variableInput = document.createElement("div");
variableInput.classList.add("input-group");
const variableNameInput = document.createElement("input");
variableNameInput.classList.add("form-control");
variableNameInput.readOnly = true;
variableNameInput.value = variable.key;
const variableSeparator = document.createElement("span");
variableSeparator.classList.add("input-group-text")
variableSeparator.textContent = "=";
const variableValueInput = document.createElement("input");
variableValueInput.classList.add("form-control");
variableValueInput.readOnly = true;
variableValueInput.value = variable.value;
const variableButtonRemove = document.createElement("button");
variableButtonRemove.type = "button";
variableButtonRemove.classList.add("btn");
variableButtonRemove.classList.add("btn-outline-danger");
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
variableButtonRemove.onclick = _ => {
$.ajax({
url: `/api/v1/packages/${packageBase}/patches/${variable.key}`,
type: "DELETE",
dataType: "json",
success: _ => variableInput.remove(),
});
};
// bring them together
variableInput.appendChild(variableNameInput);
variableInput.appendChild(variableSeparator);
variableInput.appendChild(variableValueInput);
variableInput.appendChild(variableButtonRemove);
packageInfoVariablesDiv.append(variableInput);
}
function loadLogs(packageBase, onFailure) {
$.ajax({
url: `/api/v2/packages/${packageBase}/logs`,
data: {
architecture: repository.architecture,
repository: repository.repository,
},
type: "GET",
dataType: "json",
success: response => {
const logs = response.map(log_record => {
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
});
packageInfoLogsInput.text(logs.join("\n"));
},
error: onFailure,
});
}
function loadPackage(packageBase, onFailure) {
const headerClass = status => {
if (status === "pending") return ["bg-warning"];
if (status === "building") return ["bg-warning"];
@ -45,30 +171,96 @@
};
$.ajax({
url: `/api/v2/packages/${packageBase}/logs`,
url: `/api/v1/packages/${packageBase}`,
data: {
architecture: repository.architecture,
repository: repository.repository,
},
type: "GET",
dataType: "json",
success: response => {
packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`);
const logs = response.logs.map(log_record => {
const [timestamp, record] = log_record;
return `[${new Date(1000 * timestamp).toISOString()}] ${record}`;
});
packageInfoLogsInput.text(logs.join("\n"));
const description = response.find(Boolean);
const packages = Object.keys(description.package.packages);
const aurUrl = description.package.remote.web_url;
const upstreamUrls = Array.from(
new Set(
Object.values(description.package.packages)
.map(single => single.url)
)
).sort();
packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`);
packageInfoModalHeader.removeClass();
packageInfoModalHeader.addClass("modal-header");
headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz));
headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz));
if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again
packageInfoAurUrl.html(aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : "");
packageInfoDepends.html(listToTable(
Object.values(description.package.packages)
.reduce((accumulator, currentValue) => {
return accumulator.concat(currentValue.depends.filter(v => packages.indexOf(v) === -1))
.concat(currentValue.make_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (make)`))
.concat(currentValue.opt_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (optional)`));
}, [])
));
packageInfoGroups.html(listToTable(extractListProperties(description.package, "groups")));
packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses")));
packageInfoPackager.text(description.package.packager);
packageInfoPackages.html(listToTable(packages));
packageInfoUpstreamUrl.html(upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>"));
packageInfoVersion.text(description.package.version);
hideInfoControls(false);
},
error: (jqXHR, _, errorThrown) => {
// show failed modal in case if first time loading
if (isPackageBaseSet) {
const message = error => `Could not load package ${packageBase} logs: ${error}`;
showFailure("Load failure", message, jqXHR, errorThrown);
}
hideInfoControls(true);
onFailure(jqXHR, null, errorThrown);
},
});
}
function loadPatches(packageBase, onFailure) {
$.ajax({
url: `/api/v1/packages/${packageBase}/patches`,
type: "GET",
dataType: "json",
success: response => {
packageInfoVariablesDiv.empty();
response.map(patch => insertVariable(packageBase, patch));
},
error: onFailure,
});
}
function packageInfoRemove() {
const packageBase = packageInfoModal.data("package");
if (packageBase) return packagesRemove([packageBase]);
}
function packageInfoUpdate() {
const packageBase = packageInfoModal.data("package");
if (packageBase) return packagesAdd(packageBase, [], repository);
}
function showPackageInfo(packageBase) {
const isPackageBaseSet = packageBase !== undefined;
if (isPackageBaseSet)
packageInfoModal.data("package", packageBase); // set package base as currently used
else
packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
const onFailure = (jqXHR, _, errorThrown) => {
if (isPackageBaseSet) {
const message = error => `Could not load package ${packageBase} info: ${error}`;
showFailure("Load failure", message, jqXHR, errorThrown);
}
};
loadPackage(packageBase, onFailure);
loadPatches(packageBase, onFailure);
loadLogs(packageBase, onFailure);
if (isPackageBaseSet) packageInfoModal.modal("show");
}
</script>

View File

@ -1,5 +1,5 @@
<div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<form id="package-rebuild-form" onsubmit="return false">
<div class="modal-header">
@ -8,9 +8,19 @@
</div>
<div class="modal-body">
<div class="form-group row">
<label for="dependency-input" class="col-sm-4 col-form-label">dependency</label>
<div class="col-sm-8">
<input id="dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
<label for="package-rebuild-repository-input" class="col-3 col-form-label">repository</label>
<div class="col-9">
<select id="package-rebuild-repository-input" class="form-control" name="repository" required>
{% for repository in repositories %}
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group row">
<label for="package-rebuild-dependency-input" class="col-3 col-form-label">dependency</label>
<div class="col-9">
<input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
</div>
</div>
</div>
@ -25,17 +35,23 @@
<script>
const packageRebuildModal = $("#package-rebuild-modal");
const packageRebuildForm = $("#package-rebuild-form");
packageRebuildModal.on("shown.bs.modal", () => {
$(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
const dependencyInput = $("#dependency-input");
const packageRebuildDependencyInput = $("#package-rebuild-dependency-input");
const packageRebuildRepositoryInput = $("#package-rebuild-repository-input");
function packagesRebuild() {
const packages = dependencyInput.val();
const packages = packageRebuildDependencyInput.val();
const repository = getRepositorySelector(packageRebuildRepositoryInput);
if (packages) {
packageRebuildModal.modal("hide");
const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`;
const onFailure = error => `Repository rebuild failed: ${error}`;
doPackageAction("/api/v1/service/rebuild", [packages], onSuccess, onFailure);
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure);
}
}
</script>

View File

@ -1,9 +1,24 @@
<script>
const keyImportButton = $("#key-import-btn");
const packageAddButton = $("#package-add-btn");
const packageRebuildButton = $("#package-rebuild-btn");
const packageRemoveButton = $("#package-remove-btn");
const packageUpdateButton = $("#package-update-btn");
const keyImportButton = $("#key-import-button");
const packageAddButton = $("#package-add-button");
const packageRebuildButton = $("#package-rebuild-button");
const packageRemoveButton = $("#package-remove-button");
const packageUpdateButton = $("#package-update-button");
const packageInfoRemoveButton = $("#package-info-remove-button");
const packageInfoUpdateButton = $("#package-info-update-button");
let repository = null;
$("#repositories a").on("click", (event) => {
const element = event.target;
repository = {
architecture: element.dataset.architecture,
repository: element.dataset.repository,
};
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`);
$(`#${element.id}`).tab("show");
reload();
});
const table = $("#packages");
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
@ -13,7 +28,7 @@
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showLogs(data.id);
} else showPackageInfo(data.id);
});
table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
@ -35,14 +50,18 @@
});
});
const repositoryBadge = $("#badge-repository");
const statusBadge = $("#badge-status");
const versionBadge = $("#badge-version");
function doPackageAction(uri, packages, successText, failureText) {
function doPackageAction(uri, packages, repository, successText, failureText, data) {
const queryParams = $.param({
architecture: repository.architecture,
repository: repository.repository,
}); // it will never be empty btw
$.ajax({
url: uri,
data: JSON.stringify({packages: packages}),
url: `${uri}?${queryParams}`,
data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})),
type: "POST",
contentType: "application/json",
success: _ => {
@ -55,25 +74,30 @@
});
}
function filterListGroups() {
return extractDataList(table.bootstrapTable("getData"), "groups");
}
function filterListLicenses() {
return extractDataList(table.bootstrapTable("getData"), "licenses");
}
function filterListPackagers() {
return extractDataList(table.bootstrapTable("getData"), "packager");
}
function getRepositorySelector(selector) {
const selected = selector.find(":selected");
return {
architecture: selected.data("architecture"),
repository: selected.data("repository"),
};
}
function getSelection() {
return table.bootstrapTable("getSelections").map(row => row.id);
}
function removePackages() {
const onSuccess = update => `Packages ${update} have been removed`;
const onFailure = error => `Could not remove packages: ${error}`;
doPackageAction("/api/v1/service/remove", getSelection(), onSuccess, onFailure);
}
function updatePackages() {
const currentSelection = getSelection();
const [url, onSuccess] = currentSelection.length === 0
? ["/api/v1/service/update", _ => "Repository update has been run"]
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
const onFailure = error => `Packages update failed: ${error}`;
doPackageAction(url, currentSelection, onSuccess, onFailure);
}
function hideControls(hidden) {
keyImportButton.attr("hidden", hidden);
packageAddButton.attr("hidden", hidden);
@ -82,6 +106,24 @@
packageUpdateButton.attr("hidden", hidden);
}
function packagesRemove(packages) {
packages = packages ?? getSelection();
const onSuccess = update => `Packages ${update} have been removed`;
const onFailure = error => `Could not remove packages: ${error}`;
doPackageAction("/api/v1/service/remove", packages, repository, onSuccess, onFailure);
}
function packagesUpdate() {
const currentSelection = getSelection();
const [url, onSuccess] = currentSelection.length === 0
? ["/api/v1/service/update", _ => `Repository update has been run`]
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
const onFailure = error => `Packages update failed: ${error}`;
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
}
function reload() {
table.bootstrapTable("showLoading");
@ -95,33 +137,25 @@
$.ajax({
url: "/api/v1/packages",
data: {
architecture: repository.architecture,
repository: repository.repository,
},
type: "GET",
dataType: "json",
success: response => {
const extractListProperties = (description, property) => {
return Object.values(description.packages)
.map(pkg => pkg[property])
.reduce((left, right) => left.concat(right), []);
};
const listToTable = data => {
return Array.from(new Set(data))
.sort()
.map(entry => safe(entry))
.join("<br>");
};
const payload = response.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote.web_url;
return {
id: package_base,
base: web_url ? `<a href="${safe(web_url)}" title="${safe(package_base)}">${safe(package_base)}</a>` : safe(package_base),
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")),
timestamp: new Date(1000 * description.status.timestamp).toISOString(),
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
status: description.status.status,
};
});
@ -149,15 +183,18 @@
$.ajax({
url: "/api/v1/status",
data: {
architecture: repository.architecture,
repository: repository.repository,
},
type: "GET",
dataType: "json",
success: response => {
repositoryBadge.text(`${response.repository} ${response.architecture}`);
versionBadge.html(`<i class="bi bi-github"></i> ahriman ${safe(response.version)}`);
statusBadge
.popover("dispose")
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`)
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`)
.popover();
statusBadge.removeClass();
statusBadge.addClass("btn");
@ -166,6 +203,12 @@
});
}
function selectRepository() {
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
const element = $(`#${fragment}-link`);
element.click();
}
function statusFormat(value) {
const cellClass = status => {
if (status === "pending") return "table-warning";
@ -177,21 +220,9 @@
return {classes: cellClass(value)};
}
function filterListGroups() {
return extractDataList(table.bootstrapTable("getData"), "groups");
}
function filterListLicenses() {
return extractDataList(table.bootstrapTable("getData"), "licenses");
}
function filterListPackagers() {
return extractDataList(table.bootstrapTable("getData"), "packager");
}
$(() => {
table.bootstrapTable({});
statusBadge.popover();
reload();
selectRepository();
});
</script>

View File

@ -5,8 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/static/favicon.ico">
{% include "utils/style.jinja2" %}
{% include "user-style.jinja2" ignore missing %}
</head>

View File

@ -30,7 +30,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
</div>
<div class="container">
<table id="packages" class="table table-striped table-hover"
<table id="packages"
data-classes="table table-hover"
data-export-options='{"fileName": "packages"}'
data-filter-control="true"
data-filter-control-visible="false"
@ -53,16 +54,16 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<thead class="table-primary">
<tr>
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
<th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
<th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th>
<th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th>
<th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th>
<th data-sortable="true" data-visible="false" data-field="url">upstream url</th>
<th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th>
<th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th>
<th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th>
<th data-sortable="true" data-field="archive_size">archive size</th>
<th data-sortable="true" data-field="installed_size">installed size</th>
<th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
<th data-sortable="true" data-align="right" data-field="archive_size">archive size</th>
<th data-sortable="true" data-align="right" data-field="installed_size">installed size</th>
<th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th>
</tr>
</thead>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="256.000000pt" height="256.000000pt" viewBox="0 0 256.000000 256.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,256.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1019 2546 c-51 -18 -92 -49 -136 -103 -34 -41 -83 -73 -112 -73 -6
0 4 22 20 49 17 27 30 54 28 59 -7 17 -153 -102 -176 -143 -13 -23 -30 -40
-40 -40 -15 0 -18 9 -18 61 0 49 6 73 33 125 l33 64 -42 -40 c-50 -49 -82 -97
-98 -150 -31 -102 -43 -125 -77 -146 -19 -11 -48 -42 -65 -67 -17 -26 -62 -76
-100 -112 -69 -66 -69 -66 -69 -120 0 -30 5 -62 10 -70 5 -8 10 -10 10 -4 0 7
21 35 46 63 69 77 154 95 154 33 0 -42 -27 -126 -50 -155 l-21 -26 25 -7 c34
-8 63 10 86 56 27 53 74 105 127 142 42 29 44 32 38 68 -6 33 -3 40 22 60 25
20 36 22 96 17 83 -7 121 5 137 43 7 16 19 31 28 34 10 3 -9 3 -40 1 -65 -6
-84 8 -48 37 34 27 118 24 180 -7 57 -29 115 -32 150 -8 l24 17 -24 6 c-14 3
-27 14 -28 24 -4 17 1 18 46 13 58 -7 118 -34 147 -67 38 -44 107 -80 149 -80
36 1 38 2 21 12 -41 23 -115 108 -115 133 0 13 58 -27 94 -64 42 -43 86 -58
133 -46 28 7 28 7 6 18 -13 5 -23 19 -23 29 0 14 6 19 23 16 17 -2 32 -23 62
-85 48 -99 63 -115 123 -141 97 -41 101 -40 50 16 -54 60 -80 113 -76 149 l3
26 58 -47 c61 -50 87 -89 87 -131 0 -43 27 -93 59 -109 38 -20 37 -21 36 11 0
45 24 38 47 -14 19 -41 22 -63 20 -146 -2 -91 0 -103 25 -148 45 -83 54 -83
47 2 -6 74 11 155 32 153 20 -2 47 -101 48 -179 1 -65 -3 -88 -22 -122 -24
-46 -28 -98 -10 -138 14 -30 24 -32 31 -5 8 30 37 25 37 -6 0 -53 -23 -113
-62 -160 -52 -63 -58 -81 -58 -167 l0 -72 35 65 c40 75 98 135 103 107 4 -19
-37 -114 -69 -158 -11 -16 -49 -46 -85 -66 -64 -37 -94 -76 -94 -122 0 -20 1
-21 24 -6 19 13 25 13 35 2 17 -21 -15 -43 -73 -51 -85 -11 -126 -26 -164 -61
-35 -31 -80 -95 -67 -95 3 0 29 12 57 26 52 26 168 45 168 28 0 -12 -92 -91
-123 -105 -20 -9 -34 -8 -71 6 -52 20 -104 14 -130 -14 -17 -19 -17 -20 6 -23
12 -2 24 -11 26 -20 4 -22 -37 -23 -189 -7 -99 11 -104 10 -151 -12 -26 -13
-48 -27 -48 -30 0 -3 25 -16 55 -28 30 -13 55 -26 55 -31 0 -8 -73 -20 -119
-20 -44 0 -99 20 -130 46 -37 31 -87 46 -124 38 l-27 -6 22 -18 c16 -13 20
-22 12 -29 -18 -19 -125 42 -174 100 -45 53 -95 82 -165 95 l-40 7 52 -41 c96
-76 114 -128 31 -86 -105 52 -120 69 -147 172 -9 31 -21 48 -44 62 -41 25 -61
26 -49 2 5 -9 7 -23 5 -29 -10 -27 -32 -11 -57 39 -21 43 -26 66 -26 132 0 71
-4 87 -29 131 -32 55 -80 106 -67 71 21 -58 18 -206 -4 -206 -5 0 -30 43 -56
95 l-48 96 17 47 c21 57 21 89 2 122 -14 25 -14 25 -15 3 0 -14 -6 -23 -16
-23 -14 0 -16 8 -10 53 4 28 18 70 31 92 14 22 31 55 40 73 18 37 32 152 19
152 -5 0 -17 -21 -28 -46 -18 -45 -61 -104 -75 -104 -4 0 -4 32 0 72 6 55 16
83 40 122 24 37 34 66 37 108 6 75 -1 91 -22 51 -16 -32 -48 -44 -61 -23 -8
12 22 69 40 76 19 8 41 43 65 105 l21 52 -29 -29 c-16 -16 -48 -40 -71 -54
-23 -14 -56 -45 -72 -69 -28 -43 -29 -47 -29 -175 0 -186 -12 -220 -43 -122
-10 29 -15 62 -11 72 4 14 -2 11 -20 -9 -31 -33 -33 -65 -7 -115 21 -40 38
-112 27 -112 -16 0 -58 35 -71 59 l-14 26 -1 -37 c0 -24 13 -63 36 -108 39
-76 51 -155 24 -155 -8 0 -20 16 -27 35 l-11 35 -7 -29 c-11 -40 11 -127 43
-175 15 -22 56 -72 90 -111 70 -78 66 -86 -28 -51 -71 27 -60 7 30 -54 71 -48
106 -93 86 -112 -4 -4 -46 12 -93 37 -110 58 -109 58 -87 20 29 -49 70 -81
188 -145 114 -63 148 -94 126 -120 -13 -16 -52 -6 -82 22 -28 25 -21 0 11 -44
45 -63 100 -83 259 -99 154 -15 221 -29 272 -54 l35 -18 -35 -6 c-56 -10 -164
-26 -175 -26 -5 -1 15 -10 45 -21 62 -23 124 -27 255 -15 72 6 94 5 109 -7 28
-21 11 -37 -42 -38 l-47 -1 40 -19 c63 -31 153 -22 244 25 80 41 166 66 224
66 l38 0 -26 -35 c-15 -19 -48 -48 -73 -66 l-47 -31 72 4 c71 4 72 4 130 59
66 64 104 86 160 95 27 4 42 2 50 -8 16 -19 4 -34 -43 -58 l-39 -19 54 -1 c71
0 129 28 181 88 22 26 68 80 102 120 35 41 74 89 87 108 13 19 26 33 28 31 2
-2 -3 -46 -12 -98 -20 -128 -20 -128 26 -73 52 63 64 91 83 196 18 108 39 150
69 146 23 -3 27 -20 14 -70 -11 -42 18 -25 49 28 34 58 41 139 20 222 -21 84
-21 83 19 172 19 43 38 99 41 125 l7 46 38 -68 39 -68 0 88 c0 80 -3 93 -35
157 -41 80 -47 140 -15 140 11 0 26 -12 34 -27 15 -27 15 -27 16 35 0 54 -4
70 -29 105 -16 23 -49 57 -74 76 -56 42 -111 153 -122 248 l-7 63 58 -26 c32
-14 71 -35 86 -45 40 -29 35 -10 -18 60 -34 45 -65 72 -117 103 -118 69 -153
102 -141 132 9 22 25 20 68 -9 42 -28 43 -26 14 26 -36 65 -136 119 -217 119
-48 0 -233 88 -211 100 8 4 44 11 80 14 129 14 130 15 58 47 -62 29 -64 29
-198 23 -121 -6 -138 -5 -153 11 -25 24 -6 43 46 47 l42 3 -35 20 c-25 13 -53
19 -98 19 -54 0 -74 -6 -143 -42 -76 -39 -139 -54 -139 -33 0 6 45 32 100 60
l99 50 -55 7 c-76 9 -111 2 -214 -46 -99 -46 -155 -51 -155 -15 0 14 12 24 44
36 40 14 42 16 20 22 -37 10 -80 8 -120 -7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,4 +1,5 @@
{#simplified version of full report#}
<b>{{ repository }} update</b>
{% for package in packages %}
<a href="{{ link_path }}/{{ package.filename }}">{{ package.name }}</a> {{ package.version }}{% endfor %}
<a href="{{ link_path }}/{{ package.filename }}">{{ package.name }}</a> {{ package.version }}
{% endfor %}

View File

@ -1,4 +1,4 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js" integrity="sha384-NXgwF8Kv9SSAr+jemKKcbvQsz+teULH/a5UNJvZc6kP47hZgl62M1vGnw6gHQhb1" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-8hHkOkbWN1TLWwet/jpbJ0zbx3FJDeYJgQ8dX1mRrv/vfCfHCqFSFZYCgaMML3z9" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" integrity="sha384-u4eJN1VWrTf/FnYYQJo2kqJyVxEQf5UmWY4iUcNAoLenOEtEuCkfwc5bKvZOWBi5" crossorigin="anonymous" type="application/javascript"></script>
@ -8,14 +8,12 @@
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" integrity="sha384-IazMVNyYoUNx6357fWJoqtHYUWWCNHIXxFVtbpVgvImQNWuRP2WbHPaIb3QF8j97" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.js" integrity="sha384-GVLHfbEvuGA/RFiQ3MK2ClEJkWYJXABg55t9LpoDPZFGIsSq8xhFlQydm5poV2jW" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/export/bootstrap-table-export.min.js" integrity="sha384-jeldDadm+qM2RwGER3qVqxFgWVpAEJ7Jie+0rlYj8ni3KkQA654T8TSXDtol022X" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/resizable/bootstrap-table-resizable.js" integrity="sha384-wd8Vc6Febikdnsnk9vthRWRvMwffw246vhqiqNO3aSNe1maTEA07Vh3zAQiSyDji" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/filter-control/bootstrap-table-filter-control.js" integrity="sha384-B6xNXlSOaOFxjlKo9OW3htbox+9/DcaEcjPPEi1+pTMwH5Tzc/s2wNTYriHz7Tb8" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/export/bootstrap-table-export.min.js" integrity="sha384-g9OAB1Moamcy8+l1Q/tajHlMf6NTkS79ehKLTYbA80aQRbRhFCjrSuezv+FE2Kwe" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/resizable/bootstrap-table-resizable.js" integrity="sha384-wd8Vc6Febikdnsnk9vthRWRvMwffw246vhqiqNO3aSNe1maTEA07Vh3zAQiSyDji" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" integrity="sha384-NIqcjpr/3eZI1iNzz7hgT5rgp70qFUzkZffeCgVva9gi80B5vqcm7gn+8QvlWxko" crossorigin="anonymous" type="application/javascript"></script>
<script>
async function copyToClipboard(text, button) {
@ -36,19 +34,17 @@
}, 2000);
}
function safe(string) {
return String(string)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function extractDataList(data, column) {
const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array
return Array.from(new Set(elements)).sort();
}
function extractListProperties(description, property) {
return Object.values(description.packages)
.map(pkg => pkg[property])
.reduce((left, right) => left.concat(right), []);
}
function filterContains(text, value) {
return value.includes(text.toLowerCase().trim());
}
@ -59,7 +55,7 @@
const [minDate, maxDate] = text.split(" - ");
const buildDate = asOfStartOfDay(new Date(value));
return (buildDate >= asOfStartOfDay(new Date(minDate))) && (buildDate <= asOfStartOfDay(new Date(maxDate)));
return (buildDate >= new Date(minDate)) && (buildDate <= new Date(maxDate));
}
function filterList(index, value, field, data) {
@ -67,4 +63,33 @@
// the library removes all symbols from string, so it is just string
return value.includes(dataList[index].toLowerCase());
}
function listToTable(data) {
return Array.from(new Set(data))
.sort()
.map(entry => safe(entry))
.join("<br>");
}
function safe(string) {
return String(string)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
// because I'm tired of safe element generation
function safeLink(url, text, title) {
const element = document.createElement("a");
element.href = url;
element.innerText = text;
if (title) element.title = title;
return element;
}
Date.prototype.toISOStringShort = function() {
const pad = number => String(number).padStart(2, "0");
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
}
</script>

View File

@ -1,13 +1,13 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css" integrity="sha384-LrVLJJYk9OiJmjNDakUBU7kS9qCT8wk1j2OU7ncpsfB3QS37UPdkCuq3ZD1MugNY" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" integrity="sha384-4LISF5TTJX/fLmGSxO53rV4miRxdg84mZsxmO8Rx5jGtp/LbrixFETvWa5a6sESd" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/bootstrap-table.min.css" integrity="sha384-pTEAhytv7JmEG2D7qiW5gY0lI5EKZ9n3CNmj6Qp+U3qhnmH2qnnN9KJbVwbtMHN0" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.css" integrity="sha384-sN3NwxbjH33ZidqZnPmX+nQ5IF+LoiI7HvZSoZj5wGacmu0/q4RJfsN0xqN+LIa5" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" integrity="sha384-1sLxvR8mXzjhvFY9f8mzXl97DNLepeZ0PnRiMMdm/rQsKjsrPZPJxYle2wwT2PMg" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.21.4/dist/extensions/filter-control/bootstrap-table-filter-control.css" integrity="sha384-4Glx18jZ0Un+yDG6KUpYJ/af8hkssJ02jRASuFv23gfCl0mTXaVMPI9cB4cn3GvE" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" integrity="sha384-4Glx18jZ0Un+yDG6KUpYJ/af8hkssJ02jRASuFv23gfCl0mTXaVMPI9cB4cn3GvE" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.2.3/dist/cosmo/bootstrap.min.css" integrity="sha384-P1PBFVifKf1Ww0gS5B8A0siIeDpcFd4uU7S68LA1XMdE0R+y1WN3DR4HcLc9csRC" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/cosmo/bootstrap.min.css" integrity="sha384-RfV5VNj9uqyOdZbN0hFNmoq56291KK2Y4iKhoRAbcfBfjYlpasjxK6TefPjxiAiN" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" integrity="sha384-zLkQsiLfAQqGeIJeKLC+rcCR1YoYaQFLCL7cLDUoKE1ajKJzySpjzWGfYS2vjSG+" crossorigin="anonymous" type="text/css">

View File

@ -1,18 +1,18 @@
# AUTOMATICALLY GENERATED by `shtab`
_shtab_ahriman_subparsers=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_subparsers=('aur-search' 'search' 'help-commands-unsafe' 'help' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-repositories' 'service-run' 'run' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '--wait-timeout' '-V' '--version')
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '-V' '--version' '--wait-timeout')
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_help_option_strings=('-h' '--help')
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help')
_shtab_ahriman_help_option_strings=('-h' '--help')
_shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_help_version_option_strings=('-h' '--help')
_shtab_ahriman_version_option_strings=('-h' '--help')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
_shtab_ahriman_package_remove_option_strings=('-h' '--help')
_shtab_ahriman_remove_option_strings=('-h' '--help')
_shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
@ -50,14 +50,17 @@ _shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--depend
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_service_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_repo_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_service_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
_shtab_ahriman_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
_shtab_ahriman_repo_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
_shtab_ahriman_service_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_service_repositories_option_strings=('-h' '--help' '--id-only' '--no-id-only')
_shtab_ahriman_service_run_option_strings=('-h' '--help')
_shtab_ahriman_run_option_strings=('-h' '--help')
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
@ -73,7 +76,7 @@ _shtab_ahriman_web_option_strings=('-h' '--help')
_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help-commands-unsafe' 'help' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-repositories' 'service-run' 'run' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald')
_shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
_shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
@ -132,11 +135,11 @@ _shtab_ahriman_search__e_nargs=0
_shtab_ahriman_search___exit_code_nargs=0
_shtab_ahriman_search___info_nargs=0
_shtab_ahriman_search___no_info_nargs=0
_shtab_ahriman_help__h_nargs=0
_shtab_ahriman_help___help_nargs=0
_shtab_ahriman_help_commands_unsafe_pos_0_nargs=*
_shtab_ahriman_help_commands_unsafe__h_nargs=0
_shtab_ahriman_help_commands_unsafe___help_nargs=0
_shtab_ahriman_help__h_nargs=0
_shtab_ahriman_help___help_nargs=0
_shtab_ahriman_help_updates__h_nargs=0
_shtab_ahriman_help_updates___help_nargs=0
_shtab_ahriman_help_updates__e_nargs=0
@ -400,14 +403,20 @@ _shtab_ahriman_repo_clean___pacman_nargs=0
_shtab_ahriman_repo_clean___no_pacman_nargs=0
_shtab_ahriman_service_config__h_nargs=0
_shtab_ahriman_service_config___help_nargs=0
_shtab_ahriman_service_config___info_nargs=0
_shtab_ahriman_service_config___no_info_nargs=0
_shtab_ahriman_service_config___secure_nargs=0
_shtab_ahriman_service_config___no_secure_nargs=0
_shtab_ahriman_config__h_nargs=0
_shtab_ahriman_config___help_nargs=0
_shtab_ahriman_config___info_nargs=0
_shtab_ahriman_config___no_info_nargs=0
_shtab_ahriman_config___secure_nargs=0
_shtab_ahriman_config___no_secure_nargs=0
_shtab_ahriman_repo_config__h_nargs=0
_shtab_ahriman_repo_config___help_nargs=0
_shtab_ahriman_repo_config___info_nargs=0
_shtab_ahriman_repo_config___no_info_nargs=0
_shtab_ahriman_repo_config___secure_nargs=0
_shtab_ahriman_repo_config___no_secure_nargs=0
_shtab_ahriman_service_config_validate__h_nargs=0
@ -426,6 +435,16 @@ _shtab_ahriman_service_key_import__h_nargs=0
_shtab_ahriman_service_key_import___help_nargs=0
_shtab_ahriman_key_import__h_nargs=0
_shtab_ahriman_key_import___help_nargs=0
_shtab_ahriman_service_repositories__h_nargs=0
_shtab_ahriman_service_repositories___help_nargs=0
_shtab_ahriman_service_repositories___id_only_nargs=0
_shtab_ahriman_service_repositories___no_id_only_nargs=0
_shtab_ahriman_service_run_pos_0_nargs=+
_shtab_ahriman_service_run__h_nargs=0
_shtab_ahriman_service_run___help_nargs=0
_shtab_ahriman_run_pos_0_nargs=+
_shtab_ahriman_run__h_nargs=0
_shtab_ahriman_run___help_nargs=0
_shtab_ahriman_service_setup__h_nargs=0
_shtab_ahriman_service_setup___help_nargs=0
_shtab_ahriman_service_setup___generate_salt_nargs=0

View File

@ -1,16 +1,16 @@
.TH AHRIMAN "1" "2023\-09\-02" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2023\-11\-13" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [--wait-timeout WAIT_TIMEOUT] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [-V] [--wait-timeout WAIT_TIMEOUT] {aur-search,search,help-commands-unsafe,help,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-repositories,service-run,run,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
.SH DESCRIPTION
ArcH linux ReposItory MANager
.SH OPTIONS
.TP
\fB\-a\fR \fI\,ARCHITECTURE\/\fR, \fB\-\-architecture\fR \fI\,ARCHITECTURE\/\fR
target architectures. For several subcommands it can be used multiple times
filter by target architecture
.TP
\fB\-c\fR \fI\,CONFIGURATION\/\fR, \fB\-\-configuration\fR \fI\,CONFIGURATION\/\fR
@ -38,33 +38,33 @@ force enable or disable reporting to web service
.TP
\fB\-r\fR \fI\,REPOSITORY\/\fR, \fB\-\-repository\fR \fI\,REPOSITORY\/\fR
target repository. For several subcommands it can be used multiple times
filter by target repository
.TP
\fB\-\-unsafe\fR
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
.TP
\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
.TP
\fB\-\-wait\-timeout\fR \fI\,WAIT_TIMEOUT\/\fR
wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of
zero value, the application will wait infinitely
.TP
\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
.SH
COMMAND
.TP
\fBahriman\fR \fI\,aur\-search\/\fR
search for package
.TP
\fBahriman\fR \fI\,help\/\fR
show help message
.TP
\fBahriman\fR \fI\,help\-commands\-unsafe\/\fR
list unsafe commands
.TP
\fBahriman\fR \fI\,help\/\fR
show help message
.TP
\fBahriman\fR \fI\,help\-updates\/\fR
check for service updates
.TP
@ -155,6 +155,12 @@ validate system configuration
\fBahriman\fR \fI\,service\-key\-import\/\fR
import PGP key
.TP
\fBahriman\fR \fI\,service\-repositories\/\fR
show repositories
.TP
\fBahriman\fR \fI\,service\-run\/\fR
run multiple commands
.TP
\fBahriman\fR \fI\,service\-setup\/\fR
initial service configuration
.TP
@ -201,25 +207,25 @@ show additional package information
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 COMMAND \fI\,'ahriman help'\/\fR
usage: ahriman help [\-h] [command]
show help message for application or command and exit
.TP
\fBcommand\fR
show help message for specific command
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
usage: ahriman help\-commands\-unsafe [\-h] [command ...]
usage: ahriman help\-commands\-unsafe [\-h] [subcommand ...]
list unsafe commands as defined in default args
.TP
\fBcommand\fR
\fBsubcommand\fR
instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
otherwise
.SH COMMAND \fI\,'ahriman help'\/\fR
usage: ahriman help [\-h] [subcommand]
show help message for application or command and exit
.TP
\fBsubcommand\fR
show help message for specific command
.SH COMMAND \fI\,'ahriman help\-updates'\/\fR
usage: ahriman help\-updates [\-h] [\-e]
@ -237,7 +243,7 @@ print application and its dependencies versions
.SH COMMAND \fI\,'ahriman package\-add'\/\fR
usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-\-increment | \-\-no\-increment] [\-n] [\-y]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] [\-v VARIABLE]
package [package ...]
add existing or new package to the build queue
@ -275,6 +281,10 @@ explicitly specify the package source for this command
\fB\-u\fR \fI\,USERNAME\/\fR, \fB\-\-username\fR \fI\,USERNAME\/\fR
build as user
.TP
\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
apply specified makepkg variables to the next build
.SH COMMAND \fI\,'ahriman package\-remove'\/\fR
usage: ahriman package\-remove [\-h] package [package ...]
@ -664,11 +674,23 @@ clear directory with built packages
clear directory with pacman local database cache
.SH COMMAND \fI\,'ahriman service\-config'\/\fR
usage: ahriman service\-config [\-h] [\-\-secure | \-\-no\-secure]
usage: ahriman service\-config [\-h] [\-\-info | \-\-no\-info] [\-\-secure | \-\-no\-secure] [section] [key]
dump configuration for the specified architecture
.TP
\fBsection\fR
filter settings by section
.TP
\fBkey\fR
filter settings by key
.SH OPTIONS \fI\,'ahriman service\-config'\/\fR
.TP
\fB\-\-info\fR, \fB\-\-no\-info\fR
show additional information, e.g. configuration files
.TP
\fB\-\-secure\fR, \fB\-\-no\-secure\fR
hide passwords and secrets from output
@ -697,6 +719,25 @@ PGP key to import from public server
\fB\-\-key\-server\fR \fI\,KEY_SERVER\/\fR
key server for key import
.SH COMMAND \fI\,'ahriman service\-repositories'\/\fR
usage: ahriman service\-repositories [\-h] [\-\-id\-only | \-\-no\-id\-only]
list all available repositories
.SH OPTIONS \fI\,'ahriman service\-repositories'\/\fR
.TP
\fB\-\-id\-only\fR, \fB\-\-no\-id\-only\fR
show machine readable identifier instead
.SH COMMAND \fI\,'ahriman service\-run'\/\fR
usage: ahriman service\-run [\-h] command [command ...]
run multiple commands on success run of the previous command
.TP
\fBcommand\fR
command to be run (quoted) without ``ahriman``
.SH COMMAND \fI\,'ahriman service\-setup'\/\fR
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-from\-configuration FROM_CONFIGURATION]
[\-\-generate\-salt | \-\-no\-generate\-salt] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs]

View File

@ -52,11 +52,14 @@ _shtab_ahriman_commands() {
"repo-triggers:run triggers on empty build result as configured by settings"
"repo-update:check for packages updates and run build process if requested"
"report:generate repository report according to current settings"
"run:run multiple commands on success run of the previous command"
"search:search for package in AUR using API"
"service-clean:remove local caches"
"service-config:dump configuration for the specified architecture"
"service-config-validate:validate configuration and print found errors"
"service-key-import:import PGP key from public sources to the repository user"
"service-repositories:list all available repositories"
"service-run:run multiple commands on success run of the previous command"
"service-setup:create initial service configuration, requires root"
"service-shell:drop into python shell"
"service-tree-migrate:migrate repository tree between versions"
@ -78,17 +81,17 @@ _shtab_ahriman_commands() {
_shtab_ahriman_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"*"{-a,--architecture}"[target architectures. For several subcommands it can be used multiple times (default\: None)]:architecture:"
{-a,--architecture}"[filter by target architecture (default\: None)]:architecture:"
{-c,--configuration}"[configuration path (default\: \/etc\/ahriman.ini)]:configuration:"
"--force[force run, remove file lock (default\: False)]"
{-l,--lock}"[lock file (default\: \/tmp\/ahriman.lock)]:lock:"
"--log-handler[explicit log handler specification. If none set, the handler will be guessed from environment (default\: None)]:log_handler:(console syslog journald)"
{-q,--quiet}"[force disable any logging (default\: False)]"
{--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:"
"*"{-r,--repository}"[target repository. For several subcommands it can be used multiple times (default\: None)]:repository:"
{-r,--repository}"[filter by target repository (default\: None)]:repository:"
"--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable (default\: False)]"
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:"
"(- : *)"{-V,--version}"[show program\'s version number and exit]"
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:"
)
_shtab_ahriman_add_options=(
@ -98,8 +101,9 @@ _shtab_ahriman_add_options=(
{--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
{-s,--source}"[explicitly specify the package source for this command (default\: PackageSource.Auto)]:source:(auto archive aur directory local remote repository)"
{-s,--source}"[explicitly specify the package source for this command (default\: auto)]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user (default\: None)]:username:"
"*"{-v,--variable}"[apply specified makepkg variables to the next build (default\: None)]:variable:"
"(*):package source (base name, path to local files, remote URL):"
)
@ -130,7 +134,10 @@ _shtab_ahriman_clean_options=(
_shtab_ahriman_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--info,--no-info}"[show additional information, e.g. configuration files (default\: True)]:info:"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: True)]:secure:"
":filter settings by section (default\: None):"
":filter settings by key (default\: None):"
)
_shtab_ahriman_config_validate_options=(
@ -197,8 +204,9 @@ _shtab_ahriman_package_add_options=(
{--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
{-s,--source}"[explicitly specify the package source for this command (default\: PackageSource.Auto)]:source:(auto archive aur directory local remote repository)"
{-s,--source}"[explicitly specify the package source for this command (default\: auto)]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user (default\: None)]:username:"
"*"{-v,--variable}"[apply specified makepkg variables to the next build (default\: None)]:variable:"
"(*):package source (base name, path to local files, remote URL):"
)
@ -223,7 +231,7 @@ _shtab_ahriman_package_status_remove_options=(
_shtab_ahriman_package_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-s,--status}"[new package build status (default\: BuildStatusEnum.Success)]:status:(unknown pending building failed success)"
{-s,--status}"[new package build status (default\: success)]:status:(unknown pending building failed success)"
"(*)::set status for specified packages. If no packages supplied, service status will be updated (default\: None):"
)
@ -234,8 +242,9 @@ _shtab_ahriman_package_update_options=(
{--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
{-s,--source}"[explicitly specify the package source for this command (default\: PackageSource.Auto)]:source:(auto archive aur directory local remote repository)"
{-s,--source}"[explicitly specify the package source for this command (default\: auto)]:source:(auto archive aur directory local remote repository)"
{-u,--username}"[build as user (default\: None)]:username:"
"*"{-v,--variable}"[apply specified makepkg variables to the next build (default\: None)]:variable:"
"(*):package source (base name, path to local files, remote URL):"
)
@ -310,7 +319,10 @@ _shtab_ahriman_repo_clean_options=(
_shtab_ahriman_repo_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--info,--no-info}"[show additional information, e.g. configuration files (default\: True)]:info:"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: True)]:secure:"
":filter settings by section (default\: None):"
":filter settings by key (default\: None):"
)
_shtab_ahriman_repo_config_validate_options=(
@ -402,7 +414,7 @@ _shtab_ahriman_repo_sign_options=(
_shtab_ahriman_repo_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-s,--status}"[new status (default\: BuildStatusEnum.Success)]:status:(unknown pending building failed success)"
{-s,--status}"[new status (default\: success)]:status:(unknown pending building failed success)"
)
_shtab_ahriman_repo_sync_options=(
@ -438,6 +450,11 @@ _shtab_ahriman_report_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
_shtab_ahriman_run_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
)
_shtab_ahriman_search_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@ -457,7 +474,10 @@ _shtab_ahriman_service_clean_options=(
_shtab_ahriman_service_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--info,--no-info}"[show additional information, e.g. configuration files (default\: True)]:info:"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: True)]:secure:"
":filter settings by section (default\: None):"
":filter settings by key (default\: None):"
)
_shtab_ahriman_service_config_validate_options=(
@ -471,6 +491,16 @@ _shtab_ahriman_service_key_import_options=(
":PGP key to import from public server:"
)
_shtab_ahriman_service_repositories_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--id-only,--no-id-only}"[show machine readable identifier instead (default\: False)]:id_only:"
)
_shtab_ahriman_service_run_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
)
_shtab_ahriman_service_setup_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
@ -533,7 +563,7 @@ _shtab_ahriman_status_options=(
_shtab_ahriman_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-s,--status}"[new package build status (default\: BuildStatusEnum.Success)]:status:(unknown pending building failed success)"
{-s,--status}"[new package build status (default\: success)]:status:(unknown pending building failed success)"
"(*)::set status for specified packages. If no packages supplied, service status will be updated (default\: None):"
)
@ -561,7 +591,7 @@ _shtab_ahriman_user_add_options=(
"--key[optional PGP key used by this user. The private key must be imported (default\: None)]:key:"
"--packager[optional packager id used for build process in form of \`Name Surname \<mail\@example.com\>\` (default\: None)]:packager:"
{-p,--password}"[user password. Blank password will be treated as empty password, which is in particular must be used for OAuth2 authorization type. (default\: None)]:password:"
{-R,--role}"[user access level (default\: UserAccess.Read)]:role:(unauthorized read reporter full)"
{-R,--role}"[user access level (default\: read)]:role:(unauthorized read reporter full)"
":username for web service:"
)
@ -647,11 +677,14 @@ _shtab_ahriman() {
repo-triggers) _arguments -C -s $_shtab_ahriman_repo_triggers_options ;;
repo-update) _arguments -C -s $_shtab_ahriman_repo_update_options ;;
report) _arguments -C -s $_shtab_ahriman_report_options ;;
run) _arguments -C -s $_shtab_ahriman_run_options ;;
search) _arguments -C -s $_shtab_ahriman_search_options ;;
service-clean) _arguments -C -s $_shtab_ahriman_service_clean_options ;;
service-config) _arguments -C -s $_shtab_ahriman_service_config_options ;;
service-config-validate) _arguments -C -s $_shtab_ahriman_service_config_validate_options ;;
service-key-import) _arguments -C -s $_shtab_ahriman_service_key_import_options ;;
service-repositories) _arguments -C -s $_shtab_ahriman_service_repositories_options ;;
service-run) _arguments -C -s $_shtab_ahriman_service_run_options ;;
service-setup) _arguments -C -s $_shtab_ahriman_service_setup_options ;;
service-shell) _arguments -C -s $_shtab_ahriman_service_shell_options ;;
service-tree-migrate) _arguments -C -s $_shtab_ahriman_service_tree_migrate_options ;;

View File

@ -0,0 +1,212 @@
#
# Copyright (c) 2021-2023 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 astroid import nodes
from collections.abc import Iterable
from enum import StrEnum
from pylint.checkers import BaseRawFileChecker
from pylint.lint import PyLinter
from typing import Any
class MethodTypeOrder(StrEnum):
"""
method type enumeration
Attributes:
Class(MethodTypeOrder): (class attribute) class method
Delete(MethodTypeOrder): (class attribute) destructor-like methods
Init(MethodTypeOrder): (class attribute) initialization method
Magic(MethodTypeOrder): (class attribute) other magical methods
New(MethodTypeOrder): (class attribute) constructor method
Normal(MethodTypeOrder): (class attribute) usual method
Property(MethodTypeOrder): (class attribute) property method
Static(MethodTypeOrder): (class attribute) static method
"""
Class = "classmethod"
Delete = "del"
Init = "init"
Magic = "magic"
New = "new"
Normal = "regular"
Property = "property"
Static = "staticmethod"
class DefinitionOrder(BaseRawFileChecker):
"""
check if methods are defined in recommended order
Attributes:
DECORATED_METHODS_ORDER(list[str]): (class attribute) predefined list of known function decorators
"""
DECORATED_METHODS_ORDER = {
"cached_property": MethodTypeOrder.Property,
"classmethod": MethodTypeOrder.Class,
"property": MethodTypeOrder.Property,
"staticmethod": MethodTypeOrder.Static,
}
name = "method-ordering"
msgs = {
"W6001": (
"Invalid method order %s, expected %s",
"methods-out-of-order",
"Methods are defined out of recommended order.",
)
}
options = (
(
"method-type-order",
{
"default": [
"init",
"new",
"del",
"property",
"classmethod",
"staticmethod",
"regular",
"magic",
],
"type": "csv",
"metavar": "<comma-separated types>",
"help": "Method types order to check.",
},
),
)
@staticmethod
def methods(source: Iterable[Any], start_lineno: int = 0) -> list[nodes.FunctionDef]:
"""
extract function nodes from list of raw nodes
Args:
source(Iterable[Any]): all available nodes
start_lineno(int, optional): minimal allowed line number (Default value = 0)
Returns:
list[nodes.FunctionDef]: list of function nodes
"""
def is_defined_function(function: Any) -> bool:
return isinstance(function, nodes.FunctionDef) \
and function.lineno is not None \
and function.lineno >= start_lineno
return list(filter(is_defined_function, source))
@staticmethod
def resolve_type(function: nodes.FunctionDef) -> MethodTypeOrder:
"""
resolve type of the function
Args:
function(nodes.FunctionDef): function definition
Returns:
MethodTypeOrder: resolved function type
"""
# init methods
if function.name in ("__init__", "__post_init__"):
return MethodTypeOrder.Init
if function.name in ("__new__",):
return MethodTypeOrder.New
if function.name in ("__del__",):
return MethodTypeOrder.Delete
# decorated methods
decorators = []
if function.decorators is not None:
decorators = [getattr(decorator, "name", None) for decorator in function.decorators.get_children()]
for decorator in decorators:
if decorator in DefinitionOrder.DECORATED_METHODS_ORDER:
return DefinitionOrder.DECORATED_METHODS_ORDER[decorator]
# magic methods
if function.name.startswith("__") and function.name.endswith("__"):
return MethodTypeOrder.Magic
# normal method
return MethodTypeOrder.Normal
def check_class(self, clazz: nodes.ClassDef) -> None:
"""
check class functions ordering
Args:
clazz(nodes.ClassDef): class definition
"""
methods = self.methods(clazz.values(), clazz.lineno)
self.check_functions(methods)
def check_functions(self, functions: list[nodes.FunctionDef]) -> None:
"""
check global functions ordering
Args:
functions(list[nodes.FunctionDef]): list of functions in their defined order
"""
for real, expected in zip(functions, sorted(functions, key=self.comparator)):
if real == expected:
continue
self.add_message("methods-out-of-order", line=real.lineno, args=(real.name, expected.name))
break
def comparator(self, function: nodes.FunctionDef) -> tuple[int, str]:
"""
compare key for sorting function
Args:
function(nodes.FunctionDef): function definition
Returns:
tuple[int, str]: comparison key for the specified function definition
"""
function_type = self.resolve_type(function)
try:
function_type_index = self.linter.config.method_type_order.index(function_type)
except ValueError:
function_type_index = 10 # not in the list
return function_type_index, function.name
def process_module(self, node: nodes.Module) -> None:
"""
process module
Args:
node(nodes.Module): module node to check
"""
# check global methods
self.check_functions(self.methods(node.values()))
# check class definitions
for clazz in filter(lambda method: isinstance(method, nodes.ClassDef), node.values()):
self.check_class(clazz)
def register(linter: PyLinter) -> None:
"""
register custom checker
Args:
linter(PyLinter): linter in which checker should be registered
"""
linter.register_checker(DefinitionOrder(linter))

View File

@ -40,7 +40,7 @@ check = [
"mypy",
"pylint",
]
docs = [
docs = [
"Sphinx",
"argparse-manpage",
"pydeps",

View File

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

View File

@ -69,8 +69,7 @@ def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH linux ReposItory MANager",
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. For several subcommands it can be used "
"multiple times", action="append")
parser.add_argument("-a", "--architecture", help="filter by target architecture")
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")
@ -82,24 +81,23 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
parser.add_argument("--report", help="force enable or disable reporting to web service",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-r", "--repository", help="target repository. For several subcommands it can be used "
"multiple times", action="append")
parser.add_argument("-r", "--repository", help="filter by target repository")
# special secret argument for systemd unit. The issue is that systemd doesn't allow multiple arguments to template
# name. This parameter accepts [[arch]-repo] in order to keep backward compatibility
parser.add_argument("--repository-id", help=argparse.SUPPRESS)
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__)
parser.add_argument("--wait-timeout", help="wait for lock to be free. Negative value will lead to "
"immediate application run even if there is lock file. "
"In case of zero value, the application will wait infinitely",
type=int, default=-1)
parser.add_argument("-V", "--version", action="version", version=__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command")
_set_aur_search_parser(subparsers)
_set_help_parser(subparsers)
_set_help_commands_unsafe_parser(subparsers)
_set_help_parser(subparsers)
_set_help_updates_parser(subparsers)
_set_help_version_parser(subparsers)
_set_package_add_parser(subparsers)
@ -130,6 +128,8 @@ def _parser() -> argparse.ArgumentParser:
_set_service_config_parser(subparsers)
_set_service_config_validate_parser(subparsers)
_set_service_key_import_parser(subparsers)
_set_service_repositories(subparsers)
_set_service_run(subparsers)
_set_service_setup_parser(subparsers)
_set_service_shell_parser(subparsers)
_set_service_tree_migrate_parser(subparsers)
@ -161,8 +161,27 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
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, quiet=True, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Search, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True)
return parser
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing unsafe commands
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("subcommand", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True, parser=_parser)
return parser
@ -179,31 +198,12 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("help", help="show help message",
description="show help message for application or command and exit",
formatter_class=_formatter)
parser.add_argument("command", help="show help message for specific command", nargs="?")
parser.set_defaults(handler=handlers.Help, architecture=[""], lock=None, quiet=True, report=False, repository=[""],
parser.add_argument("subcommand", help="show help message for specific command", nargs="?")
parser.set_defaults(handler=handlers.Help, architecture="", lock=None, quiet=True, report=False, repository="",
unsafe=True, parser=_parser)
return parser
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing unsafe commands
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, quiet=True, report=False,
repository=[""], unsafe=True, parser=_parser)
return parser
def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for service update check subcommand
@ -218,8 +218,8 @@ def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="request AUR for current version and compare with current service version",
formatter_class=_formatter)
parser.add_argument("-e", "--exit-code", help="return non-zero exit code if updates available", action="store_true")
parser.set_defaults(handler=handlers.ServiceUpdates, architecture=[""], lock=None, quiet=True, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.ServiceUpdates, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True)
return parser
@ -235,8 +235,8 @@ def _set_help_version_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("help-version", aliases=["version"], help="application version",
description="print application and its dependencies versions", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, quiet=True, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Versions, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True)
return parser
@ -276,6 +276,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append")
parser.set_defaults(handler=handlers.Add)
return parser
@ -387,8 +388,8 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"it must end with ()")
parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
"the value will be read from stdin", type=Path, nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False,
repository=[""])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture="", lock=None, report=False,
repository="")
return parser
@ -408,8 +409,8 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture="", lock=None, report=False,
repository="", unsafe=True)
return parser
@ -430,8 +431,8 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"to remove only specified PKGBUILD variables. In case if not set, "
"it will remove all patches related to the package",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, report=False,
repository=[""])
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture="", lock=None, report=False,
repository="")
return parser
@ -455,8 +456,8 @@ def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path)
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False,
repository=[""], variable=None)
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture="", lock=None, report=False,
repository="", variable=None)
return parser
@ -473,7 +474,7 @@ def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("repo-backup", help="backup repository data",
description="backup repository settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the output archive", type=Path)
parser.set_defaults(handler=handlers.Backup, architecture=[""], lock=None, report=False, repository=[""],
parser.set_defaults(handler=handlers.Backup, architecture="", lock=None, report=False, repository="",
unsafe=True)
return parser
@ -653,7 +654,7 @@ def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="restore settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the input archive", type=Path)
parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/"))
parser.set_defaults(handler=handlers.Restore, architecture=[""], lock=None, report=False, repository=[""],
parser.set_defaults(handler=handlers.Restore, architecture="", lock=None, report=False, repository="",
unsafe=True)
return parser
@ -830,6 +831,10 @@ def _set_service_config_parser(root: SubParserAction) -> argparse.ArgumentParser
parser = root.add_parser("service-config", aliases=["config", "repo-config"], help="dump configuration",
description="dump configuration for the specified architecture",
formatter_class=_formatter)
parser.add_argument("section", help="filter settings by section", nargs="?")
parser.add_argument("key", help="filter settings by key", nargs="?")
parser.add_argument("--info", help="show additional information, e.g. configuration files",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--secure", help="hide passwords and secrets from output",
action=argparse.BooleanOptionalAction, default=True)
parser.set_defaults(handler=handlers.Dump, lock=None, quiet=True, report=False, unsafe=True)
@ -875,7 +880,48 @@ def _set_service_key_import_parser(root: SubParserAction) -> argparse.ArgumentPa
formatter_class=_formatter)
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False, repository=[""])
parser.set_defaults(handler=handlers.KeyImport, architecture="", lock=None, report=False, repository="")
return parser
def _set_service_repositories(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repositories listing
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-repositories", help="show repositories",
description="list all available repositories",
formatter_class=_formatter)
parser.add_argument("--id-only", help="show machine readable identifier instead",
action=argparse.BooleanOptionalAction, default=False)
parser.set_defaults(handler=handlers.Repositories, architecture="", lock=None, report=False, repository="",
unsafe=True)
return parser
def _set_service_run(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for multicommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-run", aliases=["run"], help="run multiple commands",
description="run multiple commands on success run of the previous command",
epilog="Commands must be quoted by using usual bash rules. Processes will be spawned "
"under the same user as this command",
formatter_class=_formatter)
parser.add_argument("command", help="command to be run (quoted) without ``ahriman``", nargs="+")
parser.set_defaults(handler=handlers.Run, architecture="", lock=None, report=False, repository="",
unsafe=True, parser=_parser)
return parser
@ -971,8 +1017,8 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-R", "--role", help="user access level",
type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read)
parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, quiet=True,
report=False, repository=[""])
parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture="", lock=None, quiet=True,
report=False, repository="")
return parser
@ -992,8 +1038,8 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-R", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess))
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, quiet=True,
report=False, repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture="", lock=None, quiet=True,
report=False, repository="", unsafe=True)
return parser
@ -1011,8 +1057,8 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="remove user from the user mapping and update the configuration",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, quiet=True,
report=False, repository=[""])
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture="", lock=None, quiet=True,
report=False, repository="")
return parser
@ -1027,8 +1073,8 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("web", help="web server", description="start web server", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Web, lock=Path(tempfile.gettempdir()) / "ahriman-web.lock", report=False,
parser=_parser)
parser.set_defaults(handler=handlers.Web, architecture="", lock=Path(tempfile.gettempdir()) / "ahriman-web.lock",
report=False, repository="", parser=_parser)
return parser

View File

@ -32,8 +32,8 @@ class Application(ApplicationPackages, ApplicationRepository):
base application class
Examples:
This class groups ``Repository`` methods into specific method which process all supposed actions caused by
underlying action. E.g.::
This class groups :class:`ahriman.core.repository.repository.Repository` methods into specific method which
process all supposed actions caused by underlying action. E.g.::
>>> from ahriman.core.configuration import Configuration
>>> from ahriman.models.package_source import PackageSource
@ -49,8 +49,8 @@ class Application(ApplicationPackages, ApplicationRepository):
>>> # updates for specified packages
>>> application.update(updates)
In case if specific actions or their order are required, the direct access to ``Repository`` must
be used instead.
In case if specific actions or their order are required, the direct access to
:class:`ahriman.core.repository.repository.Repository` must be used instead.
"""
def _known_packages(self) -> set[str]:
@ -117,7 +117,7 @@ class Application(ApplicationPackages, ApplicationRepository):
Returns:
list[Package]: updated packages list. Packager for dependencies will be copied from
original package
original package
"""
def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
# append list of known packages with packages which are in current sources
@ -146,7 +146,7 @@ class Application(ApplicationPackages, ApplicationRepository):
# there is local cache, load package from it
package = Package.from_build(source_dir, self.repository.architecture, username)
else:
package = Package.from_aur(package_name, self.repository.pacman, username)
package = Package.from_aur(package_name, username)
with_dependencies[package.base] = package
# register package in local database

View File

@ -63,7 +63,7 @@ class ApplicationPackages(ApplicationProperties):
source(str): package base name
username(str | None): optional override of username for build process
"""
package = Package.from_aur(source, self.repository.pacman, username)
package = Package.from_aur(source, username)
self.database.build_queue_insert(package)
self.database.remote_update(package)
@ -167,12 +167,16 @@ class ApplicationPackages(ApplicationProperties):
"""
raise NotImplementedError
def remove(self, names: Iterable[str]) -> None:
def remove(self, names: Iterable[str]) -> Result:
"""
remove packages from repository
Args:
names(Iterable[str]): list of packages (either base or name) to remove
Returns:
Result: removal result
"""
self.repository.process_remove(names)
self.on_result(Result())
result = self.repository.process_remove(names)
self.on_result(result)
return result

View File

@ -104,7 +104,7 @@ class ApplicationRepository(ApplicationProperties):
packages: list[str] = []
for single in probe.packages:
try:
_ = Package.from_aur(single, self.repository.pacman, None)
_ = Package.from_aur(single, None)
except Exception:
packages.append(single)
return packages

View File

@ -30,7 +30,9 @@ 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
from ahriman.application.handlers.repositories import Repositories
from ahriman.application.handlers.restore import Restore
from ahriman.application.handlers.run import Run
from ahriman.application.handlers.search import Search
from ahriman.application.handlers.service_updates import ServiceUpdates
from ahriman.application.handlers.setup import Setup

View File

@ -23,6 +23,7 @@ from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@ -45,7 +46,12 @@ class Add(Handler):
"""
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start()
application.add(args.package, args.source, args.username)
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
for package in args.package: # for each requested package insert patch
application.database.patches_insert(package, patches)
if not args.now:
return

View File

@ -44,12 +44,18 @@ class Dump(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
root, _ = configuration.check_loaded()
ConfigurationPathsPrinter(root, configuration.includes)(verbose=True, separator=" = ")
if args.info:
root, _ = configuration.check_loaded()
ConfigurationPathsPrinter(root, configuration.includes)(verbose=True, separator=" = ")
# empty line
StringPrinter("")(verbose=False)
dump = configuration.dump()
for section, values in sorted(dump.items()):
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
match (args.section, args.key):
case None, None: # full configuration
dump = configuration.dump()
for section, values in sorted(dump.items()):
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
case section, None: # section only
values = dict(configuration.items(section)) if configuration.has_section(section) else {}
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
case section, key: # key only
value = configuration.get(section, key, fallback="")
StringPrinter(value)(verbose=False)

View File

@ -20,6 +20,7 @@
import argparse
import logging
from collections.abc import Iterable
from multiprocessing import Pool
from ahriman.application.lock import Lock
@ -38,8 +39,8 @@ class Handler:
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures
Examples:
Wrapper for all command line actions, though each derived class implements ``run`` method, it usually must not
be called directly. The recommended way is to call ``execute`` class method, e.g.::
Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually
must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.::
>>> from ahriman.application.handlers import Add
>>>
@ -64,7 +65,7 @@ class Handler:
configuration = Configuration.from_path(args.configuration, repository_id)
log_handler = LogLoader.handler(args.log_handler)
LogLoader.load(configuration, log_handler, quiet=args.quiet, report=args.report)
LogLoader.load(repository_id, configuration, log_handler, quiet=args.quiet, report=args.report)
with Lock(args, repository_id, configuration):
cls.run(args, repository_id, configuration, report=args.report)
@ -105,59 +106,6 @@ class Handler:
return 0 if all(result) else 1
@classmethod
def repositories_extract(cls, args: argparse.Namespace) -> list[RepositoryId]:
"""
get known architectures
Args:
args(argparse.Namespace): command line args
Returns:
list[RepositoryId]: list of repository names and architectures for which tree is created
Raises:
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
"""
configuration = Configuration()
configuration.load(args.configuration)
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
# preparse systemd repository-id argument
# we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes
if args.repository_id is not None:
# repository parts is optional for backward compatibility
architecture, *repository_parts = args.repository_id.split("/")
args.architecture = [architecture]
if repository_parts:
args.repository = ["-".join(repository_parts)] # replace slash with dash
# extract repository names first
names = args.repository
if names is None: # try to read file system first
names = RepositoryPaths.known_repositories(root)
if not names: # try to read configuration now
names = [configuration.get("repository", "name")]
# extract architecture names
if (architectures := args.architecture) is not None:
repositories = set(
RepositoryId(architecture, name)
for name in names
for architecture in architectures
)
else: # try to read from file system
repositories = set(
RepositoryId(architecture, name)
for name in names
for architecture in RepositoryPaths.known_architectures(root, name)
)
if not repositories:
raise MissingArchitectureError(args.command)
return sorted(repositories)
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
@ -189,3 +137,57 @@ class Handler:
"""
if enabled and predicate:
raise ExitCode
@staticmethod
def repositories_extract(args: argparse.Namespace) -> list[RepositoryId]:
"""
get known architectures
Args:
args(argparse.Namespace): command line args
Returns:
list[RepositoryId]: list of repository names and architectures for which tree is created
Raises:
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
"""
configuration = Configuration()
configuration.load(args.configuration)
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
# preparse systemd repository-id argument
# we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes
if args.repository_id is not None:
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
# repository parts is optional for backward compatibility
architecture, *repository_parts = args.repository_id.split(separator) # maxsplit isn't used intentionally
args.architecture = architecture
if repository_parts:
args.repository = "-".join(repository_parts) # replace slash with dash
# extract repository names first
if (from_args := args.repository) is not None:
repositories: Iterable[str] = [from_args]
elif (from_filesystem := RepositoryPaths.known_repositories(root)):
repositories = from_filesystem
else: # try to read configuration now
repositories = [configuration.get("repository", "name")]
# extract architecture names
if (architecture := args.architecture) is not None:
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
)
else: # try to read from file system
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
for architecture in RepositoryPaths.known_architectures(root, repository)
)
if not parsed:
raise MissingArchitectureError(args.command)
return sorted(parsed)

View File

@ -44,7 +44,7 @@ class Help(Handler):
report(bool): force enable or disable reporting
"""
parser: argparse.ArgumentParser = args.parser()
if args.command is None:
if args.subcommand is None:
parser.parse_args(["--help"])
else:
parser.parse_args([args.command, "--help"])
parser.parse_args([args.subcommand, "--help"])

View File

@ -115,7 +115,7 @@ class Patch(Handler):
package_base(str): package base
patch(PkgbuildPatch): patch descriptor
"""
application.database.patches_insert(package_base, patch)
application.database.patches_insert(package_base, [patch])
@staticmethod
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,

View File

@ -24,6 +24,7 @@ from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.repository_id import RepositoryId
@ -55,7 +56,7 @@ class Rebuild(Handler):
application.print_updates(updates, log_fn=print)
return
result = application.update(updates, args.username, bump_pkgrel=args.increment)
result = application.update(updates, Packagers(args.username), bump_pkgrel=args.increment)
Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -0,0 +1,54 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import RepositoryPrinter
from ahriman.models.repository_id import RepositoryId
class Repositories(Handler):
"""
repositories listing handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
dummy_args = argparse.Namespace(
architecture=None,
configuration=args.configuration,
repository=None,
repository_id=None,
)
for repository in cls.repositories_extract(dummy_args):
RepositoryPrinter(repository)(verbose=not args.id_only)

View File

@ -0,0 +1,66 @@
#
# Copyright (c) 2021-2023 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 shlex
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.repository_id import RepositoryId
class Run(Handler):
"""
multicommand handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
parser = args.parser()
for command in args.command:
status = Run.run_command(shlex.split(command), parser)
Run.check_if_empty(True, not status)
@staticmethod
def run_command(command: list[str], parser: argparse.ArgumentParser) -> bool:
"""
run command specified by the argument
Args:
command(list[str]): command to run
parser(argparse.ArgumentParser): generated argument parser
Returns:
bool: status of the command
"""
args = parser.parse_args(command)
handler: Handler = args.handler
return handler.execute(args) == 0

View File

@ -22,7 +22,6 @@ import argparse
from dataclasses import fields
from collections.abc import Callable, Iterable
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.alpm.remote import AUR, Official
from ahriman.core.configuration import Configuration
@ -59,10 +58,8 @@ class Search(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
official_packages_list = Official.multisearch(*args.search, pacman=application.repository.pacman)
aur_packages_list = AUR.multisearch(*args.search, pacman=application.repository.pacman)
official_packages_list = Official.multisearch(*args.search)
aur_packages_list = AUR.multisearch(*args.search)
Search.check_if_empty(args.exit_code, not official_packages_list and not aur_packages_list)
for packages_list in (official_packages_list, aur_packages_list):

View File

@ -20,7 +20,6 @@
import argparse
from ahriman import __version__
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import UpdatePrinter
@ -47,9 +46,7 @@ class ServiceUpdates(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
remote = Package.from_aur("ahriman", application.repository.pacman, None)
remote = Package.from_aur("ahriman", None)
_, release = remote.version.rsplit("-", 1) # we don't store pkgrel locally, so we just append it
local_version = f"{__version__}-{release}"

View File

@ -21,6 +21,7 @@ import argparse
from pathlib import Path
from pwd import getpwuid
from urllib.parse import quote_plus as urlencode
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
@ -126,16 +127,20 @@ class Setup(Handler):
configuration.set_option(section, "target", " ".join([target.name.lower() for target in sign_targets]))
configuration.set_option(section, "key", args.sign_key)
section = Configuration.section_name("web", repository_id.name, repository_id.architecture)
if args.web_port is not None:
configuration.set_option(section, "port", str(args.web_port))
configuration.set_option("web", "port", str(args.web_port))
if (host := root.get("web", "host", fallback=None)) is not None:
configuration.set_option("status", "address", f"http://{host}:{args.web_port}")
if args.web_unix_socket is not None:
configuration.set_option(section, "unix_socket", str(args.web_unix_socket))
unix_socket = str(args.web_unix_socket)
configuration.set_option("web", "unix_socket", unix_socket)
configuration.set_option("status", "address", f"http+unix://{urlencode(unix_socket)}")
if args.generate_salt:
configuration.set_option("auth", "salt", User.generate_password(20))
target = root.include / "00-setup-overrides.ini"
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
target = root.include / f"00-setup-overrides-{repository_id.id}.ini"
with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)

View File

@ -45,14 +45,18 @@ class StatusUpdate(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
# we are using reporter here
client = Application(repository_id, configuration, report=True).repository.reporter
application = Application(repository_id, configuration, report=True)
client = application.repository.reporter
match args.action:
case Action.Update if args.package:
# update packages statuses
for package in args.package:
client.package_update(package, args.status)
packages = application.repository.packages()
for base in args.package:
if (local := next((package for package in packages if package.base == base), None)) is not None:
client.package_add(local, args.status)
else:
client.package_update(base, args.status)
case Action.Update:
# update service status
client.status_update(args.status)

View File

@ -46,8 +46,8 @@ class UnsafeCommands(Handler):
"""
parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
if args.command:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
if args.subcommand:
UnsafeCommands.check_unsafe(args.subcommand, unsafe_commands, parser)
else:
for command in unsafe_commands:
StringPrinter(command)(verbose=True)

View File

@ -67,8 +67,8 @@ class Versions(Handler):
Args:
root(str): root package name
Returns:
Generator[tuple[str, str], None, None]: map of installed dependency to its version
Yields:
tuple[str, str]: map of installed dependency to its version
"""
def dependencies_by_key(key: str) -> Generator[str, None, None]:
# in importlib it returns requires in the following format

View File

@ -32,7 +32,7 @@ class Web(Handler):
web server handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
@ -47,13 +47,20 @@ class Web(Handler):
report(bool): force enable or disable reporting
"""
# we are using local import for optional dependencies
from ahriman.web.web import run_server, setup_service
from ahriman.web.web import run_server, setup_server
spawner_args = Web.extract_arguments(args, repository_id, configuration)
spawner = Spawn(args.parser(), repository_id, list(spawner_args))
spawner_args = Web.extract_arguments(args, configuration)
spawner = Spawn(args.parser(), list(spawner_args))
spawner.start()
application = setup_service(repository_id, configuration, spawner)
dummy_args = argparse.Namespace(
architecture=None,
configuration=args.configuration,
repository=None,
repository_id=None,
)
repositories = cls.repositories_extract(dummy_args)
application = setup_server(configuration, spawner, repositories)
run_server(application)
# terminate spawn process at the last
@ -61,22 +68,17 @@ class Web(Handler):
spawner.join()
@staticmethod
def extract_arguments(args: argparse.Namespace, repository_id: RepositoryId,
configuration: Configuration) -> Generator[str, None, None]:
def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Generator[str, None, None]:
"""
extract list of arguments used for current command, except for command specific ones
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
Returns:
Generator[str, None, None]: command line arguments which were used for this specific command
Yields:
str: command line arguments which were used for this specific command
"""
# read architecture from the same argument list
yield from ["--architecture", repository_id.architecture]
yield from ["--repository", repository_id.name]
# read configuration path from current settings
if (configuration_path := configuration.path) is not None:
yield from ["--configuration", str(configuration_path)]

View File

@ -70,16 +70,22 @@ class Lock(LazyLogging):
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
"""
lock_suffix = f"{repository_id.name}_{repository_id.architecture}"
self.path: Path | None = \
args.lock.with_stem(f"{args.lock.stem}_{lock_suffix}") if args.lock is not None else None
args.lock.with_stem(f"{args.lock.stem}_{repository_id.id}") if args.lock is not None else None
self.force: bool = args.force
self.unsafe: bool = args.unsafe
self.wait_timeout: int = args.wait_timeout
self.paths = configuration.repository_paths
self.reporter = Client.load(configuration, report=args.report)
self.reporter = Client.load(repository_id, configuration, report=args.report)
def check_user(self) -> None:
"""
check if current user is actually owner of ahriman root
"""
check_user(self.paths, unsafe=self.unsafe)
self.paths.tree_create()
def check_version(self) -> None:
"""
@ -90,13 +96,6 @@ class Lock(LazyLogging):
self.logger.warning("status watcher version mismatch, our %s, their %s",
__version__, status.version)
def check_user(self) -> None:
"""
check if current user is actually owner of ahriman root
"""
check_user(self.paths, unsafe=self.unsafe)
self.paths.tree_create()
def clear(self) -> None:
"""
remove lock file

View File

@ -113,13 +113,13 @@ class AUR(Remote):
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -133,13 +133,13 @@ class AUR(Remote):
except StopIteration:
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@ -107,13 +107,13 @@ class Official(Remote):
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -127,13 +127,13 @@ class Official(Remote):
except StopIteration:
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@ -38,13 +38,13 @@ class OfficialSyncdb(Official):
Still we leave search function based on the official repositories RPC.
"""
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -52,6 +52,9 @@ class OfficialSyncdb(Official):
Raises:
UnknownPackageError: package doesn't exist
"""
if pacman is None:
raise UnknownPackageError(package_name)
try:
return next(AURPackage.from_pacman(package) for package in pacman.package_get(package_name))
except StopIteration:

View File

@ -28,25 +28,27 @@ class Remote(SyncHttpClient):
Examples:
These classes are designed to be used without instancing. In order to achieve it several class methods are
provided: ``info``, ``multisearch`` and ``search``. Thus, the basic flow is the following::
provided: :func:`info()`, :func:`multisearch()` and :func:`search()`. Thus, the basic flow is the following::
>>> from ahriman.core.alpm.remote import AUR, Official
>>>
>>> package = AUR.info("ahriman", pacman=pacman)
>>> search_result = Official.multisearch("pacman", "manager", pacman=pacman)
Differnece between ``search`` and ``multisearch`` is that ``search`` passes all arguments to underlying wrapper
directly, whereas ``multisearch`` splits search one by one and finds intersection between search results.
Differnece between :func:`search()` and :func:`multisearch()` is that :func:`search()` passes all arguments to
underlying wrapper directly, whereas :func:`multisearch()` splits search one by one and finds intersection
between search results.
"""
@classmethod
def info(cls, package_name: str, *, pacman: Pacman) -> AURPackage:
def info(cls, package_name: str, *, pacman: Pacman | None = None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
Returns:
AURPackage: package which match the package name
@ -54,14 +56,15 @@ class Remote(SyncHttpClient):
return cls().package_info(package_name, pacman=pacman)
@classmethod
def multisearch(cls, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def multisearch(cls, *keywords: str, pacman: Pacman | None = None) -> list[AURPackage]:
"""
search in remote repository 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
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
Returns:
list[AURPackage]: list of packages each of them matches all search terms
@ -111,26 +114,27 @@ class Remote(SyncHttpClient):
raise NotImplementedError
@classmethod
def search(cls, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def search(cls, *keywords: str, pacman: Pacman | None = None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return cls().package_search(*keywords, pacman=pacman)
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -140,13 +144,13 @@ class Remote(SyncHttpClient):
"""
raise NotImplementedError
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@ -36,8 +36,6 @@ class Repo(LazyLogging):
uid(int): uid of the repository owner user
"""
_check_output = check_output
def __init__(self, name: str, paths: RepositoryPaths, sign_args: list[str]) -> None:
"""
default constructor
@ -69,7 +67,7 @@ class Repo(LazyLogging):
Args:
path(Path): path to archive to add
"""
Repo._check_output(
check_output(
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
exception=BuildError.from_process(path.name),
cwd=self.paths.repository,
@ -80,8 +78,8 @@ class Repo(LazyLogging):
"""
create empty repository database
"""
Repo._check_output("repo-add", *self.sign_args, str(self.repo_path),
cwd=self.paths.repository, logger=self.logger, user=self.uid)
check_output("repo-add", *self.sign_args, str(self.repo_path),
cwd=self.paths.repository, logger=self.logger, user=self.uid)
def remove(self, package: str, filename: Path) -> None:
"""
@ -96,7 +94,7 @@ class Repo(LazyLogging):
full_path.unlink()
# remove package from registry
Repo._check_output(
check_output(
"repo-remove", *self.sign_args, str(self.repo_path), package,
exception=BuildError.from_process(package),
cwd=self.paths.repository,

View File

@ -42,8 +42,6 @@ class Sources(LazyLogging):
DEFAULT_BRANCH = "master" # default fallback branch
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost")
_check_output = check_output
@staticmethod
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
"""
@ -82,20 +80,20 @@ class Sources(LazyLogging):
branch = remote.branch or instance.DEFAULT_BRANCH
if is_initialized_git:
instance.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch)
Sources._check_output("git", "fetch", "--quiet", "--depth", "1", "origin", branch,
cwd=sources_dir, logger=instance.logger)
check_output("git", "fetch", "--quiet", "--depth", "1", "origin", branch,
cwd=sources_dir, logger=instance.logger)
elif remote.git_url is not None:
instance.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch)
Sources._check_output("git", "clone", "--quiet", "--depth", "1", "--branch", branch, "--single-branch",
remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger)
check_output("git", "clone", "--quiet", "--depth", "1", "--branch", branch, "--single-branch",
remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger)
else:
# it will cause an exception later
instance.logger.error("%s is not initialized, but no remote provided", sources_dir)
# and now force reset to our branch
Sources._check_output("git", "checkout", "--force", branch, cwd=sources_dir, logger=instance.logger)
Sources._check_output("git", "reset", "--quiet", "--hard", f"origin/{branch}",
cwd=sources_dir, logger=instance.logger)
check_output("git", "checkout", "--force", branch, cwd=sources_dir, logger=instance.logger)
check_output("git", "reset", "--quiet", "--hard", f"origin/{branch}",
cwd=sources_dir, logger=instance.logger)
# move content if required
# we are using full path to source directory in order to make append possible
@ -114,7 +112,7 @@ class Sources(LazyLogging):
bool: True in case if there is any remote and false otherwise
"""
instance = Sources()
remotes = Sources._check_output("git", "remote", cwd=sources_dir, logger=instance.logger)
remotes = check_output("git", "remote", cwd=sources_dir, logger=instance.logger)
return bool(remotes)
@staticmethod
@ -128,8 +126,8 @@ class Sources(LazyLogging):
instance = Sources()
if not (sources_dir / ".git").is_dir():
# skip initializing in case if it was already
Sources._check_output("git", "init", "--quiet", "--initial-branch", instance.DEFAULT_BRANCH,
cwd=sources_dir, logger=instance.logger)
check_output("git", "init", "--quiet", "--initial-branch", instance.DEFAULT_BRANCH,
cwd=sources_dir, logger=instance.logger)
# extract local files...
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Package.local_files(sources_dir)]
@ -193,7 +191,7 @@ class Sources(LazyLogging):
return # no changes to push, just skip action
git_url, branch = remote.git_source()
Sources._check_output("git", "push", "--quiet", git_url, branch, cwd=sources_dir, logger=instance.logger)
check_output("git", "push", "--quiet", git_url, branch, cwd=sources_dir, logger=instance.logger)
def add(self, sources_dir: Path, *pattern: str, intent_to_add: bool = False) -> None:
"""
@ -214,8 +212,8 @@ class Sources(LazyLogging):
self.logger.info("found matching files %s", found_files)
# add them to index
args = ["--intent-to-add"] if intent_to_add else []
Sources._check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files],
cwd=sources_dir, logger=self.logger)
check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files],
cwd=sources_dir, logger=self.logger)
def commit(self, sources_dir: Path, message: str | None = None,
commit_author: tuple[str, str] | None = None) -> bool:
@ -245,8 +243,7 @@ class Sources(LazyLogging):
environment["GIT_AUTHOR_NAME"] = environment["GIT_COMMITTER_NAME"] = user
environment["GIT_AUTHOR_EMAIL"] = environment["GIT_COMMITTER_EMAIL"] = email
Sources._check_output("git", "commit", "--quiet", *args,
cwd=sources_dir, logger=self.logger, environment=environment)
check_output("git", "commit", "--quiet", *args, cwd=sources_dir, logger=self.logger, environment=environment)
return True
@ -260,7 +257,7 @@ class Sources(LazyLogging):
Returns:
str: patch as plain string
"""
return Sources._check_output("git", "diff", cwd=sources_dir, logger=self.logger)
return check_output("git", "diff", cwd=sources_dir, logger=self.logger)
def has_changes(self, sources_dir: Path) -> bool:
"""
@ -273,7 +270,7 @@ class Sources(LazyLogging):
bool: True if there are uncommitted changes and False otherwise
"""
# there is --exit-code argument to diff, however, there might be other process errors
changes = Sources._check_output("git", "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
changes = check_output("git", "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
return bool(changes)
def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None:
@ -302,7 +299,7 @@ class Sources(LazyLogging):
# create patch
self.logger.info("apply patch %s from database at %s", patch.key, sources_dir)
if patch.is_plain_diff:
Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
else:
patch.write(sources_dir / "PKGBUILD")

View File

@ -45,8 +45,6 @@ class Task(LazyLogging):
uid(int): uid of the repository owner user
"""
_check_output = check_output
def __init__(self, package: Package, configuration: Configuration, architecture: str,
paths: RepositoryPaths) -> None:
"""
@ -68,13 +66,13 @@ class Task(LazyLogging):
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
def build(self, sources_dir: Path, packager: str | None = None) -> list[Path]:
def build(self, sources_dir: Path, **kwargs: str | None) -> list[Path]:
"""
run package build
Args:
sources_dir(Path): path to where sources are
packager(str | None, optional): optional packager override (Default value = None)
**kwargs(str | None): environment variables to be passed to build processes
Returns:
list[Path]: paths of produced packages
@ -85,25 +83,29 @@ class Task(LazyLogging):
command.extend(["--"] + self.makepkg_flags)
self.logger.info("using %s for %s", command, self.package.base)
environment: dict[str, str] = {}
if packager is not None:
environment["PACKAGER"] = packager
environment: dict[str, str] = {
key: value
for key, value in kwargs.items()
if value is not None
}
self.logger.info("using environment variables %s", environment)
Task._check_output(
check_output(
*command,
exception=BuildError.from_process(self.package.base),
cwd=sources_dir,
logger=self.logger,
user=self.uid,
environment=environment)
environment=environment,
)
# well it is not actually correct, but we can deal with it
packages = Task._check_output(
packages = check_output(
"makepkg", "--packagelist",
exception=BuildError.from_process(self.package.base),
cwd=sources_dir,
logger=self.logger
logger=self.logger,
environment=environment,
).splitlines()
return [Path(package) for package in packages]

View File

@ -45,8 +45,8 @@ class Configuration(configparser.RawConfigParser):
Examples:
Configuration class provides additional method in order to handle application configuration. Since this class is
derived from built-in ``configparser.RawConfigParser`` class, the same flow is applicable here. Nevertheless,
it is recommended to use ``from_path`` class method which also calls initialization methods::
derived from built-in :class:`configparser.RawConfigParser` class, the same flow is applicable here.
Nevertheless, it is recommended to use :func:`from_path` class method which also calls initialization methods::
>>> from pathlib import Path
>>>
@ -57,13 +57,14 @@ class Configuration(configparser.RawConfigParser):
The configuration instance loaded in this way will contain only sections which are defined for the specified
architecture according to the merge rules. Moreover, the architecture names will be removed from section names.
In order to get current settings, the ``check_loaded`` method can be used. This method will raise an
``InitializeError`` in case if configuration was not yet loaded::
In order to get current settings, the :func:`check_loaded` method can be used. This method will raise an
:exc:`ahriman.core.exceptions.InitializeError` in case if configuration was not yet loaded::
>>> path, repository_id = configuration.check_loaded()
"""
ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign", "web"]
_LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS = ["web"]
ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
converters: dict[str, Callable[[str], Any]] # typing guard
@ -72,7 +73,7 @@ class Configuration(configparser.RawConfigParser):
default constructor. In the most cases must not be called directly
Args:
allow_no_value(bool, optional): copies ``configparser.RawConfigParser`` behaviour. In case if it is set
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behaviour. In case if it is set
to ``True``, the keys without values will be allowed (Default value = False)
"""
configparser.RawConfigParser.__init__(
@ -160,25 +161,6 @@ class Configuration(configparser.RawConfigParser):
configuration.merge_sections(repository_id)
return configuration
@staticmethod
def override_sections(section: str, repository_id: RepositoryId) -> list[str]:
"""
extract override sections
Args:
section(str): section name
repository_id(RepositoryId): repository unique identifier
Returns:
list[str]: architecture and repository specific sections in correct order
"""
# the valid order is global < per architecture < per repository < per repository and architecture
return [
Configuration.section_name(section, repository_id.architecture), # architecture specific override
Configuration.section_name(section, repository_id.name), # override with repository name
Configuration.section_name(section, repository_id.name, repository_id.architecture), # both
]
@staticmethod
def section_name(section: str, *suffixes: str | None) -> str:
"""
@ -232,7 +214,7 @@ class Configuration(configparser.RawConfigParser):
dict[str, dict[str, str]]: configuration dump for specific architecture
"""
return {
section: dict(self[section])
section: dict(self.items(section))
for section in self.sections()
}
@ -282,20 +264,27 @@ class Configuration(configparser.RawConfigParser):
if not path.is_file(): # fallback to the system file
path = self.SYSTEM_CONFIGURATION_PATH
self.path = path
self.read(self.path)
self.load_includes()
def load_includes(self) -> None:
self.read(self.path)
self.load_includes() # load includes
def load_includes(self, path: Path | None = None) -> None:
"""
load configuration includes
load configuration includes from specified path
Args:
path(Path | None, optional): path to directory with include files. If none set, the default path will be
used (Default value = None)
"""
self.includes = [] # reset state
try:
for path in sorted(self.include.glob("*.ini")):
if path == self.logging_path:
path = path or self.include
for include in sorted(path.glob("*.ini")):
if include == self.logging_path:
continue # we don't want to load logging explicitly
self.read(path)
self.includes.append(path)
self.read(include)
self.includes.append(include)
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
pass
@ -308,7 +297,7 @@ class Configuration(configparser.RawConfigParser):
"""
self.repository_id = repository_id
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS:
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS + self._LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS:
for specific in self.override_sections(section, repository_id):
if self.has_section(specific):
# if there is no such section it means that there is no overrides for this arch,
@ -323,6 +312,26 @@ class Configuration(configparser.RawConfigParser):
continue
self.remove_section(foreign)
def override_sections(self, section: str, repository_id: RepositoryId) -> list[str]:
"""
extract override sections
Args:
section(str): section name
repository_id(RepositoryId): repository unique identifier
Returns:
list[str]: architecture and repository specific sections in correct order
"""
if repository_id.is_empty: # special case, guess sections from configuration
return sorted(specific for specific in self.sections() if specific.startswith(f"{section}:"))
# the valid order is global < per architecture < per repository < per repository and architecture
return [
Configuration.section_name(section, repository_id.architecture), # architecture specific override
Configuration.section_name(section, repository_id.name), # override with repository name
Configuration.section_name(section, repository_id.name, repository_id.architecture), # both
]
def reload(self) -> None:
"""
reload configuration if possible or raise exception otherwise
@ -335,7 +344,7 @@ class Configuration(configparser.RawConfigParser):
def set_option(self, section: str, option: str, value: str) -> None:
"""
set option. Unlike default ``configparser.RawConfigParser.set`` it also creates section if it does not exist
set option. Unlike default :func:`configparser.RawConfigParser.set` it also creates section if it does not exist
Args:
section(str): section name

View File

@ -42,7 +42,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"include": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
"path_type": "dir",
},
@ -250,6 +249,32 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
},
},
},
"status": {
"type": "dict",
"schema": {
"enabled": {
"type": "boolean",
"coerce": "boolean",
},
"address": {
"type": "string",
"empty": False,
"is_url": [],
},
"password": {
"type": "string",
"empty": False,
},
"suppress_http_log_errors": {
"type": "boolean",
"coerce": "boolean",
},
"username": {
"type": "string",
"empty": False,
},
},
},
"web": {
"type": "dict",
"schema": {

View File

@ -46,6 +46,6 @@ class ShellInterpolator(configparser.Interpolation):
"""
# At the moment it seems that it is the most legit way to handle environment variables
# Template behaviour is literally the same as shell
# In addition, we are using shell-like variables in some cases (see ``alpm.mirror`` option), thus we would like
# to keep them alive
# In addition, we are using shell-like variables in some cases (see :attr:`alpm.mirror` option),
# thus we would like to keep them alive
return Template(value).safe_substitute(os.environ)

View File

@ -21,6 +21,7 @@ from sqlite3 import Connection
from ahriman.core.database.operations import Operations
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
class BuildOperations(Operations):
@ -28,13 +29,16 @@ class BuildOperations(Operations):
operations for build queue functions
"""
def build_queue_clear(self, package_base: str | None) -> None:
def build_queue_clear(self, package_base: str | None, repository_id: RepositoryId | None = None) -> None:
"""
remove packages from build queue
Args:
package_base(str | None): optional filter by package base
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
@ -43,36 +47,44 @@ class BuildOperations(Operations):
""",
{
"package_base": package_base,
"repository": self.repository_id.id,
"repository": repository_id.id,
})
return self.with_connection(run, commit=True)
def build_queue_get(self) -> list[Package]:
def build_queue_get(self, repository_id: RepositoryId | None = None) -> list[Package]:
"""
retrieve packages from build queue
Args:
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Return:
list[Package]: list of packages to be built
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> list[Package]:
return [
Package.from_json(row["properties"])
for row in connection.execute(
"""select properties from build_queue where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": repository_id.id}
)
]
return self.with_connection(run)
def build_queue_insert(self, package: Package) -> None:
def build_queue_insert(self, package: Package, repository_id: RepositoryId | None = None) -> None:
"""
insert packages to build queue
Args:
package(Package): package to be inserted
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
@ -86,7 +98,7 @@ class BuildOperations(Operations):
{
"package_base": package.base,
"properties": package.view(),
"repository": self.repository_id.id,
"repository": repository_id.id,
})
return self.with_connection(run, commit=True)

View File

@ -21,6 +21,7 @@ from sqlite3 import Connection
from ahriman.core.database.operations import Operations
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.repository_id import RepositoryId
class LogsOperations(Operations):
@ -28,7 +29,8 @@ class LogsOperations(Operations):
logs operations
"""
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0,
repository_id: RepositoryId | None = None) -> list[tuple[float, str]]:
"""
extract logs for specified package base
@ -36,10 +38,13 @@ class LogsOperations(Operations):
package_base(str): package base to extract logs
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Return:
list[tuple[float, str]]: sorted package log records and their timestamps
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> list[tuple[float, str]]:
return [
(row["created"], row["record"])
@ -51,7 +56,7 @@ class LogsOperations(Operations):
""",
{
"package_base": package_base,
"repository": self.repository_id.id,
"repository": repository_id.id,
"limit": limit,
"offset": offset,
})
@ -59,7 +64,8 @@ class LogsOperations(Operations):
return self.with_connection(run)
def logs_insert(self, log_record_id: LogRecordId, created: float, record: str) -> None:
def logs_insert(self, log_record_id: LogRecordId, created: float, record: str,
repository_id: RepositoryId | None = None) -> None:
"""
write new log record to database
@ -67,7 +73,10 @@ class LogsOperations(Operations):
log_record_id(LogRecordId): current log record id
created(float): log created timestamp from log record attribute
record(str): log record
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
@ -81,21 +90,23 @@ class LogsOperations(Operations):
"version": log_record_id.version,
"created": created,
"record": record,
"repository": self.repository_id.id,
"repository": repository_id.id,
}
)
return self.with_connection(run, commit=True)
def logs_remove(self, package_base: str, version: str | None) -> None:
def logs_remove(self, package_base: str, version: str | None, repository_id: RepositoryId | None = None) -> None:
"""
remove log records for the specified package
Args:
package_base(str): package base to remove logs
version(str): package version. If set it will remove only logs belonging to another
version
version(str | None): package version. If set it will remove only logs belonging to another version
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
@ -107,7 +118,7 @@ class LogsOperations(Operations):
{
"package_base": package_base,
"version": version,
"repository": self.repository_id.id,
"repository": repository_id.id,
}
)

View File

@ -36,7 +36,6 @@ class Operations(LazyLogging):
Attributes:
path(Path): path to the database file
repository_id(RepositoryId): repository unique identifier to perform implicit filtering
"""
def __init__(self, path: Path, repository_id: RepositoryId) -> None:
@ -48,7 +47,7 @@ class Operations(LazyLogging):
repository_id(RepositoryId): repository unique identifier
"""
self.path = path
self.repository_id = repository_id
self._repository_id = repository_id
@staticmethod
def factory(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> dict[str, Any]:

View File

@ -25,6 +25,7 @@ from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_id import RepositoryId
class PackageOperations(Operations):
@ -32,23 +33,26 @@ class PackageOperations(Operations):
package operations
"""
def _package_remove_package_base(self, connection: Connection, package_base: str) -> None:
@staticmethod
def _package_remove_package_base(connection: Connection, package_base: str, repository_id: RepositoryId) -> None:
"""
remove package base information
Args:
connection(Connection): database connection
package_base(str): package base name
repository_id(RepositoryId): repository unique identifier
"""
connection.execute(
"""delete from package_statuses where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": self.repository_id.id})
{"package_base": package_base, "repository": repository_id.id})
connection.execute(
"""delete from package_bases where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": self.repository_id.id})
{"package_base": package_base, "repository": repository_id.id})
def _package_remove_packages(self, connection: Connection, package_base: str,
current_packages: Iterable[str]) -> None:
@staticmethod
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str],
repository_id: RepositoryId) -> None:
"""
remove packages belong to the package base
@ -56,6 +60,7 @@ class PackageOperations(Operations):
connection(Connection): database connection
package_base(str): package base name
current_packages(Iterable[str]): current packages list which has to be left in database
repository_id(RepositoryId): repository unique identifier
"""
packages = [
package
@ -63,20 +68,22 @@ class PackageOperations(Operations):
"""
select package, repository from packages
where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": self.repository_id.id})
{"package_base": package_base, "repository": repository_id.id})
if package["package"] not in current_packages
]
connection.executemany(
"""delete from packages where package = :package and repository = :repository""",
packages)
def _package_update_insert_base(self, connection: Connection, package: Package) -> None:
@staticmethod
def _package_update_insert_base(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
"""
insert base package into table
Args:
connection(Connection): database connection
package(Package): package properties
repository_id(RepositoryId): repository unique identifier
"""
connection.execute(
"""
@ -97,17 +104,19 @@ class PackageOperations(Operations):
"web_url": package.remote.web_url,
"source": package.remote.source.value,
"packager": package.packager,
"repository": self.repository_id.id,
"repository": repository_id.id,
}
)
def _package_update_insert_packages(self, connection: Connection, package: Package) -> None:
@staticmethod
def _package_update_insert_packages(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
"""
insert packages into table
Args:
connection(Connection): database connection
package(Package): package properties
repository_id(RepositoryId): repository unique identifier
"""
package_list = []
for name, description in package.packages.items():
@ -116,7 +125,7 @@ class PackageOperations(Operations):
package_list.append({
"package": name,
"package_base": package.base,
"repository": self.repository_id.id,
"repository": repository_id.id,
**description.view(),
})
connection.executemany(
@ -141,7 +150,9 @@ class PackageOperations(Operations):
""",
package_list)
def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus) -> None:
@staticmethod
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus,
repository_id: RepositoryId) -> None:
"""
insert base package status into table
@ -149,6 +160,7 @@ class PackageOperations(Operations):
connection(Connection): database connection
package_base(str): package base name
status(BuildStatus): new build status
repository_id(RepositoryId): repository unique identifier
"""
connection.execute(
"""
@ -163,15 +175,17 @@ class PackageOperations(Operations):
"package_base": package_base,
"status": status.status.value,
"last_updated": status.timestamp,
"repository": self.repository_id.id,
"repository": repository_id.id,
})
def _packages_get_select_package_bases(self, connection: Connection) -> dict[str, Package]:
@staticmethod
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
"""
select package bases from the table
Args:
connection(Connection): database connection
repository_id(RepositoryId): repository unique identifier
Returns:
dict[str, Package]: map of the package base to its descriptor (without packages themselves)
@ -185,36 +199,41 @@ class PackageOperations(Operations):
packager=row["packager"] or None,
) for row in connection.execute(
"""select * from package_bases where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": repository_id.id}
)
}
def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package]) -> dict[str, Package]:
@staticmethod
def _packages_get_select_packages(connection: Connection, packages: dict[str, Package],
repository_id: RepositoryId) -> dict[str, Package]:
"""
select packages from the table
Args:
connection(Connection): database connection
packages(dict[str, Package]): packages descriptor map
repository_id(RepositoryId): repository unique identifier
Returns:
dict[str, Package]: map of the package base to its descriptor including individual packages
"""
for row in connection.execute(
"""select * from packages where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": repository_id.id}
):
if row["package_base"] not in packages:
continue # normally must never happen though
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages
def _packages_get_select_statuses(self, connection: Connection) -> dict[str, BuildStatus]:
@staticmethod
def _packages_get_select_statuses(connection: Connection, repository_id: RepositoryId) -> dict[str, BuildStatus]:
"""
select package build statuses from the table
Args:
connection(Connection): database connection
repository_id(RepositoryId): repository unique identifier
Returns:
dict[str, BuildStatus]: map of the package base to its status
@ -223,74 +242,97 @@ class PackageOperations(Operations):
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
for row in connection.execute(
"""select * from package_statuses where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": repository_id.id}
)
}
def package_remove(self, package_base: str) -> None:
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
"""
remove package from database
Args:
package_base(str): package base name
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
self._package_remove_packages(connection, package_base, [])
self._package_remove_package_base(connection, package_base)
self._package_remove_packages(connection, package_base, [], repository_id)
self._package_remove_package_base(connection, package_base, repository_id)
return self.with_connection(run, commit=True)
def package_update(self, package: Package, status: BuildStatus) -> None:
def package_update(self, package: Package, status: BuildStatus, repository_id: RepositoryId | None = None) -> None:
"""
update package status
Args:
package(Package): package properties
status(BuildStatus): new build status
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package)
self._package_update_insert_status(connection, package.base, status)
self._package_update_insert_packages(connection, package)
self._package_remove_packages(connection, package.base, package.packages.keys())
self._package_update_insert_base(connection, package, repository_id)
self._package_update_insert_status(connection, package.base, status, repository_id)
self._package_update_insert_packages(connection, package, repository_id)
self._package_remove_packages(connection, package.base, package.packages.keys(), repository_id)
return self.with_connection(run, commit=True)
def packages_get(self) -> list[tuple[Package, BuildStatus]]:
def packages_get(self, repository_id: RepositoryId | None = None) -> list[tuple[Package, BuildStatus]]:
"""
get package list and their build statuses from database
Args:
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Return:
list[tuple[Package, BuildStatus]]: list of package properties and their statuses
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> Generator[tuple[Package, BuildStatus], None, None]:
packages = self._packages_get_select_package_bases(connection)
statuses = self._packages_get_select_statuses(connection)
for package_base, package in self._packages_get_select_packages(connection, packages).items():
packages = self._packages_get_select_package_bases(connection, repository_id)
statuses = self._packages_get_select_statuses(connection, repository_id)
per_package_base = self._packages_get_select_packages(connection, packages, repository_id)
for package_base, package in per_package_base.items():
yield package, statuses.get(package_base, BuildStatus())
return self.with_connection(lambda connection: list(run(connection)))
def remote_update(self, package: Package) -> None:
def remote_update(self, package: Package, repository_id: RepositoryId | None = None) -> None:
"""
update package remote source
Args:
package(Package): package properties
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
return self.with_connection(
lambda connection: self._package_update_insert_base(connection, package),
commit=True)
repository_id = repository_id or self._repository_id
def remotes_get(self) -> dict[str, RemoteSource]:
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package, repository_id)
return self.with_connection(run, commit=True)
def remotes_get(self, repository_id: RepositoryId | None = None) -> dict[str, RemoteSource]:
"""
get packages remotes based on current settings
Args:
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Returns:
dict[str, RemoteSource]: map of package base to its remote sources
"""
packages = self.with_connection(self._packages_get_select_package_bases)
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> dict[str, Package]:
return self._packages_get_select_package_bases(connection, repository_id)
return {
package_base: package.remote
for package_base, package in packages.items()
for package_base, package in self.with_connection(run).items()
}

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections import defaultdict
from sqlite3 import Connection
from ahriman.core.database.operations import Operations
@ -42,16 +41,16 @@ class PatchOperations(Operations):
"""
return self.patches_list(package_base, None).get(package_base, [])
def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
def patches_insert(self, package_base: str, patches: list[PkgbuildPatch]) -> None:
"""
insert or update patch in database
Args:
package_base(str): package base to insert
patch(PkgbuildPatch): patch content
patches(list[PkgbuildPatch]): patch content
"""
def run(connection: Connection) -> None:
connection.execute(
connection.executemany(
"""
insert into patches
(package_base, variable, patch)
@ -60,7 +59,14 @@ class PatchOperations(Operations):
on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch
""",
{"package_base": package_base, "variable": patch.key, "patch": patch.value})
[
{
"package_base": package_base,
"variable": patch.key,
"patch": patch.value,
} for patch in patches
]
)
return self.with_connection(run, commit=True)
@ -89,7 +95,7 @@ class PatchOperations(Operations):
if variables is not None and patch.key not in variables:
continue
patches[package].append(patch)
return dict(patches)
return patches
def patches_remove(self, package_base: str, variables: list[str] | None) -> None:
"""
@ -102,12 +108,21 @@ class PatchOperations(Operations):
def run_many(connection: Connection) -> None:
patches = variables or [] # suppress mypy warning
connection.executemany(
"""delete from patches where package_base = :package_base and variable = :variable""",
[{"package_base": package_base, "variable": variable} for variable in patches])
"""
delete from patches where package_base = :package_base and variable = :variable
""",
[
{
"package_base": package_base,
"variable": variable,
} for variable in patches
])
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
"""
delete from patches where package_base = :package_base
""",
{"package_base": package_base})
if variables is not None:

View File

@ -35,7 +35,7 @@ class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations,
Examples:
Database wrapper must be used together with migration and SQLite3 setup. In order to perform it there is
``load`` class method::
:func:`load()` class method::
>>> from ahriman.core.configuration import Configuration
>>>

View File

@ -62,7 +62,7 @@ class BuildError(RuntimeError):
class CalledProcessError(subprocess.CalledProcessError):
"""
like ``subprocess.CalledProcessError``, but better
like :exc:`subprocess.CalledProcessError`, but better
"""
def __init__(self, status_code: int, process: list[str], stderr: str) -> None:
@ -316,21 +316,6 @@ class UnknownPackageError(ValueError):
ValueError.__init__(self, f"Package base {package_base} is unknown")
class UnprocessedPackageStatusError(ValueError):
"""
exception for merging invalid statues
"""
def __init__(self, package_base: str) -> None:
"""
default constructor
Args:
package_base(str): package base name
"""
ValueError.__init__(self, f"Package base {package_base} had status failed, but new status is success")
class UnsafeRunError(RuntimeError):
"""
exception which will be raised in case if user is not owner of repository

View File

@ -26,6 +26,7 @@ from ahriman.core.formatters.configuration_paths_printer import ConfigurationPat
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.repository_printer import RepositoryPrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.tree_printer import TreePrinter
from ahriman.core.formatters.update_printer import UpdatePrinter

View File

@ -57,6 +57,7 @@ class Printer:
"""
return []
# pylint: disable=redundant-returns-doc
def title(self) -> str | None:
"""
generate entry title from content
@ -68,7 +69,7 @@ class Printer:
def __call__(self, *, verbose: bool, log_fn: Callable[[str], None] = _print, separator: str = ": ") -> None:
"""
print content. Shortcut for ``Printer.print``
print content. Shortcut for :func:`print()`
Args:
verbose(bool): print all fields

View File

@ -0,0 +1,53 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.formatters import StringPrinter
from ahriman.models.property import Property
from ahriman.models.repository_id import RepositoryId
class RepositoryPrinter(StringPrinter):
"""
print repository unique identifier
Attributes:
repository_id(RepositoryId): repository unique identifier
"""
def __init__(self, repository_id: RepositoryId) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
"""
StringPrinter.__init__(self, repository_id.id)
self.repository_id = repository_id
def properties(self) -> list[Property]:
"""
convert content into printable data
Returns:
list[Property]: list of content properties
"""
return [
Property("Name", self.repository_id.name),
Property("Architecture", self.repository_id.architecture),
]

View File

@ -67,7 +67,7 @@ class FilteredAccessLogger(AccessLogger):
Args:
request(BaseRequest): http reqeust descriptor
response(StreamResponse): streaming response object
time(float):
time(float): log record timestamp
"""
if self.is_logs_post(request) \
or self.is_process_get(request):

View File

@ -22,23 +22,26 @@ import logging
from typing import Self
from ahriman.core.configuration import Configuration
from ahriman.models.repository_id import RepositoryId
class HttpLogHandler(logging.Handler):
"""
handler for the http logging. Because default ``logging.handlers.HTTPHandler`` does not support cookies
authorization, we have to implement own handler which overrides the ``logging.handlers.HTTPHandler.emit`` method
handler for the http logging. Because default :class:`logging.handlers.HTTPHandler` does not support cookies
authorization, we have to implement own handler which overrides the :func:`logging.handlers.HTTPHandler.emit` method
Attributes:
reporter(Client): build status reporter instance
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
"""
def __init__(self, configuration: Configuration, *, report: bool, suppress_errors: bool) -> None:
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
report: bool, suppress_errors: bool) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
@ -48,16 +51,17 @@ class HttpLogHandler(logging.Handler):
# client has to be imported here because of circular imports
from ahriman.core.status.client import Client
self.reporter = Client.load(configuration, report=report)
self.reporter = Client.load(repository_id, configuration, report=report)
self.suppress_errors = suppress_errors
@classmethod
def load(cls, configuration: Configuration, *, report: bool) -> Self:
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
"""
install logger. This function creates handler instance and adds it to the handler list in case if no other
http handler found
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
@ -68,8 +72,10 @@ class HttpLogHandler(logging.Handler):
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
return handler # there is already registered instance
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
handler = cls(configuration, report=report, suppress_errors=suppress_errors)
suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style
"settings", "suppress_http_log_errors",
fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False))
handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
root.addHandler(handler)
return handler

View File

@ -25,6 +25,7 @@ from pathlib import Path
from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler
from ahriman.models.log_handler import LogHandler
from ahriman.models.repository_id import RepositoryId
class LogLoader:
@ -45,9 +46,11 @@ class LogLoader:
def handler(selected: LogHandler | None) -> LogHandler:
"""
try to guess default log handler. In case if ``selected`` is set, it will return specified value with appended
_handler suffix. Otherwise, it will try to import journald handler and returns ``journald_handler`` if library
is available. Otherwise, it will check if there is ``/dev/log`` device and returns ``syslog_handler`` in this
case. And, finally, it will fall back to ``console_handler`` if none were found
_handler suffix. Otherwise, it will try to import journald handler and returns
:attr:`ahriman.models.log_handler.LogHandler.Journald` if library is available. Otherwise, it will check if
there is ``/dev/log`` device and returns :attr:`ahriman.models.log_handler.LogHandler.Syslog` in this
case. And, finally, it will fall back to :attr:`ahriman.models.log_handler.LogHandler.Console`
if none were found
Args:
selected(LogHandler | None): user specified handler if any
@ -68,11 +71,13 @@ class LogLoader:
return LogHandler.Console
@staticmethod
def load(configuration: Configuration, handler: LogHandler, *, quiet: bool, report: bool) -> None:
def load(repository_id: RepositoryId, configuration: Configuration, handler: LogHandler, *,
quiet: bool, report: bool) -> None:
"""
setup logging settings from configuration
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
handler(LogHandler): selected default log handler, which will be used if no handlers were set
quiet(bool): force disable any log messages
@ -97,7 +102,7 @@ class LogLoader:
logging.basicConfig(filename=None, format=LogLoader.DEFAULT_LOG_FORMAT, level=LogLoader.DEFAULT_LOG_LEVEL)
logging.exception("could not load logging from configuration, fallback to stderr")
HttpLogHandler.load(configuration, report=report)
HttpLogHandler.load(repository_id, configuration, report=report)
if quiet:
logging.disable(logging.WARNING) # only print errors here

View File

@ -120,6 +120,6 @@ class Email(Report, JinjaTemplate):
text = self.make_html(result, self.template)
attachments = {}
if self.template_full is not None:
attachments["index.html"] = self.make_html(Result(success=packages), self.template_full)
attachments["index.html"] = self.make_html(Result(updated=packages), self.template_full)
self._send(text, attachments)

View File

@ -58,5 +58,5 @@ class HTML(Report, JinjaTemplate):
packages(list[Package]): list of packages to generate report
result(Result): build result
"""
html = self.make_html(Result(success=packages), self.template)
html = self.make_html(Result(updated=packages), self.template)
self.report_path.write_text(html, encoding="utf8")

View File

@ -51,7 +51,7 @@ class RemoteCall(Report):
"""
Report.__init__(self, repository_id, configuration)
self.client = WebClient(configuration)
self.client = WebClient(repository_id, configuration)
self.update_aur = configuration.getboolean(section, "aur", fallback=False)
self.update_local = configuration.getboolean(section, "local", fallback=False)
@ -100,11 +100,13 @@ class RemoteCall(Report):
Returns:
str: remote process id
"""
response = self.client.make_request("POST", "/api/v1/service/update", json={
"aur": self.update_aur,
"local": self.update_local,
"manual": self.update_manual,
})
response = self.client.make_request("POST", "/api/v1/service/update",
params=self.repository_id.query(),
json={
"aur": self.update_aur,
"local": self.update_local,
"manual": self.update_manual,
})
response_json = response.json()
process_id: str = response_json["process_id"]

View File

@ -37,14 +37,14 @@ class Report(LazyLogging):
repository_id(RepositoryId): repository unique identifier
Examples:
``Report`` classes provide several method in order to operate with the report generation and additional class
method ``load`` which can be used in order to determine right report instance::
:class:`Report` subclasses provide several method in order to operate with the report generation and additional
class method :func:`load()` which can be used in order to determine right report instance::
>>> configuration = Configuration()
>>> report = Report.load(RepositoryId("x86_64", "aur-clone"), configuration, "email")
The ``generate`` method can be used in order to perform the report itself, whereas ``run`` method handles
exception and raises ``ReportFailed`` instead::
The :func:`generate()` method can be used in order to perform the report itself, whereas :func:`run()` method
handles exception and raises :exc:`ahriman.core.exceptions.ReportError` instead::
>>> try:
>>> report.generate([], Result())

View File

@ -83,7 +83,7 @@ class Executor(Cleaner):
task = Task(package, self.configuration, self.architecture, self.paths)
local_version = local_versions.get(package.base) if bump_pkgrel else None
task.init(local_path, self.database, local_version)
built = task.build(local_path, packager_id)
built = task.build(local_path, PACKAGER=packager_id)
for src in built:
dst = self.paths.packages / src.name
shutil.move(src, dst)
@ -98,7 +98,7 @@ class Executor(Cleaner):
try:
packager = self.packager(packagers, single.base)
build_single(single, Path(dir_name), packager.packager_id)
result.add_success(single)
result.add_updated(single)
except Exception:
self.reporter.set_failed(single.base)
result.add_failed(single)
@ -106,7 +106,7 @@ class Executor(Cleaner):
return result
def process_remove(self, packages: Iterable[str]) -> Path:
def process_remove(self, packages: Iterable[str]) -> Result:
"""
remove packages from list
@ -114,7 +114,7 @@ class Executor(Cleaner):
packages(Iterable[str]): list of package names or bases to remove
Returns:
Path: path to repository database
Result: remove result
"""
def remove_base(package_base: str) -> None:
try:
@ -126,9 +126,9 @@ class Executor(Cleaner):
except Exception:
self.logger.exception("could not remove base %s", package_base)
def remove_package(package: str, fn: Path) -> None:
def remove_package(package: str, archive_path: Path) -> None:
try:
self.repo.remove(package, fn) # remove the package itself
self.repo.remove(package, archive_path) # remove the package itself
except Exception:
self.logger.exception("could not remove %s", package)
@ -136,6 +136,7 @@ class Executor(Cleaner):
bases_to_remove: list[str] = []
# build package list based on user input
result = Result()
requested = set(packages)
for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages):
@ -145,6 +146,7 @@ class Executor(Cleaner):
if properties.filepath is not None
})
bases_to_remove.append(local.base)
result.add_removed(local)
elif requested.intersection(local.packages.keys()):
packages_to_remove.update({
package: properties.filepath
@ -167,7 +169,7 @@ class Executor(Cleaner):
for package in bases_to_remove:
remove_base(package)
return self.repo.repo_path
return result
def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result:
"""
@ -219,7 +221,7 @@ class Executor(Cleaner):
rename(description, local.base)
update_single(description.filename, local.base, packager.key)
self.reporter.set_success(local)
result.add_success(local)
result.add_updated(local)
current_package_archives: set[str] = set()
if local.base in current_packages:

View File

@ -75,7 +75,7 @@ class RepositoryProperties(LazyLogging):
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
self.sign = GPG(configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration, report=report)
self.reporter = Client.load(repository_id, configuration, report=report)
self.triggers = TriggerLoader.load(repository_id, configuration)
@property

View File

@ -61,7 +61,7 @@ class UpdateHandler(Cleaner):
try:
if package.remote.source == PackageSource.Repository:
return Package.from_official(probe, self.pacman, None)
return Package.from_aur(probe, self.pacman, None)
return Package.from_aur(probe, None)
except UnknownPackageError:
continue
raise UnknownPackageError(package.base)
@ -123,11 +123,11 @@ class UpdateHandler(Cleaner):
local = packages.get(remote.base)
if local is None:
self.reporter.set_unknown(remote)
result.append(remote)
elif local.is_outdated(remote, self.paths,
vcs_allowed_age=self.vcs_allowed_age,
calculate_version=vcs):
continue # we don't add packages automatically
if local.is_outdated(remote, self.paths,
vcs_allowed_age=self.vcs_allowed_age,
calculate_version=vcs):
self.reporter.set_pending(local.base)
result.append(remote)
except Exception:

View File

@ -36,8 +36,6 @@ class GPG(SyncHttpClient):
targets(set[SignSettings]): list of targets to sign (repository, package etc.)
"""
_check_output = check_output
def __init__(self, configuration: Configuration) -> None:
"""
default constructor
@ -140,7 +138,7 @@ class GPG(SyncHttpClient):
Returns:
str: PGP key in .asc format
"""
return GPG._check_output("gpg", "--armor", "--no-emit-version", "--export", key, logger=self.logger)
return check_output("gpg", "--armor", "--no-emit-version", "--export", key, logger=self.logger)
def key_fingerprint(self, key: str) -> str:
"""
@ -152,7 +150,7 @@ class GPG(SyncHttpClient):
Returns:
str: full PGP key fingerprint
"""
metadata = GPG._check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
metadata = check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
# fingerprint line will be like
# fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:
fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
@ -167,7 +165,7 @@ class GPG(SyncHttpClient):
key(str): key ID to import
"""
key_body = self.key_download(server, key)
GPG._check_output("gpg", "--import", input_data=key_body, logger=self.logger)
check_output("gpg", "--import", input_data=key_body, logger=self.logger)
def process(self, path: Path, key: str) -> list[Path]:
"""
@ -180,7 +178,7 @@ class GPG(SyncHttpClient):
Returns:
list[Path]: list of generated files including original file
"""
GPG._check_output(
check_output(
*GPG.sign_command(path, key),
exception=BuildError.from_process(path.name),
logger=self.logger)
@ -215,7 +213,7 @@ class GPG(SyncHttpClient):
sign repository if required by configuration
Notes:
More likely you just want to pass ``repository_sign_args`` to repo wrapper
More likely you just want to pass :attr:`repository_sign_args` to repo wrapper
Args:
path(Path): path to repository database

View File

@ -28,6 +28,8 @@ from multiprocessing import Process, Queue
from threading import Lock, Thread
from ahriman.core.log import LazyLogging
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.process_status import ProcessStatus
from ahriman.models.repository_id import RepositoryId
@ -39,22 +41,18 @@ class Spawn(Thread, LazyLogging):
Attributes:
active(dict[str, Process]): map of active child processes required to avoid zombies
command_arguments(list[str]): base command line arguments
queue(Queue[tuple[str, bool, int]]): multiprocessing queue to read updates from processes
repository_id(RepositoryId): repository unique identifier
queue(Queue[ProcessStatus | None]): multiprocessing queue to read updates from processes
"""
def __init__(self, args_parser: argparse.ArgumentParser, repository_id: RepositoryId,
command_arguments: list[str]) -> None:
def __init__(self, args_parser: argparse.ArgumentParser, command_arguments: list[str]) -> None:
"""
default constructor
Args:
args_parser(argparse.ArgumentParser): command line parser for the application
repository_id(RepositoryId): repository unique identifier
command_arguments(list[str]): base command line arguments
"""
Thread.__init__(self, name="spawn")
self.repository_id = repository_id
self.args_parser = args_parser
self.command_arguments = command_arguments
@ -62,7 +60,7 @@ class Spawn(Thread, LazyLogging):
self.lock = Lock()
self.active: dict[str, Process] = {}
# stupid pylint does not know that it is possible
self.queue: Queue[tuple[str, bool, int] | None] = Queue() # pylint: disable=unsubscriptable-object
self.queue: Queue[ProcessStatus | None] = Queue() # pylint: disable=unsubscriptable-object
@staticmethod
def boolean_action_argument(name: str, value: bool) -> str:
@ -80,16 +78,17 @@ class Spawn(Thread, LazyLogging):
@staticmethod
def process(callback: Callable[[argparse.Namespace, RepositoryId], bool], args: argparse.Namespace,
repository_id: RepositoryId, process_id: str, queue: Queue[tuple[str, bool, int]]) -> None: # pylint: disable=unsubscriptable-object
repository_id: RepositoryId, process_id: str, queue: Queue[ProcessStatus | None]) -> None: # pylint: disable=unsubscriptable-object
"""
helper to run external process
Args:
callback(Callable[[argparse.Namespace, str], bool]): application run function (i.e. Handler.run method)
callback(Callable[[argparse.Namespace, str], bool]): application run function
(i.e. :func:`ahriman.application.handlers.handler.Handler.call()` method)
args(argparse.Namespace): command line arguments
repository_id(RepositoryId): repository unique identifier
process_id(str): process unique identifier
queue(Queue[tuple[str, bool, int]]): output queue
queue(Queue[ProcessStatus | None]): output queue
"""
start_time = time.monotonic()
result = callback(args, repository_id)
@ -97,19 +96,21 @@ class Spawn(Thread, LazyLogging):
consumed_time = int(1000 * (stop_time - start_time))
queue.put((process_id, result, consumed_time))
queue.put(ProcessStatus(process_id, result, consumed_time))
def _spawn_process(self, command: str, *args: str, **kwargs: str | None) -> str:
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str,
**kwargs: str | list[str] | None) -> str:
"""
spawn external ahriman process with supplied arguments
Args:
repository_id(RepositoryId): repository unique identifier
command(str): subcommand to run
*args(str): positional command arguments
**kwargs(str): named command arguments
**kwargs(str | list[str] | None): named command arguments
Returns:
str: spawned process id
str: spawned process identifier
"""
# default arguments
arguments = self.command_arguments[:]
@ -120,17 +121,23 @@ class Spawn(Thread, LazyLogging):
for argument, value in kwargs.items():
if value is None:
continue # skip null values
arguments.append(f"--{argument}")
if value:
arguments.append(value)
flag = f"--{argument}"
if isinstance(value, list):
arguments.extend(list(sum(((flag, v) for v in value), ())))
elif value:
arguments.extend([flag, value])
else:
arguments.append(flag) # boolean argument
process_id = str(uuid.uuid4())
self.logger.info("full command line arguments of %s are %s", process_id, arguments)
self.logger.info("full command line arguments of %s are %s using repository %s",
process_id, arguments, repository_id)
parsed = self.args_parser.parse_args(arguments)
callback = parsed.handler.call
process = Process(target=self.process,
args=(callback, parsed, self.repository_id, process_id, self.queue),
args=(callback, parsed, repository_id, process_id, self.queue),
daemon=True)
process.start()
@ -143,7 +150,7 @@ class Spawn(Thread, LazyLogging):
check if given process is alive
Args:
process_id(str): process id to be checked as returned by ``Spawn._spawn_process``
process_id(str): process id to be checked as returned by :func:`_spawn_process()`
Returns:
bool: True in case if process still counts as active and False otherwise
@ -160,66 +167,77 @@ class Spawn(Thread, LazyLogging):
server(str | None): PGP key server
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {} if server is None else {"key-server": server}
return self._spawn_process("service-key-import", key, **kwargs)
repository_id = RepositoryId("", "")
return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], username: str | None, *, now: bool) -> str:
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
patches: list[PkgbuildPatch], now: bool) -> str:
"""
add packages
Args:
repository_id(RepositoryId): repository unique identifier
packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
patches(list[PkgbuildPatch]): list of patches to be passed
now(bool): build packages now
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {"username": username}
kwargs: dict[str, str | list[str] | None] = {"username": username}
if now:
kwargs["now"] = ""
return self._spawn_process("package-add", *packages, **kwargs)
if patches:
kwargs["variable"] = [patch.serialize() for patch in patches]
def packages_rebuild(self, depends_on: str, username: str | None) -> str:
return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:
"""
rebuild packages which depend on the specified package
Args:
repository_id(RepositoryId): repository unique identifier
depends_on(str): packages dependency
username(str | None): optional override of username for build process
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {"depends-on": depends_on, "username": username}
return self._spawn_process("repo-rebuild", **kwargs)
return self._spawn_process(repository_id, "repo-rebuild", **kwargs)
def packages_remove(self, packages: Iterable[str]) -> str:
def packages_remove(self, repository_id: RepositoryId, packages: Iterable[str]) -> str:
"""
remove packages
Args:
repository_id(RepositoryId): repository unique identifier
packages(Iterable[str]): packages list to remove
Returns:
str: spawned process id
str: spawned process identifier
"""
return self._spawn_process("package-remove", *packages)
return self._spawn_process(repository_id, "package-remove", *packages)
def packages_update(self, username: str | None, *, aur: bool, local: bool, manual: bool) -> str:
def packages_update(self, repository_id: RepositoryId, username: str | None, *,
aur: bool, local: bool, manual: bool) -> str:
"""
run full repository update
Args:
repository_id(RepositoryId): repository unique identifier
username(str | None): optional override of username for build process
aur(bool): check for aur updates
local(bool): check for local packages updates
manual(bool): check for manual packages
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {
"username": username,
@ -227,18 +245,18 @@ class Spawn(Thread, LazyLogging):
self.boolean_action_argument("local", local): "",
self.boolean_action_argument("manual", manual): "",
}
return self._spawn_process("repo-update", **kwargs)
return self._spawn_process(repository_id, "repo-update", **kwargs)
def run(self) -> None:
"""
thread run method
"""
for process_id, status, consumed_time in iter(self.queue.get, None):
self.logger.info("process %s has been terminated with status %s, consumed time %s",
process_id, status, consumed_time / 1000)
for terminated in iter(self.queue.get, None):
self.logger.info("process %s has been terminated with status %s, consumed time %ss",
terminated.process_id, terminated.status, terminated.consumed_time / 1000)
with self.lock:
process = self.active.pop(process_id, None)
process = self.active.pop(terminated.process_id, None)
if process is not None:
process.join()

View File

@ -26,6 +26,7 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
class Client:
@ -34,11 +35,12 @@ class Client:
"""
@staticmethod
def load(configuration: Configuration, *, report: bool) -> Client:
def load(repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Client:
"""
load client from settings
Args:
repository_id(RepositoryId): repository unqiue identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
@ -47,18 +49,21 @@ class Client:
"""
if not report:
return Client()
if not configuration.getboolean("status", "enabled", fallback=True): # global switch
return Client()
address = configuration.get("web", "address", fallback=None)
# new-style section
address = configuration.get("status", "address", fallback=None)
# old-style section
legacy_address = configuration.get("web", "address", fallback=None)
host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None)
socket = configuration.get("web", "unix_socket", fallback=None)
# basically we just check if there is something we can use for interaction with remote server
# at the moment (end of 2022) I think it would be much better idea to introduce flag like `enabled`,
# but it will totally break used experience
if address or (host and port) or socket:
if address or legacy_address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient
return WebClient(configuration)
return WebClient(repository_id, configuration)
return Client()
def package_add(self, package: Package, status: BuildStatusEnum) -> None:
@ -102,7 +107,7 @@ class Client:
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike ``add`` it does not update package properties
update package build status. Unlike :func:`package_add()` it does not update package properties
Args:
package_base(str): package base to update

View File

@ -17,14 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.log import LazyLogging
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@ -34,25 +33,22 @@ class Watcher(LazyLogging):
Attributes:
database(SQLite): database instance
known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases ``packages`` should
be used instead
repository(Repository): repository object
known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases :attr:`packages`
should be used instead
repository_id(RepositoryId): repository unique identifier
status(BuildStatus): daemon status
"""
def __init__(self, repository_id: RepositoryId, configuration: Configuration, database: SQLite) -> None:
def __init__(self, repository_id: RepositoryId, database: SQLite) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
database(SQLite): database instance
"""
self.repository_id = repository_id
self.database = database
self.repository = Repository.load(repository_id, configuration, database, report=False)
self.known: dict[str, tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()
@ -72,20 +68,12 @@ class Watcher(LazyLogging):
def load(self) -> None:
"""
load packages from local repository. In case if last status is known, it will use it
load packages from local database
"""
for package in self.repository.packages():
# get status of build or assign unknown
if (current := self.known.get(package.base)) is not None:
_, status = current
else:
status = BuildStatus()
self.known = {} # reset state
for package, status in self.database.packages_get(self.repository_id):
self.known[package.base] = (package, status)
for package, status in self.database.packages_get():
if package.base in self.known:
self.known[package.base] = (package, status)
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
"""
extract logs for the package base
@ -98,7 +86,7 @@ class Watcher(LazyLogging):
Returns:
list[tuple[float, str]]: package logs
"""
return self.database.logs_get(package_base, limit, offset)
return self.database.logs_get(package_base, limit, offset, self.repository_id)
def logs_remove(self, package_base: str, version: str | None) -> None:
"""
@ -108,7 +96,7 @@ class Watcher(LazyLogging):
package_base(str): package base
version(str): package versio
"""
self.database.logs_remove(package_base, version)
self.database.logs_remove(package_base, version, self.repository_id)
def logs_update(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
@ -123,7 +111,7 @@ class Watcher(LazyLogging):
# there is new log record, so we remove old ones
self.logs_remove(log_record_id.package_base, log_record_id.version)
self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record)
self.database.logs_insert(log_record_id, created, record, self.repository_id)
def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
"""
@ -151,7 +139,7 @@ class Watcher(LazyLogging):
package_base(str): package base
"""
self.known.pop(package_base, None)
self.database.package_remove(package_base)
self.database.package_remove(package_base, self.repository_id)
self.logs_remove(package_base, None)
def package_update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None:
@ -173,7 +161,41 @@ class Watcher(LazyLogging):
raise UnknownPackageError(package_base) from None
full_status = BuildStatus(status)
self.known[package_base] = (package, full_status)
self.database.package_update(package, full_status)
self.database.package_update(package, full_status, self.repository_id)
def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
"""
get patches for the package
Args:
package_base(str): package base
variable(str | None): patch variable name if any
Returns:
list[PkgbuildPatch]: list of patches which are stored for the package
"""
variables = [variable] if variable is not None else None
return self.database.patches_list(package_base, variables).get(package_base, [])
def patches_remove(self, package_base: str, variable: str) -> None:
"""
remove package patch
Args:
package_base(str): package base
variable(str): patch variable name
"""
self.database.patches_remove(package_base, [variable])
def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
update package patch
Args:
package_base(str): package base
patch(PkgbuildPatch): package patch
"""
self.database.patches_insert(package_base, [patch])
def status_update(self, status: BuildStatusEnum) -> None:
"""

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