mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-04-07 19:03:38 +00:00
Compare commits
29 Commits
49cf91ea52
...
2.20.0rc3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d009cba6d | |||
| f6defbf90d | |||
| c4fefa6071 | |||
| b0f1828ae7 | |||
| dec025b45a | |||
| 89008e5350 | |||
| 422196d413 | |||
| 6fe2eade26 | |||
| 5266f54257 | |||
| bbf9e38fda | |||
| ba80a91d95 | |||
| 536d040a6a | |||
| bed8752f3a | |||
| 4093ca8986 | |||
| f027155885 | |||
| 443d4ae667 | |||
| c8f7fa8c51 | |||
| 93c36fb429 | |||
| 2d6d42f969 | |||
| 6a2454548d | |||
| 389bad6725 | |||
| 5738b8b911 | |||
| 5ac2e3de19 | |||
| 799dc73d8a | |||
| 4e79cbf71a | |||
| 4fa5d55317 | |||
| a7fa3b90e4 | |||
| ce07cda8ab | |||
| 00c4f32294 |
2
.github/workflows/setup.sh
vendored
2
.github/workflows/setup.sh
vendored
@@ -10,7 +10,7 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
|
||||
# refresh the image
|
||||
pacman -Syyu --noconfirm
|
||||
# main dependencies
|
||||
pacman -S --noconfirm devtools git pyalpm python-bcrypt python-inflection python-pyelftools python-requests python-systemd sudo
|
||||
pacman -S --noconfirm devtools git pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
|
||||
# make dependencies
|
||||
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
|
||||
# optional dependencies
|
||||
|
||||
@@ -165,6 +165,11 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
|
||||
# Blank line again and package imports
|
||||
from ahriman.core.configuration import Configuration
|
||||
# Multiline import example
|
||||
from ahriman.core.database.operations import (
|
||||
AuthOperations,
|
||||
BuildOperations,
|
||||
)
|
||||
```
|
||||
|
||||
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
|
||||
@@ -215,6 +220,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
* 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.
|
||||
* `Generator` return type is not allowed. Generator functions must return generic `Iterator` object. Documentation should be described as `Yields`, however, because of pylint checks. Unfortunately, `Iterable` return type is not available for generators also, because of specific `contextlib.contextmanager` case.
|
||||
|
||||
### Other checks
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ RUN pacman -S --noconfirm --asdeps \
|
||||
devtools \
|
||||
git \
|
||||
pyalpm \
|
||||
python-bcrypt \
|
||||
python-bcrypt \
|
||||
python-filelock \
|
||||
python-inflection \
|
||||
python-pyelftools \
|
||||
python-requests \
|
||||
|
||||
2942
docs/_static/architecture.dot
vendored
2942
docs/_static/architecture.dot
vendored
File diff suppressed because it is too large
Load Diff
29
docs/ahriman.core.archive.rst
Normal file
29
docs/ahriman.core.archive.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
ahriman.core.archive package
|
||||
============================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
ahriman.core.archive.archive\_tree module
|
||||
-----------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.archive.archive_tree
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.archive.archive\_trigger module
|
||||
--------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.archive.archive_trigger
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: ahriman.core.archive
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -12,6 +12,14 @@ ahriman.core.build\_tools.package\_archive module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.build\_tools.package\_version module
|
||||
-------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.build_tools.package_version
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.build\_tools.sources module
|
||||
----------------------------------------
|
||||
|
||||
|
||||
@@ -132,6 +132,14 @@ ahriman.core.database.migrations.m015\_logs\_process\_id module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.database.migrations.m016\_archive module
|
||||
-----------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.database.migrations.m016_archive
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@ ahriman.core.housekeeping package
|
||||
Submodules
|
||||
----------
|
||||
|
||||
ahriman.core.housekeeping.archive\_rotation\_trigger module
|
||||
-----------------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.housekeeping.archive_rotation_trigger
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.housekeeping.logs\_rotation\_trigger module
|
||||
--------------------------------------------------------
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ ahriman.core.repository.executor module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.repository.explorer module
|
||||
---------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.repository.explorer
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.repository.package\_info module
|
||||
--------------------------------------------
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ Subpackages
|
||||
:maxdepth: 4
|
||||
|
||||
ahriman.core.alpm
|
||||
ahriman.core.archive
|
||||
ahriman.core.auth
|
||||
ahriman.core.build_tools
|
||||
ahriman.core.configuration
|
||||
|
||||
@@ -120,6 +120,20 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
|
||||
|
||||
/var/lib/ahriman/
|
||||
├── ahriman.db
|
||||
├── archive
|
||||
│ ├── packages
|
||||
│ │ └── a
|
||||
│ │ └── ahriman
|
||||
│ │ └── ahriman-2.0.0-1-any.pkg.tar.zst
|
||||
│ └── repos
|
||||
│ └── 2026
|
||||
│ └── 01
|
||||
│ └── 01
|
||||
│ └── aur
|
||||
│ └── x86_64
|
||||
│ ├── ahriman-2.0.0-1-any.pkg.tar.zst -> ../../../../../../packages/a/ahriman/ahriman-2.0.0-1-any.pkg.tar.zst
|
||||
│ ├── aur.db -> aur.db.tar.gz
|
||||
│ └── aur.db.tar.gz
|
||||
├── cache
|
||||
├── chroot
|
||||
│ └── aur
|
||||
@@ -139,6 +153,7 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
|
||||
└── repository
|
||||
└── aur
|
||||
└── x86_64
|
||||
├── ahriman-2.0.0-1-any.pkg.tar.zst -> ../../../archive/packages/a/ahriman/ahriman-2.0.0-1-any.pkg.tar.zst
|
||||
├── aur.db -> aur.db.tar.gz
|
||||
├── aur.db.tar.gz
|
||||
├── aur.files -> aur.files.tar.gz
|
||||
@@ -146,11 +161,18 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
|
||||
|
||||
There are multiple subdirectories, some of them are commons for any repository, but some of them are not.
|
||||
|
||||
* ``archive`` is the package archive directory. It is common for all repositories and architectures and contains two subdirectories:
|
||||
|
||||
* ``archive/packages/{first_letter}/{package_base}`` stores the actual built package files and their signatures.
|
||||
* ``archive/repos/{YYYY}/{MM}/{DD}/{repository}/{architecture}`` contains daily repository snapshots. Each snapshot is a repository database with symlinks pointing to the corresponding packages in the ``archive/packages`` tree.
|
||||
|
||||
The archive also allows the build process to skip rebuilding a package if a matching version already exists.
|
||||
|
||||
* ``cache`` is a directory with locally stored PKGBUILD's and VCS packages. It is common for all repositories and architectures.
|
||||
* ``chroot/{repository}`` is a chroot directory for ``devtools``. It is specific for each repository, but shared for different architectures inside (the ``devtools`` handles architectures automatically).
|
||||
* ``packages/{repository}/{architecture}`` is a directory with prebuilt packages. When a package is built, first it will be uploaded to this directory and later will be handled by update process. It is architecture and repository specific.
|
||||
* ``pacman/{repository}/{architecture}`` is the repository and architecture specific caches for pacman's databases.
|
||||
* ``repository/{repository}/{architecture}`` is a repository packages directory.
|
||||
* ``repository/{repository}/{architecture}`` is a repository packages directory. Package files in this directory are symlinks to the archive.
|
||||
|
||||
Normally you should avoid direct interaction with the application tree. For tree migration process refer to the :doc:`migration notes <migrations/index>`.
|
||||
|
||||
|
||||
@@ -97,6 +97,15 @@ libalpm and AUR related configuration. Group name can refer to architecture, e.g
|
||||
* ``sync_files_database`` - download files database from mirror, boolean, required.
|
||||
* ``use_ahriman_cache`` - use local pacman package cache instead of system one, boolean, required. With this option enabled you might want to refresh database periodically (available as additional flag for some subcommands). If set to ``no``, databases must be synchronized manually.
|
||||
|
||||
``aur`` group
|
||||
-------------
|
||||
|
||||
Archlinux User Repository related configuration.
|
||||
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
|
||||
``auth`` group
|
||||
--------------
|
||||
|
||||
@@ -158,7 +167,9 @@ Reporting to web service related settings. In most cases there is fallback to we
|
||||
|
||||
* ``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.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``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.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
|
||||
@@ -182,6 +193,13 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
|
||||
* ``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.
|
||||
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
|
||||
|
||||
``archive`` group
|
||||
-----------------
|
||||
|
||||
Describes settings for packages archives management extensions.
|
||||
|
||||
* ``keep_built_packages`` - keep this amount of built packages with different versions, integer, required. ``0`` will effectively disable archives removal.
|
||||
|
||||
``keyring`` group
|
||||
-----------------
|
||||
|
||||
@@ -201,12 +219,12 @@ Keyring generator plugin
|
||||
* ``revoked`` - list of revoked packagers keys, space separated list of strings, optional.
|
||||
* ``trusted`` - list of master keys, space separated list of strings, optional, if not set, the ``key`` option from ``sign`` group will be used.
|
||||
|
||||
``housekeeping`` group
|
||||
----------------------
|
||||
``logs-rotation`` group
|
||||
-----------------------
|
||||
|
||||
This section describes settings for the ``ahriman.core.housekeeping.LogsRotationTrigger`` plugin.
|
||||
|
||||
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
|
||||
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, required. Logs will be cleared at the end of each process.
|
||||
|
||||
``mirrorlist`` group
|
||||
--------------------
|
||||
@@ -243,6 +261,7 @@ Available options are:
|
||||
Remote pull trigger
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* ``type`` - type of the pull, string, optional, must be set to ``gitremote`` if exists.
|
||||
* ``pull_url`` - URL of the remote repository from which PKGBUILDs can be pulled before build process, string, required.
|
||||
* ``pull_branch`` - branch of the remote repository from which PKGBUILDs can be pulled before build process, string, optional, default is ``master``.
|
||||
|
||||
@@ -263,6 +282,7 @@ Available options are:
|
||||
Remote push trigger
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* ``type`` - type of the push, string, optional, must be set to ``gitremote`` if exists.
|
||||
* ``commit_email`` - git commit email, string, optional, default is ``ahriman@localhost``.
|
||||
* ``commit_user`` - git commit user, string, optional, default is ``ahriman``.
|
||||
* ``push_url`` - URL of the remote repository to which PKGBUILDs should be pushed after build process, string, required.
|
||||
@@ -358,6 +378,8 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
|
||||
* ``chat_id`` - telegram chat id, either string with ``@`` or integer value, required.
|
||||
* ``homepage`` - link to homepage, string, optional.
|
||||
* ``link_path`` - prefix for HTML links, string, required.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``rss_url`` - link to RSS feed, string, optional.
|
||||
* ``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``.
|
||||
@@ -383,6 +405,7 @@ Type will be read from several sources:
|
||||
This feature requires GitHub key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set.
|
||||
|
||||
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``owner`` - GitHub repository owner, string, required.
|
||||
* ``password`` - created GitHub API key. In order to create it do the following:
|
||||
|
||||
@@ -392,6 +415,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).
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``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``.
|
||||
@@ -402,6 +426,8 @@ 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.
|
||||
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
|
||||
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
|
||||
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
|
||||
|
||||
``rsync`` type
|
||||
|
||||
@@ -40,6 +40,8 @@ docutils==0.21.2
|
||||
# sphinx
|
||||
# sphinx-argparse
|
||||
# sphinx-rtd-theme
|
||||
filelock==3.24.0
|
||||
# via ahriman (pyproject.toml)
|
||||
frozenlist==1.6.0
|
||||
# via
|
||||
# aiohttp
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Triggers
|
||||
========
|
||||
|
||||
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides three types of extensions - reporting, files uploading and PKGBUILD synchronization. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and should implement at least one of the abstract methods:
|
||||
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides several types of extensions - reporting, files uploading, PKGBUILD synchronization, repository archiving, housekeeping and distributed builds support. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and should implement at least one of the abstract methods:
|
||||
|
||||
* ``on_result`` - trigger action which will be called after build process, the build result and the list of repository packages will be supplied as arguments.
|
||||
* ``on_start`` - trigger action which will be called right before the start of the application process.
|
||||
@@ -14,6 +14,11 @@ Built-in triggers
|
||||
|
||||
For the configuration details and settings explanation kindly refer to the :doc:`documentation <configuration>`.
|
||||
|
||||
``ahriman.core.archive.ArchiveTrigger``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This trigger provides date-based snapshots of the repository. It organizes packages into a daily directory tree (``repos/YYYY/MM/DD``) with its own pacman database. On each run it creates symlinks from the daily snapshot to the actual package archives and maintains the database accordingly. It also takes care of cleaning up broken symlinks and empty directories for packages which have been removed.
|
||||
|
||||
``ahriman.core.distributed.WorkerLoaderTrigger``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -36,6 +41,16 @@ In order to update those packages you would need to clone your repository separa
|
||||
|
||||
This trigger will be called right after build process (``on_result``). It will pick PKGBUILDs for the updated packages, pull them (together with any other files) and commit and push changes to remote repository. No real use cases, but the most of user repositories do it.
|
||||
|
||||
``ahriman.core.housekeeping.ArchiveRotationTrigger``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This trigger removes old package versions from the archive directory. It implements ``on_result`` and, after each build, compares available versions for updated packages and removes the older ones, keeping only the last N versions as configured by ``keep_built_packages`` option.
|
||||
|
||||
``ahriman.core.housekeeping.LogsRotationTrigger``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Simple trigger to rotate build logs. It implements ``on_result`` and removes old log records after each build process, keeping only the last N records as configured by ``keep_last_logs`` option.
|
||||
|
||||
``ahriman.core.report.ReportTrigger``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
pkgbase='ahriman'
|
||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||
pkgver=2.19.0
|
||||
pkgver=2.20.0rc3
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
url="https://ahriman.readthedocs.io/"
|
||||
license=('GPL-3.0-or-later')
|
||||
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-bcrypt' 'python-inflection' 'python-pyelftools' 'python-requests')
|
||||
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-bcrypt' 'python-filelock' 'python-inflection' 'python-pyelftools' 'python-requests')
|
||||
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
|
||||
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgbase-$pkgver.tar.gz"
|
||||
"$pkgbase.sysusers"
|
||||
|
||||
@@ -23,6 +23,14 @@ sync_files_database = yes
|
||||
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
|
||||
use_ahriman_cache = yes
|
||||
|
||||
[aur]
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
max_retries = 3
|
||||
; Retry exponential backoff.
|
||||
retry_backoff = 1.0
|
||||
; HTTP request timeout in seconds.
|
||||
;timeout = 30
|
||||
|
||||
[build]
|
||||
; List of additional flags passed to archbuild command.
|
||||
;archbuild_flags =
|
||||
@@ -44,9 +52,11 @@ triggers[] = ahriman.core.report.ReportTrigger
|
||||
triggers[] = ahriman.core.upload.UploadTrigger
|
||||
triggers[] = ahriman.core.gitremote.RemotePushTrigger
|
||||
triggers[] = ahriman.core.housekeeping.LogsRotationTrigger
|
||||
triggers[] = ahriman.core.housekeeping.ArchiveRotationTrigger
|
||||
; List of well-known triggers. Used only for configuration purposes.
|
||||
triggers_known[] = ahriman.core.gitremote.RemotePullTrigger
|
||||
triggers_known[] = ahriman.core.gitremote.RemotePushTrigger
|
||||
triggers_known[] = ahriman.core.housekeeping.ArchiveRotationTrigger
|
||||
triggers_known[] = ahriman.core.housekeeping.LogsRotationTrigger
|
||||
triggers_known[] = ahriman.core.report.ReportTrigger
|
||||
triggers_known[] = ahriman.core.upload.UploadTrigger
|
||||
@@ -71,8 +81,12 @@ enabled = yes
|
||||
; In case if unix sockets are used, it might point to the valid socket with encoded path, e.g.:
|
||||
; address = http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket
|
||||
;address = http://${web:host}:${web:port}
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; Optional password for authentication (if enabled).
|
||||
;password =
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; Do not log HTTP errors if occurs.
|
||||
suppress_http_log_errors = yes
|
||||
; HTTP request timeout in seconds.
|
||||
@@ -214,6 +228,10 @@ templates[] = ${prefix}/share/ahriman/templates
|
||||
;homepage=
|
||||
; Prefix for packages links. Link to a package will be formed as link_path / filename.
|
||||
;link_path =
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; Optional link to the RSS feed.
|
||||
;rss_url =
|
||||
; Template name to be used.
|
||||
@@ -234,12 +252,16 @@ target =
|
||||
[github]
|
||||
; Trigger type name.
|
||||
;type = github
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; GitHub repository owner username.
|
||||
;owner =
|
||||
; GitHub API key. public_repo (repo) scope is required.
|
||||
;password =
|
||||
; GitHub repository name.
|
||||
;repository =
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; HTTP request timeout in seconds.
|
||||
;timeout = 30
|
||||
; Include repository name to release name (recommended).
|
||||
@@ -251,6 +273,10 @@ target =
|
||||
[remote-service]
|
||||
; Trigger type name.
|
||||
;type = remote-service
|
||||
; Maximum amount of retries of HTTP requests.
|
||||
;max_retries = 0
|
||||
; Retry exponential backoff.
|
||||
;retry_backoff = 0.0
|
||||
; HTTP request timeout in seconds.
|
||||
;timeout = 30
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
[archive]
|
||||
; Keep amount of last built packages in archive. 0 means keep all packages
|
||||
keep_built_packages = 1
|
||||
|
||||
[logs-rotation]
|
||||
; Keep last build logs for each package
|
||||
keep_last_logs = 5
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[build]
|
||||
; List of well-known triggers. Used only for configuration purposes.
|
||||
triggers_known[] = ahriman.core.archive.ArchiveTrigger
|
||||
triggers_known[] = ahriman.core.distributed.WorkerLoaderTrigger
|
||||
triggers_known[] = ahriman.core.distributed.WorkerTrigger
|
||||
triggers_known[] = ahriman.core.support.KeyringTrigger
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
};
|
||||
});
|
||||
|
||||
updateTable(table, payload);
|
||||
updateTable(table, payload, row => row.timestamp);
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
onFailure,
|
||||
|
||||
@@ -195,16 +195,19 @@
|
||||
return intervalId;
|
||||
}
|
||||
|
||||
function updateTable(table, rows) {
|
||||
function updateTable(table, rows, rowChangedKey) {
|
||||
// instead of using load method here, we just update rows manually to avoid table reinitialization
|
||||
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
|
||||
accumulator[row.id] = row["0"];
|
||||
accumulator[row.id] = {state: row["0"], key: rowChangedKey(row)};
|
||||
return accumulator;
|
||||
}, {});
|
||||
// insert or update rows
|
||||
// insert or update rows, skipping ones whose status hasn't changed
|
||||
rows.forEach(row => {
|
||||
if (Object.hasOwn(currentData, row.id)) {
|
||||
row["0"] = currentData[row.id]; // copy checkbox state
|
||||
if (rowChangedKey(row) === currentData[row.id].key) {
|
||||
return;
|
||||
}
|
||||
row["0"] = currentData[row.id].state; // copy checkbox state
|
||||
table.bootstrapTable("updateByUniqueId", {
|
||||
id: row.id,
|
||||
row: row,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AUTOMATICALLY GENERATED by `shtab`
|
||||
|
||||
_shtab_ahriman_subparsers=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web')
|
||||
_shtab_ahriman_subparsers=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web' 'web-reload')
|
||||
|
||||
_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_add_option_strings=('-h' '--help' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
|
||||
@@ -78,10 +78,11 @@ _shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-R' '
|
||||
_shtab_ahriman_user_remove_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_version_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_web_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_web_reload_option_strings=('-h' '--help')
|
||||
|
||||
|
||||
|
||||
_shtab_ahriman_pos_0_choices=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web')
|
||||
_shtab_ahriman_pos_0_choices=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web' 'web-reload')
|
||||
_shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald')
|
||||
_shtab_ahriman_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||
_shtab_ahriman_add___source_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
|
||||
@@ -572,6 +573,8 @@ _shtab_ahriman_version__h_nargs=0
|
||||
_shtab_ahriman_version___help_nargs=0
|
||||
_shtab_ahriman_web__h_nargs=0
|
||||
_shtab_ahriman_web___help_nargs=0
|
||||
_shtab_ahriman_web_reload__h_nargs=0
|
||||
_shtab_ahriman_web_reload___help_nargs=0
|
||||
|
||||
|
||||
# $1=COMP_WORDS[1]
|
||||
@@ -674,6 +677,7 @@ _shtab_ahriman() {
|
||||
|
||||
if [[ "$current_action_nargs" != "*" ]] && \
|
||||
[[ "$current_action_nargs" != "+" ]] && \
|
||||
[[ "$current_action_nargs" != "?" ]] && \
|
||||
[[ "$current_action_nargs" != *"..." ]] && \
|
||||
(( $word_index + 1 - $current_action_args_start_index - $pos_only >= \
|
||||
$current_action_nargs )); then
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.TH AHRIMAN "1" "2025\-06\-29" "ahriman 2.19.0" "ArcH linux ReposItory MANager"
|
||||
.TH AHRIMAN "1" "2026\-02\-20" "ahriman 2.20.0rc3" "ArcH linux ReposItory MANager"
|
||||
.SH NAME
|
||||
ahriman \- ArcH linux ReposItory MANager
|
||||
.SH SYNOPSIS
|
||||
.B ahriman
|
||||
[-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] {add,aur-search,check,clean,config,config-validate,copy,daemon,help,help-commands-unsafe,help-updates,help-version,init,key-import,package-add,package-changes,package-changes-remove,package-copy,package-remove,package-status,package-status-remove,package-status-update,package-update,patch-add,patch-list,patch-remove,patch-set-add,rebuild,remove,remove-unknown,repo-backup,repo-check,repo-clean,repo-config,repo-config-validate,repo-create-keyring,repo-create-mirrorlist,repo-daemon,repo-init,repo-rebuild,repo-remove-unknown,repo-report,repo-restore,repo-setup,repo-sign,repo-statistics,repo-status-update,repo-sync,repo-tree,repo-triggers,repo-update,report,run,search,service-clean,service-config,service-config-validate,service-key-import,service-repositories,service-run,service-setup,service-shell,service-tree-migrate,setup,shell,sign,status,status-update,sync,update,user-add,user-list,user-remove,version,web} ...
|
||||
.B [0m[1;35mahriman[0m
|
||||
[[32m-h[0m] [[32m-a [33mARCHITECTURE[0m] [[32m-c [33mCONFIGURATION[0m] [[36m--force[0m] [[32m-l [33mLOCK[0m] [[36m--log-handler [33m{console,syslog,journald}[0m] [[32m-q[0m] [[36m--report | --no-report[0m] [[32m-r [33mREPOSITORY[0m] [[36m--unsafe[0m] [[32m-V[0m] [[36m--wait-timeout [33mWAIT_TIMEOUT[0m] [32m{add,aur-search,check,clean,config,config-validate,copy,daemon,help,help-commands-unsafe,help-updates,help-version,init,key-import,package-add,package-changes,package-changes-remove,package-copy,package-remove,package-status,package-status-remove,package-status-update,package-update,patch-add,patch-list,patch-remove,patch-set-add,rebuild,remove,remove-unknown,repo-backup,repo-check,repo-clean,repo-config,repo-config-validate,repo-create-keyring,repo-create-mirrorlist,repo-daemon,repo-init,repo-rebuild,repo-remove-unknown,repo-report,repo-restore,repo-setup,repo-sign,repo-statistics,repo-status-update,repo-sync,repo-tree,repo-triggers,repo-update,report,run,search,service-clean,service-config,service-config-validate,service-key-import,service-repositories,service-run,service-setup,service-shell,service-tree-migrate,setup,shell,sign,status,status-update,sync,update,user-add,user-list,user-remove,version,web,web-reload} ...[0m
|
||||
.SH DESCRIPTION
|
||||
ArcH linux ReposItory MANager
|
||||
|
||||
@@ -193,11 +193,14 @@ remove user
|
||||
.TP
|
||||
\fBahriman\fR \fI\,web\/\fR
|
||||
web server
|
||||
.TP
|
||||
\fBahriman\fR \fI\,web\-reload\/\fR
|
||||
reload configuration
|
||||
|
||||
.SH COMMAND \fI\,'ahriman aur\-search'\/\fR
|
||||
usage: ahriman aur\-search [\-h] [\-e] [\-\-info | \-\-no\-info]
|
||||
[\-\-sort\-by {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}]
|
||||
search [search ...]
|
||||
[1;34musage: [0m[1;35mahriman aur\-search[0m [[32m\-h[0m] [[32m\-e[0m] [[36m\-\-info | \-\-no\-info[0m]
|
||||
[[36m\-\-sort\-by [33m{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}[0m]
|
||||
[32msearch [search ...][0m
|
||||
|
||||
search for package in AUR using API
|
||||
|
||||
@@ -220,7 +223,7 @@ sort field by this field. In case if two packages have the same value of the spe
|
||||
by name
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help'\/\fR
|
||||
usage: ahriman help [\-h] [subcommand]
|
||||
[1;34musage: [0m[1;35mahriman help[0m [[32m\-h[0m] [32m[subcommand][0m
|
||||
|
||||
show help message for application or command and exit
|
||||
|
||||
@@ -229,7 +232,7 @@ show help message for application or command and exit
|
||||
show help message for specific command
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
|
||||
usage: ahriman help\-commands\-unsafe [\-h] [subcommand ...]
|
||||
[1;34musage: [0m[1;35mahriman help\-commands\-unsafe[0m [[32m\-h[0m] [32m[subcommand ...][0m
|
||||
|
||||
list unsafe commands as defined in default args
|
||||
|
||||
@@ -239,7 +242,7 @@ instead of showing commands, just test command line for unsafe subcommand and re
|
||||
otherwise
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help\-updates'\/\fR
|
||||
usage: ahriman help\-updates [\-h] [\-e]
|
||||
[1;34musage: [0m[1;35mahriman help\-updates[0m [[32m\-h[0m] [[32m\-e[0m]
|
||||
|
||||
request AUR for current version and compare with current service version
|
||||
|
||||
@@ -249,15 +252,15 @@ request AUR for current version and compare with current service version
|
||||
return non\-zero exit code if updates available
|
||||
|
||||
.SH COMMAND \fI\,'ahriman help\-version'\/\fR
|
||||
usage: ahriman help\-version [\-h]
|
||||
[1;34musage: [0m[1;35mahriman help\-version[0m [[32m\-h[0m]
|
||||
|
||||
print application and its dependencies versions
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-add'\/\fR
|
||||
usage: ahriman package\-add [\-h] [\-\-changes | \-\-no\-changes] [\-\-dependencies | \-\-no\-dependencies] [\-e]
|
||||
[\-\-increment | \-\-no\-increment] [\-n] [\-y]
|
||||
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] [\-v VARIABLE]
|
||||
package [package ...]
|
||||
[1;34musage: [0m[1;35mahriman package\-add[0m [[32m\-h[0m] [[36m\-\-changes | \-\-no\-changes[0m] [[36m\-\-dependencies | \-\-no\-dependencies[0m] [[32m\-e[0m]
|
||||
[[36m\-\-increment | \-\-no\-increment[0m] [[32m\-n[0m] [[32m\-y[0m]
|
||||
[[32m\-s [33m{auto,archive,aur,directory,local,remote,repository}[0m] [[32m\-u [33mUSERNAME[0m] [[32m\-v [33mVARIABLE[0m]
|
||||
[32mpackage [package ...][0m
|
||||
|
||||
add existing or new package to the build queue
|
||||
|
||||
@@ -303,7 +306,7 @@ build as user
|
||||
apply specified makepkg variables to the next build
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-changes'\/\fR
|
||||
usage: ahriman package\-changes [\-h] [\-e] package
|
||||
[1;34musage: [0m[1;35mahriman package\-changes[0m [[32m\-h[0m] [[32m\-e[0m] [32mpackage[0m
|
||||
|
||||
retrieve package changes stored in database
|
||||
|
||||
@@ -317,7 +320,7 @@ package base
|
||||
return non\-zero exit status if result is empty
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-changes\-remove'\/\fR
|
||||
usage: ahriman package\-changes\-remove [\-h] package
|
||||
[1;34musage: [0m[1;35mahriman package\-changes\-remove[0m [[32m\-h[0m] [32mpackage[0m
|
||||
|
||||
remove the package changes stored remotely
|
||||
|
||||
@@ -326,7 +329,7 @@ remove the package changes stored remotely
|
||||
package base
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-copy'\/\fR
|
||||
usage: ahriman package\-copy [\-h] [\-e] [\-\-remove] source package [package ...]
|
||||
[1;34musage: [0m[1;35mahriman package\-copy[0m [[32m\-h[0m] [[32m\-e[0m] [[36m\-\-remove[0m] [32msource[0m [32mpackage [package ...][0m
|
||||
|
||||
copy package and its metadata from another repository
|
||||
|
||||
@@ -348,7 +351,7 @@ return non\-zero exit status if result is empty
|
||||
remove package from the source repository after
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-remove'\/\fR
|
||||
usage: ahriman package\-remove [\-h] package [package ...]
|
||||
[1;34musage: [0m[1;35mahriman package\-remove[0m [[32m\-h[0m] [32mpackage [package ...][0m
|
||||
|
||||
remove package from the repository
|
||||
|
||||
@@ -357,8 +360,8 @@ remove package from the repository
|
||||
package name or base
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-status'\/\fR
|
||||
usage: ahriman package\-status [\-h] [\-\-ahriman] [\-e] [\-\-info | \-\-no\-info] [\-s {unknown,pending,building,failed,success}]
|
||||
[package ...]
|
||||
[1;34musage: [0m[1;35mahriman package\-status[0m [[32m\-h[0m] [[36m\-\-ahriman[0m] [[32m\-e[0m] [[36m\-\-info | \-\-no\-info[0m] [[32m\-s [33m{unknown,pending,building,failed,success}[0m]
|
||||
[32m[package ...][0m
|
||||
|
||||
request status of the package
|
||||
|
||||
@@ -384,7 +387,7 @@ show additional package information
|
||||
filter packages by status
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-status\-remove'\/\fR
|
||||
usage: ahriman package\-status\-remove [\-h] package [package ...]
|
||||
[1;34musage: [0m[1;35mahriman package\-status\-remove[0m [[32m\-h[0m] [32mpackage [package ...][0m
|
||||
|
||||
remove the package from the status page
|
||||
|
||||
@@ -393,7 +396,7 @@ remove the package from the status page
|
||||
remove specified packages from status page
|
||||
|
||||
.SH COMMAND \fI\,'ahriman package\-status\-update'\/\fR
|
||||
usage: ahriman package\-status\-update [\-h] [\-s {unknown,pending,building,failed,success}] [package ...]
|
||||
[1;34musage: [0m[1;35mahriman package\-status\-update[0m [[32m\-h[0m] [[32m\-s [33m{unknown,pending,building,failed,success}[0m] [32m[package ...][0m
|
||||
|
||||
update package status on the status page
|
||||
|
||||
@@ -407,7 +410,7 @@ set status for specified packages. If no packages supplied, service status will
|
||||
new package build status
|
||||
|
||||
.SH COMMAND \fI\,'ahriman patch\-add'\/\fR
|
||||
usage: ahriman patch\-add [\-h] package variable [patch]
|
||||
[1;34musage: [0m[1;35mahriman patch\-add[0m [[32m\-h[0m] [32mpackage[0m [32mvariable[0m [32m[patch][0m
|
||||
|
||||
create or update patched PKGBUILD function or variable
|
||||
|
||||
@@ -424,7 +427,7 @@ PKGBUILD variable or function name. If variable is a function, it must end with
|
||||
path to file which contains function or variable value. If not set, the value will be read from stdin
|
||||
|
||||
.SH COMMAND \fI\,'ahriman patch\-list'\/\fR
|
||||
usage: ahriman patch\-list [\-h] [\-e] [\-v VARIABLE] package
|
||||
[1;34musage: [0m[1;35mahriman patch\-list[0m [[32m\-h[0m] [[32m\-e[0m] [[32m\-v [33mVARIABLE[0m] [32mpackage[0m
|
||||
|
||||
list available patches for the package
|
||||
|
||||
@@ -442,7 +445,7 @@ return non\-zero exit status if result is empty
|
||||
if set, show only patches for specified PKGBUILD variables
|
||||
|
||||
.SH COMMAND \fI\,'ahriman patch\-remove'\/\fR
|
||||
usage: ahriman patch\-remove [\-h] [\-v VARIABLE] package
|
||||
[1;34musage: [0m[1;35mahriman patch\-remove[0m [[32m\-h[0m] [[32m\-v [33mVARIABLE[0m] [32mpackage[0m
|
||||
|
||||
remove patches for the package
|
||||
|
||||
@@ -457,7 +460,7 @@ should be used for single\-function patches in case if you wold like to remove o
|
||||
if not set, it will remove all patches related to the package
|
||||
|
||||
.SH COMMAND \fI\,'ahriman patch\-set\-add'\/\fR
|
||||
usage: ahriman patch\-set\-add [\-h] [\-t TRACK] package
|
||||
[1;34musage: [0m[1;35mahriman patch\-set\-add[0m [[32m\-h[0m] [[32m\-t [33mTRACK[0m] [32mpackage[0m
|
||||
|
||||
create or update source patches
|
||||
|
||||
@@ -471,7 +474,7 @@ path to directory with changed files for patch addition/update
|
||||
files which has to be tracked
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-backup'\/\fR
|
||||
usage: ahriman repo\-backup [\-h] path
|
||||
[1;34musage: [0m[1;35mahriman repo\-backup[0m [[32m\-h[0m] [32mpath[0m
|
||||
|
||||
backup repository settings and database
|
||||
|
||||
@@ -480,9 +483,9 @@ backup repository settings and database
|
||||
path of the output archive
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-check'\/\fR
|
||||
usage: ahriman repo\-check [\-h] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files] [\-e] [\-\-vcs | \-\-no\-vcs]
|
||||
[\-y]
|
||||
[package ...]
|
||||
[1;34musage: [0m[1;35mahriman repo\-check[0m [[32m\-h[0m] [[36m\-\-changes | \-\-no\-changes[0m] [[36m\-\-check\-files | \-\-no\-check\-files[0m] [[32m\-e[0m] [[36m\-\-vcs | \-\-no\-vcs[0m]
|
||||
[[32m\-y[0m]
|
||||
[32m[package ...][0m
|
||||
|
||||
check for packages updates. Same as repo\-update \-\-dry\-run \-\-no\-manual
|
||||
|
||||
@@ -512,20 +515,20 @@ fetch actual version of VCS packages
|
||||
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-create\-keyring'\/\fR
|
||||
usage: ahriman repo\-create\-keyring [\-h]
|
||||
[1;34musage: [0m[1;35mahriman repo\-create\-keyring[0m [[32m\-h[0m]
|
||||
|
||||
create package which contains list of trusted keys as set by configuration. Note, that this action will only create package, the package itself has to be built manually
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-create\-mirrorlist'\/\fR
|
||||
usage: ahriman repo\-create\-mirrorlist [\-h]
|
||||
[1;34musage: [0m[1;35mahriman repo\-create\-mirrorlist[0m [[32m\-h[0m]
|
||||
|
||||
create package which contains list of available mirrors as set by configuration. Note, that this action will only create package, the package itself has to be built manually
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-daemon'\/\fR
|
||||
usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes]
|
||||
[\-\-check\-files | \-\-no\-check\-files] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run]
|
||||
[\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual]
|
||||
[\-\-partitions | \-\-no\-partitions] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
|
||||
[1;34musage: [0m[1;35mahriman repo\-daemon[0m [[32m\-h[0m] [[32m\-i [33mINTERVAL[0m] [[36m\-\-aur | \-\-no\-aur[0m] [[36m\-\-changes | \-\-no\-changes[0m]
|
||||
[[36m\-\-check\-files | \-\-no\-check\-files[0m] [[36m\-\-dependencies | \-\-no\-dependencies[0m] [[36m\-\-dry\-run[0m]
|
||||
[[36m\-\-increment | \-\-no\-increment[0m] [[36m\-\-local | \-\-no\-local[0m] [[36m\-\-manual | \-\-no\-manual[0m]
|
||||
[[36m\-\-partitions | \-\-no\-partitions[0m] [[32m\-u [33mUSERNAME[0m] [[36m\-\-vcs | \-\-no\-vcs[0m] [[32m\-y[0m]
|
||||
|
||||
start process which periodically will run update process
|
||||
|
||||
@@ -583,8 +586,8 @@ fetch actual version of VCS packages
|
||||
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-rebuild'\/\fR
|
||||
usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-\-increment | \-\-no\-increment]
|
||||
[\-e] [\-s {unknown,pending,building,failed,success}] [\-u USERNAME]
|
||||
[1;34musage: [0m[1;35mahriman repo\-rebuild[0m [[32m\-h[0m] [[36m\-\-depends\-on [33mDEPENDS_ON[0m] [[36m\-\-dry\-run[0m] [[36m\-\-from\-database[0m] [[36m\-\-increment | \-\-no\-increment[0m]
|
||||
[[32m\-e[0m] [[32m\-s [33m{unknown,pending,building,failed,success}[0m] [[32m\-u [33mUSERNAME[0m]
|
||||
|
||||
force rebuild whole repository
|
||||
|
||||
@@ -620,7 +623,7 @@ filter packages by status. Requires \-\-from\-database to be set
|
||||
build as user
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-remove\-unknown'\/\fR
|
||||
usage: ahriman repo\-remove\-unknown [\-h] [\-\-dry\-run]
|
||||
[1;34musage: [0m[1;35mahriman repo\-remove\-unknown[0m [[32m\-h[0m] [[36m\-\-dry\-run[0m]
|
||||
|
||||
remove packages which are missing in AUR and do not have local PKGBUILDs
|
||||
|
||||
@@ -630,12 +633,12 @@ remove packages which are missing in AUR and do not have local PKGBUILDs
|
||||
just perform check for packages without removal
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-report'\/\fR
|
||||
usage: ahriman repo\-report [\-h]
|
||||
[1;34musage: [0m[1;35mahriman repo\-report[0m [[32m\-h[0m]
|
||||
|
||||
generate repository report according to current settings
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-restore'\/\fR
|
||||
usage: ahriman repo\-restore [\-h] [\-o OUTPUT] path
|
||||
[1;34musage: [0m[1;35mahriman repo\-restore[0m [[32m\-h[0m] [[32m\-o [33mOUTPUT[0m] [32mpath[0m
|
||||
|
||||
restore settings and database
|
||||
|
||||
@@ -649,7 +652,7 @@ path of the input archive
|
||||
root path of the extracted files
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-sign'\/\fR
|
||||
usage: ahriman repo\-sign [\-h] [package ...]
|
||||
[1;34musage: [0m[1;35mahriman repo\-sign[0m [[32m\-h[0m] [32m[package ...][0m
|
||||
|
||||
(re\-)sign packages and repository database according to current settings
|
||||
|
||||
@@ -658,10 +661,10 @@ usage: ahriman repo\-sign [\-h] [package ...]
|
||||
sign only specified packages
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-statistics'\/\fR
|
||||
usage: ahriman repo\-statistics [\-h] [\-\-chart CHART]
|
||||
[\-e {package\-outdated,package\-removed,package\-update\-failed,package\-updated}]
|
||||
[\-\-from\-date FROM_DATE] [\-\-limit LIMIT] [\-\-offset OFFSET] [\-\-to\-date TO_DATE]
|
||||
[package]
|
||||
[1;34musage: [0m[1;35mahriman repo\-statistics[0m [[32m\-h[0m] [[36m\-\-chart [33mCHART[0m]
|
||||
[[32m\-e [33m{package\-outdated,package\-removed,package\-update\-failed,package\-updated}[0m]
|
||||
[[36m\-\-from\-date [33mFROM_DATE[0m] [[36m\-\-limit [33mLIMIT[0m] [[36m\-\-offset [33mOFFSET[0m] [[36m\-\-to\-date [33mTO_DATE[0m]
|
||||
[32m[package][0m
|
||||
|
||||
fetch repository statistics
|
||||
|
||||
@@ -695,7 +698,7 @@ skip specified amount of events
|
||||
only fetch events which are older than the date
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-status\-update'\/\fR
|
||||
usage: ahriman repo\-status\-update [\-h] [\-s {unknown,pending,building,failed,success}]
|
||||
[1;34musage: [0m[1;35mahriman repo\-status\-update[0m [[32m\-h[0m] [[32m\-s [33m{unknown,pending,building,failed,success}[0m]
|
||||
|
||||
update repository status on the status page
|
||||
|
||||
@@ -705,12 +708,12 @@ update repository status on the status page
|
||||
new status
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-sync'\/\fR
|
||||
usage: ahriman repo\-sync [\-h]
|
||||
[1;34musage: [0m[1;35mahriman repo\-sync[0m [[32m\-h[0m]
|
||||
|
||||
sync repository files to remote server according to current settings
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-tree'\/\fR
|
||||
usage: ahriman repo\-tree [\-h] [\-p PARTITIONS]
|
||||
[1;34musage: [0m[1;35mahriman repo\-tree[0m [[32m\-h[0m] [[32m\-p [33mPARTITIONS[0m]
|
||||
|
||||
dump repository tree based on packages dependencies
|
||||
|
||||
@@ -720,7 +723,7 @@ dump repository tree based on packages dependencies
|
||||
also divide packages by independent partitions
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-triggers'\/\fR
|
||||
usage: ahriman repo\-triggers [\-h] [trigger ...]
|
||||
[1;34musage: [0m[1;35mahriman repo\-triggers[0m [[32m\-h[0m] [32m[trigger ...][0m
|
||||
|
||||
run triggers on empty build result as configured by settings
|
||||
|
||||
@@ -729,10 +732,10 @@ run triggers on empty build result as configured by settings
|
||||
instead of running all triggers as set by configuration, just process specified ones in order of mention
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR
|
||||
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files]
|
||||
[\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment]
|
||||
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
|
||||
[package ...]
|
||||
[1;34musage: [0m[1;35mahriman repo\-update[0m [[32m\-h[0m] [[36m\-\-aur | \-\-no\-aur[0m] [[36m\-\-changes | \-\-no\-changes[0m] [[36m\-\-check\-files | \-\-no\-check\-files[0m]
|
||||
[[36m\-\-dependencies | \-\-no\-dependencies[0m] [[36m\-\-dry\-run[0m] [[32m\-e[0m] [[36m\-\-increment | \-\-no\-increment[0m]
|
||||
[[36m\-\-local | \-\-no\-local[0m] [[36m\-\-manual | \-\-no\-manual[0m] [[32m\-u [33mUSERNAME[0m] [[36m\-\-vcs | \-\-no\-vcs[0m] [[32m\-y[0m]
|
||||
[32m[package ...][0m
|
||||
|
||||
check for packages updates and run build process if requested
|
||||
|
||||
@@ -790,8 +793,8 @@ fetch actual version of VCS packages
|
||||
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-clean'\/\fR
|
||||
usage: ahriman service\-clean [\-h] [\-\-cache | \-\-no\-cache] [\-\-chroot | \-\-no\-chroot] [\-\-manual | \-\-no\-manual]
|
||||
[\-\-packages | \-\-no\-packages] [\-\-pacman | \-\-no\-pacman]
|
||||
[1;34musage: [0m[1;35mahriman service\-clean[0m [[32m\-h[0m] [[36m\-\-cache | \-\-no\-cache[0m] [[36m\-\-chroot | \-\-no\-chroot[0m] [[36m\-\-manual | \-\-no\-manual[0m]
|
||||
[[36m\-\-packages | \-\-no\-packages[0m] [[36m\-\-pacman | \-\-no\-pacman[0m]
|
||||
|
||||
remove local caches
|
||||
|
||||
@@ -817,7 +820,7 @@ clear directory with built packages
|
||||
clear directory with pacman local database cache
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-config'\/\fR
|
||||
usage: ahriman service\-config [\-h] [\-\-info | \-\-no\-info] [\-\-secure | \-\-no\-secure] [section] [key]
|
||||
[1;34musage: [0m[1;35mahriman service\-config[0m [[32m\-h[0m] [[36m\-\-info | \-\-no\-info[0m] [[36m\-\-secure | \-\-no\-secure[0m] [32m[section][0m [32m[key][0m
|
||||
|
||||
dump configuration for the specified architecture
|
||||
|
||||
@@ -839,7 +842,7 @@ show additional information, e.g. configuration files
|
||||
hide passwords and secrets from output
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-config\-validate'\/\fR
|
||||
usage: ahriman service\-config\-validate [\-h] [\-e]
|
||||
[1;34musage: [0m[1;35mahriman service\-config\-validate[0m [[32m\-h[0m] [[32m\-e[0m]
|
||||
|
||||
validate configuration and print found errors
|
||||
|
||||
@@ -849,7 +852,7 @@ validate configuration and print found errors
|
||||
return non\-zero exit status if configuration is invalid
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-key\-import'\/\fR
|
||||
usage: ahriman service\-key\-import [\-h] [\-\-key\-server KEY_SERVER] key
|
||||
[1;34musage: [0m[1;35mahriman service\-key\-import[0m [[32m\-h[0m] [[36m\-\-key\-server [33mKEY_SERVER[0m] [32mkey[0m
|
||||
|
||||
import PGP key from public sources to the repository user
|
||||
|
||||
@@ -863,7 +866,7 @@ PGP key to import from public server
|
||||
key server for key import
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-repositories'\/\fR
|
||||
usage: ahriman service\-repositories [\-h] [\-\-id\-only | \-\-no\-id\-only]
|
||||
[1;34musage: [0m[1;35mahriman service\-repositories[0m [[32m\-h[0m] [[36m\-\-id\-only | \-\-no\-id\-only[0m]
|
||||
|
||||
list all available repositories
|
||||
|
||||
@@ -873,7 +876,7 @@ list all available repositories
|
||||
show machine readable identifier instead
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-run'\/\fR
|
||||
usage: ahriman service\-run [\-h] command [command ...]
|
||||
[1;34musage: [0m[1;35mahriman service\-run[0m [[32m\-h[0m] [32mcommand [command ...][0m
|
||||
|
||||
run multiple commands on success run of the previous command
|
||||
|
||||
@@ -882,11 +885,11 @@ run multiple commands on success run of the previous command
|
||||
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]
|
||||
[\-\-mirror MIRROR] [\-\-multilib | \-\-no\-multilib] \-\-packager PACKAGER [\-\-server SERVER]
|
||||
[\-\-sign\-key SIGN_KEY] [\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT]
|
||||
[\-\-web\-unix\-socket WEB_UNIX_SOCKET]
|
||||
[1;34musage: [0m[1;35mahriman service\-setup[0m [[32m\-h[0m] [[36m\-\-build\-as\-user [33mBUILD_AS_USER[0m] [[36m\-\-from\-configuration [33mFROM_CONFIGURATION[0m]
|
||||
[[36m\-\-generate\-salt | \-\-no\-generate\-salt[0m] [[36m\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs[0m]
|
||||
[[36m\-\-mirror [33mMIRROR[0m] [[36m\-\-multilib | \-\-no\-multilib[0m] [36m\-\-packager [33mPACKAGER[0m [[36m\-\-server [33mSERVER[0m]
|
||||
[[36m\-\-sign\-key [33mSIGN_KEY[0m] [[36m\-\-sign\-target [33m{disabled,packages,repository}[0m] [[36m\-\-web\-port [33mWEB_PORT[0m]
|
||||
[[36m\-\-web\-unix\-socket [33mWEB_UNIX_SOCKET[0m]
|
||||
|
||||
create initial service configuration, requires root
|
||||
|
||||
@@ -940,7 +943,7 @@ port of the web service
|
||||
path to unix socket used for interprocess communications
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-shell'\/\fR
|
||||
usage: ahriman service\-shell [\-h] [\-o OUTPUT] [code]
|
||||
[1;34musage: [0m[1;35mahriman service\-shell[0m [[32m\-h[0m] [[32m\-o [33mOUTPUT[0m] [32m[code][0m
|
||||
|
||||
drop into python shell
|
||||
|
||||
@@ -954,13 +957,13 @@ instead of dropping into shell, just execute the specified code
|
||||
output commands and result to the file
|
||||
|
||||
.SH COMMAND \fI\,'ahriman service\-tree\-migrate'\/\fR
|
||||
usage: ahriman service\-tree\-migrate [\-h]
|
||||
[1;34musage: [0m[1;35mahriman service\-tree\-migrate[0m [[32m\-h[0m]
|
||||
|
||||
migrate repository tree between versions
|
||||
|
||||
.SH COMMAND \fI\,'ahriman user\-add'\/\fR
|
||||
usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-R {unauthorized,read,reporter,full}]
|
||||
username
|
||||
[1;34musage: [0m[1;35mahriman user\-add[0m [[32m\-h[0m] [[36m\-\-key [33mKEY[0m] [[36m\-\-packager [33mPACKAGER[0m] [[32m\-p [33mPASSWORD[0m] [[32m\-R [33m{unauthorized,read,reporter,full}[0m]
|
||||
[32musername[0m
|
||||
|
||||
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
|
||||
|
||||
@@ -987,7 +990,7 @@ authorization type.
|
||||
user access level
|
||||
|
||||
.SH COMMAND \fI\,'ahriman user\-list'\/\fR
|
||||
usage: ahriman user\-list [\-h] [\-e] [\-R {unauthorized,read,reporter,full}] [username]
|
||||
[1;34musage: [0m[1;35mahriman user\-list[0m [[32m\-h[0m] [[32m\-e[0m] [[32m\-R [33m{unauthorized,read,reporter,full}[0m] [32m[username][0m
|
||||
|
||||
list users from the user mapping and their roles
|
||||
|
||||
@@ -1005,7 +1008,7 @@ return non\-zero exit status if result is empty
|
||||
filter users by role
|
||||
|
||||
.SH COMMAND \fI\,'ahriman user\-remove'\/\fR
|
||||
usage: ahriman user\-remove [\-h] username
|
||||
[1;34musage: [0m[1;35mahriman user\-remove[0m [[32m\-h[0m] [32musername[0m
|
||||
|
||||
remove user from the user mapping and update the configuration
|
||||
|
||||
@@ -1014,10 +1017,15 @@ remove user from the user mapping and update the configuration
|
||||
username for web service
|
||||
|
||||
.SH COMMAND \fI\,'ahriman web'\/\fR
|
||||
usage: ahriman web [\-h]
|
||||
[1;34musage: [0m[1;35mahriman web[0m [[32m\-h[0m]
|
||||
|
||||
start web server
|
||||
|
||||
.SH COMMAND \fI\,'ahriman web\-reload'\/\fR
|
||||
[1;34musage: [0m[1;35mahriman web\-reload[0m [[32m\-h[0m]
|
||||
|
||||
reload web server configuration
|
||||
|
||||
.SH COMMENTS
|
||||
Quick setup command (replace repository name, architecture and packager as needed):
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ _shtab_ahriman_commands() {
|
||||
"user-remove:remove user from the user mapping and update the configuration"
|
||||
"version:print application and its dependencies versions"
|
||||
"web:start web server"
|
||||
"web-reload:reload web server configuration"
|
||||
)
|
||||
_describe 'ahriman commands' _commands
|
||||
}
|
||||
@@ -99,6 +100,9 @@ _shtab_ahriman_options=(
|
||||
"--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:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_defaults_added=0
|
||||
|
||||
_shtab_ahriman_add_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
|
||||
@@ -113,6 +117,9 @@ _shtab_ahriman_add_options=(
|
||||
"(*):package source (base name, path to local files, remote URL):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_add_defaults_added=0
|
||||
|
||||
_shtab_ahriman_aur_search_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@@ -121,6 +128,9 @@ _shtab_ahriman_aur_search_options=(
|
||||
"(*):search terms, can be specified multiple times, the result will match all terms:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_aur_search_defaults_added=0
|
||||
|
||||
_shtab_ahriman_check_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
|
||||
@@ -131,6 +141,9 @@ _shtab_ahriman_check_options=(
|
||||
"(*)::filter check by package base (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_check_defaults_added=0
|
||||
|
||||
_shtab_ahriman_clean_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--cache,--no-cache}"[clear directory with package caches (default\: False)]:cache:"
|
||||
@@ -140,6 +153,9 @@ _shtab_ahriman_clean_options=(
|
||||
{--pacman,--no-pacman}"[clear directory with pacman local database cache (default\: False)]:pacman:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_clean_defaults_added=0
|
||||
|
||||
_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:"
|
||||
@@ -148,11 +164,17 @@ _shtab_ahriman_config_options=(
|
||||
":filter settings by key (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_config_defaults_added=0
|
||||
|
||||
_shtab_ahriman_config_validate_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if configuration is invalid (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_config_validate_defaults_added=0
|
||||
|
||||
_shtab_ahriman_copy_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@@ -161,6 +183,9 @@ _shtab_ahriman_copy_options=(
|
||||
"(*):package base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_copy_defaults_added=0
|
||||
|
||||
_shtab_ahriman_daemon_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
|
||||
@@ -178,25 +203,40 @@ _shtab_ahriman_daemon_options=(
|
||||
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_daemon_defaults_added=0
|
||||
|
||||
_shtab_ahriman_help_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
":show help message for specific command (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_help_defaults_added=0
|
||||
|
||||
_shtab_ahriman_help_commands_unsafe_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*)::instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_help_commands_unsafe_defaults_added=0
|
||||
|
||||
_shtab_ahriman_help_updates_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit code if updates available (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_help_updates_defaults_added=0
|
||||
|
||||
_shtab_ahriman_help_version_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_help_version_defaults_added=0
|
||||
|
||||
_shtab_ahriman_init_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
|
||||
@@ -213,12 +253,18 @@ _shtab_ahriman_init_options=(
|
||||
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_init_defaults_added=0
|
||||
|
||||
_shtab_ahriman_key_import_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--key-server[key server for key import (default\: keyserver.ubuntu.com)]:key_server:"
|
||||
":PGP key to import from public server:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_key_import_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_add_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
|
||||
@@ -233,17 +279,26 @@ _shtab_ahriman_package_add_options=(
|
||||
"(*):package source (base name, path to local files, remote URL):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_add_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_changes_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
":package base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_changes_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_changes_remove_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
":package base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_changes_remove_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_copy_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@@ -252,11 +307,17 @@ _shtab_ahriman_package_copy_options=(
|
||||
"(*):package base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_copy_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_remove_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*):package name or base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_remove_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_status_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--ahriman[get service status itself (default\: False)]"
|
||||
@@ -266,17 +327,26 @@ _shtab_ahriman_package_status_options=(
|
||||
"(*)::filter status by package base (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_status_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_status_remove_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*):remove specified packages from status page:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_status_remove_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_status_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-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):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_status_update_defaults_added=0
|
||||
|
||||
_shtab_ahriman_package_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
|
||||
@@ -291,6 +361,9 @@ _shtab_ahriman_package_update_options=(
|
||||
"(*):package source (base name, path to local files, remote URL):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_package_update_defaults_added=0
|
||||
|
||||
_shtab_ahriman_patch_add_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
":package base:"
|
||||
@@ -298,6 +371,9 @@ _shtab_ahriman_patch_add_options=(
|
||||
":path to file which contains function or variable value. If not set, the value will be read from stdin (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_patch_add_defaults_added=0
|
||||
|
||||
_shtab_ahriman_patch_list_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@@ -305,18 +381,27 @@ _shtab_ahriman_patch_list_options=(
|
||||
":package base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_patch_list_defaults_added=0
|
||||
|
||||
_shtab_ahriman_patch_remove_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"*"{-v,--variable}"[should be used for single-function patches in case if you wold like to remove only specified PKGBUILD variables. In case if not set, it will remove all patches related to the package (default\: None)]:variable:"
|
||||
":package base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_patch_remove_defaults_added=0
|
||||
|
||||
_shtab_ahriman_patch_set_add_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"*"{-t,--track}"[files which has to be tracked (default\: \[\'\*.diff\', \'\*.patch\'\])]:track:"
|
||||
":path to directory with changed files for patch addition\/update:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_patch_set_add_defaults_added=0
|
||||
|
||||
_shtab_ahriman_rebuild_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"*--depends-on[only rebuild packages that depend on specified packages (default\: None)]:depends_on:"
|
||||
@@ -328,21 +413,33 @@ _shtab_ahriman_rebuild_options=(
|
||||
{-u,--username}"[build as user (default\: None)]:username:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_rebuild_defaults_added=0
|
||||
|
||||
_shtab_ahriman_remove_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*):package name or base:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_remove_defaults_added=0
|
||||
|
||||
_shtab_ahriman_remove_unknown_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--dry-run[just perform check for packages without removal (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_remove_unknown_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_backup_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
":path of the output archive:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_backup_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_check_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
|
||||
@@ -353,6 +450,9 @@ _shtab_ahriman_repo_check_options=(
|
||||
"(*)::filter check by package base (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_check_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_clean_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--cache,--no-cache}"[clear directory with package caches (default\: False)]:cache:"
|
||||
@@ -362,6 +462,9 @@ _shtab_ahriman_repo_clean_options=(
|
||||
{--pacman,--no-pacman}"[clear directory with pacman local database cache (default\: False)]:pacman:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_clean_defaults_added=0
|
||||
|
||||
_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:"
|
||||
@@ -370,19 +473,31 @@ _shtab_ahriman_repo_config_options=(
|
||||
":filter settings by key (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_config_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_config_validate_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if configuration is invalid (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_config_validate_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_create_keyring_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_create_keyring_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_create_mirrorlist_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_create_mirrorlist_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_daemon_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
|
||||
@@ -400,6 +515,9 @@ _shtab_ahriman_repo_daemon_options=(
|
||||
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_daemon_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_init_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
|
||||
@@ -416,6 +534,9 @@ _shtab_ahriman_repo_init_options=(
|
||||
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_init_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_rebuild_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"*--depends-on[only rebuild packages that depend on specified packages (default\: None)]:depends_on:"
|
||||
@@ -427,21 +548,33 @@ _shtab_ahriman_repo_rebuild_options=(
|
||||
{-u,--username}"[build as user (default\: None)]:username:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_rebuild_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_remove_unknown_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--dry-run[just perform check for packages without removal (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_remove_unknown_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_report_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_report_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_restore_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-o,--output}"[root path of the extracted files (default\: \/)]:output:"
|
||||
":path of the input archive:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_restore_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_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:"
|
||||
@@ -458,11 +591,17 @@ _shtab_ahriman_repo_setup_options=(
|
||||
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_setup_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_sign_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*)::sign only specified packages (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_sign_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_statistics_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--chart[create updates chart and save it to the specified path (default\: None)]:chart:"
|
||||
@@ -474,25 +613,40 @@ _shtab_ahriman_repo_statistics_options=(
|
||||
":fetch only events for the specified package (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_statistics_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_status_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-s,--status}"[new status (default\: success)]:status:(unknown pending building failed success)"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_status_update_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_sync_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_sync_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_tree_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-p,--partitions}"[also divide packages by independent partitions (default\: 1)]:partitions:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_tree_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_triggers_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*)::instead of running all triggers as set by configuration, just process specified ones in order of mention (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_triggers_defaults_added=0
|
||||
|
||||
_shtab_ahriman_repo_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
|
||||
@@ -510,15 +664,24 @@ _shtab_ahriman_repo_update_options=(
|
||||
"(*)::filter check by package base (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_repo_update_defaults_added=0
|
||||
|
||||
_shtab_ahriman_report_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_report_defaults_added=0
|
||||
|
||||
_shtab_ahriman_run_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_run_defaults_added=0
|
||||
|
||||
_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)]"
|
||||
@@ -527,6 +690,9 @@ _shtab_ahriman_search_options=(
|
||||
"(*):search terms, can be specified multiple times, the result will match all terms:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_search_defaults_added=0
|
||||
|
||||
_shtab_ahriman_service_clean_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--cache,--no-cache}"[clear directory with package caches (default\: False)]:cache:"
|
||||
@@ -536,6 +702,9 @@ _shtab_ahriman_service_clean_options=(
|
||||
{--pacman,--no-pacman}"[clear directory with pacman local database cache (default\: False)]:pacman:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_clean_defaults_added=0
|
||||
|
||||
_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:"
|
||||
@@ -544,27 +713,42 @@ _shtab_ahriman_service_config_options=(
|
||||
":filter settings by key (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_config_defaults_added=0
|
||||
|
||||
_shtab_ahriman_service_config_validate_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if configuration is invalid (default\: False)]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_config_validate_defaults_added=0
|
||||
|
||||
_shtab_ahriman_service_key_import_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--key-server[key server for key import (default\: keyserver.ubuntu.com)]:key_server:"
|
||||
":PGP key to import from public server:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_key_import_defaults_added=0
|
||||
|
||||
_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:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_repositories_defaults_added=0
|
||||
|
||||
_shtab_ahriman_service_run_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_run_defaults_added=0
|
||||
|
||||
_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:"
|
||||
@@ -581,16 +765,25 @@ _shtab_ahriman_service_setup_options=(
|
||||
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_setup_defaults_added=0
|
||||
|
||||
_shtab_ahriman_service_shell_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-o,--output}"[output commands and result to the file (default\: None)]:output:"
|
||||
":instead of dropping into shell, just execute the specified code (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_shell_defaults_added=0
|
||||
|
||||
_shtab_ahriman_service_tree_migrate_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_service_tree_migrate_defaults_added=0
|
||||
|
||||
_shtab_ahriman_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:"
|
||||
@@ -607,17 +800,26 @@ _shtab_ahriman_setup_options=(
|
||||
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_setup_defaults_added=0
|
||||
|
||||
_shtab_ahriman_shell_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-o,--output}"[output commands and result to the file (default\: None)]:output:"
|
||||
":instead of dropping into shell, just execute the specified code (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_shell_defaults_added=0
|
||||
|
||||
_shtab_ahriman_sign_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"(*)::sign only specified packages (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_sign_defaults_added=0
|
||||
|
||||
_shtab_ahriman_status_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--ahriman[get service status itself (default\: False)]"
|
||||
@@ -627,16 +829,25 @@ _shtab_ahriman_status_options=(
|
||||
"(*)::filter status by package base (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_status_defaults_added=0
|
||||
|
||||
_shtab_ahriman_status_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-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):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_status_update_defaults_added=0
|
||||
|
||||
_shtab_ahriman_sync_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_sync_defaults_added=0
|
||||
|
||||
_shtab_ahriman_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
|
||||
@@ -654,6 +865,9 @@ _shtab_ahriman_update_options=(
|
||||
"(*)::filter check by package base (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_update_defaults_added=0
|
||||
|
||||
_shtab_ahriman_user_add_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
"--key[optional PGP key used by this user. The private key must be imported (default\: None)]:key:"
|
||||
@@ -663,6 +877,9 @@ _shtab_ahriman_user_add_options=(
|
||||
":username for web service:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_user_add_defaults_added=0
|
||||
|
||||
_shtab_ahriman_user_list_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@@ -670,25 +887,48 @@ _shtab_ahriman_user_list_options=(
|
||||
":filter users by username (default\: None):"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_user_list_defaults_added=0
|
||||
|
||||
_shtab_ahriman_user_remove_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
":username for web service:"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_user_remove_defaults_added=0
|
||||
|
||||
_shtab_ahriman_version_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_version_defaults_added=0
|
||||
|
||||
_shtab_ahriman_web_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_web_defaults_added=0
|
||||
|
||||
_shtab_ahriman_web_reload_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
)
|
||||
|
||||
# guard to ensure default positional specs are added only once per session
|
||||
_shtab_ahriman_web_reload_defaults_added=0
|
||||
|
||||
|
||||
_shtab_ahriman() {
|
||||
local context state line curcontext="$curcontext" one_or_more='(-)*' remainder='(*)'
|
||||
local context state line curcontext="$curcontext" one_or_more='(*)' remainder='(-)*' default='*::: :->ahriman'
|
||||
|
||||
if ((${_shtab_ahriman_options[(I)${(q)one_or_more}*]} + ${_shtab_ahriman_options[(I)${(q)remainder}*]} == 0)); then # noqa: E501
|
||||
_shtab_ahriman_options+=(': :_shtab_ahriman_commands' '*::: :->ahriman')
|
||||
# Add default positional/remainder specs only if none exist, and only once per session
|
||||
if (( ! _shtab_ahriman_defaults_added )); then
|
||||
if (( ${_shtab_ahriman_options[(I)${(q)one_or_more}*]} + ${_shtab_ahriman_options[(I)${(q)remainder}*]} + ${_shtab_ahriman_options[(I)${(q)default}]} == 0 )); then
|
||||
_shtab_ahriman_options+=(': :_shtab_ahriman_commands' '*::: :->ahriman')
|
||||
fi
|
||||
_shtab_ahriman_defaults_added=1
|
||||
fi
|
||||
_arguments -C -s $_shtab_ahriman_options
|
||||
|
||||
@@ -773,6 +1013,7 @@ _shtab_ahriman() {
|
||||
user-remove) _arguments -C -s $_shtab_ahriman_user_remove_options ;;
|
||||
version) _arguments -C -s $_shtab_ahriman_version_options ;;
|
||||
web) _arguments -C -s $_shtab_ahriman_web_options ;;
|
||||
web-reload) _arguments -C -s $_shtab_ahriman_web_reload_options ;;
|
||||
esac
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ authors = [
|
||||
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"filelock",
|
||||
"inflection",
|
||||
"pyelftools",
|
||||
"requests",
|
||||
|
||||
@@ -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.19.0"
|
||||
__version__ = "2.20.0rc3"
|
||||
|
||||
@@ -22,6 +22,7 @@ from collections.abc import Iterable
|
||||
from ahriman.application.application.application_properties import ApplicationProperties
|
||||
from ahriman.application.application.workers import Updater
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.packagers import Packagers
|
||||
from ahriman.models.result import Result
|
||||
@@ -116,7 +117,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
for single in probe.packages:
|
||||
try:
|
||||
_ = Package.from_aur(single, None)
|
||||
except Exception:
|
||||
except UnknownPackageError:
|
||||
packages.append(single)
|
||||
return packages
|
||||
|
||||
|
||||
@@ -81,11 +81,13 @@ class Backup(Handler):
|
||||
Returns:
|
||||
set[Path]: map of the filesystem paths
|
||||
"""
|
||||
paths = set(configuration.include.glob("*.ini"))
|
||||
|
||||
# configuration files
|
||||
root, _ = configuration.check_loaded()
|
||||
paths.add(root) # the configuration itself
|
||||
paths.add(SQLite.database_path(configuration)) # database
|
||||
paths = set(configuration.includes)
|
||||
paths.add(root)
|
||||
|
||||
# database
|
||||
paths.add(SQLite.database_path(configuration))
|
||||
|
||||
# local caches
|
||||
repository_paths = configuration.repository_paths
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable
|
||||
from multiprocessing import Pool
|
||||
from typing import ClassVar, TypeVar
|
||||
|
||||
@@ -28,9 +28,9 @@ from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError
|
||||
from ahriman.core.log.log_loader import LogLoader
|
||||
from ahriman.core.repository import Explorer
|
||||
from ahriman.core.types import ExplicitBool
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
# this workaround is for several things
|
||||
@@ -169,11 +169,6 @@ class Handler:
|
||||
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:
|
||||
@@ -184,27 +179,10 @@ class Handler:
|
||||
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")]
|
||||
configuration = Configuration()
|
||||
configuration.load(args.configuration)
|
||||
repositories = Explorer.repositories_extract(configuration, args.repository, args.architecture)
|
||||
|
||||
# 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:
|
||||
if not repositories:
|
||||
raise MissingArchitectureError(args.command)
|
||||
return sorted(parsed)
|
||||
return sorted(repositories)
|
||||
|
||||
@@ -47,7 +47,7 @@ class Restore(Handler):
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
with tarfile.open(args.path) as archive:
|
||||
archive.extractall(path=args.output) # nosec
|
||||
archive.extractall(path=args.output, filter="data")
|
||||
|
||||
@staticmethod
|
||||
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
|
||||
@@ -72,14 +72,16 @@ class Setup(Handler):
|
||||
|
||||
application = Application(repository_id, configuration, report=report)
|
||||
|
||||
with application.repository.paths.preserve_owner():
|
||||
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
|
||||
Setup.executable_create(application.repository.paths, repository_id)
|
||||
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
|
||||
Setup.configuration_create_devtools(
|
||||
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
|
||||
Setup.configuration_create_sudo(application.repository.paths, repository_id)
|
||||
# basically we create configuration here as root, but it is ok, because those files are only used for reading
|
||||
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
|
||||
Setup.executable_create(application.repository.paths, repository_id)
|
||||
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
|
||||
Setup.configuration_create_devtools(
|
||||
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
|
||||
Setup.configuration_create_sudo(application.repository.paths, repository_id)
|
||||
|
||||
# finish initialization
|
||||
with application.repository.paths.preserve_owner():
|
||||
application.repository.repo.init()
|
||||
# lazy database sync
|
||||
application.repository.pacman.handle # pylint: disable=pointless-statement
|
||||
|
||||
@@ -66,7 +66,7 @@ class Status(Handler):
|
||||
Status.check_status(args.exit_code, packages)
|
||||
|
||||
comparator: Callable[[tuple[Package, BuildStatus]], Comparable] = lambda item: item[0].base
|
||||
filter_fn: Callable[[tuple[Package, BuildStatus]], bool] =\
|
||||
filter_fn: Callable[[tuple[Package, BuildStatus]], bool] = \
|
||||
lambda item: args.status is None or item[1].status == args.status
|
||||
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
|
||||
PackagePrinter(package, package_status)(verbose=args.info)
|
||||
|
||||
@@ -21,6 +21,7 @@ import argparse
|
||||
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.utils import symlink_relative, walk
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
@@ -49,6 +50,7 @@ class TreeMigrate(Handler):
|
||||
target_tree.tree_create()
|
||||
# perform migration
|
||||
TreeMigrate.tree_move(current_tree, target_tree)
|
||||
TreeMigrate.symlinks_fix(target_tree)
|
||||
|
||||
@staticmethod
|
||||
def _set_service_tree_migrate_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
@@ -66,6 +68,22 @@ class TreeMigrate(Handler):
|
||||
parser.set_defaults(lock=None, quiet=True, report=False)
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def symlinks_fix(paths: RepositoryPaths) -> None:
|
||||
"""
|
||||
fix package archive symlinks
|
||||
|
||||
Args:
|
||||
paths(RepositoryPaths): new repository paths
|
||||
"""
|
||||
archives = {path.name: path for path in walk(paths.archive)}
|
||||
for symlink in walk(paths.repository):
|
||||
if symlink.exists(): # no need to check for symlinks as we have just walked through the tree
|
||||
continue
|
||||
if (source_archive := archives.get(symlink.name)) is not None:
|
||||
symlink.unlink()
|
||||
symlink_relative(symlink, source_archive)
|
||||
|
||||
@staticmethod
|
||||
def tree_move(from_tree: RepositoryPaths, to_tree: RepositoryPaths) -> None:
|
||||
"""
|
||||
|
||||
@@ -21,7 +21,7 @@ import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from importlib import metadata
|
||||
from typing import ClassVar
|
||||
|
||||
@@ -77,7 +77,7 @@ class Versions(Handler):
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def package_dependencies(root: str) -> Generator[tuple[str, str], None, None]:
|
||||
def package_dependencies(root: str) -> Iterator[tuple[str, str]]:
|
||||
"""
|
||||
extract list of ahriman package dependencies installed into system with their versions
|
||||
|
||||
@@ -87,7 +87,7 @@ class Versions(Handler):
|
||||
Yields:
|
||||
tuple[str, str]: map of installed dependency to its version
|
||||
"""
|
||||
def dependencies_by_key(key: str) -> Generator[str, None, None]:
|
||||
def dependencies_by_key(key: str) -> Iterator[str]:
|
||||
# in importlib it returns requires in the following format
|
||||
# ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
|
||||
try:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
import argparse
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
@@ -86,7 +86,7 @@ class Web(Handler):
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Generator[str, None, None]:
|
||||
def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Iterator[str]:
|
||||
"""
|
||||
extract list of arguments used for current command, except for command specific ones
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ class Lock(LazyLogging):
|
||||
"""
|
||||
check if current user is actually owner of ahriman root
|
||||
"""
|
||||
check_user(self.paths, unsafe=self.unsafe)
|
||||
check_user(self.paths.root, unsafe=self.unsafe)
|
||||
self.paths.tree_create()
|
||||
|
||||
def check_version(self) -> None:
|
||||
|
||||
@@ -21,7 +21,7 @@ import itertools
|
||||
import shutil
|
||||
import tarfile
|
||||
|
||||
from collections.abc import Generator, Iterable
|
||||
from collections.abc import Iterable, Iterator
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
||||
@@ -130,7 +130,7 @@ class Pacman(LazyLogging):
|
||||
return # database for some reason deos not exist
|
||||
|
||||
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
|
||||
with self.repository_paths.preserve_owner(dst.parent):
|
||||
with self.repository_paths.preserve_owner():
|
||||
shutil.copy(src, dst)
|
||||
|
||||
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
|
||||
@@ -188,7 +188,7 @@ class Pacman(LazyLogging):
|
||||
Returns:
|
||||
dict[str, set[str]]: map of package name to its list of files
|
||||
"""
|
||||
def extract(tar: tarfile.TarFile, versions: dict[str, str]) -> Generator[tuple[str, set[str]], None, None]:
|
||||
def extract(tar: tarfile.TarFile, versions: dict[str, str]) -> Iterator[tuple[str, set[str]]]:
|
||||
for package_name, version in versions.items():
|
||||
path = Path(f"{package_name}-{version}") / "files"
|
||||
try:
|
||||
@@ -223,7 +223,7 @@ class Pacman(LazyLogging):
|
||||
|
||||
return result
|
||||
|
||||
def package(self, package_name: str) -> Generator[Package, None, None]:
|
||||
def package(self, package_name: str) -> Iterator[Package]:
|
||||
"""
|
||||
retrieve list of the packages from the repository by name
|
||||
|
||||
@@ -256,7 +256,7 @@ class Pacman(LazyLogging):
|
||||
|
||||
return result
|
||||
|
||||
def provided_by(self, package_name: str) -> Generator[Package, None, None]:
|
||||
def provided_by(self, package_name: str) -> Iterator[Package]:
|
||||
"""
|
||||
search through databases and emit packages which provides the ``package_name``
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import itertools
|
||||
import re
|
||||
import shlex
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from enum import StrEnum
|
||||
from typing import IO
|
||||
|
||||
@@ -209,7 +209,7 @@ class PkgbuildParser(shlex.shlex):
|
||||
Raises:
|
||||
PkgbuildParserError: if array is not closed
|
||||
"""
|
||||
def extract() -> Generator[str, None, None]:
|
||||
def extract() -> Iterator[str]:
|
||||
while token := self.get_token():
|
||||
match token:
|
||||
case _ if self._is_escaped():
|
||||
@@ -276,7 +276,7 @@ class PkgbuildParser(shlex.shlex):
|
||||
|
||||
return content
|
||||
|
||||
def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]:
|
||||
def _parse_token(self, token: str) -> Iterator[PkgbuildPatch]:
|
||||
"""
|
||||
parse single token to the PKGBUILD field
|
||||
|
||||
@@ -360,7 +360,7 @@ class PkgbuildParser(shlex.shlex):
|
||||
|
||||
raise PkgbuildParserError("reached starting position, no valid symbols found")
|
||||
|
||||
def parse(self) -> Generator[PkgbuildPatch, None, None]:
|
||||
def parse(self) -> Iterator[PkgbuildPatch]:
|
||||
"""
|
||||
parse source stream and yield parsed entries
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class AUR(Remote):
|
||||
"""
|
||||
generate remote git url from the package base
|
||||
|
||||
Args
|
||||
Args:
|
||||
package_base(str): package base
|
||||
repository(str): repository name
|
||||
|
||||
@@ -58,7 +58,7 @@ class AUR(Remote):
|
||||
"""
|
||||
generate remote web url from the package base
|
||||
|
||||
Args
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -46,7 +46,7 @@ class Official(Remote):
|
||||
"""
|
||||
generate remote git url from the package base
|
||||
|
||||
Args
|
||||
Args:
|
||||
package_base(str): package base
|
||||
repository(str): repository name
|
||||
|
||||
@@ -60,7 +60,7 @@ class Official(Remote):
|
||||
"""
|
||||
generate remote web url from the package base
|
||||
|
||||
Args
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -110,7 +110,7 @@ class Remote(SyncHttpClient):
|
||||
"""
|
||||
generate remote git url from the package base
|
||||
|
||||
Args
|
||||
Args:
|
||||
package_base(str): package base
|
||||
repository(str): repository name
|
||||
|
||||
@@ -127,7 +127,7 @@ class Remote(SyncHttpClient):
|
||||
"""
|
||||
generate remote web url from the package base
|
||||
|
||||
Args
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -31,20 +31,21 @@ class Repo(LazyLogging):
|
||||
|
||||
Attributes:
|
||||
name(str): repository name
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
root(Path): repository root
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
uid(int): uid of the repository owner user
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, paths: RepositoryPaths, sign_args: list[str]) -> None:
|
||||
def __init__(self, name: str, paths: RepositoryPaths, sign_args: list[str], root: Path | None = None) -> None:
|
||||
"""
|
||||
Args:
|
||||
name(str): repository name
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
root(Path | None, optional): repository root. If none set, the default will be used (Default value = None)
|
||||
"""
|
||||
self.name = name
|
||||
self.paths = paths
|
||||
self.root = root or paths.repository
|
||||
self.uid, _ = paths.root_owner
|
||||
self.sign_args = sign_args
|
||||
|
||||
@@ -56,7 +57,7 @@ class Repo(LazyLogging):
|
||||
Returns:
|
||||
Path: path to repository database
|
||||
"""
|
||||
return self.paths.repository / f"{self.name}.db.tar.gz"
|
||||
return self.root / f"{self.name}.db.tar.gz"
|
||||
|
||||
def add(self, path: Path) -> None:
|
||||
"""
|
||||
@@ -66,35 +67,37 @@ class Repo(LazyLogging):
|
||||
path(Path): path to archive to add
|
||||
"""
|
||||
check_output(
|
||||
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
|
||||
"repo-add", *self.sign_args, "--remove", str(self.repo_path), str(path),
|
||||
exception=BuildError.from_process(path.name),
|
||||
cwd=self.paths.repository,
|
||||
cwd=self.root,
|
||||
logger=self.logger,
|
||||
user=self.uid)
|
||||
user=self.uid,
|
||||
)
|
||||
|
||||
def init(self) -> None:
|
||||
"""
|
||||
create empty repository database. It just calls add with empty arguments
|
||||
"""
|
||||
check_output("repo-add", *self.sign_args, str(self.repo_path),
|
||||
cwd=self.paths.repository, logger=self.logger, user=self.uid)
|
||||
cwd=self.root, logger=self.logger, user=self.uid)
|
||||
|
||||
def remove(self, package: str, filename: Path) -> None:
|
||||
def remove(self, package_name: str, filename: Path) -> None:
|
||||
"""
|
||||
remove package from repository
|
||||
|
||||
Args:
|
||||
package(str): package name to remove
|
||||
package_name(str): package name to remove
|
||||
filename(Path): package filename to remove
|
||||
"""
|
||||
# remove package and signature (if any) from filesystem
|
||||
for full_path in self.paths.repository.glob(f"{filename}*"):
|
||||
for full_path in self.root.glob(f"{filename.name}*"):
|
||||
full_path.unlink()
|
||||
|
||||
# remove package from registry
|
||||
check_output(
|
||||
"repo-remove", *self.sign_args, str(self.repo_path), package,
|
||||
exception=BuildError.from_process(package),
|
||||
cwd=self.paths.repository,
|
||||
"repo-remove", *self.sign_args, str(self.repo_path), package_name,
|
||||
exception=BuildError.from_process(package_name),
|
||||
cwd=self.root,
|
||||
logger=self.logger,
|
||||
user=self.uid)
|
||||
user=self.uid,
|
||||
)
|
||||
|
||||
20
src/ahriman/core/archive/__init__.py
Normal file
20
src/ahriman/core/archive/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 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.archive.archive_trigger import ArchiveTrigger
|
||||
185
src/ahriman/core/archive/archive_tree.py
Normal file
185
src/ahriman/core/archive/archive_tree.py
Normal file
@@ -0,0 +1,185 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 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 datetime
|
||||
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import package_like, symlink_relative, utcnow, walk
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class ArchiveTree(LazyLogging):
|
||||
"""
|
||||
wrapper around archive tree
|
||||
|
||||
Attributes:
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
"""
|
||||
|
||||
def __init__(self, repository_path: RepositoryPaths, sign_args: list[str]) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_path(RepositoryPaths): repository paths instance
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
"""
|
||||
self.paths = repository_path
|
||||
self.repository_id = repository_path.repository_id
|
||||
self.sign_args = sign_args
|
||||
|
||||
@staticmethod
|
||||
def _package_symlinks_create(package_description: PackageDescription, root: Path, archive: Path) -> bool:
|
||||
"""
|
||||
process symlinks creation for single package
|
||||
|
||||
Args:
|
||||
package_description(PackageDescription): archive descriptor
|
||||
root(Path): path to the archive repository root
|
||||
archive(Path): path to directory with archives
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if symlinks were created and ``False`` otherwise
|
||||
"""
|
||||
symlinks_created = False
|
||||
# here we glob for archive itself and signature if any
|
||||
for file in archive.glob(f"{package_description.filename}*"):
|
||||
try:
|
||||
symlink_relative(root / file.name, file)
|
||||
symlinks_created = True
|
||||
except FileExistsError:
|
||||
continue # symlink is already created, skip processing
|
||||
|
||||
return symlinks_created
|
||||
|
||||
def _repo(self, root: Path) -> Repo:
|
||||
"""
|
||||
constructs :class:`ahriman.core.alpm.repo.Repo` object for given path
|
||||
|
||||
Args:
|
||||
root(Path): root of the repository
|
||||
|
||||
Returns:
|
||||
Repo: constructed object with correct properties
|
||||
"""
|
||||
return Repo(self.repository_id.name, self.paths, self.sign_args, root)
|
||||
|
||||
def directories_fix(self, paths: set[Path]) -> None:
|
||||
"""
|
||||
remove empty repository directories recursively
|
||||
|
||||
Args:
|
||||
paths(set[Path]): repositories to check
|
||||
"""
|
||||
root = self.paths.archive / "repos"
|
||||
for repository in paths:
|
||||
parents = [repository] + list(repository.parents[:-1])
|
||||
for parent in parents:
|
||||
path = root / parent
|
||||
if list(path.iterdir()):
|
||||
continue # directory is not empty
|
||||
path.rmdir()
|
||||
|
||||
def repository_for(self, date: datetime.date | None = None) -> Path:
|
||||
"""
|
||||
get full path to repository at the specified date
|
||||
|
||||
Args:
|
||||
date(datetime.date | None, optional): date to generate path. If none supplied then today will be used
|
||||
(Default value = None)
|
||||
|
||||
Returns:
|
||||
Path: path to the repository root
|
||||
"""
|
||||
date = date or utcnow().date()
|
||||
return (
|
||||
self.paths.archive
|
||||
/ "repos"
|
||||
/ date.strftime("%Y")
|
||||
/ date.strftime("%m")
|
||||
/ date.strftime("%d")
|
||||
/ self.repository_id.name
|
||||
/ self.repository_id.architecture
|
||||
)
|
||||
|
||||
def symlinks_create(self, packages: list[Package]) -> None:
|
||||
"""
|
||||
create symlinks for the specified packages in today's repository
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to be updated
|
||||
"""
|
||||
root = self.repository_for()
|
||||
repo = self._repo(root)
|
||||
|
||||
for package in packages:
|
||||
archive = self.paths.archive_for(package.base)
|
||||
|
||||
for package_name, single in package.packages.items():
|
||||
if single.filename is None:
|
||||
self.logger.warning("received empty package filename for %s", package_name)
|
||||
continue
|
||||
|
||||
if self._package_symlinks_create(single, root, archive):
|
||||
repo.add(root / single.filename)
|
||||
|
||||
def symlinks_fix(self) -> Iterator[Path]:
|
||||
"""
|
||||
remove broken symlinks across repositories for all dates
|
||||
|
||||
Yields:
|
||||
Path: path of the sub-repository with removed symlinks
|
||||
"""
|
||||
for path in walk(self.paths.archive / "repos"):
|
||||
root = path.parent
|
||||
*_, name, architecture = root.parts
|
||||
if self.repository_id.name != name or self.repository_id.architecture != architecture:
|
||||
continue # we only process same name repositories
|
||||
|
||||
if not package_like(path):
|
||||
continue
|
||||
if not path.is_symlink():
|
||||
continue # find symlinks only
|
||||
if path.exists():
|
||||
continue # filter out not broken symlinks
|
||||
|
||||
# here we don't have access to original archive, so we have to guess name based on archive name
|
||||
# normally it should be fine to do so
|
||||
package_name = path.name.rsplit("-", maxsplit=3)[0]
|
||||
self._repo(root).remove(package_name, path)
|
||||
yield path.parent.relative_to(self.paths.archive / "repos")
|
||||
|
||||
def tree_create(self) -> None:
|
||||
"""
|
||||
create repository tree for current repository
|
||||
"""
|
||||
root = self.repository_for()
|
||||
if root.exists():
|
||||
return
|
||||
|
||||
with self.paths.preserve_owner():
|
||||
root.mkdir(0o755, parents=True)
|
||||
# init empty repository here
|
||||
self._repo(root).init()
|
||||
70
src/ahriman/core/archive/archive_trigger.py
Normal file
70
src/ahriman/core/archive/archive_trigger.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 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.archive.archive_tree import ArchiveTree
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class ArchiveTrigger(Trigger):
|
||||
"""
|
||||
archive repository extension
|
||||
|
||||
Attributes:
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
tree(ArchiveTree): archive tree wrapper
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, configuration)
|
||||
|
||||
self.paths = configuration.repository_paths
|
||||
self.tree = ArchiveTree(self.paths, GPG(configuration).repository_sign_args)
|
||||
|
||||
def on_result(self, result: Result, packages: list[Package]) -> None:
|
||||
"""
|
||||
run trigger
|
||||
|
||||
Args:
|
||||
result(Result): build result
|
||||
packages(list[Package]): list of all available packages
|
||||
"""
|
||||
self.tree.symlinks_create(packages)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
self.tree.tree_create()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
trigger action which will be called before the stop of the application
|
||||
"""
|
||||
repositories = set(self.tree.symlinks_fix())
|
||||
self.tree.directories_fix(repositories)
|
||||
@@ -22,6 +22,11 @@ try:
|
||||
except ImportError:
|
||||
aiohttp_security = None # type: ignore[assignment]
|
||||
|
||||
try:
|
||||
import aiohttp_session
|
||||
except ImportError:
|
||||
aiohttp_session = None # type: ignore[assignment]
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -50,7 +55,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
|
||||
|
||||
Args:
|
||||
*args(Any): argument list as provided by check_authorized function
|
||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||
**kwargs(Any): named argument list as provided by check_authorized function
|
||||
|
||||
Returns:
|
||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||
@@ -66,7 +71,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
|
||||
|
||||
Args:
|
||||
*args(Any): argument list as provided by forget function
|
||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||
**kwargs(Any): named argument list as provided by forget function
|
||||
|
||||
Returns:
|
||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||
@@ -76,13 +81,29 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
async def get_session(*args: Any, **kwargs: Any) -> Any:
|
||||
"""
|
||||
handle aiohttp session methods
|
||||
|
||||
Args:
|
||||
*args(Any): argument list as provided by get_session function
|
||||
**kwargs(Any): named argument list as provided by get_session function
|
||||
|
||||
Returns:
|
||||
Any: empty dictionary in case if no aiohttp_session module found and function call otherwise
|
||||
"""
|
||||
if aiohttp_session is not None:
|
||||
return await aiohttp_session.get_session(*args, **kwargs)
|
||||
return {}
|
||||
|
||||
|
||||
async def remember(*args: Any, **kwargs: Any) -> Any:
|
||||
"""
|
||||
handle disabled auth
|
||||
|
||||
Args:
|
||||
*args(Any): argument list as provided by remember function
|
||||
**kwargs(Any): named argument list as provided by authorized_userid function
|
||||
**kwargs(Any): named argument list as provided by remember function
|
||||
|
||||
Returns:
|
||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#
|
||||
import aioauth_client
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
@@ -53,7 +55,7 @@ class OAuth(Mapping):
|
||||
self.client_secret = configuration.get("auth", "client_secret")
|
||||
# in order to use OAuth feature the service must be publicity available
|
||||
# thus we expect that address is set
|
||||
self.redirect_uri = f"""{configuration.get("web", "address")}/api/v1/login"""
|
||||
self.redirect_uri = f"{configuration.get("web", "address")}/api/v1/login"
|
||||
self.provider = self.get_provider(configuration.get("auth", "oauth_provider"))
|
||||
# it is list, but we will have to convert to string it anyway
|
||||
self.scopes = configuration.get("auth", "oauth_scopes")
|
||||
@@ -84,7 +86,7 @@ class OAuth(Mapping):
|
||||
Raises:
|
||||
OptionError: in case if invalid OAuth provider name supplied
|
||||
"""
|
||||
provider: type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
|
||||
provider: type = getattr(aioauth_client, name, type(None))
|
||||
try:
|
||||
is_oauth2_client = issubclass(provider, aioauth_client.OAuth2Client)
|
||||
except TypeError: # what if it is random string?
|
||||
@@ -102,27 +104,35 @@ class OAuth(Mapping):
|
||||
"""
|
||||
return self.provider(client_id=self.client_id, client_secret=self.client_secret)
|
||||
|
||||
def get_oauth_url(self) -> str:
|
||||
def get_oauth_url(self, state: str) -> str:
|
||||
"""
|
||||
get authorization URI for the specified settings
|
||||
|
||||
Args:
|
||||
state(str): CSRF token to pass to OAuth2 provider
|
||||
|
||||
Returns:
|
||||
str: authorization URI as a string
|
||||
"""
|
||||
client = self.get_client()
|
||||
uri: str = client.get_authorize_url(scope=self.scopes, redirect_uri=self.redirect_uri)
|
||||
uri: str = client.get_authorize_url(scope=self.scopes, redirect_uri=self.redirect_uri, state=state)
|
||||
return uri
|
||||
|
||||
async def get_oauth_username(self, code: str) -> str | None:
|
||||
async def get_oauth_username(self, code: str, state: str | None, session: dict[str, Any]) -> str | None:
|
||||
"""
|
||||
extract OAuth username from remote
|
||||
|
||||
Args:
|
||||
code(str): authorization code provided by external service
|
||||
state(str | None): CSRF token returned by external service
|
||||
session(dict[str, Any]): current session instance
|
||||
|
||||
Returns:
|
||||
str | None: username as is in OAuth provider
|
||||
"""
|
||||
if state is None or state != session.get("state"):
|
||||
return None
|
||||
|
||||
try:
|
||||
client = self.get_client()
|
||||
access_token, _ = await client.get_access_token(code, redirect_uri=self.redirect_uri)
|
||||
|
||||
114
src/ahriman/core/build_tools/package_version.py
Normal file
114
src/ahriman/core/build_tools/package_version.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import full_version, utcnow
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild import Pkgbuild
|
||||
|
||||
|
||||
class PackageVersion(LazyLogging):
|
||||
"""
|
||||
package version extractor and helper
|
||||
|
||||
Attributes:
|
||||
package(Package): package definitions
|
||||
"""
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
"""
|
||||
Args:
|
||||
package(Package): package definitions
|
||||
"""
|
||||
self.package = package
|
||||
|
||||
def actual_version(self, configuration: Configuration) -> str:
|
||||
"""
|
||||
additional method to handle VCS package versions
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
str: package version if package is not VCS and current version according to VCS otherwise
|
||||
"""
|
||||
if not self.package.is_vcs:
|
||||
return self.package.version
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
paths = configuration.repository_paths
|
||||
task = Task(self.package, configuration, repository_id.architecture, paths)
|
||||
|
||||
try:
|
||||
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
|
||||
task.init(paths.cache_for(self.package.base), [], None)
|
||||
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.package.base) / "PKGBUILD")
|
||||
|
||||
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
|
||||
except Exception:
|
||||
self.logger.exception("cannot determine version of VCS package")
|
||||
finally:
|
||||
# clear log files generated by devtools
|
||||
for log_file in paths.cache_for(self.package.base).glob("*.log"):
|
||||
log_file.unlink()
|
||||
|
||||
return self.package.version
|
||||
|
||||
def is_newer_than(self, timestamp: float | int) -> bool:
|
||||
"""
|
||||
check if package was built after the specified timestamp
|
||||
|
||||
Args:
|
||||
timestamp(float | int): timestamp to check build date against
|
||||
|
||||
Returns:
|
||||
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise.
|
||||
In case if build date is not set by any of packages, it returns False
|
||||
"""
|
||||
return any(
|
||||
package.build_date > timestamp
|
||||
for package in self.package.packages.values()
|
||||
if package.build_date is not None
|
||||
)
|
||||
|
||||
def is_outdated(self, remote: Package, configuration: Configuration, *,
|
||||
calculate_version: bool = True) -> bool:
|
||||
"""
|
||||
check if package is out-of-dated
|
||||
|
||||
Args:
|
||||
remote(Package): package properties from remote source
|
||||
configuration(Configuration): configuration instance
|
||||
calculate_version(bool, optional): expand version to actual value (by calculating git versions)
|
||||
(Default value = True)
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
|
||||
"""
|
||||
vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0)
|
||||
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
|
||||
|
||||
if calculate_version and not self.is_newer_than(min_vcs_build_date):
|
||||
remote_version = PackageVersion(remote).actual_version(configuration)
|
||||
else:
|
||||
remote_version = remote.version
|
||||
|
||||
return self.package.vercmp(remote_version) < 0
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
import shutil
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
@@ -27,6 +27,7 @@ from ahriman.core.exceptions import CalledProcessError
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import check_output, utcnow, walk
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild import Pkgbuild
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
@@ -81,7 +82,7 @@ class Sources(LazyLogging):
|
||||
Returns:
|
||||
list[PkgbuildPatch]: generated patch for PKGBUILD architectures if required
|
||||
"""
|
||||
architectures = Package.supported_architectures(sources_dir)
|
||||
architectures = Pkgbuild.supported_architectures(sources_dir)
|
||||
if "any" in architectures: # makepkg does not like when there is any other arch except for any
|
||||
return []
|
||||
architectures.add(architecture)
|
||||
@@ -161,7 +162,7 @@ class Sources(LazyLogging):
|
||||
cwd=sources_dir, logger=instance.logger)
|
||||
|
||||
# extract local files...
|
||||
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Package.local_files(sources_dir)]
|
||||
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Pkgbuild.local_files(sources_dir)]
|
||||
instance.add(sources_dir, *files)
|
||||
# ...and commit them
|
||||
instance.commit(sources_dir)
|
||||
@@ -347,7 +348,7 @@ class Sources(LazyLogging):
|
||||
"""
|
||||
gitconfig = gitconfig or {}
|
||||
|
||||
def configuration_flags() -> Generator[str, None, None]:
|
||||
def configuration_flags() -> Iterator[str]:
|
||||
for option, value in (self.GITCONFIG | gitconfig).items():
|
||||
yield "-c"
|
||||
yield f"{option}=\"{value}\""
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
@@ -77,7 +77,7 @@ class Task(LazyLogging):
|
||||
Returns:
|
||||
list[Path]: list of file paths which looks like freshly generated archives
|
||||
"""
|
||||
def files() -> Generator[Path, None, None]:
|
||||
def files() -> Iterator[Path]:
|
||||
for filepath in sources_dir.iterdir():
|
||||
if filepath in source_files:
|
||||
continue # skip files which were already there
|
||||
|
||||
@@ -97,6 +97,26 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
"aur": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"auth": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
@@ -296,10 +316,20 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"suppress_http_log_errors": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
|
||||
@@ -21,7 +21,7 @@ import configparser
|
||||
import os
|
||||
import sys
|
||||
|
||||
from collections.abc import Generator, Mapping, MutableMapping
|
||||
from collections.abc import Iterator, Mapping, MutableMapping
|
||||
from string import Template
|
||||
from typing import Any, ClassVar
|
||||
|
||||
@@ -37,7 +37,7 @@ class ShellInterpolator(configparser.Interpolation):
|
||||
|
||||
@staticmethod
|
||||
def _extract_variables(parser: MutableMapping[str, Mapping[str, str]], value: str,
|
||||
defaults: Mapping[str, str]) -> Generator[tuple[str, str], None, None]:
|
||||
defaults: Mapping[str, str]) -> Iterator[tuple[str, str]]:
|
||||
"""
|
||||
extract keys and values (if available) from the configuration. In case if a key is not available, it will be
|
||||
silently skipped from the result
|
||||
@@ -50,7 +50,7 @@ class ShellInterpolator(configparser.Interpolation):
|
||||
Yields:
|
||||
tuple[str, str]: variable name used for substitution and its value
|
||||
"""
|
||||
def identifiers() -> Generator[tuple[str | None, str], None, None]:
|
||||
def identifiers() -> Iterator[tuple[str | None, str]]:
|
||||
# extract all found identifiers and parse them
|
||||
for identifier in ShellTemplate(value).get_identifiers():
|
||||
match identifier.rsplit(":", maxsplit=1):
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import fnmatch
|
||||
import re
|
||||
|
||||
from collections.abc import Generator, Mapping
|
||||
from collections.abc import Iterator, Mapping
|
||||
from string import Template
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ class ShellTemplate(Template):
|
||||
(self._REPLACE, self._replace, "/"),
|
||||
)
|
||||
|
||||
def generator(variables: dict[str, str]) -> Generator[tuple[str, str], None, None]:
|
||||
def generator(variables: dict[str, str]) -> Iterator[tuple[str, str]]:
|
||||
for identifier in self.get_identifiers():
|
||||
for regex, function, greediness in substitutions:
|
||||
if m := regex.match(identifier):
|
||||
|
||||
@@ -76,6 +76,19 @@ class Validator(RootValidator):
|
||||
converted: bool = self.configuration._convert_to_boolean(value) # type: ignore[attr-defined]
|
||||
return converted
|
||||
|
||||
def _normalize_coerce_float(self, value: str) -> float:
|
||||
"""
|
||||
extract float from string value
|
||||
|
||||
Args:
|
||||
value(str): converting value
|
||||
|
||||
Returns:
|
||||
float: value converted to float according to configuration rules
|
||||
"""
|
||||
del self
|
||||
return float(value)
|
||||
|
||||
def _normalize_coerce_integer(self, value: str) -> int:
|
||||
"""
|
||||
extract integer from string value
|
||||
@@ -109,7 +122,7 @@ class Validator(RootValidator):
|
||||
Args:
|
||||
constraint(list[str]): optional list of allowed special words (e.g. ``localhost``)
|
||||
field(str): field name to be checked
|
||||
value(Path): value to be checked
|
||||
value(str): value to be checked
|
||||
|
||||
Examples:
|
||||
The rule's arguments are validated against this schema:
|
||||
|
||||
81
src/ahriman/core/database/migrations/m016_archive.py
Normal file
81
src/ahriman/core/database/migrations/m016_archive.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from dataclasses import replace
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Explorer
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.utils import atomic_move, package_like, symlink_relative
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
__all__ = ["migrate_data"]
|
||||
|
||||
|
||||
def migrate_data(connection: Connection, configuration: Configuration) -> None:
|
||||
"""
|
||||
perform data migration
|
||||
|
||||
Args:
|
||||
connection(Connection): database connection
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
del connection
|
||||
|
||||
for repository_id in Explorer.repositories_extract(configuration):
|
||||
paths = replace(configuration.repository_paths, repository_id=repository_id)
|
||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
||||
|
||||
# create archive directory if required
|
||||
if not paths.archive.is_dir():
|
||||
with paths.preserve_owner():
|
||||
paths.archive.mkdir(mode=0o755, parents=True)
|
||||
|
||||
move_packages(paths, pacman)
|
||||
|
||||
|
||||
def move_packages(repository_paths: RepositoryPaths, pacman: Pacman) -> None:
|
||||
"""
|
||||
move packages from repository to archive and create symbolic links
|
||||
|
||||
Args:
|
||||
repository_paths(RepositoryPaths): repository paths instance
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
"""
|
||||
for archive in filter(package_like, repository_paths.repository.iterdir()):
|
||||
if not archive.is_file(follow_symlinks=False):
|
||||
continue # skip symbolic links if any
|
||||
|
||||
package = Package.from_archive(archive, pacman)
|
||||
artifacts = [archive]
|
||||
# check if there are signatures for this package and append it here too
|
||||
if (signature := GPG.signature(archive)).exists():
|
||||
artifacts.append(signature)
|
||||
|
||||
for source in artifacts:
|
||||
target = repository_paths.ensure_exists(repository_paths.archive_for(package.base)) / source.name
|
||||
# move package to the archive directory
|
||||
atomic_move(source, target)
|
||||
# create symlink to the archive
|
||||
symlink_relative(source, target)
|
||||
@@ -141,14 +141,15 @@ class LogsOperations(Operations):
|
||||
connection.execute(
|
||||
"""
|
||||
delete from logs
|
||||
where (package_base, version, repository, process_id) not in (
|
||||
select package_base, version, repository, process_id from logs
|
||||
where (package_base, version, repository, created) in (
|
||||
select package_base, version, repository, max(created) from logs
|
||||
where repository = :repository
|
||||
group by package_base, version, repository
|
||||
where repository = :repository
|
||||
and (package_base, version, repository, process_id) not in (
|
||||
select package_base, version, repository, process_id from logs
|
||||
where (package_base, version, repository, created) in (
|
||||
select package_base, version, repository, max(created) from logs
|
||||
where repository = :repository
|
||||
group by package_base, version, repository
|
||||
)
|
||||
)
|
||||
)
|
||||
""",
|
||||
{
|
||||
"repository": repository_id.id,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Generator, Iterable
|
||||
from collections.abc import Iterable, Iterator
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations.operations import Operations
|
||||
@@ -263,7 +263,7 @@ class PackageOperations(Operations):
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> Generator[tuple[Package, BuildStatus], None, None]:
|
||||
def run(connection: Connection) -> Iterator[tuple[Package, BuildStatus]]:
|
||||
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)
|
||||
|
||||
@@ -25,8 +25,16 @@ from typing import Self
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.migrations import Migrations
|
||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
||||
DependenciesOperations, EventOperations, LogsOperations, PackageOperations, PatchOperations
|
||||
from ahriman.core.database.operations import (
|
||||
AuthOperations,
|
||||
BuildOperations,
|
||||
ChangesOperations,
|
||||
DependenciesOperations,
|
||||
EventOperations,
|
||||
LogsOperations,
|
||||
PackageOperations,
|
||||
PatchOperations,
|
||||
)
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
import subprocess
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
@@ -229,20 +228,6 @@ class PkgbuildParserError(ValueError):
|
||||
ValueError.__init__(self, message)
|
||||
|
||||
|
||||
class PathError(ValueError):
|
||||
"""
|
||||
exception which will be raised on path which is not belong to root directory
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path, root: Path) -> None:
|
||||
"""
|
||||
Args:
|
||||
path(Path): path which raised an exception
|
||||
root(Path): repository root (i.e. ahriman home)
|
||||
"""
|
||||
ValueError.__init__(self, f"Path `{path}` does not belong to repository root `{root}`")
|
||||
|
||||
|
||||
class PasswordError(ValueError):
|
||||
"""
|
||||
exception which will be raised in case of password related errors
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
@@ -44,8 +44,7 @@ class ValidationPrinter(StringPrinter):
|
||||
self.errors = errors
|
||||
|
||||
@staticmethod
|
||||
def get_error_messages(node: str, errors: list[str | dict[str, Any]],
|
||||
current_level: int = 1) -> Generator[Property, None, None]:
|
||||
def get_error_messages(node: str, errors: list[str | dict[str, Any]], current_level: int = 1) -> Iterator[Property]:
|
||||
"""
|
||||
extract default error message from cerberus class
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ class RemotePullTrigger(Trigger):
|
||||
"gitremote": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["gitremote"],
|
||||
},
|
||||
"pull_url": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
@@ -60,7 +64,6 @@ class RemotePullTrigger(Trigger):
|
||||
},
|
||||
},
|
||||
}
|
||||
CONFIGURATION_SCHEMA_FALLBACK = "gitremote"
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
@@ -89,7 +92,6 @@ class RemotePullTrigger(Trigger):
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
for target in self.targets:
|
||||
section, _ = self.configuration.gettype(
|
||||
target, self.repository_id, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
|
||||
section, _ = self.configuration.gettype(target, self.repository_id, fallback="gitremote")
|
||||
runner = RemotePull(self.repository_id, self.configuration, section)
|
||||
runner.run()
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
import shutil
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
@@ -96,7 +96,7 @@ class RemotePush(LazyLogging):
|
||||
# ...and finally return path to the copied directory
|
||||
return package.base
|
||||
|
||||
def packages_update(self, result: Result, target_dir: Path) -> Generator[str, None, None]:
|
||||
def packages_update(self, result: Result, target_dir: Path) -> Iterator[str]:
|
||||
"""
|
||||
update all packages from the build result
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ class RemotePushTrigger(Trigger):
|
||||
"gitremote": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["gitremote"],
|
||||
},
|
||||
"commit_email": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
@@ -72,7 +76,6 @@ class RemotePushTrigger(Trigger):
|
||||
},
|
||||
},
|
||||
}
|
||||
CONFIGURATION_SCHEMA_FALLBACK = "gitremote"
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
@@ -111,7 +114,6 @@ class RemotePushTrigger(Trigger):
|
||||
reporter = ctx.get(Client)
|
||||
|
||||
for target in self.targets:
|
||||
section, _ = self.configuration.gettype(
|
||||
target, self.repository_id, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
|
||||
section, _ = self.configuration.gettype(target, self.repository_id, fallback="gitremote")
|
||||
runner = RemotePush(reporter, self.configuration, section)
|
||||
runner.run(result)
|
||||
|
||||
@@ -17,4 +17,5 @@
|
||||
# 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.housekeeping.archive_rotation_trigger import ArchiveRotationTrigger
|
||||
from ahriman.core.housekeeping.logs_rotation_trigger import LogsRotationTrigger
|
||||
|
||||
116
src/ahriman/core/housekeeping/archive_rotation_trigger.py
Normal file
116
src/ahriman/core/housekeeping/archive_rotation_trigger.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 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 collections.abc import Callable
|
||||
from functools import cmp_to_key
|
||||
|
||||
from ahriman.core import context
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.core.utils import package_like
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class ArchiveRotationTrigger(Trigger):
|
||||
"""
|
||||
remove packages from archive
|
||||
|
||||
Attributes:
|
||||
keep_built_packages(int): number of last packages to keep
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"archive": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"keep_built_packages": {
|
||||
"type": "integer",
|
||||
"required": True,
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, configuration)
|
||||
|
||||
section = next(iter(self.configuration_sections(configuration)))
|
||||
self.keep_built_packages = max(configuration.getint(section, "keep_built_packages"), 0)
|
||||
self.paths = configuration.repository_paths
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls, configuration: Configuration) -> list[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
list[str]: read configuration sections belong to this trigger
|
||||
"""
|
||||
return list(cls.CONFIGURATION_SCHEMA.keys())
|
||||
|
||||
def archives_remove(self, package: Package, pacman: Pacman) -> None:
|
||||
"""
|
||||
remove older versions of the specified package
|
||||
|
||||
Args:
|
||||
package(Package): package which has been updated to check for older versions
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
"""
|
||||
packages: dict[tuple[str, str], Package] = {}
|
||||
# we can't use here load_archives, because it ignores versions
|
||||
for full_path in filter(package_like, self.paths.archive_for(package.base).iterdir()):
|
||||
local = Package.from_archive(full_path, pacman)
|
||||
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
|
||||
|
||||
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
||||
to_remove = sorted(packages.values(), key=cmp_to_key(comparator))
|
||||
# 0 will implicitly be translated into [:0], meaning we keep all packages
|
||||
for single in to_remove[:-self.keep_built_packages]:
|
||||
self.logger.info("removing version %s of package %s", single.version, single.base)
|
||||
for archive in single.packages.values():
|
||||
for path in self.paths.archive_for(single.base).glob(f"{archive.filename}*"):
|
||||
path.unlink()
|
||||
|
||||
def on_result(self, result: Result, packages: list[Package]) -> None:
|
||||
"""
|
||||
run trigger
|
||||
|
||||
Args:
|
||||
result(Result): build result
|
||||
packages(list[Package]): list of all available packages
|
||||
"""
|
||||
ctx = context.get()
|
||||
pacman = ctx.get(Pacman)
|
||||
|
||||
for package in result.success:
|
||||
self.archives_remove(package, pacman)
|
||||
@@ -47,7 +47,6 @@ class LogsRotationTrigger(Trigger):
|
||||
},
|
||||
},
|
||||
}
|
||||
REQUIRES_REPOSITORY = True
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
import contextlib
|
||||
import requests
|
||||
|
||||
from functools import cached_property
|
||||
from requests.adapters import BaseAdapter
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.core.http.sync_http_client import SyncHttpClient
|
||||
|
||||
|
||||
@@ -37,32 +36,36 @@ class SyncAhrimanClient(SyncHttpClient):
|
||||
|
||||
address: str
|
||||
|
||||
@cached_property
|
||||
def session(self) -> requests.Session:
|
||||
def _login_url(self) -> str:
|
||||
"""
|
||||
get or create session
|
||||
get url for the login api
|
||||
|
||||
Returns:
|
||||
request.Session: created session object
|
||||
str: full url for web service to log in
|
||||
"""
|
||||
if urlparse(self.address).scheme == "http+unix":
|
||||
import requests_unixsocket
|
||||
session: requests.Session = requests_unixsocket.Session() # type: ignore[no-untyped-call]
|
||||
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
||||
return session
|
||||
return f"{self.address}/api/v1/login"
|
||||
|
||||
session = requests.Session()
|
||||
session.headers["User-Agent"] = f"ahriman/{__version__}"
|
||||
self._login(session)
|
||||
|
||||
return session
|
||||
|
||||
def _login(self, session: requests.Session) -> None:
|
||||
def adapters(self) -> dict[str, BaseAdapter]:
|
||||
"""
|
||||
process login to the service
|
||||
get registered adapters
|
||||
|
||||
Returns:
|
||||
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
|
||||
"""
|
||||
adapters = SyncHttpClient.adapters(self)
|
||||
|
||||
if (scheme := urlparse(self.address).scheme) == "http+unix":
|
||||
from requests_unixsocket.adapters import UnixAdapter
|
||||
adapters[f"{scheme}://"] = UnixAdapter() # type: ignore[no-untyped-call]
|
||||
|
||||
return adapters
|
||||
|
||||
def on_session_creation(self, session: requests.Session) -> None:
|
||||
"""
|
||||
method which will be called on session creation
|
||||
|
||||
Args:
|
||||
session(requests.Session): request session to login
|
||||
session(requests.Session): created requests session
|
||||
"""
|
||||
if self.auth is None:
|
||||
return # no auth configured
|
||||
@@ -74,12 +77,3 @@ class SyncAhrimanClient(SyncHttpClient):
|
||||
}
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._login_url(), json=payload, session=session)
|
||||
|
||||
def _login_url(self) -> str:
|
||||
"""
|
||||
get url for the login api
|
||||
|
||||
Returns:
|
||||
str: full url for web service to log in
|
||||
"""
|
||||
return f"{self.address}/api/v1/login"
|
||||
|
||||
@@ -21,7 +21,9 @@ import requests
|
||||
import sys
|
||||
|
||||
from functools import cached_property
|
||||
from requests.adapters import BaseAdapter, HTTPAdapter
|
||||
from typing import Any, IO, Literal
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.core.configuration import Configuration
|
||||
@@ -38,10 +40,14 @@ class SyncHttpClient(LazyLogging):
|
||||
|
||||
Attributes:
|
||||
auth(tuple[str, str] | None): HTTP basic auth object if set
|
||||
retry(Retry): retry policy of the HTTP client. Disabled by default
|
||||
suppress_errors(bool): suppress logging of request errors
|
||||
timeout(int | None): HTTP request timeout in seconds
|
||||
"""
|
||||
|
||||
retry: Retry = Retry()
|
||||
timeout: int | None = None
|
||||
|
||||
def __init__(self, configuration: Configuration | None = None, section: str | None = None, *,
|
||||
suppress_errors: bool = False) -> None:
|
||||
"""
|
||||
@@ -50,18 +56,21 @@ class SyncHttpClient(LazyLogging):
|
||||
section(str | None, optional): settings section name (Default value = None)
|
||||
suppress_errors(bool, optional): suppress logging of request errors (Default value = False)
|
||||
"""
|
||||
if configuration is None:
|
||||
configuration = Configuration() # dummy configuration
|
||||
if section is None:
|
||||
section = configuration.default_section
|
||||
configuration = configuration or Configuration() # dummy configuration
|
||||
section = section or configuration.default_section
|
||||
|
||||
username = configuration.get(section, "username", fallback=None)
|
||||
password = configuration.get(section, "password", fallback=None)
|
||||
self.auth = (username, password) if username and password else None
|
||||
|
||||
self.timeout: int | None = configuration.getint(section, "timeout", fallback=30)
|
||||
self.suppress_errors = suppress_errors
|
||||
|
||||
self.timeout = configuration.getint(section, "timeout", fallback=30)
|
||||
self.retry = SyncHttpClient.retry_policy(
|
||||
max_retries=configuration.getint(section, "max_retries", fallback=0),
|
||||
retry_backoff=configuration.getfloat(section, "retry_backoff", fallback=0.0),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def session(self) -> requests.Session:
|
||||
"""
|
||||
@@ -71,11 +80,17 @@ class SyncHttpClient(LazyLogging):
|
||||
request.Session: created session object
|
||||
"""
|
||||
session = requests.Session()
|
||||
|
||||
for protocol, adapter in self.adapters().items():
|
||||
session.mount(protocol, adapter)
|
||||
|
||||
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
|
||||
session.headers["User-Agent"] = f"ahriman/{__version__} " \
|
||||
f"{requests.utils.default_user_agent()} " \
|
||||
f"python/{python_version}"
|
||||
|
||||
self.on_session_creation(session)
|
||||
|
||||
return session
|
||||
|
||||
@staticmethod
|
||||
@@ -92,6 +107,39 @@ class SyncHttpClient(LazyLogging):
|
||||
result: str = exception.response.text if exception.response is not None else ""
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def retry_policy(max_retries: int = 0, retry_backoff: float = 0.0) -> Retry:
|
||||
"""
|
||||
build retry policy for class
|
||||
|
||||
Args:
|
||||
max_retries(int, optional): maximum amount of retries allowed (Default value = 0)
|
||||
retry_backoff(float, optional): retry exponential backoff (Default value = 0.0)
|
||||
|
||||
Returns:
|
||||
Retry: built retry policy
|
||||
"""
|
||||
return Retry(
|
||||
total=max_retries,
|
||||
connect=max_retries,
|
||||
read=max_retries,
|
||||
status=max_retries,
|
||||
status_forcelist=[429, 500, 502, 503, 504],
|
||||
backoff_factor=retry_backoff,
|
||||
)
|
||||
|
||||
def adapters(self) -> dict[str, BaseAdapter]:
|
||||
"""
|
||||
get registered adapters
|
||||
|
||||
Returns:
|
||||
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
|
||||
"""
|
||||
return {
|
||||
"http://": HTTPAdapter(max_retries=self.retry),
|
||||
"https://": HTTPAdapter(max_retries=self.retry),
|
||||
}
|
||||
|
||||
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
|
||||
headers: dict[str, str] | None = None,
|
||||
params: list[tuple[str, str]] | None = None,
|
||||
@@ -139,3 +187,11 @@ class SyncHttpClient(LazyLogging):
|
||||
if not suppress_errors:
|
||||
self.logger.exception("could not perform http request")
|
||||
raise
|
||||
|
||||
def on_session_creation(self, session: requests.Session) -> None:
|
||||
"""
|
||||
method which will be called on session creation
|
||||
|
||||
Args:
|
||||
session(requests.Session): created requests session
|
||||
"""
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
@@ -80,7 +80,7 @@ class LazyLogging:
|
||||
logging.setLogRecordFactory(package_record_factory)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def in_package_context(self, package_base: str, version: str | None) -> Generator[None, None, None]:
|
||||
def in_package_context(self, package_base: str, version: str | None) -> Iterator[None]:
|
||||
"""
|
||||
execute function while setting package context
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
import inspect
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from pkgutil import ModuleInfo, walk_packages
|
||||
@@ -33,7 +33,7 @@ __all__ = ["implementations"]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _modules(module_root: Path, prefix: str) -> Generator[ModuleInfo, None, None]:
|
||||
def _modules(module_root: Path, prefix: str) -> Iterator[ModuleInfo]:
|
||||
"""
|
||||
extract available modules from package
|
||||
|
||||
@@ -52,7 +52,7 @@ def _modules(module_root: Path, prefix: str) -> Generator[ModuleInfo, None, None
|
||||
yield module_info
|
||||
|
||||
|
||||
def implementations(root_module: ModuleType, base_class: type[T]) -> Generator[type[T], None, None]:
|
||||
def implementations(root_module: ModuleType, base_class: type[T]) -> Iterator[type[T]]:
|
||||
"""
|
||||
extract implementations of the ``base_class`` from the module
|
||||
|
||||
|
||||
@@ -74,6 +74,18 @@ class Email(Report, JinjaTemplate):
|
||||
self.ssl = SmtpSSLSettings.from_option(configuration.get(section, "ssl", fallback="disabled"))
|
||||
self.user = configuration.get(section, "user", fallback=None)
|
||||
|
||||
@property
|
||||
def _smtp_session(self) -> type[smtplib.SMTP]:
|
||||
"""
|
||||
build SMTP session based on configuration settings
|
||||
|
||||
Returns:
|
||||
type[smtplib.SMTP]: SMTP or SMTP_SSL session depending on whether SSL is enabled or not
|
||||
"""
|
||||
if self.ssl == SmtpSSLSettings.SSL:
|
||||
return smtplib.SMTP_SSL
|
||||
return smtplib.SMTP
|
||||
|
||||
def _send(self, text: str, attachment: dict[str, str]) -> None:
|
||||
"""
|
||||
send email callback
|
||||
@@ -93,16 +105,13 @@ class Email(Report, JinjaTemplate):
|
||||
attach.add_header("Content-Disposition", "attachment", filename=filename)
|
||||
message.attach(attach)
|
||||
|
||||
if self.ssl != SmtpSSLSettings.SSL:
|
||||
session = smtplib.SMTP(self.host, self.port)
|
||||
with self._smtp_session(self.host, self.port) as session:
|
||||
if self.ssl == SmtpSSLSettings.STARTTLS:
|
||||
session.starttls()
|
||||
else:
|
||||
session = smtplib.SMTP_SSL(self.host, self.port)
|
||||
if self.user is not None and self.password is not None:
|
||||
session.login(self.user, self.password)
|
||||
session.sendmail(self.sender, self.receivers, message.as_string())
|
||||
session.quit()
|
||||
|
||||
if self.user is not None and self.password is not None:
|
||||
session.login(self.user, self.password)
|
||||
session.sendmail(self.sender, self.receivers, message.as_string())
|
||||
|
||||
def generate(self, packages: list[Package], result: Result) -> None:
|
||||
"""
|
||||
|
||||
@@ -302,6 +302,16 @@ class ReportTrigger(Trigger):
|
||||
"empty": False,
|
||||
"is_url": [],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"rss_url": {
|
||||
"type": "string",
|
||||
"empty": False,
|
||||
@@ -336,7 +346,6 @@ class ReportTrigger(Trigger):
|
||||
},
|
||||
},
|
||||
}
|
||||
REQUIRES_REPOSITORY = True
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
||||
@@ -17,4 +17,5 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.repository.explorer import Explorer
|
||||
from ahriman.core.repository.repository import Repository
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
import contextlib
|
||||
|
||||
from typing import Generator
|
||||
from collections.abc import Iterator
|
||||
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.models.event import Event, EventType
|
||||
@@ -55,7 +55,7 @@ class EventLogger:
|
||||
|
||||
@contextlib.contextmanager
|
||||
def in_event(self, package_base: str, event: EventType, message: str | None = None,
|
||||
failure: EventType | None = None) -> Generator[None, None, None]:
|
||||
failure: EventType | None = None) -> Iterator[None]:
|
||||
"""
|
||||
perform action in package context and log event with time elapsed
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from ahriman.core.build_tools.package_archive import PackageArchive
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.utils import safe_filename
|
||||
from ahriman.core.utils import atomic_move, filelock, list_flatmap, package_like, safe_filename, symlink_relative
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.event import EventType
|
||||
from ahriman.models.package import Package
|
||||
@@ -41,6 +41,140 @@ class Executor(PackageInfo, Cleaner):
|
||||
trait for common repository update processes
|
||||
"""
|
||||
|
||||
def _archive_lookup(self, package: Package) -> list[Path]:
|
||||
"""
|
||||
check if there is a rebuilt package already
|
||||
|
||||
Args:
|
||||
package(Package): package to check
|
||||
|
||||
Returns:
|
||||
list[Path]: list of built packages and signatures if available, empty list otherwise
|
||||
"""
|
||||
archive = self.paths.archive_for(package.base)
|
||||
if not archive.is_dir():
|
||||
return []
|
||||
|
||||
for path in filter(package_like, archive.iterdir()):
|
||||
# check if package version is the same
|
||||
built = Package.from_archive(path, self.pacman)
|
||||
if built.version != package.version:
|
||||
continue
|
||||
|
||||
packages = built.packages.values()
|
||||
# all packages must be either any or same architecture
|
||||
if not all(single.architecture in ("any", self.architecture) for single in packages):
|
||||
continue
|
||||
|
||||
return list_flatmap(packages, lambda single: archive.glob(f"{single.filename}*"))
|
||||
|
||||
return []
|
||||
|
||||
def _archive_rename(self, description: PackageDescription, package_base: str) -> None:
|
||||
"""
|
||||
rename package archive removing special symbols
|
||||
|
||||
Args:
|
||||
description(PackageDescription): package description
|
||||
package_base(str): package base name
|
||||
"""
|
||||
if description.filename is None:
|
||||
self.logger.warning("received empty package filename for base %s", package_base)
|
||||
return # suppress type checking, it never can be none actually
|
||||
|
||||
if (safe := safe_filename(description.filename)) != description.filename:
|
||||
atomic_move(self.paths.packages / description.filename, self.paths.packages / safe)
|
||||
description.filename = safe
|
||||
|
||||
def _package_build(self, package: Package, path: Path, packager: str | None,
|
||||
local_version: str | None) -> str | None:
|
||||
"""
|
||||
build single package
|
||||
|
||||
Args:
|
||||
package(Package): package to build
|
||||
path(Path): path to directory with package files
|
||||
packager(str | None): packager identifier used for this package
|
||||
local_version(str | None): local version of the package
|
||||
|
||||
Returns:
|
||||
str | None: current commit sha if available
|
||||
"""
|
||||
self.reporter.set_building(package.base)
|
||||
|
||||
task = Task(package, self.configuration, self.architecture, self.paths)
|
||||
patches = self.reporter.package_patches_get(package.base, None)
|
||||
commit_sha = task.init(path, patches, local_version)
|
||||
|
||||
loaded_package = Package.from_build(path, self.architecture, None)
|
||||
if prebuilt := list(self._archive_lookup(loaded_package)):
|
||||
self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version)
|
||||
built = []
|
||||
for artifact in prebuilt:
|
||||
with filelock(artifact):
|
||||
shutil.copy(artifact, path)
|
||||
built.append(path / artifact.name)
|
||||
else:
|
||||
built = task.build(path, PACKAGER=packager)
|
||||
|
||||
package.with_packages(built, self.pacman)
|
||||
for src in built:
|
||||
dst = self.paths.packages / src.name
|
||||
atomic_move(src, dst)
|
||||
|
||||
return commit_sha
|
||||
|
||||
def _package_remove(self, package_name: str, path: Path) -> None:
|
||||
"""
|
||||
remove single package from repository
|
||||
|
||||
Args:
|
||||
package_name(str): package name
|
||||
path(Path): path to package archive
|
||||
"""
|
||||
try:
|
||||
self.repo.remove(package_name, path)
|
||||
except Exception:
|
||||
self.logger.exception("could not remove %s", package_name)
|
||||
|
||||
def _package_remove_base(self, package_base: str) -> None:
|
||||
"""
|
||||
remove package base from repository
|
||||
|
||||
Args:
|
||||
package_base(str): package base name
|
||||
"""
|
||||
try:
|
||||
with self.in_event(package_base, EventType.PackageRemoved):
|
||||
self.reporter.package_remove(package_base)
|
||||
except Exception:
|
||||
self.logger.exception("could not remove base %s", package_base)
|
||||
|
||||
def _package_update(self, filename: str | None, package_base: str, packager_key: str | None) -> None:
|
||||
"""
|
||||
update built package in repository database
|
||||
|
||||
Args:
|
||||
filename(str | None): archive filename
|
||||
package_base(str): package base name
|
||||
packager_key(str | None): packager key identifier
|
||||
"""
|
||||
if filename is None:
|
||||
self.logger.warning("received empty package filename for base %s", package_base)
|
||||
return # suppress type checking, it never can be none actually
|
||||
|
||||
# in theory, it might be NOT packages directory, but we suppose it is
|
||||
full_path = self.paths.packages / filename
|
||||
files = self.sign.process_sign_package(full_path, packager_key)
|
||||
|
||||
for src in files:
|
||||
dst = self.paths.ensure_exists(self.paths.archive_for(package_base)) / src.name
|
||||
atomic_move(src, dst) # move package to archive directory
|
||||
if not (symlink := self.paths.repository / dst.name).exists():
|
||||
symlink_relative(symlink, dst) # create link to archive
|
||||
|
||||
self.repo.add(self.paths.repository / filename)
|
||||
|
||||
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
|
||||
bump_pkgrel: bool = False) -> Result:
|
||||
"""
|
||||
@@ -55,21 +189,6 @@ class Executor(PackageInfo, Cleaner):
|
||||
Returns:
|
||||
Result: build result
|
||||
"""
|
||||
def build_single(package: Package, local_path: Path, packager_id: str | None) -> str | None:
|
||||
self.reporter.set_building(package.base)
|
||||
task = Task(package, self.configuration, self.architecture, self.paths)
|
||||
local_version = local_versions.get(package.base) if bump_pkgrel else None
|
||||
patches = self.reporter.package_patches_get(package.base, None)
|
||||
commit_sha = task.init(local_path, patches, local_version)
|
||||
built = task.build(local_path, PACKAGER=packager_id)
|
||||
|
||||
package.with_packages(built, self.pacman)
|
||||
for src in built:
|
||||
dst = self.paths.packages / src.name
|
||||
shutil.move(src, dst)
|
||||
|
||||
return commit_sha
|
||||
|
||||
packagers = packagers or Packagers()
|
||||
local_versions = {package.base: package.version for package in self.packages()}
|
||||
|
||||
@@ -80,16 +199,21 @@ class Executor(PackageInfo, Cleaner):
|
||||
try:
|
||||
with self.in_event(single.base, EventType.PackageUpdated, failure=EventType.PackageUpdateFailed):
|
||||
packager = self.packager(packagers, single.base)
|
||||
last_commit_sha = build_single(single, Path(dir_name), packager.packager_id)
|
||||
local_version = local_versions.get(single.base) if bump_pkgrel else None
|
||||
commit_sha = self._package_build(single, Path(dir_name), packager.packager_id, local_version)
|
||||
|
||||
# update commit hash for changes keeping current diff if there is any
|
||||
changes = self.reporter.package_changes_get(single.base)
|
||||
self.reporter.package_changes_update(single.base, Changes(last_commit_sha, changes.changes))
|
||||
self.reporter.package_changes_update(single.base, Changes(commit_sha, changes.changes))
|
||||
|
||||
# update dependencies list
|
||||
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
||||
dependencies = package_archive.depends_on()
|
||||
self.reporter.package_dependencies_update(single.base, dependencies)
|
||||
|
||||
# update result set
|
||||
result.add_updated(single)
|
||||
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
result.add_failed(single)
|
||||
@@ -107,19 +231,6 @@ class Executor(PackageInfo, Cleaner):
|
||||
Returns:
|
||||
Result: remove result
|
||||
"""
|
||||
def remove_base(package_base: str) -> None:
|
||||
try:
|
||||
with self.in_event(package_base, EventType.PackageRemoved):
|
||||
self.reporter.package_remove(package_base)
|
||||
except Exception:
|
||||
self.logger.exception("could not remove base %s", package_base)
|
||||
|
||||
def remove_package(package: str, archive_path: Path) -> None:
|
||||
try:
|
||||
self.repo.remove(package, archive_path) # remove the package itself
|
||||
except Exception:
|
||||
self.logger.exception("could not remove %s", package)
|
||||
|
||||
packages_to_remove: dict[str, Path] = {}
|
||||
bases_to_remove: list[str] = []
|
||||
|
||||
@@ -136,6 +247,7 @@ class Executor(PackageInfo, Cleaner):
|
||||
})
|
||||
bases_to_remove.append(local.base)
|
||||
result.add_removed(local)
|
||||
|
||||
elif requested.intersection(local.packages.keys()):
|
||||
packages_to_remove.update({
|
||||
package: properties.filepath
|
||||
@@ -152,11 +264,11 @@ class Executor(PackageInfo, Cleaner):
|
||||
|
||||
# remove packages from repository files
|
||||
for package, filename in packages_to_remove.items():
|
||||
remove_package(package, filename)
|
||||
self._package_remove(package, filename)
|
||||
|
||||
# remove bases from registered
|
||||
for package in bases_to_remove:
|
||||
remove_base(package)
|
||||
self._package_remove_base(package)
|
||||
|
||||
return result
|
||||
|
||||
@@ -172,27 +284,6 @@ class Executor(PackageInfo, Cleaner):
|
||||
Returns:
|
||||
Result: path to repository database
|
||||
"""
|
||||
def rename(archive: PackageDescription, package_base: str) -> None:
|
||||
if archive.filename is None:
|
||||
self.logger.warning("received empty package name for base %s", package_base)
|
||||
return # suppress type checking, it never can be none actually
|
||||
if (safe := safe_filename(archive.filename)) != archive.filename:
|
||||
shutil.move(self.paths.packages / archive.filename, self.paths.packages / safe)
|
||||
archive.filename = safe
|
||||
|
||||
def update_single(name: str | None, package_base: str, packager_key: str | None) -> None:
|
||||
if name is None:
|
||||
self.logger.warning("received empty package name for base %s", package_base)
|
||||
return # suppress type checking, it never can be none actually
|
||||
# in theory, it might be NOT packages directory, but we suppose it is
|
||||
full_path = self.paths.packages / name
|
||||
files = self.sign.process_sign_package(full_path, packager_key)
|
||||
for src in files:
|
||||
dst = self.paths.repository / safe_filename(src.name)
|
||||
shutil.move(src, dst)
|
||||
package_path = self.paths.repository / safe_filename(name)
|
||||
self.repo.add(package_path)
|
||||
|
||||
current_packages = {package.base: package for package in self.packages()}
|
||||
local_versions = {package_base: package.version for package_base, package in current_packages.items()}
|
||||
|
||||
@@ -207,8 +298,8 @@ class Executor(PackageInfo, Cleaner):
|
||||
packager = self.packager(packagers, local.base)
|
||||
|
||||
for description in local.packages.values():
|
||||
rename(description, local.base)
|
||||
update_single(description.filename, local.base, packager.key)
|
||||
self._archive_rename(description, local.base)
|
||||
self._package_update(description.filename, local.base, packager.key)
|
||||
self.reporter.set_success(local)
|
||||
result.add_updated(local)
|
||||
|
||||
@@ -216,12 +307,13 @@ class Executor(PackageInfo, Cleaner):
|
||||
if local.base in current_packages:
|
||||
current_package_archives = set(current_packages[local.base].packages.keys())
|
||||
removed_packages.extend(current_package_archives.difference(local.packages))
|
||||
|
||||
except Exception:
|
||||
self.reporter.set_failed(local.base)
|
||||
result.add_failed(local)
|
||||
self.logger.exception("could not process %s", local.base)
|
||||
self.clear_packages()
|
||||
|
||||
self.clear_packages()
|
||||
self.process_remove(removed_packages)
|
||||
|
||||
return result
|
||||
|
||||
70
src/ahriman/core/repository/explorer.py
Normal file
70
src/ahriman/core/repository/explorer.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Explorer:
|
||||
"""
|
||||
helper to read filesystem and find created repositories
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def repositories_extract(configuration: Configuration, repository: str | None = None,
|
||||
architecture: str | None = None) -> list[RepositoryId]:
|
||||
"""
|
||||
get known architectures
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
repository(str | None, optional): predefined repository name if available (Default value = None)
|
||||
architecture(str | None, optional): predefined repository architecture if available (Default value = None)
|
||||
|
||||
Returns:
|
||||
list[RepositoryId]: list of repository names and architectures for which tree is created
|
||||
"""
|
||||
# pylint, wtf???
|
||||
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
|
||||
|
||||
# extract repository names first
|
||||
if repository is not None:
|
||||
repositories: Iterable[str] = [repository]
|
||||
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 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)
|
||||
)
|
||||
|
||||
return sorted(parsed)
|
||||
@@ -17,10 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import copy
|
||||
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from ahriman.core.build_tools.package_version import PackageVersion
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.repository.repository_properties import RepositoryProperties
|
||||
from ahriman.core.utils import package_like
|
||||
@@ -33,6 +36,40 @@ class PackageInfo(RepositoryProperties):
|
||||
handler for the package information
|
||||
"""
|
||||
|
||||
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
|
||||
"""
|
||||
generate full dependencies list including transitive dependencies
|
||||
|
||||
Args:
|
||||
package(Package): package to check dependencies for
|
||||
packages(Iterable[Package]): repository package list
|
||||
|
||||
Returns:
|
||||
list[str]: all dependencies of the package
|
||||
"""
|
||||
dependencies = {}
|
||||
# load own package dependencies
|
||||
for package_base in packages:
|
||||
for name, repo_package in package_base.packages.items():
|
||||
dependencies[name] = repo_package.depends
|
||||
for provides in repo_package.provides:
|
||||
dependencies[provides] = repo_package.depends
|
||||
# load repository dependencies
|
||||
for database in self.pacman.handle.get_syncdbs():
|
||||
for pacman_package in database.pkgcache:
|
||||
dependencies[pacman_package.name] = pacman_package.depends
|
||||
for provides in pacman_package.provides:
|
||||
dependencies[provides] = pacman_package.depends
|
||||
|
||||
result = set(package.depends)
|
||||
current_depends: set[str] = set()
|
||||
while result != current_depends:
|
||||
current_depends = copy.deepcopy(result)
|
||||
for package_name in current_depends:
|
||||
result.update(dependencies.get(package_name, []))
|
||||
|
||||
return sorted(result)
|
||||
|
||||
def load_archives(self, packages: Iterable[Path]) -> list[Package]:
|
||||
"""
|
||||
load packages from list of archives
|
||||
@@ -58,7 +95,7 @@ class PackageInfo(RepositoryProperties):
|
||||
# force version to max of them
|
||||
self.logger.warning("version of %s differs, found %s and %s",
|
||||
current.base, current.version, local.version)
|
||||
if current.is_outdated(local, self.configuration, calculate_version=False):
|
||||
if PackageVersion(current).is_outdated(local, self.configuration, calculate_version=False):
|
||||
current.version = local.version
|
||||
current.packages.update(local.packages)
|
||||
except Exception:
|
||||
@@ -130,5 +167,5 @@ class PackageInfo(RepositoryProperties):
|
||||
return [
|
||||
package
|
||||
for package in packages
|
||||
if depends_on.intersection(package.full_depends(self.pacman, packages))
|
||||
if depends_on.intersection(self.full_depends(package, packages))
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ from typing import Self
|
||||
|
||||
from ahriman.core import _Context, context
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote import AUR
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.repository.executor import Executor
|
||||
@@ -73,9 +74,26 @@ class Repository(Executor, UpdateHandler):
|
||||
"""
|
||||
instance = cls(repository_id, configuration, database,
|
||||
report=report, refresh_pacman_database=refresh_pacman_database)
|
||||
|
||||
instance._set_globals(configuration)
|
||||
instance._set_context()
|
||||
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def _set_globals(configuration: Configuration) -> None:
|
||||
"""
|
||||
set global settings based on configuration via class attributes
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
AUR.timeout = configuration.getint("aur", "timeout", fallback=30)
|
||||
AUR.retry = AUR.retry_policy(
|
||||
max_retries=configuration.getint("aur", "max_retries", fallback=0),
|
||||
retry_backoff=configuration.getfloat("aur", "retry_backoff", fallback=0.0),
|
||||
)
|
||||
|
||||
def _set_context(self) -> None:
|
||||
"""
|
||||
create context variables and set their values
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
#
|
||||
from collections.abc import Iterable
|
||||
|
||||
from ahriman.core.build_tools.package_version import PackageVersion
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.event import EventType
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
@@ -67,10 +69,12 @@ class UpdateHandler(PackageInfo, Cleaner):
|
||||
try:
|
||||
remote = load_remote(local)
|
||||
|
||||
if local.is_outdated(remote, self.configuration, calculate_version=vcs):
|
||||
if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs):
|
||||
self.reporter.set_pending(local.base)
|
||||
self.event(local.base, EventType.PackageOutdated, "Remote version is newer than local")
|
||||
result.append(remote)
|
||||
else:
|
||||
self.reporter.package_status_update(local.base, BuildStatusEnum.Success)
|
||||
except Exception:
|
||||
self.reporter.set_failed(local.base)
|
||||
self.logger.exception("could not load remote package %s", local.base)
|
||||
@@ -79,7 +83,8 @@ class UpdateHandler(PackageInfo, Cleaner):
|
||||
|
||||
def updates_dependencies(self, filter_packages: Iterable[str]) -> list[Package]:
|
||||
"""
|
||||
check packages which ae required to be rebuilt based on dynamic dependencies (e.g. linking, modules paths, etc.)
|
||||
check packages which are required to be rebuilt based on dynamic dependencies
|
||||
(e.g. linking, modules paths, etc.)
|
||||
|
||||
Args:
|
||||
filter_packages(Iterable[str]): do not check every package just specified in the list
|
||||
@@ -153,7 +158,7 @@ class UpdateHandler(PackageInfo, Cleaner):
|
||||
if local.remote.is_remote:
|
||||
continue # avoid checking AUR packages
|
||||
|
||||
if local.is_outdated(remote, self.configuration, calculate_version=vcs):
|
||||
if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs):
|
||||
self.reporter.set_pending(local.base)
|
||||
self.event(local.base, EventType.PackageOutdated, "Locally pulled sources are outdated")
|
||||
result.append(remote)
|
||||
@@ -180,8 +185,9 @@ class UpdateHandler(PackageInfo, Cleaner):
|
||||
else:
|
||||
self.reporter.set_pending(local.base)
|
||||
self.event(local.base, EventType.PackageOutdated, "Manual update is requested")
|
||||
|
||||
self.clear_queue()
|
||||
except Exception:
|
||||
self.logger.exception("could not load packages from database")
|
||||
self.clear_queue()
|
||||
|
||||
return result
|
||||
|
||||
@@ -81,7 +81,7 @@ class Spawn(Thread, LazyLogging):
|
||||
helper to run external process
|
||||
|
||||
Args:
|
||||
callback(Callable[[argparse.Namespace, str], bool]): application run function
|
||||
callback(Callable[[argparse.Namespace, RepositoryId], 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
|
||||
|
||||
@@ -103,7 +103,6 @@ class KeyringTrigger(Trigger):
|
||||
},
|
||||
},
|
||||
}
|
||||
REQUIRES_REPOSITORY = True
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
||||
@@ -90,7 +90,6 @@ class MirrorlistTrigger(Trigger):
|
||||
},
|
||||
},
|
||||
}
|
||||
REQUIRES_REPOSITORY = True
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import hashlib
|
||||
import itertools
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from collections.abc import Callable, Iterator
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
@@ -187,7 +187,7 @@ class PkgbuildGenerator:
|
||||
Returns:
|
||||
list[PkgbuildPatch]: list of patches to be applied to the PKGBUILD
|
||||
"""
|
||||
def sources_generator() -> Generator[tuple[str, str], None, None]:
|
||||
def sources_generator() -> Iterator[tuple[str, str]]:
|
||||
for source, generator in sorted(self.sources().items()):
|
||||
source_path = source_dir / source
|
||||
generator(source_path)
|
||||
|
||||
@@ -34,8 +34,6 @@ class Trigger(LazyLogging):
|
||||
|
||||
Attributes:
|
||||
CONFIGURATION_SCHEMA(ConfigurationSchema): (class attribute) configuration schema template
|
||||
CONFIGURATION_SCHEMA_FALLBACK(str | None): (class attribute) optional fallback option for defining
|
||||
configuration schema type used
|
||||
REQUIRES_REPOSITORY(bool): (class attribute) either trigger requires loaded repository or not
|
||||
configuration(Configuration): configuration instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
@@ -59,7 +57,6 @@ class Trigger(LazyLogging):
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA: ClassVar[ConfigurationSchema] = {}
|
||||
CONFIGURATION_SCHEMA_FALLBACK: ClassVar[str | None] = None
|
||||
REQUIRES_REPOSITORY: ClassVar[bool] = True
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
|
||||
@@ -21,7 +21,7 @@ import atexit
|
||||
import contextlib
|
||||
import os
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from importlib import import_module, machinery
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
@@ -112,7 +112,7 @@ class TriggerLoader(LazyLogging):
|
||||
return configuration.getlist("build", "triggers", fallback=[])
|
||||
|
||||
@contextlib.contextmanager
|
||||
def __execute_trigger(self, trigger: Trigger) -> Generator[None, None, None]:
|
||||
def __execute_trigger(self, trigger: Trigger) -> Iterator[None]:
|
||||
"""
|
||||
decorator for calling triggers
|
||||
|
||||
@@ -184,8 +184,8 @@ class TriggerLoader(LazyLogging):
|
||||
trigger_type = self.load_trigger_class(module_path)
|
||||
try:
|
||||
trigger = trigger_type(repository_id, configuration)
|
||||
except Exception:
|
||||
raise ExtensionError(f"Could not load instance of trigger from {trigger_type} loaded from {module_path}")
|
||||
except Exception as ex:
|
||||
raise ExtensionError(f"Could not load trigger from {trigger_type} loaded from {module_path}") from ex
|
||||
|
||||
return trigger
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@ class UploadTrigger(Trigger):
|
||||
"type": "string",
|
||||
"allowed": ["github"],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
@@ -68,6 +73,11 @@ class UploadTrigger(Trigger):
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
@@ -90,6 +100,16 @@ class UploadTrigger(Trigger):
|
||||
"type": "string",
|
||||
"allowed": ["ahriman", "remote-service"],
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"retry_backoff": {
|
||||
"type": "float",
|
||||
"coerce": "float",
|
||||
"min": 0,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
@@ -160,7 +180,6 @@ class UploadTrigger(Trigger):
|
||||
},
|
||||
},
|
||||
}
|
||||
REQUIRES_REPOSITORY = True
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# pylint: disable=too-many-lines
|
||||
import contextlib
|
||||
import datetime
|
||||
import io
|
||||
import itertools
|
||||
@@ -25,28 +26,34 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import selectors
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from collections.abc import Callable, Generator, Iterable, Mapping
|
||||
from collections.abc import Callable, Iterable, Iterator, Mapping
|
||||
from dataclasses import asdict
|
||||
from enum import Enum
|
||||
from filelock import FileLock
|
||||
from pathlib import Path
|
||||
from pwd import getpwuid
|
||||
from typing import Any, IO, TypeVar
|
||||
|
||||
from ahriman.core.exceptions import CalledProcessError, OptionError, UnsafeRunError
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
from ahriman.core.types import Comparable
|
||||
|
||||
|
||||
__all__ = [
|
||||
"atomic_move",
|
||||
"check_output",
|
||||
"check_user",
|
||||
"dataclass_view",
|
||||
"enum_values",
|
||||
"extract_user",
|
||||
"filelock",
|
||||
"filter_json",
|
||||
"full_version",
|
||||
"list_flatmap",
|
||||
"minmax",
|
||||
"owner",
|
||||
"package_like",
|
||||
"parse_version",
|
||||
"partition",
|
||||
@@ -56,15 +63,36 @@ __all__ = [
|
||||
"safe_filename",
|
||||
"srcinfo_property",
|
||||
"srcinfo_property_list",
|
||||
"symlink_relative",
|
||||
"trim_package",
|
||||
"utcnow",
|
||||
"walk",
|
||||
]
|
||||
|
||||
|
||||
R = TypeVar("R", bound=Comparable)
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def atomic_move(src: Path, dst: Path) -> None:
|
||||
"""
|
||||
move file from ``source`` location to ``destination``. This method uses lock and :func:`shutil.move` to ensure that
|
||||
file will be copied (if not rename) atomically. This method blocks execution until lock is available
|
||||
|
||||
Args:
|
||||
src(Path): path to the source file
|
||||
dst(Path): path to the destination
|
||||
|
||||
Examples:
|
||||
This method is a drop-in replacement for :func:`shutil.move` (except it doesn't allow to override copy method)
|
||||
which first locking destination file. To use it simply call method with arguments::
|
||||
|
||||
>>> atomic_move(src, dst)
|
||||
"""
|
||||
with filelock(dst):
|
||||
shutil.move(src, dst)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def check_output(*args: str, exception: Exception | Callable[[int, list[str], str, str], Exception] | None = None,
|
||||
cwd: Path | None = None, input_data: str | None = None,
|
||||
@@ -113,7 +141,7 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
|
||||
return channel if channel is not None else io.StringIO()
|
||||
|
||||
# wrapper around selectors polling
|
||||
def poll(sel: selectors.BaseSelector) -> Generator[tuple[str, str], None, None]:
|
||||
def poll(sel: selectors.BaseSelector) -> Iterator[tuple[str, str]]:
|
||||
for key, _ in sel.select(): # we don't need to check mask here because we have only subscribed on reading
|
||||
line = key.fileobj.readline() # type: ignore[union-attr]
|
||||
if not line: # in case of empty line we remove selector as there is no data here anymore
|
||||
@@ -136,6 +164,11 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
|
||||
if key in ("PATH",) # whitelisted variables only
|
||||
} | environment
|
||||
|
||||
result: dict[str, list[str]] = {
|
||||
"stdout": [],
|
||||
"stderr": [],
|
||||
}
|
||||
|
||||
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
user=user, env=full_environment, text=True, encoding="utf8", errors="backslashreplace",
|
||||
bufsize=1) as process:
|
||||
@@ -144,38 +177,36 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
|
||||
input_channel.write(input_data)
|
||||
input_channel.close()
|
||||
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
|
||||
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
|
||||
with selectors.DefaultSelector() as selector:
|
||||
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
|
||||
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
|
||||
|
||||
result: dict[str, list[str]] = {
|
||||
"stdout": [],
|
||||
"stderr": [],
|
||||
}
|
||||
while selector.get_map(): # while there are unread selectors, keep reading
|
||||
for key_data, output in poll(selector):
|
||||
result[key_data].append(output)
|
||||
|
||||
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
|
||||
stderr = "\n".join(result["stderr"]).rstrip("\n")
|
||||
while selector.get_map(): # while there are unread selectors, keep reading
|
||||
for key_data, output in poll(selector):
|
||||
result[key_data].append(output)
|
||||
|
||||
status_code = process.wait()
|
||||
if status_code != 0:
|
||||
if isinstance(exception, Exception):
|
||||
raise exception
|
||||
if callable(exception):
|
||||
raise exception(status_code, list(args), stdout, stderr)
|
||||
raise CalledProcessError(status_code, list(args), stderr)
|
||||
|
||||
return stdout
|
||||
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
|
||||
stderr = "\n".join(result["stderr"]).rstrip("\n")
|
||||
|
||||
if status_code != 0:
|
||||
if isinstance(exception, Exception):
|
||||
raise exception
|
||||
if callable(exception):
|
||||
raise exception(status_code, list(args), stdout, stderr)
|
||||
raise CalledProcessError(status_code, list(args), stderr)
|
||||
|
||||
return stdout
|
||||
|
||||
|
||||
def check_user(paths: RepositoryPaths, *, unsafe: bool) -> None:
|
||||
def check_user(root: Path, *, unsafe: bool) -> None:
|
||||
"""
|
||||
check if current user is the owner of the root
|
||||
|
||||
Args:
|
||||
paths(RepositoryPaths): repository paths object
|
||||
root(Path): path to root directory (e.g. repository root
|
||||
:attr:`ahriman.models.repository_paths.RepositoryPaths.root`)
|
||||
unsafe(bool): if set no user check will be performed before path creation
|
||||
|
||||
Raises:
|
||||
@@ -184,14 +215,16 @@ def check_user(paths: RepositoryPaths, *, unsafe: bool) -> None:
|
||||
Examples:
|
||||
Simply run function with arguments::
|
||||
|
||||
>>> check_user(paths, unsafe=False)
|
||||
>>> check_user(root, unsafe=False)
|
||||
"""
|
||||
if not paths.root.exists():
|
||||
if not root.exists():
|
||||
return # no directory found, skip check
|
||||
if unsafe:
|
||||
return # unsafe flag is enabled, no check performed
|
||||
current_uid = os.getuid()
|
||||
root_uid, _ = paths.root_owner
|
||||
|
||||
current_uid = os.geteuid()
|
||||
root_uid, _ = owner(root)
|
||||
|
||||
if current_uid != root_uid:
|
||||
raise UnsafeRunError(current_uid, root_uid)
|
||||
|
||||
@@ -233,6 +266,25 @@ def extract_user() -> str | None:
|
||||
return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def filelock(path: Path) -> Iterator[FileLock]:
|
||||
"""
|
||||
wrapper around :class:`filelock.FileLock`, which also removes locks afterward
|
||||
|
||||
Args:
|
||||
path(Path): path to lock on. The lock file will be created as ``.{path.name}.lock``
|
||||
|
||||
Yields:
|
||||
FileLock: acquired file lock instance
|
||||
"""
|
||||
lock_path = path.with_name(f".{path.name}.lock")
|
||||
try:
|
||||
with FileLock(lock_path) as lock:
|
||||
yield lock
|
||||
finally:
|
||||
lock_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]:
|
||||
"""
|
||||
filter json object by fields used for json-to-object conversion
|
||||
@@ -273,6 +325,24 @@ def full_version(epoch: str | int | None, pkgver: str, pkgrel: str) -> str:
|
||||
return f"{prefix}{pkgver}-{pkgrel}"
|
||||
|
||||
|
||||
def list_flatmap(source: Iterable[T], extractor: Callable[[T], Iterable[R]]) -> list[R]:
|
||||
"""
|
||||
extract elements from list of lists, flatten them and apply ``extractor``
|
||||
|
||||
Args:
|
||||
source(Iterable[T]): source list
|
||||
extractor(Callable[[T], Iterable[R]]): property extractor
|
||||
|
||||
Returns:
|
||||
list[R]: combined list of unique entries in properties list
|
||||
"""
|
||||
def generator() -> Iterator[R]:
|
||||
for inner in source:
|
||||
yield from extractor(inner)
|
||||
|
||||
return sorted(set(generator()))
|
||||
|
||||
|
||||
def minmax(source: Iterable[T], *, key: Callable[[T], Any] | None = None) -> tuple[T, T]:
|
||||
"""
|
||||
get min and max value from iterable
|
||||
@@ -289,6 +359,20 @@ def minmax(source: Iterable[T], *, key: Callable[[T], Any] | None = None) -> tup
|
||||
return min(first_iter, key=key), max(second_iter, key=key) # type: ignore
|
||||
|
||||
|
||||
def owner(path: Path) -> tuple[int, int]:
|
||||
"""
|
||||
retrieve owner information by path
|
||||
|
||||
Args:
|
||||
path(Path): path for which extract ids
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: owner user and group ids of the directory
|
||||
"""
|
||||
stat = path.stat()
|
||||
return stat.st_uid, stat.st_gid
|
||||
|
||||
|
||||
def package_like(filename: Path) -> bool:
|
||||
"""
|
||||
check if file looks like package
|
||||
@@ -472,6 +556,17 @@ def srcinfo_property_list(key: str, srcinfo: Mapping[str, Any], package_srcinfo:
|
||||
return values
|
||||
|
||||
|
||||
def symlink_relative(symlink: Path, source: Path) -> None:
|
||||
"""
|
||||
create symlink with relative path to the target directory
|
||||
|
||||
Args:
|
||||
symlink(Path): path to symlink to create
|
||||
source(Path): source file to be symlinked
|
||||
"""
|
||||
symlink.symlink_to(source.relative_to(symlink.parent, walk_up=True))
|
||||
|
||||
|
||||
def trim_package(package_name: str) -> str:
|
||||
"""
|
||||
remove version bound and description from package name. Pacman allows to specify version bound (=, <=, >= etc.) for
|
||||
@@ -499,7 +594,7 @@ def utcnow() -> datetime.datetime:
|
||||
return datetime.datetime.now(datetime.UTC)
|
||||
|
||||
|
||||
def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||
def walk(directory_path: Path) -> Iterator[Path]:
|
||||
"""
|
||||
list all file paths in given directory
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class AURPackage:
|
||||
url_path(str): AUR package path
|
||||
repository(str): repository name of the package
|
||||
depends(list[str]): list of package dependencies
|
||||
make_depends(l[str]): list of package make dependencies
|
||||
make_depends(list[str]): list of package make dependencies
|
||||
opt_depends(list[str]): list of package optional dependencies
|
||||
check_depends(list[str]): list of package test dependencies
|
||||
conflicts(list[str]): conflicts list for the package
|
||||
|
||||
@@ -17,23 +17,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# pylint: disable=too-many-lines,too-many-public-methods
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
|
||||
from collections.abc import Callable, Generator, Iterable
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from pyalpm import vercmp # type: ignore[import-not-found]
|
||||
from typing import Any, Self
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import dataclass_view, full_version, parse_version, srcinfo_property_list, utcnow
|
||||
from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.pkgbuild import Pkgbuild
|
||||
@@ -89,7 +84,7 @@ class Package(LazyLogging):
|
||||
Returns:
|
||||
list[str]: sum of dependencies per each package
|
||||
"""
|
||||
return self._package_list_property(lambda package: package.depends)
|
||||
return list_flatmap(self.packages.values(), lambda package: package.depends)
|
||||
|
||||
@property
|
||||
def depends_build(self) -> set[str]:
|
||||
@@ -109,7 +104,7 @@ class Package(LazyLogging):
|
||||
Returns:
|
||||
list[str]: sum of test dependencies per each package
|
||||
"""
|
||||
return self._package_list_property(lambda package: package.check_depends)
|
||||
return list_flatmap(self.packages.values(), lambda package: package.check_depends)
|
||||
|
||||
@property
|
||||
def depends_make(self) -> list[str]:
|
||||
@@ -119,7 +114,7 @@ class Package(LazyLogging):
|
||||
Returns:
|
||||
list[str]: sum of make dependencies per each package
|
||||
"""
|
||||
return self._package_list_property(lambda package: package.make_depends)
|
||||
return list_flatmap(self.packages.values(), lambda package: package.make_depends)
|
||||
|
||||
@property
|
||||
def depends_opt(self) -> list[str]:
|
||||
@@ -129,7 +124,7 @@ class Package(LazyLogging):
|
||||
Returns:
|
||||
list[str]: sum of optional dependencies per each package
|
||||
"""
|
||||
return self._package_list_property(lambda package: package.opt_depends)
|
||||
return list_flatmap(self.packages.values(), lambda package: package.opt_depends)
|
||||
|
||||
@property
|
||||
def groups(self) -> list[str]:
|
||||
@@ -139,7 +134,7 @@ class Package(LazyLogging):
|
||||
Returns:
|
||||
list[str]: sum of groups per each package
|
||||
"""
|
||||
return self._package_list_property(lambda package: package.groups)
|
||||
return list_flatmap(self.packages.values(), lambda package: package.groups)
|
||||
|
||||
@property
|
||||
def is_single_package(self) -> bool:
|
||||
@@ -160,7 +155,7 @@ class Package(LazyLogging):
|
||||
bool: ``True`` in case if package base looks like VCS package and ``False`` otherwise
|
||||
"""
|
||||
return self.base.endswith("-bzr") \
|
||||
or self.base.endswith("-csv") \
|
||||
or self.base.endswith("-cvs") \
|
||||
or self.base.endswith("-darcs") \
|
||||
or self.base.endswith("-git") \
|
||||
or self.base.endswith("-hg") \
|
||||
@@ -174,7 +169,7 @@ class Package(LazyLogging):
|
||||
Returns:
|
||||
list[str]: sum of licenses per each package
|
||||
"""
|
||||
return self._package_list_property(lambda package: package.licenses)
|
||||
return list_flatmap(self.packages.values(), lambda package: package.licenses)
|
||||
|
||||
@property
|
||||
def packages_full(self) -> list[str]:
|
||||
@@ -205,7 +200,7 @@ class Package(LazyLogging):
|
||||
package = pacman.handle.load_pkg(str(path))
|
||||
description = PackageDescription.from_package(package, path)
|
||||
return cls(
|
||||
base=package.base,
|
||||
base=package.base or package.name,
|
||||
version=package.version,
|
||||
remote=RemoteSource(source=PackageSource.Archive),
|
||||
packages={package.name: description},
|
||||
@@ -345,184 +340,6 @@ class Package(LazyLogging):
|
||||
packager=packager,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def local_files(path: Path) -> Generator[Path, None, None]:
|
||||
"""
|
||||
extract list of local files
|
||||
|
||||
Args:
|
||||
path(Path): path to package sources directory
|
||||
|
||||
Yields:
|
||||
Path: list of paths of files which belong to the package and distributed together with this tarball.
|
||||
All paths are relative to the ``path``
|
||||
|
||||
Raises:
|
||||
PackageInfoError: if there are parsing errors
|
||||
"""
|
||||
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
|
||||
# we could use arch property, but for consistency it is better to call special method
|
||||
architectures = Package.supported_architectures(path)
|
||||
|
||||
for architecture in architectures:
|
||||
for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture):
|
||||
if "::" in source:
|
||||
_, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it
|
||||
|
||||
if urlparse(source).scheme:
|
||||
# basically file schema should use absolute path which is impossible if we are distributing
|
||||
# files together with PKGBUILD. In this case we are going to skip it also
|
||||
continue
|
||||
|
||||
yield Path(source)
|
||||
|
||||
if (install := pkgbuild.get("install")) is not None:
|
||||
yield Path(install)
|
||||
|
||||
@staticmethod
|
||||
def supported_architectures(path: Path) -> set[str]:
|
||||
"""
|
||||
load supported architectures from package sources
|
||||
|
||||
Args:
|
||||
path(Path): path to package sources directory
|
||||
|
||||
Returns:
|
||||
set[str]: list of package supported architectures
|
||||
"""
|
||||
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
|
||||
return set(pkgbuild.get("arch", []))
|
||||
|
||||
def _package_list_property(self, extractor: Callable[[PackageDescription], list[str]]) -> list[str]:
|
||||
"""
|
||||
extract list property from single packages and combine them into one list
|
||||
|
||||
Notes:
|
||||
Basically this method is generic for type of ``list[T]``, but there is no trait ``Comparable`` in default
|
||||
packages, thus we limit this method only to new types
|
||||
|
||||
Args:
|
||||
extractor(Callable[[PackageDescription], list[str]): package property extractor
|
||||
|
||||
Returns:
|
||||
list[str]: combined list of unique entries in properties list
|
||||
"""
|
||||
def generator() -> Generator[str, None, None]:
|
||||
for package in self.packages.values():
|
||||
yield from extractor(package)
|
||||
|
||||
return sorted(set(generator()))
|
||||
|
||||
def actual_version(self, configuration: Configuration) -> str:
|
||||
"""
|
||||
additional method to handle VCS package versions
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
str: package version if package is not VCS and current version according to VCS otherwise
|
||||
"""
|
||||
if not self.is_vcs:
|
||||
return self.version
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
paths = configuration.repository_paths
|
||||
task = Task(self, configuration, repository_id.architecture, paths)
|
||||
|
||||
try:
|
||||
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
|
||||
task.init(paths.cache_for(self.base), [], None)
|
||||
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
|
||||
|
||||
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
|
||||
except Exception:
|
||||
self.logger.exception("cannot determine version of VCS package")
|
||||
finally:
|
||||
# clear log files generated by devtools
|
||||
for log_file in paths.cache_for(self.base).glob("*.log"):
|
||||
log_file.unlink()
|
||||
|
||||
return self.version
|
||||
|
||||
def full_depends(self, pacman: Pacman, packages: Iterable[Package]) -> list[str]:
|
||||
"""
|
||||
generate full dependencies list including transitive dependencies
|
||||
|
||||
Args:
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
packages(Iterable[Package]): repository package list
|
||||
|
||||
Returns:
|
||||
list[str]: all dependencies of the package
|
||||
"""
|
||||
dependencies = {}
|
||||
# load own package dependencies
|
||||
for package_base in packages:
|
||||
for name, repo_package in package_base.packages.items():
|
||||
dependencies[name] = repo_package.depends
|
||||
for provides in repo_package.provides:
|
||||
dependencies[provides] = repo_package.depends
|
||||
# load repository dependencies
|
||||
for database in pacman.handle.get_syncdbs():
|
||||
for pacman_package in database.pkgcache:
|
||||
dependencies[pacman_package.name] = pacman_package.depends
|
||||
for provides in pacman_package.provides:
|
||||
dependencies[provides] = pacman_package.depends
|
||||
|
||||
result = set(self.depends)
|
||||
current_depends: set[str] = set()
|
||||
while result != current_depends:
|
||||
current_depends = copy.deepcopy(result)
|
||||
for package in current_depends:
|
||||
result.update(dependencies.get(package, []))
|
||||
|
||||
return sorted(result)
|
||||
|
||||
def is_newer_than(self, timestamp: float | int) -> bool:
|
||||
"""
|
||||
check if package was built after the specified timestamp
|
||||
|
||||
Args:
|
||||
timestamp(float | int): timestamp to check build date against
|
||||
|
||||
Returns:
|
||||
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise.
|
||||
In case if build date is not set by any of packages, it returns False
|
||||
"""
|
||||
return any(
|
||||
package.build_date > timestamp
|
||||
for package in self.packages.values()
|
||||
if package.build_date is not None
|
||||
)
|
||||
|
||||
def is_outdated(self, remote: Package, configuration: Configuration, *,
|
||||
calculate_version: bool = True) -> bool:
|
||||
"""
|
||||
check if package is out-of-dated
|
||||
|
||||
Args:
|
||||
remote(Package): package properties from remote source
|
||||
configuration(Configuration): configuration instance
|
||||
calculate_version(bool, optional): expand version to actual value (by calculating git versions)
|
||||
(Default value = True)
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
|
||||
"""
|
||||
vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0)
|
||||
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
|
||||
|
||||
if calculate_version and not self.is_newer_than(min_vcs_build_date):
|
||||
remote_version = remote.actual_version(configuration)
|
||||
else:
|
||||
remote_version = remote.version
|
||||
|
||||
result: int = vercmp(self.version, remote_version)
|
||||
return result < 0
|
||||
|
||||
def next_pkgrel(self, local_version: str | None) -> str | None:
|
||||
"""
|
||||
generate next pkgrel variable. The package release will be incremented if ``local_version`` is more or equal to
|
||||
@@ -540,7 +357,7 @@ class Package(LazyLogging):
|
||||
if local_version is None:
|
||||
return None # local version not found, keep upstream pkgrel
|
||||
|
||||
if vercmp(self.version, local_version) > 0:
|
||||
if self.vercmp(local_version) > 0:
|
||||
return None # upstream version is newer than local one, keep upstream pkgrel
|
||||
|
||||
*_, local_pkgrel = parse_version(local_version)
|
||||
@@ -561,6 +378,19 @@ class Package(LazyLogging):
|
||||
details = "" if self.is_single_package else f""" ({" ".join(sorted(self.packages.keys()))})"""
|
||||
return f"{self.base}{details}"
|
||||
|
||||
def vercmp(self, version: str) -> int:
|
||||
"""
|
||||
typed wrapper around :func:`pyalpm.vercmp()`
|
||||
|
||||
Args:
|
||||
version(str): version to compare
|
||||
|
||||
Returns:
|
||||
int: negative if current version is less than provided, positive if greater than and zero if equals
|
||||
"""
|
||||
result: int = vercmp(self.version, version)
|
||||
return result
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
"""
|
||||
generate json package view
|
||||
@@ -570,7 +400,7 @@ class Package(LazyLogging):
|
||||
"""
|
||||
return dataclass_view(self)
|
||||
|
||||
def with_packages(self, packages: list[Path], pacman: Pacman) -> None:
|
||||
def with_packages(self, packages: Iterable[Path], pacman: Pacman) -> None:
|
||||
"""
|
||||
replace packages descriptions with ones from archives
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, IO, Self
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
|
||||
from ahriman.core.utils import srcinfo_property_list
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
@@ -103,6 +105,54 @@ class Pkgbuild(Mapping[str, Any]):
|
||||
|
||||
return cls({key: value for key, value in fields.items() if key})
|
||||
|
||||
@staticmethod
|
||||
def local_files(path: Path) -> Iterator[Path]:
|
||||
"""
|
||||
extract list of local files
|
||||
|
||||
Args:
|
||||
path(Path): path to package sources directory
|
||||
|
||||
Yields:
|
||||
Path: list of paths of files which belong to the package and distributed together with this tarball.
|
||||
All paths are relative to the ``path``
|
||||
|
||||
Raises:
|
||||
PackageInfoError: if there are parsing errors
|
||||
"""
|
||||
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
|
||||
# we could use arch property, but for consistency it is better to call special method
|
||||
architectures = Pkgbuild.supported_architectures(path)
|
||||
|
||||
for architecture in architectures:
|
||||
for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture):
|
||||
if "::" in source:
|
||||
_, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it
|
||||
|
||||
if urlparse(source).scheme:
|
||||
# basically file schema should use absolute path which is impossible if we are distributing
|
||||
# files together with PKGBUILD. In this case we are going to skip it also
|
||||
continue
|
||||
|
||||
yield Path(source)
|
||||
|
||||
if (install := pkgbuild.get("install")) is not None:
|
||||
yield Path(install)
|
||||
|
||||
@staticmethod
|
||||
def supported_architectures(path: Path) -> set[str]:
|
||||
"""
|
||||
load supported architectures from package sources
|
||||
|
||||
Args:
|
||||
path(Path): path to package sources directory
|
||||
|
||||
Returns:
|
||||
set[str]: list of package supported architectures
|
||||
"""
|
||||
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
|
||||
return set(pkgbuild.get("arch", []))
|
||||
|
||||
def packages(self) -> dict[str, Self]:
|
||||
"""
|
||||
extract properties from internal package functions
|
||||
|
||||
@@ -22,7 +22,7 @@ import shlex
|
||||
|
||||
from dataclasses import dataclass, fields
|
||||
from pathlib import Path
|
||||
from typing import Any, Generator, Self
|
||||
from typing import Any, Iterator, Self
|
||||
|
||||
from ahriman.core.configuration.shell_template import ShellTemplate
|
||||
from ahriman.core.utils import dataclass_view, filter_json
|
||||
@@ -166,7 +166,7 @@ class PkgbuildPatch:
|
||||
ValueError: if no closing quotation
|
||||
"""
|
||||
|
||||
def generator() -> Generator[str, None, None]:
|
||||
def generator() -> Iterator[str]:
|
||||
token = None
|
||||
for char in source:
|
||||
if token is not None:
|
||||
|
||||
@@ -94,7 +94,7 @@ class RepositoryId:
|
||||
TypeError: if other is different from RepositoryId type
|
||||
"""
|
||||
if not isinstance(other, RepositoryId):
|
||||
raise ValueError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'")
|
||||
raise TypeError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'")
|
||||
|
||||
return (self.name, self.architecture) < (other.name, other.architecture)
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ import contextlib
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from pwd import getpwuid
|
||||
|
||||
from ahriman.core.exceptions import PathError
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import owner
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@@ -85,6 +85,16 @@ class RepositoryPaths(LazyLogging):
|
||||
return Path(self.repository_id.architecture) # legacy tree suffix
|
||||
return Path(self.repository_id.name) / self.repository_id.architecture
|
||||
|
||||
@property
|
||||
def archive(self) -> Path:
|
||||
"""
|
||||
archive directory root
|
||||
|
||||
Returns:
|
||||
Path: archive directory root
|
||||
"""
|
||||
return self.root / "archive"
|
||||
|
||||
@property
|
||||
def build_root(self) -> Path:
|
||||
"""
|
||||
@@ -93,7 +103,7 @@ class RepositoryPaths(LazyLogging):
|
||||
Returns:
|
||||
Path: path to directory in which build process is run
|
||||
"""
|
||||
uid, _ = self.owner(self.root)
|
||||
uid, _ = owner(self.root)
|
||||
return self.chroot / f"{self.repository_id.name}-{self.repository_id.architecture}" / getpwuid(uid).pw_name
|
||||
|
||||
@property
|
||||
@@ -155,7 +165,7 @@ class RepositoryPaths(LazyLogging):
|
||||
Returns:
|
||||
tuple[int, int]: owner user and group of the root directory
|
||||
"""
|
||||
return self.owner(self.root)
|
||||
return owner(self.root)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
@classmethod
|
||||
@@ -170,7 +180,7 @@ class RepositoryPaths(LazyLogging):
|
||||
Returns:
|
||||
set[str]: list of repository architectures for which there is created tree
|
||||
"""
|
||||
def walk(repository_dir: Path) -> Generator[str, None, None]:
|
||||
def walk(repository_dir: Path) -> Iterator[str]:
|
||||
for architecture in filter(lambda path: path.is_dir(), repository_dir.iterdir()):
|
||||
yield architecture.name
|
||||
|
||||
@@ -197,7 +207,7 @@ class RepositoryPaths(LazyLogging):
|
||||
is loaded in legacy mode
|
||||
"""
|
||||
# simply walk through the root. In case if there are subdirectories, emit the name
|
||||
def walk(paths: RepositoryPaths) -> Generator[str, None, None]:
|
||||
def walk(paths: RepositoryPaths) -> Iterator[str]:
|
||||
for repository in filter(lambda path: path.is_dir(), paths._repository_root.iterdir()):
|
||||
if any(path.is_dir() for path in repository.iterdir()):
|
||||
yield repository.name
|
||||
@@ -208,46 +218,17 @@ class RepositoryPaths(LazyLogging):
|
||||
|
||||
return set(walk(instance))
|
||||
|
||||
@staticmethod
|
||||
def owner(path: Path) -> tuple[int, int]:
|
||||
def archive_for(self, package_base: str) -> Path:
|
||||
"""
|
||||
retrieve owner information by path
|
||||
get path to archive specified search criteria
|
||||
|
||||
Args:
|
||||
path(Path): path for which extract ids
|
||||
package_base(str): package base name
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: owner user and group ids of the directory
|
||||
Path: path to archive directory for package base
|
||||
"""
|
||||
stat = path.stat()
|
||||
return stat.st_uid, stat.st_gid
|
||||
|
||||
def _chown(self, path: Path) -> None:
|
||||
"""
|
||||
set owner of path recursively (from root) to root owner
|
||||
|
||||
Notes:
|
||||
More likely you don't want to call this method explicitly, consider using :func:`preserve_owner`
|
||||
as context manager instead
|
||||
|
||||
Args:
|
||||
path(Path): path to be chown
|
||||
|
||||
Raises:
|
||||
PathError: if path does not belong to root
|
||||
"""
|
||||
def set_owner(current: Path) -> None:
|
||||
uid, gid = self.owner(current)
|
||||
if uid == root_uid and gid == root_gid:
|
||||
return
|
||||
os.chown(current, root_uid, root_gid, follow_symlinks=False)
|
||||
|
||||
if self.root not in path.parents:
|
||||
raise PathError(path, self.root)
|
||||
root_uid, root_gid = self.root_owner
|
||||
while path != self.root:
|
||||
set_owner(path)
|
||||
path = path.parent
|
||||
return self.archive / "packages" / package_base[0] / package_base
|
||||
|
||||
def cache_for(self, package_base: str) -> Path:
|
||||
"""
|
||||
@@ -261,13 +242,31 @@ class RepositoryPaths(LazyLogging):
|
||||
"""
|
||||
return self.cache / package_base
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_owner(self, path: Path | None = None) -> Generator[None, None, None]:
|
||||
def ensure_exists(self, directory: Path) -> Path:
|
||||
"""
|
||||
perform any action preserving owner for any newly created file or directory
|
||||
get path based on ``directory`` callable provided and ensure it exists
|
||||
|
||||
Args:
|
||||
path(Path | None, optional): use this path as root instead of repository root (Default value = None)
|
||||
directory(Path): path to directory to check
|
||||
|
||||
Returns:
|
||||
Path: original path based on extractor provided. Directory will always exist
|
||||
|
||||
Examples:
|
||||
This method calls directory accessor and then checks if there is a directory and - otherwise - creates it::
|
||||
|
||||
>>> paths.ensure_exists(paths.archive_for(package_base))
|
||||
"""
|
||||
if not directory.is_dir():
|
||||
with self.preserve_owner():
|
||||
directory.mkdir(mode=0o755, parents=True)
|
||||
|
||||
return directory
|
||||
|
||||
@contextlib.contextmanager
|
||||
def preserve_owner(self) -> Iterator[None]:
|
||||
"""
|
||||
perform any action preserving owner for any newly created file or directory
|
||||
|
||||
Examples:
|
||||
This method is designed to use as context manager when you are going to perform operations which might
|
||||
@@ -279,25 +278,26 @@ class RepositoryPaths(LazyLogging):
|
||||
Note, however, that this method doesn't handle any exceptions and will eventually interrupt
|
||||
if there will be any.
|
||||
"""
|
||||
path = path or self.root
|
||||
# guard non-root
|
||||
# the reason we do this is that it only works if permissions can be actually changed. Hence,
|
||||
# non-privileged user (e.g. personal user or ahriman user) can't change permissions.
|
||||
# The only one who can do so is root, so if user is not root we just terminate function
|
||||
current_uid, current_gid = os.geteuid(), os.getegid()
|
||||
if current_uid != 0:
|
||||
yield
|
||||
return
|
||||
|
||||
def walk(root: Path) -> Generator[Path, None, None]:
|
||||
# basically walk, but skipping some content
|
||||
for child in root.iterdir():
|
||||
yield child
|
||||
if child in (self.chroot.parent,):
|
||||
yield from child.iterdir() # we only yield top-level in chroot directory
|
||||
elif child.is_dir():
|
||||
yield from walk(child)
|
||||
# set uid and gid to root owner
|
||||
target_uid, target_gid = self.root_owner
|
||||
os.setegid(target_gid)
|
||||
os.seteuid(target_uid)
|
||||
|
||||
# get current filesystem and run action
|
||||
previous_snapshot = set(walk(path))
|
||||
yield
|
||||
|
||||
# get newly created files and directories and chown them
|
||||
new_entries = set(walk(path)).difference(previous_snapshot)
|
||||
for entry in new_entries:
|
||||
self._chown(entry)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# reset uid and gid
|
||||
os.seteuid(current_uid)
|
||||
os.setegid(current_gid)
|
||||
|
||||
def tree_clear(self, package_base: str) -> None:
|
||||
"""
|
||||
@@ -308,6 +308,7 @@ class RepositoryPaths(LazyLogging):
|
||||
"""
|
||||
for directory in (
|
||||
self.cache_for(package_base),
|
||||
self.archive_for(package_base),
|
||||
):
|
||||
shutil.rmtree(directory, ignore_errors=True)
|
||||
|
||||
@@ -318,12 +319,12 @@ class RepositoryPaths(LazyLogging):
|
||||
if self.repository_id.is_empty:
|
||||
return # do not even try to create tree in case if no repository id set
|
||||
|
||||
with self.preserve_owner():
|
||||
for directory in (
|
||||
self.cache,
|
||||
self.chroot,
|
||||
self.packages,
|
||||
self.pacman,
|
||||
self.repository,
|
||||
):
|
||||
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
for directory in (
|
||||
self.archive,
|
||||
self.cache,
|
||||
self.chroot,
|
||||
self.packages,
|
||||
self.pacman,
|
||||
self.repository,
|
||||
):
|
||||
self.ensure_exists(directory)
|
||||
|
||||
@@ -72,7 +72,7 @@ def _security() -> list[dict[str, Any]]:
|
||||
return [{
|
||||
"token": {
|
||||
"type": "apiKey", # as per specification we are using api key
|
||||
"name": "API_SESSION",
|
||||
"name": "AHRIMAN",
|
||||
"in": "cookie",
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -149,11 +149,17 @@ def setup_auth(application: Application, configuration: Configuration, validator
|
||||
Application: configured web application
|
||||
"""
|
||||
secret_key = _cookie_secret_key(configuration)
|
||||
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
|
||||
storage = EncryptedCookieStorage(
|
||||
secret_key,
|
||||
cookie_name="AHRIMAN",
|
||||
max_age=validator.max_age,
|
||||
httponly=True,
|
||||
samesite="Lax",
|
||||
)
|
||||
setup_session(application, storage)
|
||||
|
||||
authorization_policy = _AuthorizationPolicy(validator)
|
||||
identity_policy = aiohttp_security.SessionIdentityPolicy()
|
||||
identity_policy = aiohttp_security.SessionIdentityPolicy("SESSION")
|
||||
|
||||
aiohttp_security.setup(application, identity_policy, authorization_policy)
|
||||
application.middlewares.append(_auth_handler(validator.allow_read_only))
|
||||
|
||||
@@ -21,8 +21,18 @@ import aiohttp_jinja2
|
||||
import logging
|
||||
|
||||
from aiohttp.typedefs import Middleware
|
||||
from aiohttp.web import HTTPClientError, HTTPException, HTTPMethodNotAllowed, HTTPNoContent, HTTPServerError, \
|
||||
HTTPUnauthorized, Request, StreamResponse, json_response, middleware
|
||||
from aiohttp.web import (
|
||||
HTTPClientError,
|
||||
HTTPException,
|
||||
HTTPMethodNotAllowed,
|
||||
HTTPNoContent,
|
||||
HTTPServerError,
|
||||
HTTPUnauthorized,
|
||||
Request,
|
||||
StreamResponse,
|
||||
json_response,
|
||||
middleware,
|
||||
)
|
||||
|
||||
from ahriman.web.middlewares import HandlerType
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import re
|
||||
|
||||
from aiohttp.web import Application, View
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
|
||||
import ahriman.web.views
|
||||
|
||||
@@ -32,7 +32,7 @@ from ahriman.web.views.base import BaseView
|
||||
__all__ = ["setup_routes"]
|
||||
|
||||
|
||||
def _dynamic_routes(configuration: Configuration) -> Generator[tuple[str, type[View]], None, None]:
|
||||
def _dynamic_routes(configuration: Configuration) -> Iterator[tuple[str, type[View]]]:
|
||||
"""
|
||||
extract dynamic routes based on views
|
||||
|
||||
|
||||
@@ -25,6 +25,6 @@ class AuthSchema(Schema):
|
||||
request cookie authorization schema
|
||||
"""
|
||||
|
||||
API_SESSION = fields.String(required=True, metadata={
|
||||
AHRIMAN = fields.String(required=True, metadata={
|
||||
"description": "API session key as returned from authorization",
|
||||
})
|
||||
|
||||
@@ -28,3 +28,6 @@ class OAuth2Schema(Schema):
|
||||
code = fields.String(metadata={
|
||||
"description": "OAuth2 authorization code. In case if not set, the redirect to provider will be initiated",
|
||||
})
|
||||
state = fields.String(metadata={
|
||||
"description": "CSRF token returned by OAuth2 provider",
|
||||
})
|
||||
|
||||
@@ -25,8 +25,12 @@ from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.apispec.decorators import apidocs
|
||||
from ahriman.web.schemas import PackageNameSchema, PackageStatusSchema, PackageStatusSimplifiedSchema, \
|
||||
RepositoryIdSchema
|
||||
from ahriman.web.schemas import (
|
||||
PackageNameSchema,
|
||||
PackageStatusSchema,
|
||||
PackageStatusSimplifiedSchema,
|
||||
RepositoryIdSchema,
|
||||
)
|
||||
from ahriman.web.views.base import BaseView
|
||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from tempfile import NamedTemporaryFile
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.utils import atomic_move
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.apispec.decorators import apidocs
|
||||
@@ -152,10 +153,8 @@ class UploadView(BaseView):
|
||||
|
||||
files.append(await self.save_file(part, target, max_body_size=max_body_size))
|
||||
|
||||
# and now we can rename files, which is relatively fast operation
|
||||
# it is probably good way to call lock here, however
|
||||
for filename, current_location in files:
|
||||
target_location = current_location.parent / filename
|
||||
current_location.rename(target_location)
|
||||
atomic_move(current_location, target_location)
|
||||
|
||||
raise HTTPCreated
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user