Compare commits

...

29 Commits

Author SHA1 Message Date
9a1b34b08d remove excess dependencies leaves 2024-06-12 17:08:46 +03:00
e2efe21a8b build: use requests-unixsocket2 fork
Since requests-2.32.0, the http+unix url scheme is brokek, check
https://github.com/msabramo/requests-unixsocket/issues/73 for more
details
2024-06-12 17:08:28 +03:00
5995b78572 feat: implement local reporter mode (#126)
* implement local reporter mode

* simplify watcher class

* review changes

* do not update unknown status

* allow empty key patches via api

* fix some pylint warnings in tests
2024-05-21 16:27:17 +03:00
ac19c407d3 feat: allow to use simplified keys for context
Initial implementation requires explicit context key name to be set.
Though it is still useful sometimes (e.g. if there should be two
variables with the same type), in the most used scenarios internally
only type is required. This commit extends set and get methods to allow
to construct ContextKey from type directly

Also it breaks old keys, since - in order to reduce amount of possible
mistakes - internal classes uses this generation method
2024-05-12 12:00:02 +03:00
c74cd68ad6 feat: add abillity to check broken dependencies (#122)
* implement elf dynamic linking check

* load local database too in pacman wrapper
2024-05-12 11:59:57 +03:00
bb4a0d75fc Release 2.13.8 2024-05-12 11:53:19 +03:00
bca0df41d1 fix: drop integrity check for javascript
It has been added to improve security, however, it changes over time for
no reason ¯\_(ツ)_/¯ I guess either cdn was hacked or fuck js
2024-05-12 11:49:12 +03:00
07b77be6b8 Release 2.13.7 2024-05-09 13:26:40 +03:00
2b33510ada fix: parse array variable from command 2024-05-09 13:21:42 +03:00
6d05389639 Release 2.13.6 2024-05-05 21:59:30 +03:00
daf9841717 fix: update integrity checksums for momentjs and daterangepicker 2024-05-05 21:17:30 +03:00
0d243a781a refactor: update code to the latest python (3.12+) 2024-05-05 21:17:30 +03:00
cf2e66a934 fix: remove debug packages together with normal ones (#124) 2024-05-05 21:17:30 +03:00
f01f35238d Release 2.13.5 2024-04-04 13:33:03 +03:00
d30d512eb6 fix: update Repo.init to the latest pacman release 2024-04-04 13:16:05 +03:00
0437f90e5a build: install base-devel package 2024-04-04 13:16:03 +03:00
3cab65855a fix: lazy web component initialization
In some cases (probably slow internet) in place initialization can cause
exception, because elements are not available yet. This commit moves
events initialization to $()
2024-04-04 13:14:17 +03:00
ecfb615f97 feat: add ability to disable debug packages distribution
The feature is implemented as supplying !debug option to makepkg when
generating package list. In this case debug packages still will be
built, however, they will not be added to the repository
2024-04-04 13:14:17 +03:00
243983ee64 docs: update docs 2024-02-10 03:12:09 +02:00
812c03d1eb Release 2.13.4 2024-02-09 17:47:01 +02:00
01597c531b fix: return only built packages from task
Since the last updates makepkg --packagelist also adds debug packages
which causes errors
2024-02-09 17:37:50 +02:00
4fec42eac8 refactor: rename packages http methods to own package
docs: update docs import
2024-01-22 02:20:11 +02:00
7574b8e5ce Release 2.13.3 2024-01-13 01:24:30 +02:00
0f2e7f45da fix: replace logo and name in title to just icon 2024-01-12 01:25:46 +02:00
5956a8720b Release 2.13.2 2024-01-08 22:48:55 +02:00
8dd4ced5e9 fix: report only unique result entries
since builder intro the triggers are called with merged result, thus it
would lead to duplicated callouts
2024-01-08 22:46:42 +02:00
6361c41f76 Release 2.13.1 2024-01-08 21:17:35 +02:00
270084bb39 fix: do not raise 404 in case of unknown package on patches endpoints
Previous improvements raise 404 error in case if no packages were found
for patches endpoints. However, in case of multirepo setup this feature
doesn't work properly because package can be located in any other
repository different from default
2024-01-08 14:32:40 +02:00
f89a5252de build: pass ssh agent to tox release env 2024-01-08 14:22:53 +02:00
179 changed files with 11356 additions and 7376 deletions

View File

@ -10,9 +10,9 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
# refresh the image
pacman -Syu --noconfirm
# main dependencies
pacman -Sy --noconfirm devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo python-systemd sudo
pacman -Sy --noconfirm devtools git pyalpm python-cerberus python-inflection python-passlib python-pyelftools python-requests python-srcinfo python-systemd sudo
# make dependencies
pacman -Sy --noconfirm python-build python-flit python-installer python-tox python-wheel
pacman -Sy --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
# optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then
# VCS support
@ -32,10 +32,13 @@ mv dist/ahriman-*.tar.gz package/archlinux
chmod +777 package/archlinux # because fuck you that's why
cd package/archlinux
sudo -u nobody -- makepkg -cf --skipchecksums --noconfirm
sudo -u nobody -- makepkg --packagelist | pacman -U --noconfirm -
sudo -u nobody -- makepkg --packagelist | grep -v -- -debug- | pacman -U --noconfirm -
# create machine-id which is required by build tools
systemd-machine-id-setup
# remove unused dependencies
pacman -Qdtq | pacman -Rscn --noconfirm -
# initial setup command as root
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@example.com>" "${WEB_ARGS[@]}"
@ -50,7 +53,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
WEB_PID=$!
fi
# add the first package
sudo -u ahriman -- ahriman package-add --now ahriman
sudo -u ahriman -- ahriman --log-handler console package-add --now ahriman
# check if package was actually installed
test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")"
# run package check

View File

@ -3,7 +3,7 @@ version: 2
build:
os: ubuntu-20.04
tools:
python: "3.11"
python: "3.12"
python:
install:

View File

@ -32,11 +32,11 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## install package dependencies
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo && \
pacman -Sy --noconfirm --asdeps python-build python-flit python-installer python-wheel && \
pacman -Sy --noconfirm --asdeps breezy git mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-requests-unixsocket python-systemd rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-cors \
python-aiohttp-jinja2 python-aiohttp-session python-aiohttp-security
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-pyelftools python-requests python-srcinfo && \
pacman -Sy --noconfirm --asdeps base-devel python-build python-flit python-installer python-wheel && \
pacman -Sy --noconfirm --asdeps breezy git mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-systemd rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-webargs python-aiohttp-apispec-git python-aiohttp-cors \
python-aiohttp-jinja2 python-aiohttp-session python-aiohttp-security python-requests-unixsocket2
## FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
COPY "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"

View File

@ -6,7 +6,7 @@ for PACKAGE in "$@"; do
BUILD_DIR="$(mktemp -d)"
git clone https://aur.archlinux.org/"$PACKAGE".git "$BUILD_DIR"
cd "$BUILD_DIR"
makepkg --noconfirm --install --rmdeps --syncdeps
makepkg --nocheck --noconfirm --install --rmdeps --syncdeps
cd /
rm -r "$BUILD_DIR"
done

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -20,6 +20,14 @@ ahriman.core.alpm.pacman module
:no-undoc-members:
:show-inheritance:
ahriman.core.alpm.pacman\_database module
-----------------------------------------
.. automodule:: ahriman.core.alpm.pacman_database
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.alpm.repo module
-----------------------------

View File

@ -108,6 +108,14 @@ ahriman.core.database.migrations.m012\_last\_commit\_sha module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m013\_dependencies module
----------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m013_dependencies
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -28,6 +28,14 @@ ahriman.core.database.operations.changes\_operations module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.operations.dependencies\_operations module
----------------------------------------------------------------
.. automodule:: ahriman.core.database.operations.dependencies_operations
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.operations.logs\_operations module
--------------------------------------------------------

View File

@ -60,6 +60,14 @@ ahriman.models.counters module
:no-undoc-members:
:show-inheritance:
ahriman.models.dependencies module
----------------------------------
.. automodule:: ahriman.models.dependencies
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.internal\_status module
--------------------------------------
@ -108,6 +116,14 @@ ahriman.models.package module
:no-undoc-members:
:show-inheritance:
ahriman.models.package\_archive module
--------------------------------------
.. automodule:: ahriman.models.package_archive
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.package\_description module
------------------------------------------

View File

@ -0,0 +1,61 @@
ahriman.web.views.v1.packages package
=====================================
Submodules
----------
ahriman.web.views.v1.packages.changes module
--------------------------------------------
.. automodule:: ahriman.web.views.v1.packages.changes
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.logs module
-----------------------------------------
.. automodule:: ahriman.web.views.v1.packages.logs
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.package module
--------------------------------------------
.. automodule:: ahriman.web.views.v1.packages.package
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.packages module
---------------------------------------------
.. automodule:: ahriman.web.views.v1.packages.packages
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.patch module
------------------------------------------
.. automodule:: ahriman.web.views.v1.packages.patch
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.patches module
--------------------------------------------
.. automodule:: ahriman.web.views.v1.packages.patches
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.web.views.v1.packages
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -8,6 +8,7 @@ Subpackages
:maxdepth: 4
ahriman.web.views.v1.distributed
ahriman.web.views.v1.packages
ahriman.web.views.v1.service
ahriman.web.views.v1.status
ahriman.web.views.v1.user

View File

@ -4,14 +4,6 @@ ahriman.web.views.v1.status package
Submodules
----------
ahriman.web.views.v1.status.changes module
------------------------------------------
.. automodule:: ahriman.web.views.v1.status.changes
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.info module
---------------------------------------
@ -20,46 +12,6 @@ ahriman.web.views.v1.status.info module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.logs module
---------------------------------------
.. automodule:: ahriman.web.views.v1.status.logs
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.package module
------------------------------------------
.. automodule:: ahriman.web.views.v1.status.package
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.packages module
-------------------------------------------
.. automodule:: ahriman.web.views.v1.status.packages
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.patch module
----------------------------------------
.. automodule:: ahriman.web.views.v1.status.patch
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.patches module
------------------------------------------
.. automodule:: ahriman.web.views.v1.status.patches
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.repositories module
-----------------------------------------------

View File

@ -0,0 +1,21 @@
ahriman.web.views.v2.packages package
=====================================
Submodules
----------
ahriman.web.views.v2.packages.logs module
-----------------------------------------
.. automodule:: ahriman.web.views.v2.packages.logs
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.web.views.v2.packages
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -7,7 +7,7 @@ Subpackages
.. toctree::
:maxdepth: 4
ahriman.web.views.v2.status
ahriman.web.views.v2.packages
Module contents
---------------

View File

@ -1,21 +0,0 @@
ahriman.web.views.v2.status package
===================================
Submodules
----------
ahriman.web.views.v2.status.logs module
---------------------------------------
.. automodule:: ahriman.web.views.v2.status.logs
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.web.views.v2.status
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -366,7 +366,7 @@ Web application requires the following python packages to be installed:
* Additional web features also require ``aiohttp-apispec`` (autogenerated documentation), ``aiohttp_cors`` (CORS support, required by documentation).
* In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
* In addition if you would like to disable authorization for local access (recommended way in order to run the application itself with reporting support), the ``requests-unixsocket`` library is required.
* In addition if you would like to disable authorization for local access (recommended way in order to run the application itself with reporting support), the ``requests-unixsocket2`` library is required.
Middlewares
^^^^^^^^^^^

View File

@ -53,6 +53,7 @@ libalpm and AUR related configuration. Group name can refer to architecture, e.g
* ``mirror`` - package database mirror used by pacman for synchronization, string, required. This option supports standard pacman substitutions with ``$arch`` and ``$repo``. Note that the mentioned mirror should contain all repositories which are set by ``alpm.repositories`` option.
* ``repositories`` - list of pacman repositories, used for package search, space separated list of strings, required.
* ``root`` - root for alpm library, string, required. In the most cases it must point to the system root.
* ``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.
``auth`` group
@ -81,6 +82,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
* ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional.
* ``build_command`` - default build command, string, required.
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
* ``include_debug_packages`` - distribute debug packages, boolean, optional, default ``yes``.
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of definition.

View File

@ -208,6 +208,18 @@ This command will prompt for new value of the PKGBUILD variable ``version``. You
sudo -u ahriman ahriman patch-add ahriman version version.patch
The command also supports arrays, but in this case you need to specify full array, e.g.
.. code-block:: shell
sudo -u ahriman ahriman patch-add ahriman depends
Post new function or variable value below. Press Ctrl-D to finish:
(python python-aiohttp)
^D
will set depends PKGBUILD variable (exactly) to array ``["python", "python-aiohttp"]``.
Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch:
#.
@ -1301,7 +1313,7 @@ How to enable basic authorization
The ``salt`` parameter is optional, but recommended, and can be set to any (random) string.
#.
In order to provide access for reporting from application instances you can (the recommended way) use unix sockets by the following configuration (note, that it requires ``python-requests-unixsocket`` package to be installed):
In order to provide access for reporting from application instances you can (the recommended way) use unix sockets by the following configuration (note, that it requires ``python-requests-unixsocket2`` package to be installed):
.. code-block:: ini

View File

@ -1,13 +1,13 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.13.0
pkgver=2.13.8
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
url="https://github.com/arcan1s/ahriman"
license=('GPL3')
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-pyelftools' 'python-requests' 'python-srcinfo')
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support'
@ -21,7 +21,7 @@ optdepends=('breezy: -bzr packages support'
'python-aiohttp-session: web server with authorization'
'python-boto3: sync to s3'
'python-cryptography: web server with authorization'
'python-requests-unixsocket: client report to web server by unix socket'
'python-requests-unixsocket2: client report to web server by unix socket'
'python-jinja: html report generation'
'python-systemd: journal support'
'rsync: sync by using rsync'

View File

@ -17,6 +17,8 @@ mirror = https://geo.mirror.pkgbuild.com/$repo/os/$arch
repositories = core extra multilib
; Pacman's root directory. In the most cases it must point to the system root.
root = /
; Sync files databases too, which is required by deep dependencies check
sync_files_database = yes
; Use local packages cache. If this option is enabled, the service will be able to synchronize databases (available
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
use_ahriman_cache = yes
@ -46,8 +48,12 @@ allow_read_only = yes
[build]
; List of additional flags passed to archbuild command.
;archbuild_flags =
; Path to build command
;build_command =
; List of packages to be ignored during automatic updates.
;ignore_packages =
; Include debug packages
;include_debug_packages = yes
; List of additional flags passed to makechrootpkg command.
;makechrootpkg_flags =
; List of additional flags passed to makepkg command.

View File

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

View File

@ -15,7 +15,7 @@
<div class="container">
<nav class="navbar navbar-expand-lg">
<div class="navbar-brand"><img src="/static/logo.svg" width="30" height="30" alt=""> ahriman</div>
<div class="navbar-brand"><img src="/static/logo.svg" width="30" height="30" alt=""></div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar-supported-content" aria-controls="repositories-navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

View File

@ -38,10 +38,6 @@
<script>
const keyImportModal = $("#key-import-modal");
const keyImportForm = $("#key-import-form");
keyImportModal.on("hidden.bs.modal", () => {
keyImportBodyInput.text("");
keyImportForm.trigger("reset");
});
const keyImportBodyInput = $("#key-import-body-input");
const keyImportCopyButton = $("#key-import-copy-button");
@ -90,4 +86,11 @@
});
}
}
$(() => {
keyImportModal.on("hidden.bs.modal", () => {
keyImportBodyInput.text("");
keyImportForm.trigger("reset");
});
});
</script>

View File

@ -36,9 +36,6 @@
<script>
const loginModal = $("#login-modal");
const loginForm = $("#login-form");
loginModal.on("hidden.bs.modal", () => {
loginForm.trigger("reset");
});
const loginPasswordInput = $("#login-password");
const loginUsernameInput = $("#login-username");
@ -77,4 +74,10 @@
showHidePasswordButton.addClass("bi-eye");
}
}
$(() => {
loginModal.on("hidden.bs.modal", () => {
loginForm.trigger("reset");
});
});
</script>

View File

@ -43,41 +43,10 @@
<script>
const packageAddModal = $("#package-add-modal");
const packageAddForm = $("#package-add-form");
packageAddModal.on("shown.bs.modal", () => {
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageAddModal.on("hidden.bs.modal", () => {
packageAddVariablesDiv.empty();
packageAddForm.trigger("reset");
});
const packageAddInput = $("#package-add-input");
const packageAddRepositoryInput = $("#package-add-repository-input");
const packageAddKnownPackagesList = $("#package-add-known-packages-dlist");
packageAddInput.keyup(() => {
clearTimeout(packageAddInput.data("timeout"));
packageAddInput.data("timeout", setTimeout($.proxy(() => {
const value = packageAddInput.val();
if (value.length >= 3) {
$.ajax({
url: "/api/v1/service/search",
data: {"for": value},
type: "GET",
dataType: "json",
success: response => {
const options = response.map(pkg => {
const option = document.createElement("option");
option.value = pkg.package;
option.innerText = `${pkg.package} (${pkg.description})`;
return option;
});
packageAddKnownPackagesList.empty().append(options);
},
});
}
}, this), 500));
});
const packageAddVariablesDiv = $("#package-add-variables-div");
@ -156,4 +125,39 @@
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches);
}
}
$(() => {
packageAddModal.on("shown.bs.modal", () => {
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageAddModal.on("hidden.bs.modal", () => {
packageAddVariablesDiv.empty();
packageAddForm.trigger("reset");
});
packageAddInput.keyup(() => {
clearTimeout(packageAddInput.data("timeout"));
packageAddInput.data("timeout", setTimeout($.proxy(() => {
const value = packageAddInput.val();
if (value.length >= 3) {
$.ajax({
url: "/api/v1/service/search",
data: {"for": value},
type: "GET",
dataType: "json",
success: response => {
const options = response.map(pkg => {
const option = document.createElement("option");
option.value = pkg.package;
option.innerText = `${pkg.package} (${pkg.description})`;
return option;
});
packageAddKnownPackagesList.empty().append(options);
},
});
}
}, this), 500));
});
});
</script>

View File

@ -72,26 +72,6 @@
const packageInfoModal = $("#package-info-modal");
const packageInfoModalHeader = $("#package-info-modal-header");
const packageInfo = $("#package-info");
packageInfoModal.on("hidden.bs.modal", () => {
packageInfoAurUrl.empty();
packageInfoDepends.empty();
packageInfoGroups.empty();
packageInfoLicenses.empty();
packageInfoPackager.empty();
packageInfoPackages.empty();
packageInfoUpstreamUrl.empty();
packageInfoVersion.empty();
packageInfoVariablesBlock.attr("hidden", true);
packageInfoVariablesDiv.empty();
packageInfoLogsInput.empty();
packageInfoChangesInput.empty();
packageInfoModal.trigger("reset");
hideInfoControls(true);
});
const packageInfoLogsInput = $("#package-info-logs-input");
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
@ -309,4 +289,27 @@
if (isPackageBaseSet) packageInfoModal.modal("show");
}
$(() => {
packageInfoModal.on("hidden.bs.modal", () => {
packageInfoAurUrl.empty();
packageInfoDepends.empty();
packageInfoGroups.empty();
packageInfoLicenses.empty();
packageInfoPackager.empty();
packageInfoPackages.empty();
packageInfoUpstreamUrl.empty();
packageInfoVersion.empty();
packageInfoVariablesBlock.attr("hidden", true);
packageInfoVariablesDiv.empty();
packageInfoLogsInput.empty();
packageInfoChangesInput.empty();
packageInfoModal.trigger("reset");
hideInfoControls(true);
});
});
</script>

View File

@ -35,11 +35,6 @@
<script>
const packageRebuildModal = $("#package-rebuild-modal");
const packageRebuildForm = $("#package-rebuild-form");
packageRebuildModal.on("shown.bs.modal", () => {
$(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
const packageRebuildDependencyInput = $("#package-rebuild-dependency-input");
const packageRebuildRepositoryInput = $("#package-rebuild-repository-input");
@ -54,4 +49,12 @@
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure);
}
}
$(() => {
packageRebuildModal.on("shown.bs.modal", () => {
$(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
});
</script>

View File

@ -9,46 +9,8 @@
const packageInfoUpdateButton = $("#package-info-update-button");
let repository = null;
$("#repositories a").on("click", (event) => {
const element = event.target;
repository = {
architecture: element.dataset.architecture,
repository: element.dataset.repository,
};
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`);
$(`#${element.id}`).tab("show");
reload();
});
const table = $("#packages");
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length);
});
table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showPackageInfo(data.id);
});
table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
pickerInput.daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: "Clear",
},
});
pickerInput.on("apply.daterangepicker", (event, picker) => {
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
table.bootstrapTable("triggerSearch");
});
pickerInput.on("cancel.daterangepicker", () => {
pickerInput.val("");
table.bootstrapTable("triggerSearch");
});
});
const statusBadge = $("#badge-status");
const versionBadge = $("#badge-version");
@ -221,7 +183,46 @@
}
$(() => {
table.bootstrapTable({});
$("#repositories a").on("click", event => {
const element = event.target;
repository = {
architecture: element.dataset.architecture,
repository: element.dataset.repository,
};
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`);
$(`#${element.id}`).tab("show");
reload();
});
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length);
});
table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showPackageInfo(data.id);
});
table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
pickerInput.daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: "Clear",
},
});
pickerInput.on("apply.daterangepicker", (event, picker) => {
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
table.bootstrapTable("triggerSearch");
});
pickerInput.on("cancel.daterangepicker", () => {
pickerInput.val("");
table.bootstrapTable("triggerSearch");
});
});
statusBadge.popover();
selectRepository();
});

View File

@ -102,25 +102,6 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<script>
const table = $("#packages");
table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
pickerInput.daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: "Clear",
},
});
pickerInput.on("apply.daterangepicker", (event, picker) => {
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
table.bootstrapTable("triggerSearch");
});
pickerInput.on("cancel.daterangepicker", () => {
pickerInput.val("");
table.bootstrapTable("triggerSearch");
});
});
const pacmanConf = $("#pacman-conf");
const pacmanConfCopyButton = $("#copy-btn");
@ -141,6 +122,28 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
function filterListLicenses() {
return extractDataList(table.bootstrapTable("getData"), "licenses");
}
$(() => {
table.on("created-controls.bs.table", () => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
pickerInput.daterangepicker({
autoUpdateInput: false,
locale: {
cancelLabel: "Clear",
},
});
pickerInput.on("apply.daterangepicker", (event, picker) => {
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
table.bootstrapTable("triggerSearch");
});
pickerInput.on("cancel.daterangepicker", () => {
pickerInput.val("");
table.bootstrapTable("triggerSearch");
});
});
});
</script>
</body>

View File

@ -1,21 +1,21 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-8hHkOkbWN1TLWwet/jpbJ0zbx3FJDeYJgQ8dX1mRrv/vfCfHCqFSFZYCgaMML3z9" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" integrity="sha384-u4eJN1VWrTf/FnYYQJo2kqJyVxEQf5UmWY4iUcNAoLenOEtEuCkfwc5bKvZOWBi5" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.28.0/tableExport.min.js" integrity="sha384-1Rz4Kz/y1rSWw+ZsjTcxB684XgofbO8iizY+UFIzCwFeQ+QUyhBNWBMh/STOyomI" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.28.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" integrity="sha384-IazMVNyYoUNx6357fWJoqtHYUWWCNHIXxFVtbpVgvImQNWuRP2WbHPaIb3QF8j97" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.js" integrity="sha384-GVLHfbEvuGA/RFiQ3MK2ClEJkWYJXABg55t9LpoDPZFGIsSq8xhFlQydm5poV2jW" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/export/bootstrap-table-export.min.js" integrity="sha384-g9OAB1Moamcy8+l1Q/tajHlMf6NTkS79ehKLTYbA80aQRbRhFCjrSuezv+FE2Kwe" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/resizable/bootstrap-table-resizable.js" integrity="sha384-wd8Vc6Febikdnsnk9vthRWRvMwffw246vhqiqNO3aSNe1maTEA07Vh3zAQiSyDji" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" integrity="sha384-NIqcjpr/3eZI1iNzz7hgT5rgp70qFUzkZffeCgVva9gi80B5vqcm7gn+8QvlWxko" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script>
async function copyToClipboard(text, button) {

View File

@ -1,17 +1,17 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" integrity="sha384-4LISF5TTJX/fLmGSxO53rV4miRxdg84mZsxmO8Rx5jGtp/LbrixFETvWa5a6sESd" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.css" integrity="sha384-sN3NwxbjH33ZidqZnPmX+nQ5IF+LoiI7HvZSoZj5wGacmu0/q4RJfsN0xqN+LIa5" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" integrity="sha384-1sLxvR8mXzjhvFY9f8mzXl97DNLepeZ0PnRiMMdm/rQsKjsrPZPJxYle2wwT2PMg" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" integrity="sha384-4Glx18jZ0Un+yDG6KUpYJ/af8hkssJ02jRASuFv23gfCl0mTXaVMPI9cB4cn3GvE" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/cosmo/bootstrap.min.css" integrity="sha384-RfV5VNj9uqyOdZbN0hFNmoq56291KK2Y4iKhoRAbcfBfjYlpasjxK6TefPjxiAiN" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" integrity="sha384-zLkQsiLfAQqGeIJeKLC+rcCR1YoYaQFLCL7cLDUoKE1ajKJzySpjzWGfYS2vjSG+" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css" integrity="sha384-eFTL69TLRZTkNfYZOLM+G04821K1qZao/4QLJbet1pP4tcF+fdXq/9CdqAbWRl/L" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
<style>
.pre-scrollable {

View File

@ -27,12 +27,12 @@ _shtab_ahriman_patch_list_option_strings=('-h' '--help' '-e' '--exit-code' '-v'
_shtab_ahriman_patch_remove_option_strings=('-h' '--help' '-v' '--variable')
_shtab_ahriman_patch_set_add_option_strings=('-h' '--help' '-t' '--track')
_shtab_ahriman_repo_backup_option_strings=('-h' '--help')
_shtab_ahriman_repo_check_option_strings=('-h' '--help' '--changes' '--no-changes' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_check_option_strings=('-h' '--help' '--changes' '--no-changes' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_check_option_strings=('-h' '--help' '--changes' '--no-changes' '--check-files' '--no-check-files' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_check_option_strings=('-h' '--help' '--changes' '--no-changes' '--check-files' '--no-check-files' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_create_keyring_option_strings=('-h' '--help')
_shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help')
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-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' '--refresh')
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-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' '--refresh')
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run')
@ -47,8 +47,8 @@ _shtab_ahriman_repo_sync_option_strings=('-h' '--help')
_shtab_ahriman_sync_option_strings=('-h' '--help')
_shtab_ahriman_repo_tree_option_strings=('-h' '--help' '-p' '--partitions')
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
@ -243,6 +243,8 @@ _shtab_ahriman_repo_check__h_nargs=0
_shtab_ahriman_repo_check___help_nargs=0
_shtab_ahriman_repo_check___changes_nargs=0
_shtab_ahriman_repo_check___no_changes_nargs=0
_shtab_ahriman_repo_check___check_files_nargs=0
_shtab_ahriman_repo_check___no_check_files_nargs=0
_shtab_ahriman_repo_check__e_nargs=0
_shtab_ahriman_repo_check___exit_code_nargs=0
_shtab_ahriman_repo_check___vcs_nargs=0
@ -254,6 +256,8 @@ _shtab_ahriman_check__h_nargs=0
_shtab_ahriman_check___help_nargs=0
_shtab_ahriman_check___changes_nargs=0
_shtab_ahriman_check___no_changes_nargs=0
_shtab_ahriman_check___check_files_nargs=0
_shtab_ahriman_check___no_check_files_nargs=0
_shtab_ahriman_check__e_nargs=0
_shtab_ahriman_check___exit_code_nargs=0
_shtab_ahriman_check___vcs_nargs=0
@ -270,6 +274,8 @@ _shtab_ahriman_repo_daemon___aur_nargs=0
_shtab_ahriman_repo_daemon___no_aur_nargs=0
_shtab_ahriman_repo_daemon___changes_nargs=0
_shtab_ahriman_repo_daemon___no_changes_nargs=0
_shtab_ahriman_repo_daemon___check_files_nargs=0
_shtab_ahriman_repo_daemon___no_check_files_nargs=0
_shtab_ahriman_repo_daemon___dependencies_nargs=0
_shtab_ahriman_repo_daemon___no_dependencies_nargs=0
_shtab_ahriman_repo_daemon___dry_run_nargs=0
@ -291,6 +297,8 @@ _shtab_ahriman_daemon___aur_nargs=0
_shtab_ahriman_daemon___no_aur_nargs=0
_shtab_ahriman_daemon___changes_nargs=0
_shtab_ahriman_daemon___no_changes_nargs=0
_shtab_ahriman_daemon___check_files_nargs=0
_shtab_ahriman_daemon___no_check_files_nargs=0
_shtab_ahriman_daemon___dependencies_nargs=0
_shtab_ahriman_daemon___no_dependencies_nargs=0
_shtab_ahriman_daemon___dry_run_nargs=0
@ -358,6 +366,8 @@ _shtab_ahriman_repo_update___aur_nargs=0
_shtab_ahriman_repo_update___no_aur_nargs=0
_shtab_ahriman_repo_update___changes_nargs=0
_shtab_ahriman_repo_update___no_changes_nargs=0
_shtab_ahriman_repo_update___check_files_nargs=0
_shtab_ahriman_repo_update___no_check_files_nargs=0
_shtab_ahriman_repo_update___dependencies_nargs=0
_shtab_ahriman_repo_update___no_dependencies_nargs=0
_shtab_ahriman_repo_update___dry_run_nargs=0
@ -380,6 +390,8 @@ _shtab_ahriman_update___aur_nargs=0
_shtab_ahriman_update___no_aur_nargs=0
_shtab_ahriman_update___changes_nargs=0
_shtab_ahriman_update___no_changes_nargs=0
_shtab_ahriman_update___check_files_nargs=0
_shtab_ahriman_update___no_check_files_nargs=0
_shtab_ahriman_update___dependencies_nargs=0
_shtab_ahriman_update___no_dependencies_nargs=0
_shtab_ahriman_update___dry_run_nargs=0
@ -584,7 +596,7 @@ _set_new_action() {
current_action_nargs=1
fi
current_action_args_start_index=$(( $word_index + 1 ))
current_action_args_start_index=$(( $word_index + 1 - $pos_only ))
current_action_is_positional=$2
}
@ -611,6 +623,7 @@ _shtab_ahriman() {
local prefix=_shtab_ahriman
local word_index=0
local pos_only=0 # "--" delimeter not encountered yet
_set_parser_defaults
word_index=1
@ -619,6 +632,7 @@ _shtab_ahriman() {
while [ $word_index -ne $COMP_CWORD ]; do
local this_word="${COMP_WORDS[$word_index]}"
if [[ $pos_only = 1 || " $this_word " != " -- " ]]; then
if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" ${this_word} "* ]]; then
# valid subcommand: add it to the prefix & reset the current action
prefix="${prefix}_$(_shtab_replace_nonword $this_word)"
@ -635,18 +649,21 @@ _shtab_ahriman() {
if [[ "$current_action_nargs" != "*" ]] && \
[[ "$current_action_nargs" != "+" ]] && \
[[ "$current_action_nargs" != *"..." ]] && \
(( $word_index + 1 - $current_action_args_start_index >= \
(( $word_index + 1 - $current_action_args_start_index - $pos_only >= \
$current_action_nargs )); then
$current_action_is_positional && let "completed_positional_actions += 1"
_set_new_action "pos_${completed_positional_actions}" true
fi
else
pos_only=1 # "--" delimeter encountered
fi
let "word_index+=1"
done
# Generate the completions
if [[ "${completing_word}" == -* ]]; then
if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
# optional argument started: use option strings
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
else

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-01\-05" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2024\-05\-12" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
@ -447,7 +447,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] [\-e] [\-\-vcs | \-\-no\-vcs] [\-y] [package ...]
usage: ahriman repo\-check [\-h] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files] [\-e] [\-\-vcs | \-\-no\-vcs]
[\-y]
[package ...]
check for packages updates. Same as repo\-update \-\-dry\-run \-\-no\-manual
@ -460,6 +462,10 @@ filter check by package base
\fB\-\-changes\fR, \fB\-\-no\-changes\fR
calculate changes from the latest known commit if available. Only applicable in dry run mode
.TP
\fB\-\-check\-files\fR, \fB\-\-no\-check\-files\fR
enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories)
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
@ -484,9 +490,9 @@ create package which contains list of available mirrors as set by configuration.
.SH COMMAND \fI\,'ahriman repo\-daemon'\/\fR
usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes]
[\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-\-increment | \-\-no\-increment]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-partitions | \-\-no\-partitions]
[\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
[\-\-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]
start process which periodically will run update process
@ -503,6 +509,10 @@ enable or disable checking for AUR updates
\fB\-\-changes\fR, \fB\-\-no\-changes\fR
calculate changes from the latest known commit if available. Only applicable in dry run mode
.TP
\fB\-\-check\-files\fR, \fB\-\-no\-check\-files\fR
enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories)
.TP
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
process missing package dependencies
@ -649,9 +659,9 @@ 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] [\-\-dependencies | \-\-no\-dependencies]
[\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local]
[\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
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 ...]
check for packages updates and run build process if requested
@ -669,6 +679,10 @@ enable or disable checking for AUR updates
\fB\-\-changes\fR, \fB\-\-no\-changes\fR
calculate changes from the latest known commit if available. Only applicable in dry run mode
.TP
\fB\-\-check\-files\fR, \fB\-\-no\-check\-files\fR
enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories)
.TP
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
process missing package dependencies

View File

@ -120,6 +120,7 @@ _shtab_ahriman_aur_search_options=(
_shtab_ahriman_check_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: True)]:vcs:"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
@ -153,6 +154,7 @@ _shtab_ahriman_daemon_options=(
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
@ -322,6 +324,7 @@ _shtab_ahriman_repo_backup_options=(
_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. Only applicable in dry run mode (default\: True)]:changes:"
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: True)]:vcs:"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
@ -363,6 +366,7 @@ _shtab_ahriman_repo_daemon_options=(
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
@ -460,6 +464,7 @@ _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:"
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@ -601,6 +606,7 @@ _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:"
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@ -736,4 +742,12 @@ _shtab_ahriman() {
typeset -A opt_args
if [[ $zsh_eval_context[-1] == eval ]]; then
# eval/source/. command, register function for later
compdef _shtab_ahriman -N ahriman
else
# autoload from fpath, call function directly
_shtab_ahriman "$@"
fi

View File

@ -20,6 +20,7 @@ dependencies = [
"cerberus",
"inflection",
"passlib",
"pyelftools",
"requests",
"srcinfo",
]
@ -80,7 +81,8 @@ web = [
"aiohttp_session",
"aiohttp_security",
"cryptography",
"requests-unixsocket", # required by unix socket support
"requests-unixsocket2", # required by unix socket support
"setuptools", # required by aiohttp-apispec
]
[tool.flit.sdist]

View File

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

View File

@ -446,7 +446,7 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("patch-list", help="list patch sets",
description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base", nargs="?")
parser.add_argument("package", help="package base")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
action="append")
@ -537,6 +537,9 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
"Only applicable in dry run mode",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
"(e.g. dynamically linked libraries or modules directories)",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("--vcs", help="fetch actual version of VCS packages",
action=argparse.BooleanOptionalAction, default=True)
@ -605,6 +608,9 @@ def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
"Only applicable in dry run mode",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
"(e.g. dynamically linked libraries or modules directories)",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
@ -826,6 +832,9 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
"Only applicable in dry run mode",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
"(e.g. dynamically linked libraries or modules directories)",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")

View File

@ -62,10 +62,13 @@ class Application(ApplicationPackages, ApplicationRepository):
"""
known_packages: set[str] = set()
# local set
# this action is not really needed in case if ``alpm.use_ahriman_cache`` set to yes, because pacman
# will eventually contain all the local packages
for base in self.repository.packages():
for package, properties in base.packages.items():
known_packages.add(package)
known_packages.update(properties.provides)
# known pacman databases
known_packages.update(self.repository.pacman.packages())
return known_packages
@ -158,8 +161,7 @@ class Application(ApplicationPackages, ApplicationRepository):
package = Package.from_aur(package_name, username)
with_dependencies[package.base] = package
# register package in local database
self.database.package_base_update(package)
# register package in the database
self.repository.reporter.set_unknown(package)
return list(with_dependencies.values())

View File

@ -65,7 +65,7 @@ class ApplicationPackages(ApplicationProperties):
"""
package = Package.from_aur(source, username)
self.database.build_queue_insert(package)
self.database.package_base_update(package)
self.reporter.set_unknown(package)
def _add_directory(self, source: str, *_: Any) -> None:
"""
@ -139,7 +139,7 @@ class ApplicationPackages(ApplicationProperties):
"""
package = Package.from_official(source, self.repository.pacman, username)
self.database.build_queue_insert(package)
self.database.package_base_update(package)
self.reporter.set_unknown(package)
def add(self, names: Iterable[str], source: PackageSource, username: str | None = None) -> None:
"""

View File

@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.log import LazyLogging
from ahriman.core.repository import Repository
from ahriman.core.status import Client
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_id import RepositoryId
@ -63,3 +64,13 @@ class ApplicationProperties(LazyLogging):
str: repository architecture
"""
return self.repository_id.architecture
@property
def reporter(self) -> Client:
"""
instance of the web/database client
Returns:
Client: repository reposter
"""
return self.repository.reporter

View File

@ -39,15 +39,13 @@ class ApplicationRepository(ApplicationProperties):
Args:
packages(Iterable[Package]): list of packages to retrieve changes
"""
last_commit_hashes = self.database.hashes_get()
for package in packages:
last_commit_sha = last_commit_hashes.get(package.base)
last_commit_sha = self.reporter.package_changes_get(package.base).last_commit_sha
if last_commit_sha is None:
continue # skip check in case if we can't calculate diff
changes = self.repository.package_changes(package, last_commit_sha)
self.repository.reporter.package_changes_set(package.base, changes)
self.repository.reporter.package_changes_update(package.base, changes)
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
"""
@ -91,10 +89,7 @@ class ApplicationRepository(ApplicationProperties):
packages(Iterable[str]): only sign specified packages
"""
# copy to prebuilt directory
for package in self.repository.packages():
# no one requested this package
if packages and package.base not in packages:
continue
for package in self.repository.packages(packages):
for archive in package.packages.values():
if archive.filepath is None:
self.logger.warning("filepath is empty for %s", package.base)
@ -163,8 +158,8 @@ class ApplicationRepository(ApplicationProperties):
built_packages = self.repository.packages_built()
if built_packages: # speedup a bit
build_result = self.repository.process_update(built_packages, packagers)
self.on_result(build_result)
result.merge(build_result)
self.on_result(result.merge(build_result))
builder = Updater.load(self.repository_id, self.configuration, self.repository)
@ -173,12 +168,13 @@ class ApplicationRepository(ApplicationProperties):
for num, partition in enumerate(partitions):
self.logger.info("processing chunk #%i %s", num, [package.base for package in partition])
build_result = builder.update(partition, packagers, bump_pkgrel=bump_pkgrel)
self.on_result(result.merge(build_result))
self.on_result(build_result)
result.merge(build_result)
return result
def updates(self, filter_packages: Iterable[str], *,
aur: bool, local: bool, manual: bool, vcs: bool) -> list[Package]:
aur: bool, local: bool, manual: bool, vcs: bool, check_files: bool) -> list[Package]:
"""
get list of packages to run update process
@ -188,6 +184,7 @@ class ApplicationRepository(ApplicationProperties):
local(bool): enable or disable checking of local packages for updates
manual(bool): include or exclude manual updates
vcs(bool): enable or disable checking of VCS packages
check_files(bool): check for broken dependencies
Returns:
list[Package]: list of out-of-dated packages
@ -200,5 +197,7 @@ class ApplicationRepository(ApplicationProperties):
updates.update({package.base: package for package in self.repository.updates_local(vcs=vcs)})
if manual:
updates.update({package.base: package for package in self.repository.updates_manual()})
if check_files:
updates.update({package.base: package for package in self.repository.updates_dependencies(filter_packages)})
return [package for _, package in sorted(updates.items())]

View File

@ -50,12 +50,13 @@ class Add(Handler):
application.add(args.package, args.source, args.username)
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
for package in args.package: # for each requested package insert patch
application.database.patches_insert(package, patches)
for patch in patches:
application.reporter.package_patches_update(package, patch)
if not args.now:
return
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False)
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
packagers = Packagers(args.username, {package.base: package.packager for package in packages})

View File

@ -18,10 +18,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import pwd
import tarfile
from pathlib import Path
from tarfile import TarFile
from pwd import getpwuid
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
@ -49,7 +49,7 @@ class Backup(Handler):
report(bool): force enable or disable reporting
"""
backup_paths = Backup.get_paths(configuration)
with TarFile(args.path, mode="w") as archive: # well we don't actually use compression
with tarfile.open(args.path, mode="w") as archive: # well we don't actually use compression
for backup_path in backup_paths:
archive.add(backup_path)
@ -77,7 +77,7 @@ class Backup(Handler):
# gnupg home with imported keys
uid, _ = repository_paths.root_owner
system_user = pwd.getpwuid(uid)
system_user = getpwuid(uid)
gnupg_home = Path(system_user.pw_dir) / ".gnupg"
if gnupg_home.is_dir():
paths.add(gnupg_home)

View File

@ -56,4 +56,4 @@ class Change(Handler):
ChangesPrinter(changes)(verbose=True, separator="")
Change.check_if_empty(args.exit_code, changes.is_empty)
case Action.Remove:
client.package_changes_set(args.package, Changes())
client.package_changes_update(args.package, Changes())

View File

@ -102,8 +102,9 @@ class Patch(Handler):
patch = "".join(list(sys.stdin))
else:
patch = patch_path.read_text(encoding="utf8")
patch = patch.strip() # remove spaces around the patch
return PkgbuildPatch(variable, patch)
# remove spaces around the patch and parse to correct type
parsed = PkgbuildPatch.parse(patch.strip())
return PkgbuildPatch(variable, parsed)
@staticmethod
def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None:
@ -115,25 +116,28 @@ class Patch(Handler):
package_base(str): package base
patch(PkgbuildPatch): patch descriptor
"""
application.database.patches_insert(package_base, [patch])
application.reporter.package_patches_update(package_base, patch)
@staticmethod
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,
def patch_set_list(application: Application, package_base: str, variables: list[str] | None,
exit_code: bool) -> None:
"""
list patches available for the package base
Args:
application(Application): application instance
package_base(str | None): package base
package_base(str): package base
variables(list[str] | None): extract patches only for specified PKGBUILD variables
exit_code(bool): exit with error on empty search result
"""
patches = application.database.patches_list(package_base, variables)
patches = [
patch
for patch in application.reporter.package_patches_get(package_base, None)
if variables is None or patch.key in variables
]
Patch.check_if_empty(exit_code, not patches)
for base, patch in patches.items():
PatchPrinter(base, patch)(verbose=True, separator=" = ")
PatchPrinter(package_base, patches)(verbose=True, separator=" = ")
@staticmethod
def patch_set_remove(application: Application, package_base: str, variables: list[str] | None) -> None:
@ -145,4 +149,8 @@ class Patch(Handler):
package_base(str): package base
variables(list[str] | None): remove patches only for specified PKGBUILD variables
"""
application.database.patches_remove(package_base, variables)
if variables is not None:
for variable in variables: # iterate over single variable
application.reporter.package_patches_remove(package_base, variable)
else:
application.reporter.package_patches_remove(package_base, None) # just pass as is

View File

@ -76,7 +76,7 @@ class Rebuild(Handler):
if from_database:
return [
package
for (package, last_status) in application.database.packages_get()
for (package, last_status) in application.reporter.package_get(None)
if status is None or last_status.status == status
]

View File

@ -18,8 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from tarfile import TarFile
import tarfile
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
@ -45,5 +44,5 @@ class Restore(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
with TarFile(args.path) as archive:
archive.extractall(path=args.output)
with tarfile.open(args.path) as archive:
archive.extractall(path=args.output) # nosec

View File

@ -51,12 +51,8 @@ class StatusUpdate(Handler):
match args.action:
case Action.Update if args.package:
# update packages statuses
packages = application.repository.packages()
for base in args.package:
if (local := next((package for package in packages if package.base == base), None)) is not None:
client.package_add(local, args.status)
else:
client.package_update(base, args.status)
for package in args.package:
client.package_update(package, args.status)
case Action.Update:
# update service status
client.status_update(args.status)

View File

@ -48,7 +48,8 @@ class Update(Handler):
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start()
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs)
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
check_files=args.check_files)
if args.dry_run: # some check specific actions
if args.changes: # generate changes if requested
application.changes(packages)

View File

@ -27,7 +27,7 @@ from ahriman import __version__
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRunError
from ahriman.core.log import LazyLogging
from ahriman.core.status.client import Client
from ahriman.core.status import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.repository_id import RepositoryId

View File

@ -38,12 +38,12 @@ class _Context:
"""
self._content: dict[str, Any] = {}
def get(self, key: ContextKey[T]) -> T:
def get(self, key: ContextKey[T] | type[T]) -> T:
"""
get value for the specified key
Args:
key(ContextKey[T]): context key name
key(ContextKey[T] | type[T]): context key name
Returns:
T: value associated with the key
@ -52,29 +52,37 @@ class _Context:
KeyError: in case if the specified context variable was not found
ValueError: in case if type of value is not an instance of specified return type
"""
if not isinstance(key, ContextKey):
key = ContextKey.from_type(key)
if key.key not in self._content:
raise KeyError(key.key)
value = self._content[key.key]
if not isinstance(value, key.return_type):
raise ValueError(f"Value {value} is not an instance of {key.return_type}")
return value
def set(self, key: ContextKey[T], value: T) -> None:
def set(self, key: ContextKey[T] | type[T], value: T) -> None:
"""
set value for the specified key
Args:
key(ContextKey[T]): context key name
key(ContextKey[T] | type[T]): context key name
value(T): context value associated with the specified key
Raises:
KeyError: in case if the specified context variable already exists
ValueError: in case if type of value is not an instance of specified return type
"""
if not isinstance(key, ContextKey):
key = ContextKey.from_type(key)
if key.key in self._content:
raise KeyError(key.key)
if not isinstance(value, key.return_type):
raise ValueError(f"Value {value} is not an instance of {key.return_type}")
self._content[key.key] = value
def __iter__(self) -> Iterator[str]:

View File

@ -18,24 +18,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import shutil
import tarfile
from collections.abc import Callable, Generator
from collections.abc import Generator, Iterable
from functools import cached_property
from pathlib import Path
from pyalpm import DB, Handle, Package, SIG_PACKAGE, error as PyalpmError # type: ignore[import-not-found]
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
from string import Template
from ahriman.core.alpm.pacman_database import PacmanDatabase
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.util import trim_package
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
class Pacman(LazyLogging):
"""
alpm wrapper
Attributes:
configuration(Configuration): configuration instance
refresh_database(PacmanSynchronization): synchronize local cache to remote
repository_id(RepositoryId): repository unique identifier
repository_path(RepositoryPaths): repository paths instance
"""
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
@ -48,8 +55,11 @@ class Pacman(LazyLogging):
configuration(Configuration): configuration instance
refresh_database(PacmanSynchronization): synchronize local cache to remote
"""
self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle(
repository_id, configuration, refresh_database=refresh_database)
self.configuration = configuration
self.repository_id = repository_id
self.repository_paths = configuration.repository_paths
self.refresh_database = refresh_database
@cached_property
def handle(self) -> Handle:
@ -59,40 +69,39 @@ class Pacman(LazyLogging):
Returns:
Handle: generated pyalpm handle instance
"""
return self.__create_handle_fn()
return self.__create_handle(refresh_database=self.refresh_database)
def __create_handle(self, repository_id: RepositoryId, configuration: Configuration, *,
refresh_database: PacmanSynchronization) -> Handle:
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
"""
create lazy handle function
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
refresh_database(PacmanSynchronization): synchronize local cache to remote
Returns:
Handle: fully initialized pacman handle
"""
root = configuration.getpath("alpm", "root")
pacman_root = configuration.getpath("alpm", "database")
use_ahriman_cache = configuration.getboolean("alpm", "use_ahriman_cache")
mirror = configuration.get("alpm", "mirror")
paths = configuration.repository_paths
database_path = paths.pacman if use_ahriman_cache else pacman_root
pacman_root = self.configuration.getpath("alpm", "database")
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
root = self.configuration.getpath("alpm", "root")
handle = Handle(str(root), str(database_path))
for repository in configuration.getlist("alpm", "repositories"):
database = self.database_init(handle, repository, mirror, repository_id.architecture)
self.database_copy(handle, database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache)
for repository in self.configuration.getlist("alpm", "repositories"):
database = self.database_init(handle, repository, self.repository_id.architecture)
self.database_copy(handle, database, pacman_root, use_ahriman_cache=use_ahriman_cache)
# install repository database too
local_database = self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
self.database_copy(handle, local_database, pacman_root, use_ahriman_cache=use_ahriman_cache)
if use_ahriman_cache and refresh_database:
self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
return handle
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, paths: RepositoryPaths, *,
use_ahriman_cache: bool) -> None:
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
"""
copy database from the operating system root to the ahriman local home
@ -100,7 +109,6 @@ class Pacman(LazyLogging):
handle(Handle): pacman handle which will be used for database copying
database(DB): pacman database instance to be copied
pacman_root(Path): operating system pacman root
paths(RepositoryPaths): repository paths instance
use_ahriman_cache(bool): use local ahriman cache instead of system one
"""
def repository_database(root: Path) -> Path:
@ -122,30 +130,36 @@ class Pacman(LazyLogging):
return # database for some reason deos not exist
self.logger.info("copy pacman database from operating system root to ahriman's home")
shutil.copy(src, dst)
paths.chown(dst)
self.repository_paths.chown(dst)
def database_init(self, handle: Handle, repository: str, mirror: str, architecture: str) -> DB:
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
"""
create database instance from pacman handler and set its properties
Args:
handle(Handle): pacman handle which will be used for database initializing
repository(str): pacman repository name (e.g. core)
mirror(str): arch linux mirror url
architecture(str): repository architecture
Returns:
DB: loaded pacman database instance
"""
self.logger.info("loading pacman database %s", repository)
database: DB = handle.register_syncdb(repository, SIG_PACKAGE)
database: DB = handle.register_syncdb(repository, SIG_DATABASE_OPTIONAL | SIG_PACKAGE_OPTIONAL)
if repository != self.repository_id.name:
mirror = self.configuration.get("alpm", "mirror")
# replace variables in mirror address
variables = {
"arch": architecture,
"repo": repository,
}
database.servers = [Template(mirror).safe_substitute(variables)]
server = Template(mirror).safe_substitute(variables)
else:
# special case, same database, use local storage instead
server = f"file://{self.repository_paths.repository}"
database.servers = [server]
return database
@ -160,13 +174,44 @@ class Pacman(LazyLogging):
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
transaction = handle.init_transaction()
for database in handle.get_syncdbs():
try:
database.update(force)
except PyalpmError:
self.logger.exception("exception during update %s", database.name)
PacmanDatabase(database, self.configuration).sync(force=force)
transaction.release()
def package_get(self, package_name: str) -> Generator[Package, None, None]:
def files(self, packages: Iterable[str] | None = None) -> dict[str, set[str]]:
"""
extract list of known packages from the databases
Args:
packages(Iterable[str] | None, optional): filter by package names (Default value = None)
Returns:
dict[str, set[str]]: map of package name to its list of files
"""
packages = packages or []
def extract(tar: tarfile.TarFile) -> Generator[tuple[str, set[str]], None, None]:
for descriptor in filter(lambda info: info.path.endswith("/files"), tar.getmembers()):
package, *_ = str(Path(descriptor.path).parent).rsplit("-", 2)
if packages and package not in packages:
continue # skip unused packages
content = tar.extractfile(descriptor)
if content is None:
continue
files = {filename.decode("utf8").rstrip() for filename in content.readlines()}
yield package, files
result: dict[str, set[str]] = {}
for database in self.handle.get_syncdbs():
database_file = self.repository_paths.pacman / "sync" / f"{database.name}.files.tar.gz"
if not database_file.is_file():
continue # no database file found
with tarfile.open(database_file, "r:gz") as archive:
result.update(extract(archive))
return result
def package(self, package_name: str) -> Generator[Package, None, None]:
"""
retrieve list of the packages from the repository by name

View File

@ -0,0 +1,170 @@
#
# Copyright (c) 2021-2024 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 os
import shutil
from email.utils import parsedate_to_datetime
from pathlib import Path
from pyalpm import DB # type: ignore[import-not-found]
from urllib.parse import urlparse
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import PacmanError
from ahriman.core.http import SyncHttpClient
class PacmanDatabase(SyncHttpClient):
"""
implementation for database sync, because pyalpm is not always enough
Attributes:
LAST_MODIFIED_HEADER(str): last modified header name
database(DB): pyalpm database object
repository_paths(RepositoryPaths): repository paths instance
sync_files_database(bool): sync files database
"""
LAST_MODIFIED_HEADER = "Last-Modified"
def __init__(self, database: DB, configuration: Configuration) -> None:
"""
default constructor
Args:
database(DB): pyalpm database object
configuration(Configuration): configuration instance
"""
SyncHttpClient.__init__(self)
self.timeout = None # reset timeout
self.database = database
self.repository_paths = configuration.repository_paths
self.sync_files_database = configuration.getboolean("alpm", "sync_files_database")
def copy(self, remote_path: Path, local_path: Path) -> None:
"""
copy local database file
Args:
remote_path(Path): path to source (remote) file
local_path(Path): path to locally stored file
"""
shutil.copy(remote_path, local_path)
def download(self, url: str, local_path: Path) -> None:
"""
download remote file and store it to local path with the correct last modified headers
Args:
url(str): remote url to request file
local_path(Path): path to locally stored file
Raises:
PacmanError: in case if no last-modified header was found
"""
response = self.make_request("GET", url, stream=True)
if self.LAST_MODIFIED_HEADER not in response.headers:
raise PacmanError("No last-modified header found")
with local_path.open("wb") as local_file:
for chunk in response.iter_content(chunk_size=1024):
local_file.write(chunk)
# set correct (a,m)time for the file
remote_changed = parsedate_to_datetime(response.headers[self.LAST_MODIFIED_HEADER]).timestamp()
os.utime(local_path, (remote_changed, remote_changed))
def is_outdated(self, url: str, local_path: Path) -> bool:
"""
check if local file is outdated
Args:
url(str): remote url to request last modified header
local_path(Path): path to locally stored file
Returns:
bool: True in case if remote file is newer than local file
Raises:
PacmanError: in case if no last-modified header was found
"""
if not local_path.is_file():
return True # no local file found, requires to update
response = self.make_request("HEAD", url)
if self.LAST_MODIFIED_HEADER not in response.headers:
raise PacmanError("No last-modified header found")
remote_changed = parsedate_to_datetime(response.headers["Last-Modified"]).timestamp()
local_changed = local_path.stat().st_mtime
return remote_changed > local_changed
def sync(self, *, force: bool) -> None:
"""
sync packages and files databases
Args:
force(bool): force database synchronization (same as ``pacman -Syy``)
"""
try:
self.sync_packages(force=force)
if self.sync_files_database:
self.sync_files(force=force)
except Exception:
self.logger.exception("exception during update %s", self.database.name)
def sync_files(self, *, force: bool) -> None:
"""
sync files by using http request
Args:
force(bool): force database synchronization (same as ``pacman -Syy``)
"""
server = next(iter(self.database.servers))
filename = f"{self.database.name}.files.tar.gz"
url = f"{server}/{filename}"
remote_uri = urlparse(url)
local_path = Path(self.repository_paths.pacman / "sync" / filename)
match remote_uri.scheme:
case "http" | "https":
if not force and not self.is_outdated(url, local_path):
return
self.download(url, local_path)
case "file":
# just copy file as it is relatively cheap operation, no need to check timestamps
self.copy(Path(remote_uri.path), local_path)
case other:
raise PacmanError(f"Unknown or unsupported URL scheme {other}")
def sync_packages(self, *, force: bool) -> None:
"""
sync packages by using built-in pyalpm methods
Args:
force(bool): force database synchronization (same as ``pacman -Syy``)
"""
self.database.update(force)

View File

@ -56,6 +56,6 @@ class OfficialSyncdb(Official):
raise UnknownPackageError(package_name)
try:
return next(AURPackage.from_pacman(package) for package in pacman.package_get(package_name))
return next(AURPackage.from_pacman(package) for package in pacman.package(package_name))
except StopIteration:
raise UnknownPackageError(package_name) from None

View File

@ -68,7 +68,7 @@ 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,
logger=self.logger,
@ -78,8 +78,13 @@ class Repo(LazyLogging):
"""
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)
# since pacman-6.1.0 repo-add doesn't create empty database in case if no packages supplied
# this code creates empty files instead
if self.repo_path.exists():
return # database is already created, skip this part
self.repo_path.touch(exist_ok=True)
(self.paths.repository / f"{self.name}.db").symlink_to(self.repo_path)
def remove(self, package: str, filename: Path) -> None:
"""

View File

@ -69,7 +69,8 @@ class OAuth(Mapping):
Returns:
str: login control as html code to insert
"""
return f"""<a class="nav-link" href="/api/v1/login" title="login via OAuth2"><i class="bi bi-{self.icon}"></i> login</a>"""
return f"""<a class="nav-link" href="/api/v1/login" title="login via OAuth2"><i class="bi bi-{
self.icon}"></i> login</a>"""
@staticmethod
def get_provider(name: str) -> type[aioauth_client.OAuth2Client]:

View File

@ -21,7 +21,6 @@ from pathlib import Path
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import BuildError
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output
@ -38,6 +37,7 @@ class Task(LazyLogging):
archbuild_flags(list[str]): command flags for archbuild command
architecture(str): repository architecture
build_command(str): build command
include_debug_packages(bool): whether to include debug packages or not
makechrootpkg_flags(list[str]): command flags for makechrootpkg command
makepkg_flags(list[str]): command flags for makepkg command
package(Package): package definitions
@ -63,6 +63,7 @@ class Task(LazyLogging):
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
self.build_command = configuration.get("build", "build_command")
self.include_debug_packages = configuration.getboolean("build", "include_debug_packages", fallback=True)
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
@ -99,30 +100,35 @@ class Task(LazyLogging):
environment=environment,
)
# well it is not actually correct, but we can deal with it
package_list_command = ["makepkg", "--packagelist"]
if not self.include_debug_packages:
package_list_command.append("OPTIONS=(!debug)") # disable debug flag manually
packages = check_output(
"makepkg", "--packagelist",
*package_list_command,
exception=BuildError.from_process(self.package.base),
cwd=sources_dir,
logger=self.logger,
environment=environment,
).splitlines()
return [Path(package) for package in packages]
# some dirty magic here
# the filter is applied in order to make sure that result will only contain packages which were actually built
# e.g. in some cases packagelist command produces debug packages which were not actually built
return list(filter(lambda path: path.is_file(), map(Path, packages)))
def init(self, sources_dir: Path, database: SQLite, local_version: str | None) -> str | None:
def init(self, sources_dir: Path, patches: list[PkgbuildPatch], local_version: str | None) -> str | None:
"""
fetch package from git
Args:
sources_dir(Path): local path to fetch
database(SQLite): database instance
patches(list[PkgbuildPatch]): list of patches for the package
local_version(str | None): local version of the package. If set and equal to current version, it will
automatically bump pkgrel
Returns:
str | None: current commit sha if available
"""
last_commit_sha = Sources.load(sources_dir, self.package, database.patches_get(self.package.base), self.paths)
last_commit_sha = Sources.load(sources_dir, self.package, patches, self.paths)
if local_version is None:
return last_commit_sha # there is no local package or pkgrel increment is disabled

View File

@ -89,6 +89,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"path_exists": True,
"path_type": "dir",
},
"sync_files_database": {
"type": "boolean",
"coerce": "boolean",
"required": True,
},
"use_ahriman_cache": {
"type": "boolean",
"coerce": "boolean",
@ -176,6 +181,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"empty": False,
},
},
"include_debug_packages": {
"type": "boolean",
"coerce": "boolean",
},
"makepkg_flags": {
"type": "list",
"coerce": "list",

View File

@ -0,0 +1,32 @@
#
# Copyright (c) 2021-2024 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = ["steps"]
steps = [
"""
create table package_dependencies (
package_base text not null,
repository text not null,
dependencies json not null,
unique (package_base, repository)
)
""",
]

View File

@ -20,6 +20,7 @@
from ahriman.core.database.operations.auth_operations import AuthOperations
from ahriman.core.database.operations.build_operations import BuildOperations
from ahriman.core.database.operations.changes_operations import ChangesOperations
from ahriman.core.database.operations.dependencies_operations import DependenciesOperations
from ahriman.core.database.operations.logs_operations import LogsOperations
from ahriman.core.database.operations.package_operations import PackageOperations
from ahriman.core.database.operations.patch_operations import PatchOperations

View File

@ -64,7 +64,7 @@ class ChangesOperations(Operations):
def changes_insert(self, package_base: str, changes: Changes, repository_id: RepositoryId | None = None) -> None:
"""
insert packages to build queue
insert package changes
Args:
package_base(str): package base to insert
@ -117,27 +117,3 @@ class ChangesOperations(Operations):
})
return self.with_connection(run, commit=True)
def hashes_get(self, repository_id: RepositoryId | None = None) -> dict[str, str]:
"""
extract last commit hashes if available
Args:
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Returns:
dict[str, str]: map of package base to its last commit hash
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> dict[str, str]:
return {
row["package_base"]: row["last_commit_sha"]
for row in connection.execute(
"""select package_base, last_commit_sha from package_changes where repository = :repository""",
{"repository": repository_id.id}
)
}
return self.with_connection(run)

View File

@ -0,0 +1,116 @@
#
# Copyright (c) 2021-2024 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 sqlite3 import Connection
from ahriman.core.database.operations.operations import Operations
from ahriman.models.dependencies import Dependencies
from ahriman.models.repository_id import RepositoryId
class DependenciesOperations(Operations):
"""
operations for dependencies table
"""
def dependencies_get(self, package_base: str | None = None,
repository_id: RepositoryId | None = None) -> dict[str, Dependencies]:
"""
get dependencies for the specific package base if available
Args:
package_base(str | None): package base to search
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Returns:
Dependencies: changes for the package base if available
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> dict[str, Dependencies]:
return {
row["package_base"]: Dependencies(row["dependencies"])
for row in connection.execute(
"""
select package_base, dependencies from package_dependencies
where (:package_base is null or package_base = :package_base)
and repository = :repository
""",
{
"package_base": package_base,
"repository": repository_id.id,
}
)
}
return self.with_connection(run)
def dependencies_insert(self, package_base: str, dependencies: Dependencies,
repository_id: RepositoryId | None = None) -> None:
"""
insert package dependencies
Args:
package_base(str): package base
dependencies(Dependencies): package dependencies
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
insert into package_dependencies
(package_base, repository, dependencies)
values
(:package_base, :repository, :dependencies)
on conflict (package_base, repository) do update set
dependencies = :dependencies
""",
{
"package_base": package_base,
"repository": repository_id.id,
"dependencies": dependencies.paths,
})
return self.with_connection(run, commit=True)
def dependencies_remove(self, package_base: str | None, repository_id: RepositoryId | None = None) -> None:
"""
remove packages dependencies
Args:
package_base(str | None): optional filter by package base
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
delete from package_dependencies
where (:package_base is null or package_base = :package_base)
and repository = :repository
""",
{
"package_base": package_base,
"repository": repository_id.id,
})
return self.with_connection(run, commit=True)

View File

@ -25,6 +25,7 @@ from typing import Any, TypeVar
from ahriman.core.log import LazyLogging
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
T = TypeVar("T")
@ -38,7 +39,7 @@ class Operations(LazyLogging):
path(Path): path to the database file
"""
def __init__(self, path: Path, repository_id: RepositoryId) -> None:
def __init__(self, path: Path, repository_id: RepositoryId, repository_paths: RepositoryPaths) -> None:
"""
default constructor
@ -48,6 +49,7 @@ class Operations(LazyLogging):
"""
self.path = path
self._repository_id = repository_id
self._repository_paths = repository_paths
@staticmethod
def factory(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> dict[str, Any]:

View File

@ -150,34 +150,6 @@ class PackageOperations(Operations):
""",
package_list)
@staticmethod
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus,
repository_id: RepositoryId) -> None:
"""
insert base package status into table
Args:
connection(Connection): database connection
package_base(str): package base name
status(BuildStatus): new build status
repository_id(RepositoryId): repository unique identifier
"""
connection.execute(
"""
insert into package_statuses
(package_base, status, last_updated, repository)
values
(:package_base, :status, :last_updated, :repository)
on conflict (package_base, repository) do update set
status = :status, last_updated = :last_updated
""",
{
"package_base": package_base,
"status": status.status.value,
"last_updated": status.timestamp,
"repository": repository_id.id,
})
@staticmethod
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
"""
@ -246,21 +218,6 @@ class PackageOperations(Operations):
)
}
def package_base_update(self, package: Package, repository_id: RepositoryId | None = None) -> None:
"""
update package base only
Args:
package(Package): package properties
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package, repository_id)
return self.with_connection(run, commit=True)
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
"""
remove package from database
@ -277,20 +234,18 @@ class PackageOperations(Operations):
return self.with_connection(run, commit=True)
def package_update(self, package: Package, status: BuildStatus, repository_id: RepositoryId | None = None) -> None:
def package_update(self, package: Package, repository_id: RepositoryId | None = None) -> None:
"""
update package status
Args:
package(Package): package properties
status(BuildStatus): new build status
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package, repository_id)
self._package_update_insert_status(connection, package.base, status, repository_id)
self._package_update_insert_packages(connection, package, repository_id)
self._package_remove_packages(connection, package.base, package.packages.keys(), repository_id)
@ -317,22 +272,32 @@ class PackageOperations(Operations):
return self.with_connection(lambda connection: list(run(connection)))
def remotes_get(self, repository_id: RepositoryId | None = None) -> dict[str, RemoteSource]:
def status_update(self, package_base: str, status: BuildStatus, repository_id: RepositoryId | None = None) -> None:
"""
get packages remotes based on current settings
insert base package status into table
Args:
package_base(str): package base name
status(BuildStatus): new build status
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Returns:
dict[str, RemoteSource]: map of package base to its remote sources
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> dict[str, Package]:
return self._packages_get_select_package_bases(connection, repository_id)
def run(connection: Connection) -> None:
connection.execute(
"""
insert into package_statuses
(package_base, status, last_updated, repository)
values
(:package_base, :status, :last_updated, :repository)
on conflict (package_base, repository) do update set
status = :status, last_updated = :last_updated
""",
{
"package_base": package_base,
"status": status.status.value,
"last_updated": status.timestamp,
"repository": repository_id.id,
})
return {
package_base: package.remote
for package_base, package in self.with_connection(run).items()
}
return self.with_connection(run, commit=True)

View File

@ -25,12 +25,19 @@ 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, LogsOperations, \
PackageOperations, PatchOperations
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
DependenciesOperations, LogsOperations, PackageOperations, PatchOperations
# pylint: disable=too-many-ancestors
class SQLite(AuthOperations, BuildOperations, ChangesOperations, LogsOperations, PackageOperations, PatchOperations):
class SQLite(
AuthOperations,
BuildOperations,
ChangesOperations,
DependenciesOperations,
LogsOperations,
PackageOperations,
PatchOperations):
"""
wrapper for sqlite3 database
@ -59,7 +66,7 @@ class SQLite(AuthOperations, BuildOperations, ChangesOperations, LogsOperations,
path = cls.database_path(configuration)
_, repository_id = configuration.check_loaded()
database = cls(path, repository_id)
database = cls(path, repository_id, configuration.repository_paths)
database.init(configuration)
return database
@ -94,3 +101,24 @@ class SQLite(AuthOperations, BuildOperations, ChangesOperations, LogsOperations,
if configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
paths.chown(self.path)
def package_clear(self, package_base: str) -> None:
"""
completely remove package from all tables
Args:
package_base(str): package base to remove
Examples:
This method completely removes the package from all tables and must be used, e.g. on package removal::
>>> database.package_clear("ahriman")
"""
self.build_queue_clear(package_base)
self.patches_remove(package_base, [])
self.logs_remove(package_base, None)
self.changes_remove(package_base)
self.dependencies_remove(package_base)
# remove local cache too
self._repository_paths.tree_clear(package_base)

View File

@ -219,6 +219,21 @@ class PackageInfoError(RuntimeError):
RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`")
class PacmanError(RuntimeError):
"""
exception in case of pacman operation errors
"""
def __init__(self, details: Any) -> None:
"""
default constructor
Args:
details(Any): error details
"""
RuntimeError.__init__(self, f"Could not perform operation with pacman: `{details}`")
class PathError(ValueError):
"""
exception which will be raised on path which is not belong to root directory

View File

@ -25,9 +25,9 @@ from tempfile import TemporaryDirectory
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import GitRemoteError
from ahriman.core.log import LazyLogging
from ahriman.core.status import Client
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
@ -40,20 +40,20 @@ class RemotePush(LazyLogging):
Attributes:
commit_author(tuple[str, str] | None): optional commit author in form of git config
database(SQLite): database instance
remote_source(RemoteSource): repository remote source (remote pull url and branch)
reporter(Client): reporter client used for additional information retrieval
"""
def __init__(self, database: SQLite, configuration: Configuration, section: str) -> None:
def __init__(self, reporter: Client, configuration: Configuration, section: str) -> None:
"""
default constructor
Args:
database(SQLite): database instance
reporter(Client): reporter client
configuration(Configuration): configuration instance
section(str): settings section name
"""
self.database = database
self.reporter = reporter
commit_email = configuration.get(section, "commit_email", fallback="ahriman@localhost")
commit_user = configuration.get(section, "commit_user", fallback="ahriman")
@ -92,7 +92,7 @@ class RemotePush(LazyLogging):
else:
shutil.rmtree(git_file)
# ...copy all patches...
for patch in self.database.patches_get(package.base):
for patch in self.reporter.package_patches_get(package.base, None):
filename = f"ahriman-{package.base}.patch" if patch.key is None else f"ahriman-{patch.key}.patch"
patch.write(package_target_dir / filename)
# ...and finally return path to the copied directory

View File

@ -19,10 +19,9 @@
#
from ahriman.core import context
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.gitremote.remote_push import RemotePush
from ahriman.core.status import Client
from ahriman.core.triggers import Trigger
from ahriman.models.context_key import ContextKey
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
from ahriman.models.result import Result
@ -111,10 +110,10 @@ class RemotePushTrigger(Trigger):
GitRemoteError: if database is not set in context
"""
ctx = context.get()
database = ctx.get(ContextKey("database", SQLite))
reporter = ctx.get(Client)
for target in self.targets:
section, _ = self.configuration.gettype(
target, self.repository_id, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
runner = RemotePush(database, self.configuration, section)
runner = RemotePush(reporter, self.configuration, section)
runner.run(result)

View File

@ -38,7 +38,7 @@ class SyncHttpClient(LazyLogging):
Attributes:
auth(tuple[str, str] | None): HTTP basic auth object if set
suppress_errors(bool): suppress logging of request errors
timeout(int): HTTP request timeout in seconds
timeout(int | None): HTTP request timeout in seconds
"""
def __init__(self, configuration: Configuration | None = None, section: str | None = None, *,
@ -60,7 +60,7 @@ class SyncHttpClient(LazyLogging):
password = configuration.get(section, "password", fallback=None)
self.auth = (username, password) if username and password else None
self.timeout = configuration.getint(section, "timeout", fallback=30)
self.timeout: int | None = configuration.getint(section, "timeout", fallback=30)
self.suppress_errors = suppress_errors
@cached_property
@ -90,25 +90,27 @@ class SyncHttpClient(LazyLogging):
result: str = exception.response.text if exception.response is not None else ""
return result
def make_request(self, method: Literal["DELETE", "GET", "POST", "PUT"], url: str, *,
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,
data: Any | None = None,
json: dict[str, Any] | None = None,
files: dict[str, MultipartType] | None = None,
stream: bool | None = None,
session: requests.Session | None = None,
suppress_errors: bool | None = None) -> requests.Response:
"""
perform request with specified parameters
Args:
method(Literal["DELETE", "GET", "POST", "PUT"]): HTTP method to call
method(Literal["DELETE", "GET", "HEAD", "POST", "PUT"]): HTTP method to call
url(str): remote url to call
headers(dict[str, str] | None, optional): request headers (Default value = None)
params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None)
data(Any | None, optional): request raw data parameters (Default value = None)
json(dict[str, Any] | None, optional): request json parameters (Default value = None)
files(dict[str, MultipartType] | None, optional): multipart upload (Default value = None)
stream(bool | None, optional): handle response as stream (Default value = None)
session(requests.Session | None, optional): session object if any (Default value = None)
suppress_errors(bool | None, optional): suppress logging errors (e.g. if no web server available). If none
set, the instance-wide value will be used (Default value = None)
@ -124,7 +126,7 @@ class SyncHttpClient(LazyLogging):
try:
response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json,
auth=self.auth, timeout=self.timeout)
stream=stream, auth=self.auth, timeout=self.timeout)
response.raise_for_status()
return response
except requests.HTTPError as ex:

View File

@ -22,6 +22,7 @@ import logging
from typing import Self
from ahriman.core.configuration import Configuration
from ahriman.core.status import Client
from ahriman.models.repository_id import RepositoryId
@ -49,8 +50,6 @@ class HttpLogHandler(logging.Handler):
# we don't really care about those parameters because they will be handled by the reporter
logging.Handler.__init__(self)
# client has to be imported here because of circular imports
from ahriman.core.status.client import Client
self.reporter = Client.load(repository_id, configuration, report=report)
self.suppress_errors = suppress_errors
@ -92,7 +91,7 @@ class HttpLogHandler(logging.Handler):
return # in case if no package base supplied we need just skip log message
try:
self.reporter.package_logs(log_record_id, record)
self.reporter.package_logs_add(log_record_id, record.created, record.getMessage())
except Exception:
if self.suppress_errors:
return

View File

@ -29,6 +29,7 @@ from ahriman.core.repository.package_info import PackageInfo
from ahriman.core.util import safe_filename
from ahriman.models.changes import Changes
from ahriman.models.package import Package
from ahriman.models.package_archive import PackageArchive
from ahriman.models.package_description import PackageDescription
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -57,7 +58,8 @@ class Executor(PackageInfo, Cleaner):
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
commit_sha = task.init(local_path, self.database, local_version)
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)
for src in built:
dst = self.paths.packages / src.name
@ -76,7 +78,11 @@ class Executor(PackageInfo, Cleaner):
packager = self.packager(packagers, single.base)
last_commit_sha = build_single(single, Path(dir_name), packager.packager_id)
# clear changes and update commit hash
self.reporter.package_changes_set(single.base, Changes(last_commit_sha))
self.reporter.package_changes_update(single.base, Changes(last_commit_sha))
# update dependencies list
dependencies = PackageArchive(self.paths.build_directory, single, self.pacman).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)
@ -97,12 +103,7 @@ class Executor(PackageInfo, Cleaner):
"""
def remove_base(package_base: str) -> None:
try:
self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base, [])
self.database.logs_remove(package_base, None)
self.database.changes_remove(package_base)
self.reporter.package_remove(package_base) # we only update status page in case of base removal
self.reporter.package_remove(package_base)
except Exception:
self.logger.exception("could not remove base %s", package_base)
@ -117,7 +118,8 @@ class Executor(PackageInfo, Cleaner):
# build package list based on user input
result = Result()
requested = set(packages)
packages = set(packages) # remove duplicates
requested = packages | {f"{package}-debug" for package in packages} # append debug packages
for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages):
packages_to_remove.update({
@ -136,7 +138,7 @@ class Executor(PackageInfo, Cleaner):
# check for packages which were requested to remove, but weren't found locally
# it might happen for example, if there were no success build before
for unknown in requested:
for unknown in packages:
if unknown in packages_to_remove or unknown in bases_to_remove:
continue
bases_to_remove.append(unknown)

View File

@ -43,14 +43,14 @@ class PackageInfo(RepositoryProperties):
Returns:
list[Package]: list of read packages
"""
sources = self.database.remotes_get()
sources = {package.base: package.remote for package, _, in self.reporter.package_get(None)}
result: dict[str, Package] = {}
# we are iterating over bases, not single packages
for full_path in packages:
try:
local = Package.from_archive(full_path, self.pacman)
if (source := sources.get(local.base)) is not None:
if (source := sources.get(local.base)) is not None: # update source with remote
local.remote = source
current = result.setdefault(local.base, local)
@ -78,7 +78,8 @@ class PackageInfo(RepositoryProperties):
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
dir_path = Path(dir_name)
current_commit_sha = Sources.load(dir_path, package, self.database.patches_get(package.base), self.paths)
patches = self.reporter.package_patches_get(package.base, None)
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
changes: str | None = None
if current_commit_sha != last_commit_sha:
@ -86,14 +87,21 @@ class PackageInfo(RepositoryProperties):
return Changes(last_commit_sha, changes)
def packages(self) -> list[Package]:
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
"""
generate list of repository packages
Args:
filter_packages(Iterable[str] | None, optional): filter packages list by specified only
Returns:
list[Package]: list of packages properties
"""
return self.load_archives(filter(package_like, self.paths.repository.iterdir()))
packages = self.load_archives(filter(package_like, self.paths.repository.iterdir()))
if filter_packages:
packages = [package for package in packages if package.base in filter_packages]
return packages
def packages_built(self) -> list[Path]:
"""

View File

@ -26,7 +26,7 @@ from ahriman.core.database import SQLite
from ahriman.core.repository.executor import Executor
from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.core.sign.gpg import GPG
from ahriman.models.context_key import ContextKey
from ahriman.core.status import Client
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_id import RepositoryId
@ -89,11 +89,12 @@ class Repository(Executor, UpdateHandler):
# directly without loader
ctx = _Context()
ctx.set(ContextKey("database", SQLite), self.database)
ctx.set(ContextKey("configuration", Configuration), self.configuration)
ctx.set(ContextKey("pacman", Pacman), self.pacman)
ctx.set(ContextKey("sign", GPG), self.sign)
ctx.set(SQLite, self.database)
ctx.set(Configuration, self.configuration)
ctx.set(Pacman, self.pacman)
ctx.set(GPG, self.sign)
ctx.set(Client, self.reporter)
ctx.set(ContextKey("repository", type(self)), self)
ctx.set(type(self), self)
context.set(ctx)

View File

@ -23,7 +23,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.log import LazyLogging
from ahriman.core.sign.gpg import GPG
from ahriman.core.status.client import Client
from ahriman.core.status import Client
from ahriman.core.triggers import TriggerLoader
from ahriman.models.packagers import Packagers
from ahriman.models.pacman_synchronization import PacmanSynchronization
@ -75,7 +75,7 @@ class RepositoryProperties(LazyLogging):
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
self.sign = GPG(configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(repository_id, configuration, report=report)
self.reporter = Client.load(repository_id, configuration, database, report=report)
self.triggers = TriggerLoader.load(repository_id, configuration)
@property

View File

@ -55,17 +55,13 @@ class UpdateHandler(PackageInfo, Cleaner):
continue
raise UnknownPackageError(package.base)
local_versions = {package.base: package.version for package in self.packages()}
result: list[Package] = []
for local in self.packages():
with self.in_package_context(local.base, local_versions.get(local.base)):
for local in self.packages(filter_packages):
with self.in_package_context(local.base, local.version):
if not local.remote.is_remote:
continue # avoid checking local packages
if local.base in self.ignore_list:
continue
if filter_packages and local.base not in filter_packages:
continue
try:
remote = load_remote(local)
@ -82,6 +78,45 @@ class UpdateHandler(PackageInfo, Cleaner):
return result
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.)
Args:
filter_packages(Iterable[str]): do not check every package just specified in the list
Returns:
list[Package]: list of packages for which there is breaking linking
"""
def extract_files(lookup_packages: Iterable[str]) -> dict[str, set[str]]:
database_files = self.pacman.files(lookup_packages)
files: dict[str, set[str]] = {}
for package_name, package_files in database_files.items(): # invert map
for package_file in package_files:
files.setdefault(package_file, set()).add(package_name)
return files
result: list[Package] = []
for package in self.packages(filter_packages):
dependencies = self.reporter.package_dependencies_get(package.base)
if not dependencies.paths:
continue # skip check if no package dependencies found
required_packages = {dep for dep_packages in dependencies.paths.values() for dep in dep_packages}
filesystem = extract_files(required_packages)
for path, packages in dependencies.paths.items():
found = filesystem.get(path, set())
if found.intersection(packages):
continue
# there are no packages found in filesystem with the same paths
result.append(package)
break
return result
def updates_local(self, *, vcs: bool) -> list[Package]:
"""
check local packages for updates

View File

@ -17,3 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.status.client import Client

View File

@ -17,16 +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-public-methods
from __future__ import annotations
import logging
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@ -36,22 +38,31 @@ class Client:
"""
@staticmethod
def load(repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Client:
def load(repository_id: RepositoryId, configuration: Configuration, database: SQLite | None = None, *,
report: bool = True) -> Client:
"""
load client from settings
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
database(SQLite | None, optional): database instance (Default value = None)
report(bool, optional): force enable or disable reporting (Default value = True)
Returns:
Client: client according to current settings
"""
def make_local_client() -> Client:
if database is None:
return Client()
from ahriman.core.status.local_client import LocalClient
return LocalClient(repository_id, database)
if not report:
return Client()
return make_local_client()
if not configuration.getboolean("status", "enabled", fallback=True): # global switch
return Client()
return make_local_client()
# new-style section
address = configuration.get("status", "address", fallback=None)
@ -65,16 +76,8 @@ class Client:
if address or legacy_address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient
return WebClient(repository_id, configuration)
return Client()
def package_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
"""
return make_local_client()
def package_changes_get(self, package_base: str) -> Changes:
"""
@ -85,18 +88,52 @@ class Client:
Returns:
Changes: package changes if available and empty object otherwise
"""
del package_base
return Changes()
def package_changes_set(self, package_base: str, changes: Changes) -> None:
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_changes_update(self, package_base: str, changes: Changes) -> None:
"""
update package changes
Args:
package_base(str): package base to update
changes(Changes): changes descriptor
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_dependencies_get(self, package_base: str) -> Dependencies:
"""
get package dependencies
Args:
package_base(str): package base to retrieve
Returns:
list[Dependencies]: package implicit dependencies if available
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_dependencies_update(self, package_base: str, dependencies: Dependencies) -> None:
"""
update package dependencies
Args:
package_base(str): package base to update
dependencies(Dependencies): dependencies descriptor
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
@ -107,18 +144,94 @@ class Client:
Returns:
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
del package_base
return []
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
"""
post log record
Args:
log_record_id(LogRecordId): log record id
record(logging.LogRecord): log record to post to api
created(float): log created timestamp
message(str): log message
"""
# this method does not raise NotImplementedError because it is actively used as dummy client for http log
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
"""
get package logs
Args:
package_base(str): package base
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
Returns:
list[tuple[float, str]]: package logs
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove package logs
Args:
package_base(str): package base
version(str | None): package version to remove logs. If None set, all logs will be removed
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
"""
get package patches
Args:
package_base(str): package base to retrieve
variable(str | None): optional filter by patch variable
Returns:
list[PkgbuildPatch]: list of patches for the specified package
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_patches_remove(self, package_base: str, variable: str | None) -> None:
"""
remove package patch
Args:
package_base(str): package base to update
variable(str | None): patch name. If None set, all patches will be removed
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
create or update package patch
Args:
package_base(str): package base to update
patch(PkgbuildPatch): package patch
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_remove(self, package_base: str) -> None:
"""
@ -126,16 +239,37 @@ class Client:
Args:
package_base(str): package base to remove
"""
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
Raises:
NotImplementedError: not implemented method
"""
update package build status. Unlike :func:`package_add()` it does not update package properties
raise NotImplementedError
def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike :func:`package_update()` it does not update package properties
Args:
package_base(str): package base to update
status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package or update existing one with status
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def set_building(self, package_base: str) -> None:
"""
@ -144,7 +278,7 @@ class Client:
Args:
package_base(str): package base to update
"""
return self.package_update(package_base, BuildStatusEnum.Building)
self.package_status_update(package_base, BuildStatusEnum.Building)
def set_failed(self, package_base: str) -> None:
"""
@ -153,7 +287,7 @@ class Client:
Args:
package_base(str): package base to update
"""
return self.package_update(package_base, BuildStatusEnum.Failed)
self.package_status_update(package_base, BuildStatusEnum.Failed)
def set_pending(self, package_base: str) -> None:
"""
@ -162,7 +296,7 @@ class Client:
Args:
package_base(str): package base to update
"""
return self.package_update(package_base, BuildStatusEnum.Pending)
self.package_status_update(package_base, BuildStatusEnum.Pending)
def set_success(self, package: Package) -> None:
"""
@ -171,16 +305,19 @@ class Client:
Args:
package(Package): current package properties
"""
return self.package_add(package, BuildStatusEnum.Success)
self.package_update(package, BuildStatusEnum.Success)
def set_unknown(self, package: Package) -> None:
"""
set package status to unknown
set package status to unknown. Unlike other methods, this method also checks if package is known,
and - in case if it is - it silently skips updatd
Args:
package(Package): current package properties
"""
return self.package_add(package, BuildStatusEnum.Unknown)
if self.package_get(package.base):
return # skip update in case if package is already known
self.package_update(package, BuildStatusEnum.Unknown)
def status_get(self) -> InternalStatus:
"""

View File

@ -0,0 +1,214 @@
#
# Copyright (c) 2021-2024 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.database import SQLite
from ahriman.core.status import Client
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
class LocalClient(Client):
"""
local database handler
Attributes:
database(SQLite): database instance
repository_id(RepositoryId): repository unique identifier
"""
def __init__(self, repository_id: RepositoryId, database: SQLite) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
database(SQLite): database instance:
"""
self.database = database
self.repository_id = repository_id
def package_changes_get(self, package_base: str) -> Changes:
"""
get package changes
Args:
package_base(str): package base to retrieve
Returns:
Changes: package changes if available and empty object otherwise
"""
return self.database.changes_get(package_base, self.repository_id)
def package_changes_update(self, package_base: str, changes: Changes) -> None:
"""
update package changes
Args:
package_base(str): package base to update
changes(Changes): changes descriptor
"""
self.database.changes_insert(package_base, changes, self.repository_id)
def package_dependencies_get(self, package_base: str) -> Dependencies:
"""
get package dependencies
Args:
package_base(str): package base to retrieve
Returns:
list[Dependencies]: package implicit dependencies if available
"""
return self.database.dependencies_get(package_base, self.repository_id).get(package_base, Dependencies())
def package_dependencies_update(self, package_base: str, dependencies: Dependencies) -> None:
"""
update package dependencies
Args:
package_base(str): package base to update
dependencies(Dependencies): dependencies descriptor
"""
self.database.dependencies_insert(package_base, dependencies, self.repository_id)
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
get package status
Args:
package_base(str | None): package base to get
Returns:
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
packages = self.database.packages_get(self.repository_id)
if package_base is None:
return packages
return [(package, status) for package, status in packages if package.base == package_base]
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
"""
post log record
Args:
log_record_id(LogRecordId): log record id
created(float): log created timestamp
message(str): log message
"""
self.database.logs_insert(log_record_id, created, message, self.repository_id)
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
"""
get package logs
Args:
package_base(str): package base
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
Returns:
list[tuple[float, str]]: package logs
"""
return self.database.logs_get(package_base, limit, offset, self.repository_id)
def package_logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove package logs
Args:
package_base(str): package base
version(str | None): package version to remove logs. If None set, all logs will be removed
"""
self.database.logs_remove(package_base, version, self.repository_id)
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
"""
get package patches
Args:
package_base(str): package base to retrieve
variable(str | None): optional filter by patch variable
Returns:
list[PkgbuildPatch]: list of patches for the specified package
"""
variables = [variable] if variable is not None else None
return self.database.patches_list(package_base, variables).get(package_base, [])
def package_patches_remove(self, package_base: str, variable: str | None) -> None:
"""
remove package patch
Args:
package_base(str): package base to update
variable(str | None): patch name. If None set, all patches will be removed
"""
variables = [variable] if variable is not None else None
self.database.patches_remove(package_base, variables)
def package_patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
create or update package patch
Args:
package_base(str): package base to update
patch(PkgbuildPatch): package patch
"""
self.database.patches_insert(package_base, [patch])
def package_remove(self, package_base: str) -> None:
"""
remove packages from watcher
Args:
package_base(str): package base to remove
"""
self.database.package_clear(package_base)
def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike :func:`package_update()` it does not update package properties
Args:
package_base(str): package base to update
status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
"""
self.database.status_update(package_base, BuildStatus(status), self.repository_id)
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package or update existing one with status
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
"""
self.database.package_update(package, self.repository_id)
self.database.status_update(package.base, BuildStatus(status), self.repository_id)

View File

@ -17,17 +17,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections.abc import Callable
from threading import Lock
from typing import Any, Self
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.log import LazyLogging
from ahriman.core.status import Client
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
class Watcher(LazyLogging):
@ -35,21 +37,18 @@ class Watcher(LazyLogging):
package status watcher
Attributes:
database(SQLite): database instance
repository_id(RepositoryId): repository unique identifier
client(Client): reporter instance
status(BuildStatus): daemon status
"""
def __init__(self, repository_id: RepositoryId, database: SQLite) -> None:
def __init__(self, client: Client) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
database(SQLite): database instance
client(Client): reporter instance
"""
self.repository_id = repository_id
self.database = database
self.client = client
self._lock = Lock()
self._known: dict[str, tuple[Package, BuildStatus]] = {}
@ -76,61 +75,16 @@ class Watcher(LazyLogging):
with self._lock:
self._known = {
package.base: (package, status)
for package, status in self.database.packages_get(self.repository_id)
for package, status in self.client.package_get(None)
}
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
"""
extract logs for the package base
package_changes_get: Callable[[str], Changes]
Args:
package_base(str): package base
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
package_changes_update: Callable[[str, Changes], None]
Returns:
list[tuple[float, str]]: package logs
"""
self.package_get(package_base)
return self.database.logs_get(package_base, limit, offset, self.repository_id)
package_dependencies_get: Callable[[str], Dependencies]
def logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove package related logs
Args:
package_base(str): package base
version(str): package versio
"""
self.database.logs_remove(package_base, version, self.repository_id)
def logs_update(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
make new log record into database
Args:
log_record_id(LogRecordId): log record id
created(float): log created timestamp
record(str): log record
"""
if self._last_log_record_id != log_record_id:
# there is new log record, so we remove old ones
self.logs_remove(log_record_id.package_base, log_record_id.version)
self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record, self.repository_id)
def package_changes_get(self, package_base: str) -> Changes:
"""
retrieve package changes
Args:
package_base(str): package base
Returns:
Changes: package changes if available
"""
self.package_get(package_base)
return self.database.changes_get(package_base, self.repository_id)
package_dependencies_update: Callable[[str, Dependencies], None]
def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
"""
@ -151,6 +105,31 @@ class Watcher(LazyLogging):
except KeyError:
raise UnknownPackageError(package_base) from None
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
"""
make new log record into database
Args:
log_record_id(LogRecordId): log record id
created(float): log created timestamp
message(str): log message
"""
if self._last_log_record_id != log_record_id:
# there is new log record, so we remove old ones
self.package_logs_remove(log_record_id.package_base, log_record_id.version)
self._last_log_record_id = log_record_id
self.client.package_logs_add(log_record_id, created, message)
package_logs_get: Callable[[str, int, int], list[tuple[float, str]]]
package_logs_remove: Callable[[str, str | None], None]
package_patches_get: Callable[[str, str | None], list[PkgbuildPatch]]
package_patches_remove: Callable[[str, str], None]
package_patches_update: Callable[[str, PkgbuildPatch], None]
def package_remove(self, package_base: str) -> None:
"""
remove package base from known list if any
@ -160,59 +139,33 @@ class Watcher(LazyLogging):
"""
with self._lock:
self._known.pop(package_base, None)
self.database.package_remove(package_base, self.repository_id)
self.logs_remove(package_base, None)
self.client.package_remove(package_base)
self.package_logs_remove(package_base, None)
def package_update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None:
def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package status and description
update package status
Args:
package_base(str): package base to update
status(BuildStatusEnum): new build status
package(Package | None): optional package description. In case if not set current properties will be used
"""
if package is None:
package, _ = self.package_get(package_base)
full_status = BuildStatus(status)
with self._lock:
self._known[package_base] = (package, full_status)
self.database.package_update(package, full_status, self.repository_id)
self._known[package_base] = (package, BuildStatus(status))
self.client.package_status_update(package_base, status)
def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
"""
get patches for the package
update package
Args:
package_base(str): package base
variable(str | None): patch variable name if any
Returns:
list[PkgbuildPatch]: list of patches which are stored for the package
package(Package): package description
status(BuildStatusEnum): new build status
"""
self.package_get(package_base)
variables = [variable] if variable is not None else None
return self.database.patches_list(package_base, variables).get(package_base, [])
def patches_remove(self, package_base: str, variable: str) -> None:
"""
remove package patch
Args:
package_base(str): package base
variable(str): patch variable name
"""
self.database.patches_remove(package_base, [variable])
def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
update package patch
Args:
package_base(str): package base
patch(PkgbuildPatch): package patch
"""
self.database.patches_insert(package_base, [patch])
with self._lock:
self._known[package.base] = (package, BuildStatus(status))
self.client.package_update(package, status)
def status_update(self, status: BuildStatusEnum) -> None:
"""
@ -222,3 +175,34 @@ class Watcher(LazyLogging):
status(BuildStatusEnum): new service status
"""
self.status = BuildStatus(status)
def __call__(self, package_base: str | None) -> Self:
"""
extract client for future calls
Args:
package_base(str | None): package base to validate that package exists if applicable
Returns:
Self: instance of self to pass calls to the client
"""
if package_base is not None:
_ = self.package_get(package_base)
return self
def __getattr__(self, item: str) -> Any:
"""
proxy methods for reporter client
Args:
item(str): property name:
Returns:
Any: attribute by its name
Raises:
AttributeError: in case if no such attribute found
"""
if (method := getattr(self.client, item, None)) is not None:
return method
raise AttributeError(f"'{self.__class__.__qualname__}' object has no attribute '{item}'")

View File

@ -18,18 +18,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import contextlib
import logging
from urllib.parse import quote_plus as urlencode
from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncAhrimanClient
from ahriman.core.status.client import Client
from ahriman.core.status import Client
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@ -92,10 +93,22 @@ class WebClient(Client, SyncAhrimanClient):
package_base(str): package base
Returns:
str: full url for web service for logs
str: full url for web service for changes
"""
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/changes"
def _dependencies_url(self, package_base: str) -> str:
"""
get url for the dependencies api
Args:
package_base(str): package base
Returns:
str: full url for web service for dependencies
"""
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/dependencies"
def _logs_url(self, package_base: str) -> str:
"""
get url for the logs api
@ -110,7 +123,7 @@ class WebClient(Client, SyncAhrimanClient):
def _package_url(self, package_base: str = "") -> str:
"""
url generator
package url generator
Args:
package_base(str, optional): package base to generate url (Default value = "")
@ -121,6 +134,20 @@ class WebClient(Client, SyncAhrimanClient):
suffix = f"/{urlencode(package_base)}" if package_base else ""
return f"{self.address}/api/v1/packages{suffix}"
def _patches_url(self, package_base: str, variable: str = "") -> str:
"""
patches url generator
Args:
package_base(str): package base
variable(str, optional): patch variable name to generate url (Default value = "")
Returns:
str: full url of web service for the package patch
"""
suffix = f"/{urlencode(variable)}" if variable else ""
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/patches{suffix}"
def _status_url(self) -> str:
"""
get url for the status api
@ -130,22 +157,6 @@ class WebClient(Client, SyncAhrimanClient):
"""
return f"{self.address}/api/v1/status"
def package_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
"""
payload = {
"status": status.value,
"package": package.view()
}
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package.base),
params=self.repository_id.query(), json=payload)
def package_changes_get(self, package_base: str) -> Changes:
"""
get package changes
@ -165,7 +176,7 @@ class WebClient(Client, SyncAhrimanClient):
return Changes()
def package_changes_set(self, package_base: str, changes: Changes) -> None:
def package_changes_update(self, package_base: str, changes: Changes) -> None:
"""
update package changes
@ -177,6 +188,37 @@ class WebClient(Client, SyncAhrimanClient):
self.make_request("POST", self._changes_url(package_base),
params=self.repository_id.query(), json=changes.view())
def package_dependencies_get(self, package_base: str) -> Dependencies:
"""
get package dependencies
Args:
package_base(str): package base to retrieve
Returns:
list[Dependencies]: package implicit dependencies if available
"""
with contextlib.suppress(Exception):
response = self.make_request("GET", self._dependencies_url(package_base),
params=self.repository_id.query())
response_json = response.json()
return Dependencies.from_json(response_json)
return Dependencies()
def package_dependencies_update(self, package_base: str, dependencies: Dependencies) -> None:
"""
update package dependencies
Args:
package_base(str): package base to update
dependencies(Dependencies): dependencies descriptor
"""
with contextlib.suppress(Exception):
self.make_request("POST", self._dependencies_url(package_base),
params=self.repository_id.query(), json=dependencies.view())
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
get package status
@ -199,17 +241,18 @@ class WebClient(Client, SyncAhrimanClient):
return []
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
"""
post log record
Args:
log_record_id(LogRecordId): log record id
record(logging.LogRecord): log record to post to api
created(float): log created timestamp
message(str): log message
"""
payload = {
"created": record.created,
"message": record.getMessage(),
"created": created,
"message": message,
"version": log_record_id.version,
}
@ -219,6 +262,83 @@ class WebClient(Client, SyncAhrimanClient):
self.make_request("POST", self._logs_url(log_record_id.package_base),
params=self.repository_id.query(), json=payload, suppress_errors=True)
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
"""
get package logs
Args:
package_base(str): package base
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
Returns:
list[tuple[float, str]]: package logs
"""
with contextlib.suppress(Exception):
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
response = self.make_request("GET", self._logs_url(package_base), params=query)
response_json = response.json()
return [(record["created"], record["message"]) for record in response_json]
return []
def package_logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove package logs
Args:
package_base(str): package base
version(str | None): package version to remove logs. If None set, all logs will be removed
"""
with contextlib.suppress(Exception):
query = self.repository_id.query()
if version is not None:
query += [("version", version)]
self.make_request("DELETE", self._logs_url(package_base), params=query)
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
"""
get package patches
Args:
package_base(str): package base to retrieve
variable(str | None): optional filter by patch variable
Returns:
list[PkgbuildPatch]: list of patches for the specified package
"""
with contextlib.suppress(Exception):
response = self.make_request("GET", self._patches_url(package_base, variable or ""))
response_json = response.json()
patches = response_json if variable is None else [response_json]
return [PkgbuildPatch.from_json(patch) for patch in patches]
return []
def package_patches_remove(self, package_base: str, variable: str | None) -> None:
"""
remove package patch
Args:
package_base(str): package base to update
variable(str | None): patch name. If None set, all patches will be removed
"""
with contextlib.suppress(Exception):
self.make_request("DELETE", self._patches_url(package_base, variable or ""))
def package_patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
create or update package patch
Args:
package_base(str): package base to update
patch(PkgbuildPatch): package patch
"""
with contextlib.suppress(Exception):
self.make_request("POST", self._patches_url(package_base), json=patch.view())
def package_remove(self, package_base: str) -> None:
"""
remove packages from watcher
@ -229,19 +349,41 @@ class WebClient(Client, SyncAhrimanClient):
with contextlib.suppress(Exception):
self.make_request("DELETE", self._package_url(package_base), params=self.repository_id.query())
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike :func:`package_add()` it does not update package properties
update package build status. Unlike :func:`package_update()` it does not update package properties
Args:
package_base(str): package base to update
status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
"""
payload = {"status": status.value}
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package_base),
params=self.repository_id.query(), json=payload)
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package or update existing one with status
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
"""
payload = {
"status": status.value,
"package": package.view(),
}
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package.base),
params=self.repository_id.query(), json=payload)
def status_get(self) -> InternalStatus:
"""
get internal service status

View File

@ -24,7 +24,6 @@ from ahriman.core.sign.gpg import GPG
from ahriman.core.support.package_creator import PackageCreator
from ahriman.core.support.pkgbuild.keyring_generator import KeyringGenerator
from ahriman.core.triggers import Trigger
from ahriman.models.context_key import ContextKey
from ahriman.models.repository_id import RepositoryId
@ -134,8 +133,8 @@ class KeyringTrigger(Trigger):
trigger action which will be called at the start of the application
"""
ctx = context.get()
sign = ctx.get(ContextKey("sign", GPG))
database = ctx.get(ContextKey("database", SQLite))
sign = ctx.get(GPG)
database = ctx.get(SQLite)
for target in self.targets:
generator = KeyringGenerator(database, sign, self.repository_id, self.configuration, target)

View File

@ -19,13 +19,13 @@
#
import shutil
from pathlib import Path
from ahriman.core import context
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.status import Client
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
from ahriman.models.build_status import BuildStatus
from ahriman.models.context_key import ContextKey
from ahriman.models.package import Package
@ -49,23 +49,39 @@ class PackageCreator:
self.configuration = configuration
self.generator = generator
def package_create(self, path: Path) -> None:
"""
create package files
Args:
path(Path): path to directory with package files
"""
# clear old tree if any
shutil.rmtree(path, ignore_errors=True)
# create local tree
path.mkdir(mode=0o755, parents=True, exist_ok=True)
self.generator.write_pkgbuild(path)
Sources.init(path)
def package_register(self, path: Path) -> None:
"""
register package in build worker
Args:
path(Path): path to directory with package files
"""
ctx = context.get()
reporter = ctx.get(Client)
_, repository_id = self.configuration.check_loaded()
package = Package.from_build(path, repository_id.architecture, None)
reporter.set_unknown(package)
def run(self) -> None:
"""
create new local package
"""
local_path = self.configuration.repository_paths.cache_for(self.generator.pkgname)
# clear old tree if any
shutil.rmtree(local_path, ignore_errors=True)
# create local tree
local_path.mkdir(mode=0o755, parents=True, exist_ok=True)
self.generator.write_pkgbuild(local_path)
Sources.init(local_path)
# register package
ctx = context.get()
database: SQLite = ctx.get(ContextKey("database", SQLite))
_, repository_id = self.configuration.check_loaded()
package = Package.from_build(local_path, repository_id.architecture, None)
database.package_update(package, BuildStatus())
self.package_create(local_path)
self.package_register(local_path)

View File

@ -183,11 +183,12 @@ post_install() {{
Returns:
str: package() function for PKGBUILD
"""
# somehow autopep thinks that construction inside contains valid python code and reformats it
return f"""{{
install -Dm644 "{Path("$srcdir") / f"{self.name}.gpg"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}.gpg"}"
install -Dm644 "{Path("$srcdir") / f"{self.name}-revoked"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-revoked"}"
install -Dm644 "{Path("$srcdir") / f"{self.name}-trusted"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-trusted"}"
}}"""
}}""" # nopep8
def sources(self) -> dict[str, Callable[[Path], None]]:
"""

View File

@ -162,7 +162,8 @@ class GitHub(Upload, HttpUpload):
Returns:
dict[str, Any] | None: GitHub API release object if release found and None otherwise
"""
url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.github_release_tag}"
url = f"https://api.github.com/repos/{self.github_owner}/{
self.github_repository}/releases/tags/{self.github_release_tag}"
try:
response = self.make_request("GET", url)
release: dict[str, Any] = response.json()

View File

@ -56,7 +56,6 @@ __all__ = [
"srcinfo_property",
"srcinfo_property_list",
"trim_package",
"unquote",
"utcnow",
"walk",
]
@ -349,7 +348,7 @@ def pretty_datetime(timestamp: datetime.datetime | float | int | None) -> str:
if timestamp is None:
return ""
if isinstance(timestamp, (int, float)):
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
@ -466,38 +465,6 @@ def trim_package(package_name: str) -> str:
return package_name
def unquote(source: str) -> str:
"""
like :func:`shlex.quote()`, but opposite
Args:
source(str): source string to remove quotes
Returns:
str: string with quotes removed
Raises:
ValueError: if no closing quotation
"""
def generator() -> Generator[str, None, None]:
token = None
for char in source:
if token is not None:
if char == token:
token = None # closed quote
else:
yield char # character inside quotes
elif char in ("'", "\""):
token = char # first quote found
else:
yield char # normal character
if token is not None:
raise ValueError("No closing quotation")
return "".join(generator())
def utcnow() -> datetime.datetime:
"""
get current time
@ -505,7 +472,7 @@ def utcnow() -> datetime.datetime:
Returns:
datetime.datetime: current time in UTC
"""
return datetime.datetime.utcnow()
return datetime.datetime.now(datetime.UTC)
def walk(directory_path: Path) -> Generator[Path, None, None]:

View File

@ -57,6 +57,7 @@ class AURPackage:
provides(list[str]): list of packages which this package provides
license(list[str]): list of package licenses
keywords(list[str]): list of package keywords
groups(list[str]): list of package groups
Examples:
Mainly this class must be used from class methods instead of default :func:`__init__()`::
@ -100,6 +101,7 @@ class AURPackage:
provides: list[str] = field(default_factory=list)
license: list[str] = field(default_factory=list)
keywords: list[str] = field(default_factory=list)
groups: list[str] = field(default_factory=list)
@classmethod
def from_json(cls, dump: dict[str, Any]) -> Self:
@ -137,8 +139,8 @@ class AURPackage:
description=package.desc,
num_votes=0,
popularity=0.0,
first_submitted=datetime.datetime.utcfromtimestamp(0),
last_modified=datetime.datetime.utcfromtimestamp(package.builddate),
first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
last_modified=datetime.datetime.fromtimestamp(package.builddate, datetime.UTC),
url_path="",
url=package.url,
out_of_date=None,
@ -153,6 +155,7 @@ class AURPackage:
provides=package.provides,
license=package.licenses,
keywords=[],
groups=package.groups,
)
@classmethod
@ -175,13 +178,11 @@ class AURPackage:
description=dump["pkgdesc"],
num_votes=0,
popularity=0.0,
first_submitted=datetime.datetime.utcfromtimestamp(0),
last_modified=datetime.datetime.strptime(dump["last_update"], "%Y-%m-%dT%H:%M:%S.%fZ"),
first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
last_modified=datetime.datetime.fromisoformat(dump["last_update"]),
url_path="",
url=dump["url"],
out_of_date=datetime.datetime.strptime(
dump["flag_date"],
"%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None,
out_of_date=datetime.datetime.fromisoformat(dump["flag_date"]) if dump.get("flag_date") else None,
maintainer=next(iter(dump["maintainers"]), None),
submitter=None,
repository=dump["repo"],
@ -193,6 +194,7 @@ class AURPackage:
provides=dump["provides"],
license=dump["licenses"],
keywords=[],
groups=dump["groups"],
)
@staticmethod
@ -208,9 +210,9 @@ class AURPackage:
"""
identity_mapper: Callable[[Any], Any] = lambda value: value
value_mapper: dict[str, Callable[[Any], Any]] = {
"out_of_date": lambda value: datetime.datetime.utcfromtimestamp(value) if value is not None else None,
"first_submitted": datetime.datetime.utcfromtimestamp,
"last_modified": datetime.datetime.utcfromtimestamp,
"out_of_date": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC) if value is not None else None,
"first_submitted": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC),
"last_modified": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC),
}
result: dict[str, Any] = {}

View File

@ -18,7 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass
from typing import Generic, TypeVar
from typing import Generic, Self, TypeVar
T = TypeVar("T")
@ -35,3 +35,16 @@ class ContextKey(Generic[T]):
"""
key: str
return_type: type[T]
@classmethod
def from_type(cls, return_type: type[T]) -> Self:
"""
construct key from type
Args:
return_type(type[T]): return type used for the specified context key
Returns:
Self: context key with autogenerated
"""
return cls(return_type.__name__, return_type)

View File

@ -0,0 +1,66 @@
#
# Copyright (c) 2021-2024 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass, field, fields
from typing import Any, Self
from ahriman.core.util import dataclass_view, filter_json
@dataclass(frozen=True)
class Dependencies:
"""
package paths dependencies
Attributes:
paths(dict[str, list[str]]): map of the paths used by this package to set of packages in which they were found
"""
paths: dict[str, list[str]] = field(default_factory=dict)
def __post_init__(self) -> None:
"""
remove empty paths
"""
paths = {path: packages for path, packages in self.paths.items() if packages}
object.__setattr__(self, "paths", paths)
@classmethod
def from_json(cls, dump: dict[str, Any]) -> Self:
"""
construct dependencies from the json dump
Args:
dump(dict[str, Any]): json dump body
Returns:
Self: dependencies object
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields))
def view(self) -> dict[str, Any]:
"""
generate json dependencies view
Returns:
dict[str, Any]: json-friendly dictionary
"""
return dataclass_view(self)

View File

@ -0,0 +1,55 @@
#
# Copyright (c) 2021-2024 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass, field
from pathlib import Path
@dataclass(frozen=True, kw_only=True)
class FilesystemPackage:
"""
class representing a simplified model for the package installed to filesystem
Attributes:
package_name(str): package name
dependencies(list[str]): list of package dependencies
directories(list[Path]): list of directories this package contains
files(list[Path]): list of files this package contains
groups(list[str]): list of groups of the package
"""
package_name: str
groups: set[str]
dependencies: set[str]
directories: list[Path] = field(default_factory=list)
files: list[Path] = field(default_factory=list)
@property
def is_valid(self) -> bool:
"""
quick check if this package must be used for the dependencies calculation. It checks that
1) package is not in the base group
Returns:
bool: True in case if this package must be used for the dependencies calculation or False otherwise
"""
return "base" not in self.groups
def __repr__(self):
return f'FilesystemPackage(package_name="{self.package_name}", dependencies={self.dependencies})'

View File

@ -0,0 +1,208 @@
#
# Copyright (c) 2021-2024 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass
from elftools.elf.dynamic import DynamicSection
from elftools.elf.elffile import ELFFile
from pathlib import Path
from typing import IO
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import OfficialSyncdb
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.util import walk
from ahriman.models.dependencies import Dependencies
from ahriman.models.filesystem_package import FilesystemPackage
from ahriman.models.package import Package
@dataclass
class PackageArchive:
"""
helper for package archives
Attributes:
package(Package): package descriptor
root(Path): path to root filesystem
"""
root: Path
package: Package
pacman: Pacman
@staticmethod
def dynamic_needed(binary_path: Path) -> list[str]:
"""
extract dynamic libraries required by the specified file
Args:
binary_path(Path): path to library, file, etc
Returns:
list[str]: libraries which this file linked dynamically. Returns empty set in case if file is not
a binary or no dynamic section has been found
"""
with binary_path.open("rb") as binary_file:
if not PackageArchive.is_elf(binary_file):
return []
elf_file = ELFFile(binary_file) # type: ignore[no-untyped-call]
dynamic_section = next(
(section for section in elf_file.iter_sections() # type: ignore[no-untyped-call]
if isinstance(section, DynamicSection)),
None)
if dynamic_section is None:
return []
return [
tag.needed
for tag in dynamic_section.iter_tags() # type: ignore[no-untyped-call]
if tag.entry.d_tag == "DT_NEEDED"
]
@staticmethod
def is_elf(content: IO[bytes]) -> bool:
"""
check if the content is actually elf file
Args:
content(IO[bytes]): content of the file
Returns:
bool: True in case if file has elf header and False otherwise
"""
expected = b"\x7fELF"
length = len(expected)
magic_bytes = content.read(length)
content.seek(0) # reset reading position
return magic_bytes == expected
def _load_pacman_package(self, path: Path) -> FilesystemPackage:
"""
load pacman package model from path
Args:
path(Path): path to package files database
Returns:
FilesystemPackage: generated pacman package model with empty paths
"""
package_name, *_ = path.parent.name.rsplit("-", 2)
try:
pacman_package = OfficialSyncdb.info(package_name, pacman=self.pacman)
return FilesystemPackage(
package_name=package_name,
groups=set(pacman_package.groups),
dependencies=set(pacman_package.depends),
)
except UnknownPackageError:
return FilesystemPackage(package_name=package_name, groups=set(), dependencies=set())
def depends_on(self) -> Dependencies:
"""
extract packages and paths which are required for this package
Returns:
Dependencies: map of the package name to set of paths used by this package
"""
dependencies, roots = self.depends_on_paths()
installed_packages = self.installed_packages()
dependencies_per_path: dict[Path, list[FilesystemPackage]] = {}
for package_base, package in installed_packages.items():
if package_base in self.package.packages:
continue # skip package itself
required_by = [directory for directory in package.directories if directory in roots]
required_by.extend(library for library in package.files if library.name in dependencies)
for path in required_by:
dependencies_per_path.setdefault(path, []).append(package)
# reduce trees
result = {}
for path, packages in dependencies_per_path.items():
package_names = [package.package_name for package in packages]
result[str(path)] = [
package.package_name
for package in packages
# if there is any package which is dependency of this package, we can skip it here
# also skip packages which didn't pass validation
if not package.dependencies.intersection(package_names) and package.is_valid
]
if str(path) == 'usr/lib/python3.12/site-packages':
print(package_names)
print(packages)
print(result[str(path)])
return Dependencies(result)
def depends_on_paths(self) -> tuple[set[str], set[Path]]:
"""
extract dependencies from installation
Returns:
tuple[set[str], set[Path]]: tuple of dynamically linked libraries and directory paths
"""
dependencies = set()
roots: set[Path] = set()
package_dir = self.root / "build" / self.package.base / "pkg"
for path in filter(lambda p: p.is_file(), walk(package_dir)):
dependencies.update(PackageArchive.dynamic_needed(path))
filesystem_path = Path(*path.relative_to(package_dir).parts[1:])
roots.update(filesystem_path.parents[:-1]) # last element is always . because paths are relative
return dependencies, roots
def installed_packages(self) -> dict[str, FilesystemPackage]:
"""
extract list of the installed packages and their content
Returns:
dict[str, tuple[list[Path], list[Path]]]; map of package name to list of directories and files contained
by this package
"""
result = {}
pacman_local_files = self.root / "var" / "lib" / "pacman" / "local"
for path in filter(lambda fn: fn.name == "files", walk(pacman_local_files)):
package = self._load_pacman_package(path)
is_files = False
for line in path.read_text(encoding="utf8").splitlines():
if not line: # skip empty lines
continue
if line.startswith("%") and line.endswith("%"): # directive started
is_files = line == "%FILES%"
if not is_files: # not a files directive
continue
entry = Path(line)
if line.endswith("/"): # simple check if it is directory
package.directories.append(entry)
else:
package.files.append(entry)
result[package.package_name] = package
return result

View File

@ -19,11 +19,11 @@
#
import shlex
from dataclasses import dataclass
from dataclasses import dataclass, fields
from pathlib import Path
from typing import Any, Self
from typing import Any, Generator, Self
from ahriman.core.util import dataclass_view, unquote
from ahriman.core.util import dataclass_view, filter_json
@dataclass(frozen=True)
@ -81,14 +81,72 @@ class PkgbuildPatch:
Self: package properties
"""
key, *value_parts = variable.split("=", maxsplit=1)
raw_value = next(iter(value_parts), "") # extract raw value
if raw_value.startswith("(") and raw_value.endswith(")"):
value: str | list[str] = shlex.split(raw_value[1:-1]) # arrays for poor
else:
value = unquote(raw_value)
return cls(key, cls.parse(raw_value))
return cls(key, value)
@classmethod
def from_json(cls, dump: dict[str, Any]) -> Self:
"""
construct patch descriptor from the json dump
Args:
dump(dict[str, Any]): json dump body
Returns:
Self: patch object
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields))
@staticmethod
def parse(source: str) -> str | list[str]:
"""
parse string value to the PKGBUILD patch value. This method simply takes string, tries to identify it as array
or just string and return the respective value. Functions should be processed correctly, however, not guaranteed
Args:
source(str): source string to parse
Returns:
str | list[str]: parsed value either string or list of strings
"""
if source.startswith("(") and source.endswith(")"):
return shlex.split(source[1:-1]) # arrays for poor
return PkgbuildPatch.unquote(source)
@staticmethod
def unquote(source: str) -> str:
"""
like :func:`shlex.quote()`, but opposite
Args:
source(str): source string to remove quotes
Returns:
str: string with quotes removed
Raises:
ValueError: if no closing quotation
"""
def generator() -> Generator[str, None, None]:
token = None
for char in source:
if token is not None:
if char == token:
token = None # closed quote
else:
yield char # character inside quotes
elif char in ("'", "\""):
token = char # first quote found
else:
yield char # normal character
if token is not None:
raise ValueError("No closing quotation")
return "".join(generator())
def serialize(self) -> str:
"""

View File

@ -24,6 +24,7 @@ from collections.abc import Generator
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
@ -83,6 +84,17 @@ class RepositoryPaths(LazyLogging):
return Path(self.repository_id.architecture) # legacy tree suffix
return Path(self.repository_id.name) / self.repository_id.architecture
@property
def build_directory(self) -> Path:
"""
same as :attr:`chroot`, but exactly build chroot
Returns:
Path: path to directory in which build process is run
"""
uid, _ = self.owner(self.root)
return self.chroot / f"{self.repository_id.name}-{self.repository_id.architecture}" / getpwuid(uid).pw_name
@property
def cache(self) -> Path:
"""

View File

@ -22,6 +22,7 @@ from ahriman.web.schemas.auth_schema import AuthSchema
from ahriman.web.schemas.build_options_schema import BuildOptionsSchema
from ahriman.web.schemas.changes_schema import ChangesSchema
from ahriman.web.schemas.counters_schema import CountersSchema
from ahriman.web.schemas.dependencies_schema import DependenciesSchema
from ahriman.web.schemas.error_schema import ErrorSchema
from ahriman.web.schemas.file_schema import FileSchema
from ahriman.web.schemas.info_schema import InfoSchema
@ -36,6 +37,7 @@ from ahriman.web.schemas.package_patch_schema import PackagePatchSchema
from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema
from ahriman.web.schemas.package_schema import PackageSchema
from ahriman.web.schemas.package_status_schema import PackageStatusSchema, PackageStatusSimplifiedSchema
from ahriman.web.schemas.package_version_schema import PackageVersionSchema
from ahriman.web.schemas.pagination_schema import PaginationSchema
from ahriman.web.schemas.patch_name_schema import PatchNameSchema
from ahriman.web.schemas.patch_schema import PatchSchema

View File

@ -0,0 +1,31 @@
#
# Copyright (c) 2021-2024 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 marshmallow import Schema, fields
class DependenciesSchema(Schema):
"""
request/response package dependencies schema
"""
paths = fields.Dict(
keys=fields.String(), values=fields.List(fields.String()), required=True, metadata={
"description": "Map of filesystem paths to packages which contain this path",
})

View File

@ -0,0 +1,34 @@
#
# Copyright (c) 2021-2024 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 marshmallow import fields
from ahriman import __version__
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
class PackageVersionSchema(RepositoryIdSchema):
"""
request package name schema
"""
version = fields.String(metadata={
"description": "Package version",
"example": __version__,
})

View File

@ -25,8 +25,8 @@ class PatchSchema(Schema):
request and response patch schema
"""
key = fields.String(required=True, metadata={
"description": "environment variable name",
key = fields.String(metadata={
"description": "environment variable name. Required in case if it is not full diff",
})
value = fields.String(metadata={
"description": "environment variable value",

View File

@ -25,6 +25,7 @@ from typing import TypeVar
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.distributed import WorkersCache
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.sign.gpg import GPG
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
@ -218,12 +219,13 @@ class BaseView(View, CorsViewMixin):
return RepositoryId(architecture, name)
return next(iter(sorted(self.services.keys())))
def service(self, repository_id: RepositoryId | None = None) -> Watcher:
def service(self, repository_id: RepositoryId | None = None, package_base: str | None = None) -> Watcher:
"""
get status watcher instance
Args:
repository_id(RepositoryId | None, optional): repository unique identifier (Default value = None)
package_base(str | None, optional): package base to validate if exists (Default value = None)
Returns:
Watcher: build status watcher instance. If no repository provided, it will return the first one
@ -234,9 +236,11 @@ class BaseView(View, CorsViewMixin):
if repository_id is None:
repository_id = self.repository_id()
try:
return self.services[repository_id]
return self.services[repository_id](package_base)
except KeyError:
raise HTTPNotFound(reason=f"Repository {repository_id.id} is unknown")
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
async def username(self) -> str | None:
"""

View File

@ -19,9 +19,8 @@
#
import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.changes import Changes
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ChangesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
@ -70,10 +69,7 @@ class ChangesView(StatusViewGuard, BaseView):
"""
package_base = self.request.match_info["package"]
try:
changes = self.service().package_changes_get(package_base)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
changes = self.service(package_base=package_base).package_changes_get(package_base)
return json_response(changes.view())
@ -113,7 +109,6 @@ class ChangesView(StatusViewGuard, BaseView):
raise HTTPBadRequest(reason=str(ex))
changes = Changes(last_commit_sha, change)
repository_id = self.repository_id()
self.service(repository_id).database.changes_insert(package_base, changes, repository_id)
self.service().package_changes_update(package_base, changes)
raise HTTPNoContent

View File

@ -0,0 +1,113 @@
#
# Copyright (c) 2021-2024 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 aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.models.dependencies import Dependencies
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, DependenciesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard
class DependenciesView(StatusViewGuard, BaseView):
"""
package dependencies web view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/dependencies"]
@aiohttp_apispec.docs(
tags=["Build"],
summary="Get package dependencies",
description="Retrieve package implicit dependencies",
responses={
200: {"description": "Success response", "schema": DependenciesSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def get(self) -> Response:
"""
get package dependencies
Returns:
Response: 200 with package implicit dependencies on success
Raises:
HTTPNotFound: if package base is unknown
"""
package_base = self.request.match_info["package"]
dependencies = self.service(package_base=package_base).package_dependencies_get(package_base)
return json_response(dependencies.view())
@aiohttp_apispec.docs(
tags=["Build"],
summary="Update package dependencies",
description="Set package implicit dependencies",
responses={
204: {"description": "Success response"},
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(DependenciesSchema)
async def post(self) -> None:
"""
insert new package dependencies
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
package_base = self.request.match_info["package"]
try:
data = await self.request.json()
data["package_base"] = package_base # read from path instead of object
dependencies = Dependencies.from_json(data)
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service(package_base=package_base).package_dependencies_update(package_base, dependencies)
raise HTTPNoContent

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