Compare commits

...

11 Commits

Author SHA1 Message Date
0e7ca45b54 implement local reporter mode 2024-05-07 14:54:13 +03:00
1ec1bd05eb feat: add abillity to check broken dependencies (#122)
* implement elf dynamic linking check

* load local database too in pacman wrapper
2024-05-05 22:03:07 +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
105 changed files with 6297 additions and 2948 deletions

View File

@ -10,9 +10,9 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
# refresh the image # refresh the image
pacman -Syu --noconfirm pacman -Syu --noconfirm
# main dependencies # 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 # 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 # optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then if [[ -z $MINIMAL_INSTALL ]]; then
# VCS support # VCS support
@ -36,6 +36,9 @@ sudo -u nobody -- makepkg --packagelist | grep -v -- -debug- | pacman -U --nocon
# create machine-id which is required by build tools # create machine-id which is required by build tools
systemd-machine-id-setup systemd-machine-id-setup
# remove unused dependencies
pacman -Qdtq | pacman -Rscn --noconfirm -
# initial setup command as root # initial setup command as root
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080") [[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@example.com>" "${WEB_ARGS[@]}" 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=$! WEB_PID=$!
fi fi
# add the first package # 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 # check if package was actually installed
test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")" test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")"
# run package check # run package check

View File

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

View File

@ -32,10 +32,10 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package" COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## install package dependencies ## install package dependencies
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size ## 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 && \ RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-pyelftools python-requests python-srcinfo && \
pacman -Sy --noconfirm --asdeps python-build python-flit python-installer python-wheel && \ 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-requests-unixsocket python-systemd rsync subversion && \ 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 \ 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-aiohttp-jinja2 python-aiohttp-session python-aiohttp-security
## FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container ## FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container

View File

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

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -20,6 +20,14 @@ ahriman.core.alpm.pacman module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.core.alpm.repo module
----------------------------- -----------------------------

View File

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

View File

@ -28,6 +28,14 @@ ahriman.core.database.operations.changes\_operations module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.core.database.operations.logs\_operations module
-------------------------------------------------------- --------------------------------------------------------

View File

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

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. * ``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. * ``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. * ``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. * ``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 ``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. * ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional.
* ``build_command`` - default build command, string, required. * ``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. * ``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. * ``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. * ``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. * ``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

@ -1,13 +1,13 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.13.4 pkgver=2.13.6
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
url="https://github.com/arcan1s/ahriman" url="https://github.com/arcan1s/ahriman"
license=('GPL3') 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') makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support' optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support' 'darcs: -darcs packages support'

View File

@ -17,6 +17,8 @@ mirror = https://geo.mirror.pkgbuild.com/$repo/os/$arch
repositories = core extra multilib repositories = core extra multilib
; Pacman's root directory. In the most cases it must point to the system root. ; Pacman's root directory. In the most cases it must point to the system root.
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 ; 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. ; as additional option for some subcommands). If set to no, databases must be synchronized manually.
use_ahriman_cache = yes use_ahriman_cache = yes
@ -46,8 +48,12 @@ allow_read_only = yes
[build] [build]
; List of additional flags passed to archbuild command. ; List of additional flags passed to archbuild command.
;archbuild_flags = ;archbuild_flags =
; Path to build command
;build_command =
; List of packages to be ignored during automatic updates. ; List of packages to be ignored during automatic updates.
;ignore_packages = ;ignore_packages =
; Include debug packages
;include_debug_packages = yes
; List of additional flags passed to makechrootpkg command. ; List of additional flags passed to makechrootpkg command.
;makechrootpkg_flags = ;makechrootpkg_flags =
; List of additional flags passed to makepkg command. ; List of additional flags passed to makepkg command.

View File

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

View File

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

View File

@ -43,41 +43,10 @@
<script> <script>
const packageAddModal = $("#package-add-modal"); const packageAddModal = $("#package-add-modal");
const packageAddForm = $("#package-add-form"); 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 packageAddInput = $("#package-add-input");
const packageAddRepositoryInput = $("#package-add-repository-input"); const packageAddRepositoryInput = $("#package-add-repository-input");
const packageAddKnownPackagesList = $("#package-add-known-packages-dlist"); 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"); const packageAddVariablesDiv = $("#package-add-variables-div");
@ -156,4 +125,39 @@
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches); 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> </script>

View File

@ -72,26 +72,6 @@
const packageInfoModal = $("#package-info-modal"); const packageInfoModal = $("#package-info-modal");
const packageInfoModalHeader = $("#package-info-modal-header"); const packageInfoModalHeader = $("#package-info-modal-header");
const packageInfo = $("#package-info"); 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 packageInfoLogsInput = $("#package-info-logs-input");
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button"); const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
@ -309,4 +289,27 @@
if (isPackageBaseSet) packageInfoModal.modal("show"); 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> </script>

View File

@ -35,11 +35,6 @@
<script> <script>
const packageRebuildModal = $("#package-rebuild-modal"); const packageRebuildModal = $("#package-rebuild-modal");
const packageRebuildForm = $("#package-rebuild-form"); 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 packageRebuildDependencyInput = $("#package-rebuild-dependency-input");
const packageRebuildRepositoryInput = $("#package-rebuild-repository-input"); const packageRebuildRepositoryInput = $("#package-rebuild-repository-input");
@ -54,4 +49,12 @@
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure); 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> </script>

View File

@ -9,46 +9,8 @@
const packageInfoUpdateButton = $("#package-info-update-button"); const packageInfoUpdateButton = $("#package-info-update-button");
let repository = null; 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"); 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 statusBadge = $("#badge-status");
const versionBadge = $("#badge-version"); const versionBadge = $("#badge-version");
@ -221,8 +183,47 @@
} }
$(() => { $(() => {
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(); statusBadge.popover();
selectRepository(); selectRepository();
}); });
</script> </script>

View File

@ -102,25 +102,6 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
<script> <script>
const table = $("#packages"); 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 pacmanConf = $("#pacman-conf");
const pacmanConfCopyButton = $("#copy-btn"); const pacmanConfCopyButton = $("#copy-btn");
@ -141,6 +122,28 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
function filterListLicenses() { function filterListLicenses() {
return extractDataList(table.bootstrapTable("getData"), "licenses"); 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> </script>
</body> </body>

View File

@ -1,7 +1,7 @@
<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" integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-8hHkOkbWN1TLWwet/jpbJ0zbx3FJDeYJgQ8dX1mRrv/vfCfHCqFSFZYCgaMML3z9" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-4ZTAzTbfB8H7hkWtXbyNDzDvxirmBT7EmURIvfOJ3Foympc+OD9p+bZNNENaJXgW" 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/daterangepicker@3.1.0/daterangepicker.min.js" integrity="sha384-Gn1XZMJEKL3ycoWq97jYAl+FP3vXQYE2ObBgzgcPMKOZdUZdF6ZuyUxbGC2bAnUT" 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" integrity="sha384-1Rz4Kz/y1rSWw+ZsjTcxB684XgofbO8iizY+UFIzCwFeQ+QUyhBNWBMh/STOyomI" crossorigin="anonymous" type="application/javascript"></script>

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_remove_option_strings=('-h' '--help' '-v' '--variable')
_shtab_ahriman_patch_set_add_option_strings=('-h' '--help' '-t' '--track') _shtab_ahriman_patch_set_add_option_strings=('-h' '--help' '-t' '--track')
_shtab_ahriman_repo_backup_option_strings=('-h' '--help') _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_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' '-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_keyring_option_strings=('-h' '--help')
_shtab_ahriman_repo_create_mirrorlist_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_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' '--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_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_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') _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_sync_option_strings=('-h' '--help')
_shtab_ahriman_repo_tree_option_strings=('-h' '--help' '-p' '--partitions') _shtab_ahriman_repo_tree_option_strings=('-h' '--help' '-p' '--partitions')
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help') _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_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' '--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_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_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman') _shtab_ahriman_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___help_nargs=0
_shtab_ahriman_repo_check___changes_nargs=0 _shtab_ahriman_repo_check___changes_nargs=0
_shtab_ahriman_repo_check___no_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__e_nargs=0
_shtab_ahriman_repo_check___exit_code_nargs=0 _shtab_ahriman_repo_check___exit_code_nargs=0
_shtab_ahriman_repo_check___vcs_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___help_nargs=0
_shtab_ahriman_check___changes_nargs=0 _shtab_ahriman_check___changes_nargs=0
_shtab_ahriman_check___no_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__e_nargs=0
_shtab_ahriman_check___exit_code_nargs=0 _shtab_ahriman_check___exit_code_nargs=0
_shtab_ahriman_check___vcs_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___no_aur_nargs=0
_shtab_ahriman_repo_daemon___changes_nargs=0 _shtab_ahriman_repo_daemon___changes_nargs=0
_shtab_ahriman_repo_daemon___no_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___dependencies_nargs=0
_shtab_ahriman_repo_daemon___no_dependencies_nargs=0 _shtab_ahriman_repo_daemon___no_dependencies_nargs=0
_shtab_ahriman_repo_daemon___dry_run_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___no_aur_nargs=0
_shtab_ahriman_daemon___changes_nargs=0 _shtab_ahriman_daemon___changes_nargs=0
_shtab_ahriman_daemon___no_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___dependencies_nargs=0
_shtab_ahriman_daemon___no_dependencies_nargs=0 _shtab_ahriman_daemon___no_dependencies_nargs=0
_shtab_ahriman_daemon___dry_run_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___no_aur_nargs=0
_shtab_ahriman_repo_update___changes_nargs=0 _shtab_ahriman_repo_update___changes_nargs=0
_shtab_ahriman_repo_update___no_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___dependencies_nargs=0
_shtab_ahriman_repo_update___no_dependencies_nargs=0 _shtab_ahriman_repo_update___no_dependencies_nargs=0
_shtab_ahriman_repo_update___dry_run_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___no_aur_nargs=0
_shtab_ahriman_update___changes_nargs=0 _shtab_ahriman_update___changes_nargs=0
_shtab_ahriman_update___no_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___dependencies_nargs=0
_shtab_ahriman_update___no_dependencies_nargs=0 _shtab_ahriman_update___no_dependencies_nargs=0
_shtab_ahriman_update___dry_run_nargs=0 _shtab_ahriman_update___dry_run_nargs=0
@ -584,7 +596,7 @@ _set_new_action() {
current_action_nargs=1 current_action_nargs=1
fi fi
current_action_args_start_index=$(( $word_index + 1 )) current_action_args_start_index=$(( $word_index + 1 - $pos_only ))
current_action_is_positional=$2 current_action_is_positional=$2
} }
@ -611,6 +623,7 @@ _shtab_ahriman() {
local prefix=_shtab_ahriman local prefix=_shtab_ahriman
local word_index=0 local word_index=0
local pos_only=0 # "--" delimeter not encountered yet
_set_parser_defaults _set_parser_defaults
word_index=1 word_index=1
@ -619,26 +632,30 @@ _shtab_ahriman() {
while [ $word_index -ne $COMP_CWORD ]; do while [ $word_index -ne $COMP_CWORD ]; do
local this_word="${COMP_WORDS[$word_index]}" local this_word="${COMP_WORDS[$word_index]}"
if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" ${this_word} "* ]]; then if [[ $pos_only = 1 || " $this_word " != " -- " ]]; then
# valid subcommand: add it to the prefix & reset the current action if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" ${this_word} "* ]]; then
prefix="${prefix}_$(_shtab_replace_nonword $this_word)" # valid subcommand: add it to the prefix & reset the current action
_set_parser_defaults prefix="${prefix}_$(_shtab_replace_nonword $this_word)"
fi _set_parser_defaults
fi
if [[ " ${current_option_strings[@]} " == *" ${this_word} "* ]]; then if [[ " ${current_option_strings[@]} " == *" ${this_word} "* ]]; then
# a new action should be acquired (due to recognised option string or # a new action should be acquired (due to recognised option string or
# no more input expected from current action); # no more input expected from current action);
# the next positional action can fill in here # the next positional action can fill in here
_set_new_action $this_word false _set_new_action $this_word false
fi fi
if [[ "$current_action_nargs" != "*" ]] && \ if [[ "$current_action_nargs" != "*" ]] && \
[[ "$current_action_nargs" != "+" ]] && \ [[ "$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_nargs )); then
$current_action_is_positional && let "completed_positional_actions += 1" $current_action_is_positional && let "completed_positional_actions += 1"
_set_new_action "pos_${completed_positional_actions}" true _set_new_action "pos_${completed_positional_actions}" true
fi
else
pos_only=1 # "--" delimeter encountered
fi fi
let "word_index+=1" let "word_index+=1"
@ -646,7 +663,7 @@ _shtab_ahriman() {
# Generate the completions # Generate the completions
if [[ "${completing_word}" == -* ]]; then if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
# optional argument started: use option strings # optional argument started: use option strings
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") ) COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
else else

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-02\-09" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2024\-05\-05" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
@ -447,7 +447,9 @@ backup repository settings and database
path of the output archive path of the output archive
.SH COMMAND \fI\,'ahriman repo\-check'\/\fR .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 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 \fB\-\-changes\fR, \fB\-\-no\-changes\fR
calculate changes from the latest known commit if available. Only applicable in dry run mode 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 .TP
\fB\-e\fR, \fB\-\-exit\-code\fR \fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty 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 .SH COMMAND \fI\,'ahriman repo\-daemon'\/\fR
usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes]
[\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-\-increment | \-\-no\-increment] [\-\-check\-files | \-\-no\-check\-files] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-partitions | \-\-no\-partitions] [\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual]
[\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y] [\-\-partitions | \-\-no\-partitions] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
start process which periodically will run update process 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 \fB\-\-changes\fR, \fB\-\-no\-changes\fR
calculate changes from the latest known commit if available. Only applicable in dry run mode 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 .TP
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR \fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
process missing package dependencies 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 instead of running all triggers as set by configuration, just process specified ones in order of mention
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR .SH COMMAND \fI\,'ahriman repo\-update'\/\fR
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] [\-\-dependencies | \-\-no\-dependencies] usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files]
[\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment]
[\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
[package ...] [package ...]
check for packages updates and run build process if requested 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 \fB\-\-changes\fR, \fB\-\-no\-changes\fR
calculate changes from the latest known commit if available. Only applicable in dry run mode 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 .TP
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR \fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
process missing package dependencies process missing package dependencies

View File

@ -120,6 +120,7 @@ _shtab_ahriman_aur_search_options=(
_shtab_ahriman_check_options=( _shtab_ahriman_check_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-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:" {--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)]" {-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:" {--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)]" "*"{-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:" {-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:" {--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:" {--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:" {--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]" "--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:" {--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=( _shtab_ahriman_repo_check_options=(
"(- : *)"{-h,--help}"[show this help message and exit]" "(- : *)"{-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:" {--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)]" {-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:" {--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)]" "*"{-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:" {-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:" {--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:" {--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:" {--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]" "--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:" {--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]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:" {--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:" {--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:" {--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]" "--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)]" {-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]" "(- : *)"{-h,--help}"[show this help message and exit]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:" {--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:" {--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:" {--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]" "--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)]" {-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@ -736,4 +742,12 @@ _shtab_ahriman() {
typeset -A opt_args typeset -A opt_args
_shtab_ahriman "$@"
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", "cerberus",
"inflection", "inflection",
"passlib", "passlib",
"pyelftools",
"requests", "requests",
"srcinfo", "srcinfo",
] ]
@ -81,6 +82,7 @@ web = [
"aiohttp_security", "aiohttp_security",
"cryptography", "cryptography",
"requests-unixsocket", # required by unix socket support "requests-unixsocket", # required by unix socket support
"setuptools", # required by aiohttp-apispec
] ]
[tool.flit.sdist] [tool.flit.sdist]

View File

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

View File

@ -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. " parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
"Only applicable in dry run mode", "Only applicable in dry run mode",
action=argparse.BooleanOptionalAction, default=True) 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("-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", parser.add_argument("--vcs", help="fetch actual version of VCS packages",
action=argparse.BooleanOptionalAction, default=True) 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. " parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
"Only applicable in dry run mode", "Only applicable in dry run mode",
action=argparse.BooleanOptionalAction, default=True) 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", parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_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. " parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
"Only applicable in dry run mode", "Only applicable in dry run mode",
action=argparse.BooleanOptionalAction, default=True) 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", parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_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() known_packages: set[str] = set()
# local 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 base in self.repository.packages():
for package, properties in base.packages.items(): for package, properties in base.packages.items():
known_packages.add(package) known_packages.add(package)
known_packages.update(properties.provides) known_packages.update(properties.provides)
# known pacman databases
known_packages.update(self.repository.pacman.packages()) known_packages.update(self.repository.pacman.packages())
return known_packages return known_packages

View File

@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.repository import Repository from ahriman.core.repository import Repository
from ahriman.core.status.client import Client
from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -63,3 +64,13 @@ class ApplicationProperties(LazyLogging):
str: repository architecture str: repository architecture
""" """
return self.repository_id.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

@ -91,10 +91,7 @@ class ApplicationRepository(ApplicationProperties):
packages(Iterable[str]): only sign specified packages packages(Iterable[str]): only sign specified packages
""" """
# copy to prebuilt directory # copy to prebuilt directory
for package in self.repository.packages(): for package in self.repository.packages(packages):
# no one requested this package
if packages and package.base not in packages:
continue
for archive in package.packages.values(): for archive in package.packages.values():
if archive.filepath is None: if archive.filepath is None:
self.logger.warning("filepath is empty for %s", package.base) self.logger.warning("filepath is empty for %s", package.base)
@ -179,7 +176,7 @@ class ApplicationRepository(ApplicationProperties):
return result return result
def updates(self, filter_packages: Iterable[str], *, 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 get list of packages to run update process
@ -189,6 +186,7 @@ class ApplicationRepository(ApplicationProperties):
local(bool): enable or disable checking of local packages for updates local(bool): enable or disable checking of local packages for updates
manual(bool): include or exclude manual updates manual(bool): include or exclude manual updates
vcs(bool): enable or disable checking of VCS packages vcs(bool): enable or disable checking of VCS packages
check_files(bool): check for broken dependencies
Returns: Returns:
list[Package]: list of out-of-dated packages list[Package]: list of out-of-dated packages
@ -201,5 +199,7 @@ class ApplicationRepository(ApplicationProperties):
updates.update({package.base: package for package in self.repository.updates_local(vcs=vcs)}) updates.update({package.base: package for package in self.repository.updates_local(vcs=vcs)})
if manual: if manual:
updates.update({package.base: package for package in self.repository.updates_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())] return [package for _, package in sorted(updates.items())]

View File

@ -55,7 +55,7 @@ class Add(Handler):
if not args.now: if not args.now:
return 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) packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
packagers = Packagers(args.username, {package.base: package.packager for package in packages}) 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import pwd import tarfile
from pathlib import Path from pathlib import Path
from tarfile import TarFile from pwd import getpwuid
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -49,7 +49,7 @@ class Backup(Handler):
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
""" """
backup_paths = Backup.get_paths(configuration) 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: for backup_path in backup_paths:
archive.add(backup_path) archive.add(backup_path)
@ -77,7 +77,7 @@ class Backup(Handler):
# gnupg home with imported keys # gnupg home with imported keys
uid, _ = repository_paths.root_owner uid, _ = repository_paths.root_owner
system_user = pwd.getpwuid(uid) system_user = getpwuid(uid)
gnupg_home = Path(system_user.pw_dir) / ".gnupg" gnupg_home = Path(system_user.pw_dir) / ".gnupg"
if gnupg_home.is_dir(): if gnupg_home.is_dir():
paths.add(gnupg_home) paths.add(gnupg_home)

View File

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

View File

@ -56,7 +56,7 @@ class StatusUpdate(Handler):
if (local := next((package for package in packages if package.base == base), None)) is not None: if (local := next((package for package in packages if package.base == base), None)) is not None:
client.package_add(local, args.status) client.package_add(local, args.status)
else: else:
client.package_update(base, args.status) client.package_set(base, args.status)
case Action.Update: case Action.Update:
# update service status # update service status
client.status_update(args.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 = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start() 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.dry_run: # some check specific actions
if args.changes: # generate changes if requested if args.changes: # generate changes if requested
application.changes(packages) application.changes(packages)

View File

@ -18,24 +18,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import shutil import shutil
import tarfile
from collections.abc import Callable, Generator from collections.abc import Generator, Iterable
from functools import cached_property from functools import cached_property
from pathlib import Path 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 string import Template
from ahriman.core.alpm.pacman_database import PacmanDatabase
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.util import trim_package from ahriman.core.util import trim_package
from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
class Pacman(LazyLogging): class Pacman(LazyLogging):
""" """
alpm wrapper 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, *, def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
@ -48,8 +55,11 @@ class Pacman(LazyLogging):
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
refresh_database(PacmanSynchronization): synchronize local cache to remote refresh_database(PacmanSynchronization): synchronize local cache to remote
""" """
self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle( self.configuration = configuration
repository_id, configuration, refresh_database=refresh_database) self.repository_id = repository_id
self.repository_paths = configuration.repository_paths
self.refresh_database = refresh_database
@cached_property @cached_property
def handle(self) -> Handle: def handle(self) -> Handle:
@ -59,40 +69,39 @@ class Pacman(LazyLogging):
Returns: Returns:
Handle: generated pyalpm handle instance 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, *, def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
refresh_database: PacmanSynchronization) -> Handle:
""" """
create lazy handle function create lazy handle function
Args: Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
refresh_database(PacmanSynchronization): synchronize local cache to remote refresh_database(PacmanSynchronization): synchronize local cache to remote
Returns: Returns:
Handle: fully initialized pacman handle Handle: fully initialized pacman handle
""" """
root = configuration.getpath("alpm", "root") pacman_root = self.configuration.getpath("alpm", "database")
pacman_root = configuration.getpath("alpm", "database") use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
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
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)) handle = Handle(str(root), str(database_path))
for repository in configuration.getlist("alpm", "repositories"):
database = self.database_init(handle, repository, mirror, repository_id.architecture) for repository in self.configuration.getlist("alpm", "repositories"):
self.database_copy(handle, database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache) 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: if use_ahriman_cache and refresh_database:
self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force) self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
return handle return handle
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, paths: RepositoryPaths, *, def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
use_ahriman_cache: bool) -> None:
""" """
copy database from the operating system root to the ahriman local home 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 handle(Handle): pacman handle which will be used for database copying
database(DB): pacman database instance to be copied database(DB): pacman database instance to be copied
pacman_root(Path): operating system pacman root pacman_root(Path): operating system pacman root
paths(RepositoryPaths): repository paths instance
use_ahriman_cache(bool): use local ahriman cache instead of system one use_ahriman_cache(bool): use local ahriman cache instead of system one
""" """
def repository_database(root: Path) -> Path: def repository_database(root: Path) -> Path:
@ -122,30 +130,36 @@ class Pacman(LazyLogging):
return # database for some reason deos not exist return # database for some reason deos not exist
self.logger.info("copy pacman database from operating system root to ahriman's home") self.logger.info("copy pacman database from operating system root to ahriman's home")
shutil.copy(src, dst) 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 create database instance from pacman handler and set its properties
Args: Args:
handle(Handle): pacman handle which will be used for database initializing handle(Handle): pacman handle which will be used for database initializing
repository(str): pacman repository name (e.g. core) repository(str): pacman repository name (e.g. core)
mirror(str): arch linux mirror url
architecture(str): repository architecture architecture(str): repository architecture
Returns: Returns:
DB: loaded pacman database instance DB: loaded pacman database instance
""" """
self.logger.info("loading pacman database %s", repository) 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)
# replace variables in mirror address if repository != self.repository_id.name:
variables = { mirror = self.configuration.get("alpm", "mirror")
"arch": architecture, # replace variables in mirror address
"repo": repository, variables = {
} "arch": architecture,
database.servers = [Template(mirror).safe_substitute(variables)] "repo": repository,
}
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 return database
@ -160,13 +174,44 @@ class Pacman(LazyLogging):
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force) self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
transaction = handle.init_transaction() transaction = handle.init_transaction()
for database in handle.get_syncdbs(): for database in handle.get_syncdbs():
try: PacmanDatabase(database, self.configuration).sync(force=force)
database.update(force)
except PyalpmError:
self.logger.exception("exception during update %s", database.name)
transaction.release() transaction.release()
def package_get(self, package_name: str) -> Generator[Package, None, None]: def files(self, packages: Iterable[str] | None = None) -> dict[str, set[Path]]:
"""
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[Path]]: map of package name to its list of files
"""
packages = packages or []
def extract(tar: tarfile.TarFile) -> Generator[tuple[str, set[Path]], 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 = {Path(filename.decode("utf8").rstrip()) for filename in content.readlines()}
yield package, files
result: dict[str, set[Path]] = {}
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 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) raise UnknownPackageError(package_name)
try: 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: except StopIteration:
raise UnknownPackageError(package_name) from None raise UnknownPackageError(package_name) from None

View File

@ -68,7 +68,7 @@ class Repo(LazyLogging):
path(Path): path to archive to add path(Path): path to archive to add
""" """
check_output( 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), exception=BuildError.from_process(path.name),
cwd=self.paths.repository, cwd=self.paths.repository,
logger=self.logger, logger=self.logger,
@ -78,8 +78,13 @@ class Repo(LazyLogging):
""" """
create empty repository database. It just calls add with empty arguments create empty repository database. It just calls add with empty arguments
""" """
check_output("repo-add", *self.sign_args, str(self.repo_path), # since pacman-6.1.0 repo-add doesn't create empty database in case if no packages supplied
cwd=self.paths.repository, logger=self.logger, user=self.uid) # 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: def remove(self, package: str, filename: Path) -> None:
""" """

View File

@ -69,7 +69,8 @@ class OAuth(Mapping):
Returns: Returns:
str: login control as html code to insert 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 @staticmethod
def get_provider(name: str) -> type[aioauth_client.OAuth2Client]: def get_provider(name: str) -> type[aioauth_client.OAuth2Client]:

View File

@ -38,6 +38,7 @@ class Task(LazyLogging):
archbuild_flags(list[str]): command flags for archbuild command archbuild_flags(list[str]): command flags for archbuild command
architecture(str): repository architecture architecture(str): repository architecture
build_command(str): build command 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 makechrootpkg_flags(list[str]): command flags for makechrootpkg command
makepkg_flags(list[str]): command flags for makepkg command makepkg_flags(list[str]): command flags for makepkg command
package(Package): package definitions package(Package): package definitions
@ -63,6 +64,7 @@ class Task(LazyLogging):
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[]) self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
self.build_command = configuration.get("build", "build_command") 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.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[]) self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
@ -99,9 +101,11 @@ class Task(LazyLogging):
environment=environment, 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( packages = check_output(
"makepkg", "--packagelist", *package_list_command,
exception=BuildError.from_process(self.package.base), exception=BuildError.from_process(self.package.base),
cwd=sources_dir, cwd=sources_dir,
logger=self.logger, logger=self.logger,

View File

@ -89,6 +89,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"path_exists": True, "path_exists": True,
"path_type": "dir", "path_type": "dir",
}, },
"sync_files_database": {
"type": "boolean",
"coerce": "boolean",
"required": True,
},
"use_ahriman_cache": { "use_ahriman_cache": {
"type": "boolean", "type": "boolean",
"coerce": "boolean", "coerce": "boolean",
@ -176,6 +181,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"empty": False, "empty": False,
}, },
}, },
"include_debug_packages": {
"type": "boolean",
"coerce": "boolean",
},
"makepkg_flags": { "makepkg_flags": {
"type": "list", "type": "list",
"coerce": "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.auth_operations import AuthOperations
from ahriman.core.database.operations.build_operations import BuildOperations from ahriman.core.database.operations.build_operations import BuildOperations
from ahriman.core.database.operations.changes_operations import ChangesOperations 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.logs_operations import LogsOperations
from ahriman.core.database.operations.package_operations import PackageOperations from ahriman.core.database.operations.package_operations import PackageOperations
from ahriman.core.database.operations.patch_operations import PatchOperations 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: def changes_insert(self, package_base: str, changes: Changes, repository_id: RepositoryId | None = None) -> None:
""" """
insert packages to build queue insert package changes
Args: Args:
package_base(str): package base to insert package_base(str): package base to insert

View File

@ -0,0 +1,124 @@
#
# 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 pathlib import Path
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) -> list[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) -> list[Dependencies]:
return [
Dependencies(
row["package_base"],
{
Path(path): packages
for path, packages in row["dependencies"].items()
}
)
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, dependencies: Dependencies, repository_id: RepositoryId | None = None) -> None:
"""
insert package dependencies
Args:
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": dependencies.package_base,
"repository": repository_id.id,
"dependencies": {
str(path): packages
for path, packages in dependencies.paths.items()
}
})
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

@ -150,34 +150,6 @@ class PackageOperations(Operations):
""", """,
package_list) 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 @staticmethod
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]: def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
""" """
@ -277,20 +249,18 @@ class PackageOperations(Operations):
return self.with_connection(run, commit=True) 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 update package status
Args: Args:
package(Package): package properties package(Package): package properties
status(BuildStatus): new build status
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None) repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
""" """
repository_id = repository_id or self._repository_id repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None: def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package, repository_id) 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_update_insert_packages(connection, package, repository_id)
self._package_remove_packages(connection, package.base, package.packages.keys(), repository_id) self._package_remove_packages(connection, package.base, package.packages.keys(), repository_id)
@ -336,3 +306,33 @@ class PackageOperations(Operations):
package_base: package.remote package_base: package.remote
for package_base, package in self.with_connection(run).items() for package_base, package in self.with_connection(run).items()
} }
def status_update(self, package_base: str, status: BuildStatus, repository_id: RepositoryId | None = None) -> None:
"""
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)
"""
repository_id = repository_id or self._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 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.configuration import Configuration
from ahriman.core.database.migrations import Migrations from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, LogsOperations, \ from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
PackageOperations, PatchOperations DependenciesOperations, LogsOperations, PackageOperations, PatchOperations
# pylint: disable=too-many-ancestors # 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 wrapper for sqlite3 database
@ -94,3 +101,21 @@ class SQLite(AuthOperations, BuildOperations, ChangesOperations, LogsOperations,
if configuration.getboolean("settings", "apply_migrations", fallback=True): if configuration.getboolean("settings", "apply_migrations", fallback=True):
self.with_connection(lambda connection: Migrations.migrate(connection, configuration)) self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
paths.chown(self.path) 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)

View File

@ -219,6 +219,21 @@ class PackageInfoError(RuntimeError):
RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`") 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): class PathError(ValueError):
""" """
exception which will be raised on path which is not belong to root directory exception which will be raised on path which is not belong to root directory
@ -330,5 +345,5 @@ class UnsafeRunError(RuntimeError):
root_uid(int): ID of the owner of root directory root_uid(int): ID of the owner of root directory
""" """
RuntimeError.__init__(self, f"Current UID {current_uid} differs from root owner {root_uid}. " RuntimeError.__init__(self, f"Current UID {current_uid} differs from root owner {root_uid}. "
f"Note that for the most actions it is unsafe to run application as different user." f"Note that for the most actions it is unsafe to run application as different user."
f" If you are 100% sure that it must be there try --unsafe option") f" If you are 100% sure that it must be there try --unsafe option")

View File

@ -38,7 +38,7 @@ class SyncHttpClient(LazyLogging):
Attributes: Attributes:
auth(tuple[str, str] | None): HTTP basic auth object if set auth(tuple[str, str] | None): HTTP basic auth object if set
suppress_errors(bool): suppress logging of request errors 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, *, 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) password = configuration.get(section, "password", fallback=None)
self.auth = (username, password) if username and password else 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 self.suppress_errors = suppress_errors
@cached_property @cached_property
@ -90,25 +90,27 @@ class SyncHttpClient(LazyLogging):
result: str = exception.response.text if exception.response is not None else "" result: str = exception.response.text if exception.response is not None else ""
return result 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, headers: dict[str, str] | None = None,
params: list[tuple[str, str]] | None = None, params: list[tuple[str, str]] | None = None,
data: Any | None = None, data: Any | None = None,
json: dict[str, Any] | None = None, json: dict[str, Any] | None = None,
files: dict[str, MultipartType] | None = None, files: dict[str, MultipartType] | None = None,
stream: bool | None = None,
session: requests.Session | None = None, session: requests.Session | None = None,
suppress_errors: bool | None = None) -> requests.Response: suppress_errors: bool | None = None) -> requests.Response:
""" """
perform request with specified parameters perform request with specified parameters
Args: 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 url(str): remote url to call
headers(dict[str, str] | None, optional): request headers (Default value = None) headers(dict[str, str] | None, optional): request headers (Default value = None)
params(list[tuple[str, str]] | None, optional): request query parameters (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) data(Any | None, optional): request raw data parameters (Default value = None)
json(dict[str, Any] | None, optional): request json 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) 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) 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 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) set, the instance-wide value will be used (Default value = None)
@ -124,7 +126,7 @@ class SyncHttpClient(LazyLogging):
try: try:
response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json, 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() response.raise_for_status()
return response return response
except requests.HTTPError as ex: except requests.HTTPError as ex:

View File

@ -92,7 +92,7 @@ class HttpLogHandler(logging.Handler):
return # in case if no package base supplied we need just skip log message return # in case if no package base supplied we need just skip log message
try: try:
self.reporter.package_logs(log_record_id, record) self.reporter.package_logs_add(log_record_id, record.created, record.getMessage())
except Exception: except Exception:
if self.suppress_errors: if self.suppress_errors:
return return

View File

@ -106,7 +106,7 @@ class RemoteCall(Report):
"aur": self.update_aur, "aur": self.update_aur,
"local": self.update_local, "local": self.update_local,
"manual": self.update_manual, "manual": self.update_manual,
}) })
response_json = response.json() response_json = response.json()
process_id: str = response_json["process_id"] process_id: str = response_json["process_id"]

View File

@ -29,6 +29,7 @@ from ahriman.core.repository.package_info import PackageInfo
from ahriman.core.util import safe_filename from ahriman.core.util import safe_filename
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_archive import PackageArchive
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.packagers import Packagers from ahriman.models.packagers import Packagers
from ahriman.models.result import Result from ahriman.models.result import Result
@ -77,6 +78,10 @@ class Executor(PackageInfo, Cleaner):
last_commit_sha = build_single(single, Path(dir_name), packager.packager_id) last_commit_sha = build_single(single, Path(dir_name), packager.packager_id)
# clear changes and update commit hash # clear changes and update commit hash
self.reporter.package_changes_set(single.base, Changes(last_commit_sha)) self.reporter.package_changes_set(single.base, Changes(last_commit_sha))
# update dependencies list
dependencies = PackageArchive(self.paths.build_directory, single).depends_on()
self.database.dependencies_insert(dependencies)
# update result set
result.add_updated(single) result.add_updated(single)
except Exception: except Exception:
self.reporter.set_failed(single.base) self.reporter.set_failed(single.base)
@ -98,10 +103,7 @@ class Executor(PackageInfo, Cleaner):
def remove_base(package_base: str) -> None: def remove_base(package_base: str) -> None:
try: try:
self.paths.tree_clear(package_base) # remove all internal files self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base) self.database.package_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) # we only update status page in case of base removal
except Exception: except Exception:
self.logger.exception("could not remove base %s", package_base) self.logger.exception("could not remove base %s", package_base)
@ -117,7 +119,8 @@ class Executor(PackageInfo, Cleaner):
# build package list based on user input # build package list based on user input
result = Result() 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(): for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages): if local.base in packages or all(package in requested for package in local.packages):
packages_to_remove.update({ packages_to_remove.update({
@ -136,7 +139,7 @@ class Executor(PackageInfo, Cleaner):
# check for packages which were requested to remove, but weren't found locally # 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 # 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: if unknown in packages_to_remove or unknown in bases_to_remove:
continue continue
bases_to_remove.append(unknown) bases_to_remove.append(unknown)

View File

@ -86,14 +86,21 @@ class PackageInfo(RepositoryProperties):
return Changes(last_commit_sha, changes) 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 generate list of repository packages
Args:
filter_packages(Iterable[str] | None, optional): filter packages list by specified only
Returns: Returns:
list[Package]: list of packages properties 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]: def packages_built(self) -> list[Path]:
""" """

View File

@ -75,7 +75,7 @@ class RepositoryProperties(LazyLogging):
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database) self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
self.sign = GPG(configuration) self.sign = GPG(configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args) 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) self.triggers = TriggerLoader.load(repository_id, configuration)
@property @property

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from collections.abc import Iterable from collections.abc import Iterable
from pathlib import Path
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.exceptions import UnknownPackageError from ahriman.core.exceptions import UnknownPackageError
@ -55,17 +56,13 @@ class UpdateHandler(PackageInfo, Cleaner):
continue continue
raise UnknownPackageError(package.base) raise UnknownPackageError(package.base)
local_versions = {package.base: package.version for package in self.packages()}
result: list[Package] = [] result: list[Package] = []
for local in self.packages(): for local in self.packages(filter_packages):
with self.in_package_context(local.base, local_versions.get(local.base)): with self.in_package_context(local.base, local.version):
if not local.remote.is_remote: if not local.remote.is_remote:
continue # avoid checking local packages continue # avoid checking local packages
if local.base in self.ignore_list: if local.base in self.ignore_list:
continue continue
if filter_packages and local.base not in filter_packages:
continue
try: try:
remote = load_remote(local) remote = load_remote(local)
@ -82,6 +79,47 @@ class UpdateHandler(PackageInfo, Cleaner):
return result 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[Path, set[str]]:
database_files = self.pacman.files(lookup_packages)
files: dict[Path, 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
dependencies = {dependency.package_base: dependency for dependency in self.database.dependencies_get()}
result: list[Package] = []
for package in self.packages(filter_packages):
if package.base not in dependencies:
continue # skip check if no package dependencies found
required = dependencies[package.base].paths
required_packages = {dep for dep_packages in required.values() for dep in dep_packages}
filesystem = extract_files(required_packages)
for path, packages in required.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]: def updates_local(self, *, vcs: bool) -> list[Package]:
""" """
check local packages for updates check local packages for updates

View File

@ -17,16 +17,18 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
# pylint: disable=too-many-public-methods
from __future__ import annotations from __future__ import annotations
import logging
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -36,22 +38,31 @@ class Client:
""" """
@staticmethod @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 load client from settings
Args: Args:
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance 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: Returns:
Client: client according to current settings 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: if not report:
return Client() return make_local_client()
if not configuration.getboolean("status", "enabled", fallback=True): # global switch if not configuration.getboolean("status", "enabled", fallback=True): # global switch
return Client() return make_local_client()
# new-style section # new-style section
address = configuration.get("status", "address", fallback=None) address = configuration.get("status", "address", fallback=None)
@ -65,7 +76,8 @@ class Client:
if address or legacy_address or (host and port) or socket: if address or legacy_address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
return WebClient(repository_id, configuration) return WebClient(repository_id, configuration)
return Client()
return make_local_client()
def package_add(self, package: Package, status: BuildStatusEnum) -> None: def package_add(self, package: Package, status: BuildStatusEnum) -> None:
""" """
@ -74,7 +86,11 @@ class Client:
Args: Args:
package(Package): package properties package(Package): package properties
status(BuildStatusEnum): current package build status status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError
def package_changes_get(self, package_base: str) -> Changes: def package_changes_get(self, package_base: str) -> Changes:
""" """
@ -85,9 +101,11 @@ class Client:
Returns: Returns:
Changes: package changes if available and empty object otherwise Changes: package changes if available and empty object otherwise
Raises:
NotImplementedError: not implemented method
""" """
del package_base raise NotImplementedError
return Changes()
def package_changes_set(self, package_base: str, changes: Changes) -> None: def package_changes_set(self, package_base: str, changes: Changes) -> None:
""" """
@ -96,7 +114,38 @@ class Client:
Args: Args:
package_base(str): package base to update package_base(str): package base to update
changes(Changes): changes descriptor changes(Changes): changes descriptor
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError
def package_dependencies_get(self, package_base: str | None) -> list[Dependencies]:
"""
get package dependencies
Args:
package_base(str | None): package base to retrieve
Returns:
list[Dependencies]: package implicit dependencies if available
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_dependencies_set(self, dependencies: Dependencies) -> None:
"""
update package dependencies
Args:
dependencies(Dependencies): dependencies descriptor
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]: def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
""" """
@ -107,35 +156,118 @@ class Client:
Returns: Returns:
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found 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 post log record
Args: Args:
log_record_id(LogRecordId): log record id 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
""" """
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_add(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_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_remove(self, package_base: str) -> None: def package_remove(self, package_base: str) -> None:
""" """
remove packages from watcher remove packages from watcher
Args: Args:
package_base(str): package base to remove package_base(str): package base to remove
"""
def package_update(self, package_base: str, status: BuildStatusEnum) -> None: Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_set(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_add()` it does not update package properties
Args: Args:
package_base(str): package base to update package_base(str): package base to update
status(BuildStatusEnum): current package build status status(BuildStatusEnum): current package build status
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError
def set_building(self, package_base: str) -> None: def set_building(self, package_base: str) -> None:
""" """
@ -144,7 +276,7 @@ class Client:
Args: Args:
package_base(str): package base to update package_base(str): package base to update
""" """
return self.package_update(package_base, BuildStatusEnum.Building) return self.package_set(package_base, BuildStatusEnum.Building)
def set_failed(self, package_base: str) -> None: def set_failed(self, package_base: str) -> None:
""" """
@ -153,7 +285,7 @@ class Client:
Args: Args:
package_base(str): package base to update package_base(str): package base to update
""" """
return self.package_update(package_base, BuildStatusEnum.Failed) return self.package_set(package_base, BuildStatusEnum.Failed)
def set_pending(self, package_base: str) -> None: def set_pending(self, package_base: str) -> None:
""" """
@ -162,7 +294,7 @@ class Client:
Args: Args:
package_base(str): package base to update package_base(str): package base to update
""" """
return self.package_update(package_base, BuildStatusEnum.Pending) return self.package_set(package_base, BuildStatusEnum.Pending)
def set_success(self, package: Package) -> None: def set_success(self, package: Package) -> None:
""" """

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 ahriman.core.database import SQLite
from ahriman.core.status.client 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_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
"""
self.database.package_update(package, self.repository_id)
self.database.status_update(package.base, BuildStatus(status), self.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_set(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 | None) -> list[Dependencies]:
"""
get package dependencies
Args:
package_base(str | None): package base to retrieve
Returns:
list[Dependencies]: package implicit dependencies if available
"""
return self.database.dependencies_get(package_base, self.repository_id)
def package_dependencies_set(self, dependencies: Dependencies) -> None:
"""
update package dependencies
Args:
dependencies(Dependencies): dependencies descriptor
"""
self.database.dependencies_insert(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()
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_add(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_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_remove(self, package_base: str) -> None:
"""
remove packages from watcher
Args:
package_base(str): package base to remove
"""
self.database.package_remove(package_base, self.repository_id)
self.package_logs_remove(package_base, None)
def package_set(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike :func:`package_add()` it does not update package properties
Args:
package_base(str): package base to update
status(BuildStatusEnum): current package build status
"""
self.database.status_update(package_base, BuildStatus(status), self.repository_id)

View File

@ -19,15 +19,15 @@
# #
from threading import Lock from threading import Lock
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnknownPackageError from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.status.client import Client
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
class Watcher(LazyLogging): class Watcher(LazyLogging):
@ -35,21 +35,18 @@ class Watcher(LazyLogging):
package status watcher package status watcher
Attributes: Attributes:
database(SQLite): database instance client(Client): reporter instance
repository_id(RepositoryId): repository unique identifier
status(BuildStatus): daemon status status(BuildStatus): daemon status
""" """
def __init__(self, repository_id: RepositoryId, database: SQLite) -> None: def __init__(self, client: Client) -> None:
""" """
default constructor default constructor
Args: Args:
repository_id(RepositoryId): repository unique identifier client(Client): reporter instance
database(SQLite): database instance
""" """
self.repository_id = repository_id self.client = client
self.database = database
self._lock = Lock() self._lock = Lock()
self._known: dict[str, tuple[Package, BuildStatus]] = {} self._known: dict[str, tuple[Package, BuildStatus]] = {}
@ -76,7 +73,7 @@ class Watcher(LazyLogging):
with self._lock: with self._lock:
self._known = { self._known = {
package.base: (package, status) 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]]: def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
@ -91,8 +88,8 @@ class Watcher(LazyLogging):
Returns: Returns:
list[tuple[float, str]]: package logs list[tuple[float, str]]: package logs
""" """
self.package_get(package_base) _ = self.package_get(package_base)
return self.database.logs_get(package_base, limit, offset, self.repository_id) return self.client.package_logs_get(package_base, limit, offset)
def logs_remove(self, package_base: str, version: str | None) -> None: def logs_remove(self, package_base: str, version: str | None) -> None:
""" """
@ -100,24 +97,24 @@ class Watcher(LazyLogging):
Args: Args:
package_base(str): package base package_base(str): package base
version(str): package versio version(str): package version
""" """
self.database.logs_remove(package_base, version, self.repository_id) self.client.package_logs_remove(package_base, version)
def logs_update(self, log_record_id: LogRecordId, created: float, record: str) -> None: def logs_update(self, log_record_id: LogRecordId, created: float, message: str) -> None:
""" """
make new log record into database make new log record into database
Args: Args:
log_record_id(LogRecordId): log record id log_record_id(LogRecordId): log record id
created(float): log created timestamp created(float): log created timestamp
record(str): log record message(str): log message
""" """
if self._last_log_record_id != log_record_id: if self._last_log_record_id != log_record_id:
# there is new log record, so we remove old ones # there is new log record, so we remove old ones
self.logs_remove(log_record_id.package_base, log_record_id.version) self.logs_remove(log_record_id.package_base, log_record_id.version)
self._last_log_record_id = log_record_id self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record, self.repository_id) self.client.package_logs_add(log_record_id, created, message)
def package_changes_get(self, package_base: str) -> Changes: def package_changes_get(self, package_base: str) -> Changes:
""" """
@ -129,8 +126,24 @@ class Watcher(LazyLogging):
Returns: Returns:
Changes: package changes if available Changes: package changes if available
""" """
self.package_get(package_base) _ = self.package_get(package_base)
return self.database.changes_get(package_base, self.repository_id) return self.client.package_changes_get(package_base)
def package_dependencies_get(self, package_base: str) -> Dependencies:
"""
retrieve package dependencies
Args:
package_base(str): package base
Returns:
Dependencies: package dependencies if available
"""
_ = self.package_get(package_base)
try:
return next(iter(self.client.package_dependencies_get(package_base)))
except StopIteration:
return Dependencies(package_base)
def package_get(self, package_base: str) -> tuple[Package, BuildStatus]: def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
""" """
@ -160,7 +173,7 @@ class Watcher(LazyLogging):
""" """
with self._lock: with self._lock:
self._known.pop(package_base, None) self._known.pop(package_base, None)
self.database.package_remove(package_base, self.repository_id) self.client.package_remove(package_base)
self.logs_remove(package_base, None) self.logs_remove(package_base, None)
def package_update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None: def package_update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None:
@ -174,10 +187,9 @@ class Watcher(LazyLogging):
""" """
if package is None: if package is None:
package, _ = self.package_get(package_base) package, _ = self.package_get(package_base)
full_status = BuildStatus(status)
with self._lock: with self._lock:
self._known[package_base] = (package, full_status) self._known[package_base] = (package, BuildStatus(status))
self.database.package_update(package, full_status, self.repository_id) self.client.package_set(package_base, status)
def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]: def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
""" """
@ -192,8 +204,7 @@ class Watcher(LazyLogging):
""" """
# patches are package base based, we don't know (and don't differentiate) to which package does them belong # patches are package base based, we don't know (and don't differentiate) to which package does them belong
# so here we skip checking if package exists or not # so here we skip checking if package exists or not
variables = [variable] if variable is not None else None return self.client.package_patches_get(package_base, variable)
return self.database.patches_list(package_base, variables).get(package_base, [])
def patches_remove(self, package_base: str, variable: str) -> None: def patches_remove(self, package_base: str, variable: str) -> None:
""" """
@ -203,7 +214,7 @@ class Watcher(LazyLogging):
package_base(str): package base package_base(str): package base
variable(str): patch variable name variable(str): patch variable name
""" """
self.database.patches_remove(package_base, [variable]) self.client.package_patches_remove(package_base, variable)
def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None: def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
""" """
@ -213,7 +224,7 @@ class Watcher(LazyLogging):
package_base(str): package base package_base(str): package base
patch(PkgbuildPatch): package patch patch(PkgbuildPatch): package patch
""" """
self.database.patches_insert(package_base, [patch]) self.client.package_patches_add(package_base, patch)
def status_update(self, status: BuildStatusEnum) -> None: def status_update(self, status: BuildStatusEnum) -> None:
""" """

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import contextlib import contextlib
import logging
from urllib.parse import quote_plus as urlencode from urllib.parse import quote_plus as urlencode
@ -27,9 +26,11 @@ from ahriman.core.http import SyncAhrimanClient
from ahriman.core.status.client import Client from ahriman.core.status.client import Client
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -92,10 +93,22 @@ class WebClient(Client, SyncAhrimanClient):
package_base(str): package base package_base(str): package base
Returns: 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" 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, optional): package base (Default value = "")
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: def _logs_url(self, package_base: str) -> str:
""" """
get url for the logs api get url for the logs api
@ -110,7 +123,7 @@ class WebClient(Client, SyncAhrimanClient):
def _package_url(self, package_base: str = "") -> str: def _package_url(self, package_base: str = "") -> str:
""" """
url generator package url generator
Args: Args:
package_base(str, optional): package base to generate url (Default value = "") 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 "" suffix = f"/{urlencode(package_base)}" if package_base else ""
return f"{self.address}/api/v1/packages{suffix}" 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: def _status_url(self) -> str:
""" """
get url for the status api get url for the status api
@ -177,6 +204,37 @@ class WebClient(Client, SyncAhrimanClient):
self.make_request("POST", self._changes_url(package_base), self.make_request("POST", self._changes_url(package_base),
params=self.repository_id.query(), json=changes.view()) params=self.repository_id.query(), json=changes.view())
def package_dependencies_get(self, package_base: str | None) -> list[Dependencies]:
"""
get package dependencies
Args:
package_base(str | None): 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 or ""),
params=self.repository_id.query())
response_json = response.json()
dependencies = response_json if package_base is None else [response_json]
return [Dependencies.from_json(dependencies) for dependencies in dependencies]
return []
def package_dependencies_set(self, dependencies: Dependencies) -> None:
"""
update package dependencies
Args:
dependencies(Dependencies): dependencies descriptor
"""
with contextlib.suppress(Exception):
self.make_request("POST", self._dependencies_url(dependencies.package_base),
params=self.repository_id.query(), json=dependencies.view())
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]: def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
""" """
get package status get package status
@ -199,17 +257,18 @@ class WebClient(Client, SyncAhrimanClient):
return [] 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 post log record
Args: Args:
log_record_id(LogRecordId): log record id 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 = { payload = {
"created": record.created, "created": created,
"message": record.getMessage(), "message": message,
"version": log_record_id.version, "version": log_record_id.version,
} }
@ -219,6 +278,83 @@ class WebClient(Client, SyncAhrimanClient):
self.make_request("POST", self._logs_url(log_record_id.package_base), self.make_request("POST", self._logs_url(log_record_id.package_base),
params=self.repository_id.query(), json=payload, suppress_errors=True) 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_add(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_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_remove(self, package_base: str) -> None: def package_remove(self, package_base: str) -> None:
""" """
remove packages from watcher remove packages from watcher
@ -229,7 +365,7 @@ class WebClient(Client, SyncAhrimanClient):
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
self.make_request("DELETE", self._package_url(package_base), params=self.repository_id.query()) 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_set(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_add()` it does not update package properties

View File

@ -68,4 +68,6 @@ class PackageCreator:
database: SQLite = ctx.get(ContextKey("database", SQLite)) database: SQLite = ctx.get(ContextKey("database", SQLite))
_, repository_id = self.configuration.check_loaded() _, repository_id = self.configuration.check_loaded()
package = Package.from_build(local_path, repository_id.architecture, None) package = Package.from_build(local_path, repository_id.architecture, None)
database.package_update(package, BuildStatus())
database.package_update(package)
database.status_update(package.base, BuildStatus())

View File

@ -183,11 +183,12 @@ post_install() {{
Returns: Returns:
str: package() function for PKGBUILD str: package() function for PKGBUILD
""" """
# somehow autopep thinks that construction inside contains valid python code and reformats it
return f"""{{ 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}.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}-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"}" 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]]: def sources(self) -> dict[str, Callable[[Path], None]]:
""" """

View File

@ -162,7 +162,8 @@ class GitHub(Upload, HttpUpload):
Returns: Returns:
dict[str, Any] | None: GitHub API release object if release found and None otherwise 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: try:
response = self.make_request("GET", url) response = self.make_request("GET", url)
release: dict[str, Any] = response.json() release: dict[str, Any] = response.json()

View File

@ -349,7 +349,7 @@ def pretty_datetime(timestamp: datetime.datetime | float | int | None) -> str:
if timestamp is None: if timestamp is None:
return "" return ""
if isinstance(timestamp, (int, float)): 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") return timestamp.strftime("%Y-%m-%d %H:%M:%S")
@ -505,7 +505,7 @@ def utcnow() -> datetime.datetime:
Returns: Returns:
datetime.datetime: current time in UTC 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]: def walk(directory_path: Path) -> Generator[Path, None, None]:

View File

@ -137,8 +137,8 @@ class AURPackage:
description=package.desc, description=package.desc,
num_votes=0, num_votes=0,
popularity=0.0, popularity=0.0,
first_submitted=datetime.datetime.utcfromtimestamp(0), first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
last_modified=datetime.datetime.utcfromtimestamp(package.builddate), last_modified=datetime.datetime.fromtimestamp(package.builddate, datetime.UTC),
url_path="", url_path="",
url=package.url, url=package.url,
out_of_date=None, out_of_date=None,
@ -175,13 +175,11 @@ class AURPackage:
description=dump["pkgdesc"], description=dump["pkgdesc"],
num_votes=0, num_votes=0,
popularity=0.0, popularity=0.0,
first_submitted=datetime.datetime.utcfromtimestamp(0), first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
last_modified=datetime.datetime.strptime(dump["last_update"], "%Y-%m-%dT%H:%M:%S.%fZ"), last_modified=datetime.datetime.fromisoformat(dump["last_update"]),
url_path="", url_path="",
url=dump["url"], url=dump["url"],
out_of_date=datetime.datetime.strptime( out_of_date=datetime.datetime.fromisoformat(dump["flag_date"]) if dump.get("flag_date") else None,
dump["flag_date"],
"%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None,
maintainer=next(iter(dump["maintainers"]), None), maintainer=next(iter(dump["maintainers"]), None),
submitter=None, submitter=None,
repository=dump["repo"], repository=dump["repo"],
@ -208,9 +206,9 @@ class AURPackage:
""" """
identity_mapper: Callable[[Any], Any] = lambda value: value identity_mapper: Callable[[Any], Any] = lambda value: value
value_mapper: dict[str, Callable[[Any], Any]] = { value_mapper: dict[str, Callable[[Any], Any]] = {
"out_of_date": lambda value: datetime.datetime.utcfromtimestamp(value) if value is not None else None, "out_of_date": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC) if value is not None else None,
"first_submitted": datetime.datetime.utcfromtimestamp, "first_submitted": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC),
"last_modified": datetime.datetime.utcfromtimestamp, "last_modified": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC),
} }
result: dict[str, Any] = {} result: dict[str, Any] = {}

View File

@ -0,0 +1,62 @@
#
# 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 pathlib import Path
from typing import Any, Self
from ahriman.core.util import dataclass_view, filter_json
@dataclass(frozen=True)
class Dependencies:
"""
package paths dependencies
Attributes:
package_base(str): package base
paths(dict[Path, list[str]]): map of the paths used by this package to set of packages in which they were found
"""
package_base: str
paths: dict[Path, list[str]] = field(default_factory=dict)
@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,165 @@
#
# 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.util import walk
from ahriman.models.dependencies import Dependencies
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
@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 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()
result: dict[Path, list[str]] = {}
for package, (directories, files) in self.installed_packages().items():
if package in self.package.packages:
continue # skip package itself
required_by = [directory for directory in directories if directory in roots]
required_by.extend(library for library in files if library.name in dependencies)
for path in required_by:
result.setdefault(path, []).append(package)
return Dependencies(self.package.base, 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, tuple[list[Path], list[Path]]]:
"""
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, *_ = path.parent.name.rsplit("-", 2)
directories, files = [], []
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
directories.append(entry)
else:
files.append(entry)
result[package] = directories, files
return result

View File

@ -19,11 +19,11 @@
# #
import shlex import shlex
from dataclasses import dataclass from dataclasses import dataclass, fields
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Any, Self
from ahriman.core.util import dataclass_view, unquote from ahriman.core.util import dataclass_view, filter_json, unquote
@dataclass(frozen=True) @dataclass(frozen=True)
@ -90,6 +90,21 @@ class PkgbuildPatch:
return cls(key, 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))
def serialize(self) -> str: def serialize(self) -> str:
""" """
serialize key-value pair into PKGBUILD string. List values will be put inside parentheses. All string serialize key-value pair into PKGBUILD string. List values will be put inside parentheses. All string

View File

@ -24,6 +24,7 @@ from collections.abc import Generator
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from pwd import getpwuid
from ahriman.core.exceptions import PathError from ahriman.core.exceptions import PathError
from ahriman.core.log import LazyLogging 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.architecture) # legacy tree suffix
return Path(self.repository_id.name) / self.repository_id.architecture 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 @property
def cache(self) -> Path: 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.build_options_schema import BuildOptionsSchema
from ahriman.web.schemas.changes_schema import ChangesSchema from ahriman.web.schemas.changes_schema import ChangesSchema
from ahriman.web.schemas.counters_schema import CountersSchema 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.error_schema import ErrorSchema
from ahriman.web.schemas.file_schema import FileSchema from ahriman.web.schemas.file_schema import FileSchema
from ahriman.web.schemas.info_schema import InfoSchema 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_properties_schema import PackagePropertiesSchema
from ahriman.web.schemas.package_schema import PackageSchema from ahriman.web.schemas.package_schema import PackageSchema
from ahriman.web.schemas.package_status_schema import PackageStatusSchema, PackageStatusSimplifiedSchema 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.pagination_schema import PaginationSchema
from ahriman.web.schemas.patch_name_schema import PatchNameSchema from ahriman.web.schemas.patch_name_schema import PatchNameSchema
from ahriman.web.schemas.patch_schema import PatchSchema from ahriman.web.schemas.patch_schema import PatchSchema

View File

@ -0,0 +1,35 @@
#
# 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
"""
package_base = fields.String(metadata={
"description": "Package base name",
"example": "ahriman",
})
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(required=True, metadata={
"description": "Package version",
"example": __version__,
})

View File

@ -113,7 +113,6 @@ class ChangesView(StatusViewGuard, BaseView):
raise HTTPBadRequest(reason=str(ex)) raise HTTPBadRequest(reason=str(ex))
changes = Changes(last_commit_sha, change) changes = Changes(last_commit_sha, change)
repository_id = self.repository_id() self.service().client.package_changes_set(package_base, changes)
self.service(repository_id).database.changes_insert(package_base, changes, repository_id)
raise HTTPNoContent raise HTTPNoContent

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/>.
#
import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, DependenciesSchema, ErrorSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard
class DependenciesView(StatusViewGuard, BaseView):
"""
packages dependencies web view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages/dependencies"]
@aiohttp_apispec.docs(
tags=["Packages"],
summary="Get dependencies for all packages",
description="Retrieve implicit dependencies for all known packages",
responses={
200: {"description": "Success response", "schema": DependenciesSchema(many=True)},
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.querystring_schema(RepositoryIdSchema)
async def get(self) -> Response:
"""
get dependencies for all packages
Returns:
Response: 200 with package implicit dependencies on success
"""
dependencies = self.service().client.package_dependencies_get(None)
return json_response([dependency.view() for dependency in dependencies])

View File

@ -0,0 +1,117 @@
#
# 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, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackageError
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 DependencyView(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=["Packages"],
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"]
try:
dependencies = self.service().package_dependencies_get(package_base)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
return json_response(dependencies.view())
@aiohttp_apispec.docs(
tags=["Packages"],
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().client.package_dependencies_set(dependencies)
raise HTTPNoContent

View File

@ -25,8 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.util import pretty_datetime from ahriman.core.util import pretty_datetime
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \ from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, \
VersionedLogSchema RepositoryIdSchema, VersionedLogSchema
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard from ahriman.web.views.status_view_guard import StatusViewGuard
@ -60,7 +60,7 @@ class LogsView(StatusViewGuard, BaseView):
) )
@aiohttp_apispec.cookies_schema(AuthSchema) @aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema) @aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema) @aiohttp_apispec.querystring_schema(PackageVersionSchema)
async def delete(self) -> None: async def delete(self) -> None:
""" """
delete package logs delete package logs
@ -69,7 +69,8 @@ class LogsView(StatusViewGuard, BaseView):
HTTPNoContent: on success response HTTPNoContent: on success response
""" """
package_base = self.request.match_info["package"] package_base = self.request.match_info["package"]
self.service().logs_remove(package_base, None) version = self.request.query.get("version")
self.service().logs_remove(package_base, version)
raise HTTPNoContent raise HTTPNoContent

View File

@ -30,6 +30,7 @@ from ahriman.core.database import SQLite
from ahriman.core.distributed import WorkersCache from ahriman.core.distributed import WorkersCache
from ahriman.core.exceptions import InitializeError from ahriman.core.exceptions import InitializeError
from ahriman.core.spawn import Spawn from ahriman.core.spawn import Spawn
from ahriman.core.status.client import Client
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
from ahriman.web.apispec import setup_apispec from ahriman.web.apispec import setup_apispec
@ -167,7 +168,8 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
watchers: dict[RepositoryId, Watcher] = {} watchers: dict[RepositoryId, Watcher] = {}
for repository_id in repositories: for repository_id in repositories:
application.logger.info("load repository %s", repository_id) application.logger.info("load repository %s", repository_id)
watchers[repository_id] = Watcher(repository_id, database) client = Client.load(repository_id, configuration, database, report=False) # explicitly load local client
watchers[repository_id] = Watcher(client)
application[WatcherKey] = watchers application[WatcherKey] = watchers
# workers cache # workers cache
application[WorkersKey] = WorkersCache(configuration) application[WorkersKey] = WorkersCache(configuration)

View File

@ -131,25 +131,6 @@ def test_sign_skip(application_repository: ApplicationRepository, package_ahrima
application_repository.sign([]) application_repository.sign([])
def test_sign_specific(application_repository: ApplicationRepository, package_ahriman: Package,
package_python_schedule: Package, mocker: MockerFixture) -> None:
"""
must sign only specified packages
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package")
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository")
on_result_mock = mocker.patch(
"ahriman.application.application.application_repository.ApplicationRepository.on_result")
filename = package_ahriman.packages[package_ahriman.base].filepath
application_repository.sign([package_ahriman.base])
sign_package_mock.assert_called_once_with(filename, None)
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
on_result_mock.assert_called_once_with(Result())
def test_unknown_no_aur(application_repository: ApplicationRepository, package_ahriman: Package, def test_unknown_no_aur(application_repository: ApplicationRepository, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
@ -239,11 +220,13 @@ def test_updates_all(application_repository: ApplicationRepository, package_ahri
return_value=[package_ahriman]) return_value=[package_ahriman])
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local") updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=True, vcs=True) application_repository.updates([], aur=True, local=True, manual=True, vcs=True, check_files=True)
updates_aur_mock.assert_called_once_with([], vcs=True) updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True) updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with() updates_manual_mock.assert_called_once_with()
updates_deps_mock.assert_called_once_with([])
def test_updates_disabled(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_updates_disabled(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -253,11 +236,13 @@ def test_updates_disabled(application_repository: ApplicationRepository, mocker:
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur") updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local") updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=False, local=False, manual=False, vcs=True) application_repository.updates([], aur=False, local=False, manual=False, vcs=True, check_files=False)
updates_aur_mock.assert_not_called() updates_aur_mock.assert_not_called()
updates_local_mock.assert_not_called() updates_local_mock.assert_not_called()
updates_manual_mock.assert_not_called() updates_manual_mock.assert_not_called()
updates_deps_mock.assert_not_called()
def test_updates_no_aur(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_updates_no_aur(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -267,11 +252,13 @@ def test_updates_no_aur(application_repository: ApplicationRepository, mocker: M
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur") updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local") updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=False, local=True, manual=True, vcs=True) application_repository.updates([], aur=False, local=True, manual=True, vcs=True, check_files=True)
updates_aur_mock.assert_not_called() updates_aur_mock.assert_not_called()
updates_local_mock.assert_called_once_with(vcs=True) updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with() updates_manual_mock.assert_called_once_with()
updates_deps_mock.assert_called_once_with([])
def test_updates_no_local(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_updates_no_local(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -281,11 +268,13 @@ def test_updates_no_local(application_repository: ApplicationRepository, mocker:
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur") updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local") updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=False, manual=True, vcs=True) application_repository.updates([], aur=True, local=False, manual=True, vcs=True, check_files=True)
updates_aur_mock.assert_called_once_with([], vcs=True) updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_not_called() updates_local_mock.assert_not_called()
updates_manual_mock.assert_called_once_with() updates_manual_mock.assert_called_once_with()
updates_deps_mock.assert_called_once_with([])
def test_updates_no_manual(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_updates_no_manual(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -295,11 +284,13 @@ def test_updates_no_manual(application_repository: ApplicationRepository, mocker
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur") updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local") updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=False, vcs=True) application_repository.updates([], aur=True, local=True, manual=False, vcs=True, check_files=True)
updates_aur_mock.assert_called_once_with([], vcs=True) updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True) updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_not_called() updates_manual_mock.assert_not_called()
updates_deps_mock.assert_called_once_with([])
def test_updates_no_vcs(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_updates_no_vcs(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -309,11 +300,29 @@ def test_updates_no_vcs(application_repository: ApplicationRepository, mocker: M
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur") updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local") updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=True, vcs=False) application_repository.updates([], aur=True, local=True, manual=True, vcs=False, check_files=True)
updates_aur_mock.assert_called_once_with([], vcs=False) updates_aur_mock.assert_called_once_with([], vcs=False)
updates_local_mock.assert_called_once_with(vcs=False) updates_local_mock.assert_called_once_with(vcs=False)
updates_manual_mock.assert_called_once_with() updates_manual_mock.assert_called_once_with()
updates_deps_mock.assert_called_once_with([])
def test_updates_no_check_files(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
"""
must get updates without checking broken links
"""
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=True, vcs=True, check_files=False)
updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with()
updates_deps_mock.assert_not_called()
def test_updates_with_filter(application_repository: ApplicationRepository, mocker: MockerFixture) -> None: def test_updates_with_filter(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
@ -323,8 +332,10 @@ def test_updates_with_filter(application_repository: ApplicationRepository, mock
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur") updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local") updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates(["filter"], aur=True, local=True, manual=True, vcs=True) application_repository.updates(["filter"], aur=True, local=True, manual=True, vcs=True, check_files=True)
updates_aur_mock.assert_called_once_with(["filter"], vcs=True) updates_aur_mock.assert_called_once_with(["filter"], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True) updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with() updates_manual_mock.assert_called_once_with()
updates_deps_mock.assert_called_once_with(["filter"])

View File

@ -89,7 +89,8 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False) Add.run(args, repository_id, configuration, report=False)
updates_mock.assert_called_once_with(args.package, aur=False, local=False, manual=True, vcs=False) updates_mock.assert_called_once_with(args.package,
aur=False, local=False, manual=True, vcs=False, check_files=False)
application_mock.assert_called_once_with([package_ahriman], application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}), Packagers(args.username, {package_ahriman.base: "packager"}),
bump_pkgrel=args.increment) bump_pkgrel=args.increment)

View File

@ -31,7 +31,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
mocker.patch("ahriman.application.handlers.Backup.get_paths", return_value=[Path("path")]) mocker.patch("ahriman.application.handlers.Backup.get_paths", return_value=[Path("path")])
tarfile = MagicMock() tarfile = MagicMock()
add_mock = tarfile.__enter__.return_value = MagicMock() add_mock = tarfile.__enter__.return_value = MagicMock()
mocker.patch("tarfile.TarFile.__new__", return_value=tarfile) mocker.patch("ahriman.application.handlers.backup.tarfile.open", return_value=tarfile)
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Backup.run(args, repository_id, configuration, report=False) Backup.run(args, repository_id, configuration, report=False)
@ -45,7 +45,7 @@ def test_get_paths(configuration: Configuration, mocker: MockerFixture) -> None:
# gnupg export mock # gnupg export mock
mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch.object(RepositoryPaths, "root_owner", (42, 42)) mocker.patch.object(RepositoryPaths, "root_owner", (42, 42))
getpwuid_mock = mocker.patch("pwd.getpwuid", return_value=MagicMock()) getpwuid_mock = mocker.patch("ahriman.application.handlers.backup.getpwuid", return_value=MagicMock())
# well database does not exist so we override it # well database does not exist so we override it
database_mock = mocker.patch("ahriman.core.database.SQLite.database_path", return_value=configuration.path) database_mock = mocker.patch("ahriman.core.database.SQLite.database_path", return_value=configuration.path)

View File

@ -30,7 +30,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args = _default_args(args) args = _default_args(args)
tarfile = MagicMock() tarfile = MagicMock()
extract_mock = tarfile.__enter__.return_value = MagicMock() extract_mock = tarfile.__enter__.return_value = MagicMock()
mocker.patch("tarfile.TarFile.__new__", return_value=tarfile) mocker.patch("ahriman.application.handlers.restore.tarfile.open", return_value=tarfile)
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Restore.run(args, repository_id, configuration, report=False) Restore.run(args, repository_id, configuration, report=False)

View File

@ -25,6 +25,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
""" """
args.aur = True args.aur = True
args.changes = True args.changes = True
args.check_files = True
args.package = [] args.package = []
args.dependencies = True args.dependencies = True
args.dry_run = False args.dry_run = False
@ -61,7 +62,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
application_mock.assert_called_once_with([package_ahriman], application_mock.assert_called_once_with([package_ahriman],
Packagers(args.username, {package_ahriman.base: "packager"}), Packagers(args.username, {package_ahriman.base: "packager"}),
bump_pkgrel=args.increment) bump_pkgrel=args.increment)
updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs) updates_mock.assert_called_once_with(
args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, check_files=args.check_files)
changes_mock.assert_not_called() changes_mock.assert_not_called()
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies) dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)
check_mock.assert_called_once_with(False, False) check_mock.assert_called_once_with(False, False)
@ -122,7 +124,8 @@ def test_run_dry_run(args: argparse.Namespace, package_ahriman: Package, configu
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
Update.run(args, repository_id, configuration, report=False) Update.run(args, repository_id, configuration, report=False)
updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs) updates_mock.assert_called_once_with(
args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, check_files=args.check_files)
application_mock.assert_not_called() application_mock.assert_not_called()
changes_mock.assert_called_once_with([package_ahriman]) changes_mock.assert_called_once_with([package_ahriman])
check_mock.assert_called_once_with(False, pytest.helpers.anyvar(int)) check_mock.assert_called_once_with(False, pytest.helpers.anyvar(int))

View File

@ -133,8 +133,8 @@ def aur_package_ahriman() -> AURPackage:
description="ArcH linux ReposItory MANager", description="ArcH linux ReposItory MANager",
num_votes=0, num_votes=0,
popularity=0, popularity=0,
first_submitted=datetime.datetime.utcfromtimestamp(1618008285), first_submitted=datetime.datetime.fromtimestamp(1618008285, datetime.UTC),
last_modified=datetime.datetime.utcfromtimestamp(1673826351), last_modified=datetime.datetime.fromtimestamp(1673826351, datetime.UTC),
url_path="/cgit/aur.git/snapshot/ahriman.tar.gz", url_path="/cgit/aur.git/snapshot/ahriman.tar.gz",
url="https://github.com/arcan1s/ahriman", url="https://github.com/arcan1s/ahriman",
out_of_date=None, out_of_date=None,
@ -200,8 +200,8 @@ def aur_package_akonadi() -> AURPackage:
description="PIM layer, which provides an asynchronous API to access all kind of PIM data", description="PIM layer, which provides an asynchronous API to access all kind of PIM data",
num_votes=0, num_votes=0,
popularity=0.0, popularity=0.0,
first_submitted=datetime.datetime.utcfromtimestamp(0), first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
last_modified=datetime.datetime.utcfromtimestamp(1646555990.610), last_modified=datetime.datetime.fromtimestamp(1646555990.610, datetime.UTC),
url_path="", url_path="",
url="https://kontact.kde.org", url="https://kontact.kde.org",
out_of_date=None, out_of_date=None,
@ -451,6 +451,7 @@ def passwd() -> MagicMock:
""" """
passwd = MagicMock() passwd = MagicMock()
passwd.pw_dir = "home" passwd.pw_dir = "home"
passwd.pw_name = "ahriman"
return passwd return passwd

View File

@ -0,0 +1,21 @@
import pytest
from ahriman.core.alpm.pacman_database import PacmanDatabase
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
@pytest.fixture
def pacman_database(configuration: Configuration, pacman: Pacman) -> PacmanDatabase:
"""
database sync fixture
Args:
configuration(Configuration): configuration test instance
pacman(Pacman): pacman test instance
Returns:
DatabaseSync: database sync test instance
"""
database = next(iter(pacman.handle.get_syncdbs()))
return PacmanDatabase(database, configuration)

View File

@ -14,7 +14,7 @@ def test_package_info(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURP
must return package info from the database must return package info from the database
""" """
mocker.patch("ahriman.models.aur_package.AURPackage.from_pacman", return_value=aur_package_akonadi) mocker.patch("ahriman.models.aur_package.AURPackage.from_pacman", return_value=aur_package_akonadi)
get_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.package_get", return_value=[aur_package_akonadi]) get_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[aur_package_akonadi])
package = official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman) package = official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman)
get_mock.assert_called_once_with(aur_package_akonadi.name) get_mock.assert_called_once_with(aur_package_akonadi.name)
@ -26,7 +26,7 @@ def test_package_info_no_pacman(official_syncdb: OfficialSyncdb, aur_package_ako
""" """
must raise UnknownPackageError if no pacman set must raise UnknownPackageError if no pacman set
""" """
mocker.patch("ahriman.core.alpm.pacman.Pacman.package_get", return_value=[aur_package_akonadi]) mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[aur_package_akonadi])
with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name): with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name):
official_syncdb.package_info(aur_package_akonadi.name, pacman=None) official_syncdb.package_info(aur_package_akonadi.name, pacman=None)
@ -37,6 +37,6 @@ def test_package_info_not_found(official_syncdb: OfficialSyncdb, aur_package_ako
""" """
must raise UnknownPackage exception in case if no package was found must raise UnknownPackage exception in case if no package was found
""" """
mocker.patch("ahriman.core.alpm.pacman.Pacman.package_get", return_value=[]) mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[])
with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name): with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name):
assert official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman) assert official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman)

View File

@ -1,13 +1,14 @@
import pytest import pytest
import tarfile
from pathlib import Path from pathlib import Path
from pyalpm import error as PyalpmError
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from unittest.mock import MagicMock from unittest.mock import MagicMock, call as MockCall
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -48,7 +49,7 @@ def test_init_with_local_cache_forced(configuration: Configuration, mocker: Mock
sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=True) sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=True)
def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: def test_database_copy(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must copy database from root must copy database from root
""" """
@ -62,13 +63,13 @@ def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown") chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
mkdir_mock.assert_called_once_with(mode=0o755, exist_ok=True) mkdir_mock.assert_called_once_with(mode=0o755, exist_ok=True)
copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path) copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path)
chown_mock.assert_called_once_with(dst_path) chown_mock.assert_called_once_with(dst_path)
def test_database_copy_skip(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: def test_database_copy_skip(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must do not copy database from root if local cache is disabled must do not copy database from root if local cache is disabled
""" """
@ -79,11 +80,11 @@ def test_database_copy_skip(pacman: Pacman, repository_paths: RepositoryPaths, m
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path)) mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path))
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=False) pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=False)
copy_mock.assert_not_called() copy_mock.assert_not_called()
def test_database_copy_no_directory(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: def test_database_copy_no_directory(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must do not copy database if local cache already exists must do not copy database if local cache already exists
""" """
@ -94,11 +95,11 @@ def test_database_copy_no_directory(pacman: Pacman, repository_paths: Repository
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path)) mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path))
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
copy_mock.assert_not_called() copy_mock.assert_not_called()
def test_database_copy_no_root_file(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: def test_database_copy_no_root_file(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must do not copy database if no repository file exists in filesystem must do not copy database if no repository file exists in filesystem
""" """
@ -109,11 +110,11 @@ def test_database_copy_no_root_file(pacman: Pacman, repository_paths: Repository
mocker.patch("pathlib.Path.is_file", return_value=False) mocker.patch("pathlib.Path.is_file", return_value=False)
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
copy_mock.assert_not_called() copy_mock.assert_not_called()
def test_database_copy_database_exist(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: def test_database_copy_database_exist(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must do not copy database if local cache already exists must do not copy database if local cache already exists
""" """
@ -123,7 +124,7 @@ def test_database_copy_database_exist(pacman: Pacman, repository_paths: Reposito
mocker.patch("pathlib.Path.is_file", return_value=True) mocker.patch("pathlib.Path.is_file", return_value=True)
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
pacman.database_copy(pacman.handle, database, Path("root"), repository_paths, use_ahriman_cache=True) pacman.database_copy(pacman.handle, database, Path("root"), use_ahriman_cache=True)
copy_mock.assert_not_called() copy_mock.assert_not_called()
@ -131,71 +132,133 @@ def test_database_init(pacman: Pacman, configuration: Configuration) -> None:
""" """
must init database with settings must init database with settings
""" """
mirror = configuration.get("alpm", "mirror") database = pacman.database_init(pacman.handle, "testing", "x86_64")
database = pacman.database_init(pacman.handle, "testing", mirror, "x86_64")
assert database.servers == ["https://geo.mirror.pkgbuild.com/testing/os/x86_64"] assert database.servers == ["https://geo.mirror.pkgbuild.com/testing/os/x86_64"]
def test_database_sync(pacman: Pacman) -> None: def test_database_init_local(pacman: Pacman, configuration: Configuration) -> None:
"""
must set file protocol for local databases
"""
_, repository_id = configuration.check_loaded()
database = pacman.database_init(MagicMock(), repository_id.name, repository_id.architecture)
assert database.servers == [f"file://{configuration.repository_paths.repository}"]
def test_database_sync(pacman: Pacman, mocker: MockerFixture) -> None:
""" """
must sync databases must sync databases
""" """
handle_mock = MagicMock() handle_mock = MagicMock()
core_mock = MagicMock()
extra_mock = MagicMock()
transaction_mock = MagicMock() transaction_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [core_mock, extra_mock] handle_mock.get_syncdbs.return_value = [1, 2]
handle_mock.init_transaction.return_value = transaction_mock handle_mock.init_transaction.return_value = transaction_mock
pacman.handle = handle_mock
pacman.database_sync(pacman.handle, force=False) sync_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync")
pacman.database_sync(handle_mock, force=False)
handle_mock.init_transaction.assert_called_once_with() handle_mock.init_transaction.assert_called_once_with()
core_mock.update.assert_called_once_with(False) sync_mock.assert_has_calls([MockCall(force=False), MockCall(force=False)])
extra_mock.update.assert_called_once_with(False)
transaction_mock.release.assert_called_once_with() transaction_mock.release.assert_called_once_with()
def test_database_sync_failed(pacman: Pacman) -> None: def test_database_sync_forced(pacman: Pacman, mocker: MockerFixture) -> None:
"""
must sync databases even if there was exception
"""
handle_mock = MagicMock()
core_mock = MagicMock()
core_mock.update.side_effect = PyalpmError()
extra_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [core_mock, extra_mock]
pacman.handle = handle_mock
pacman.database_sync(pacman.handle, force=False)
extra_mock.update.assert_called_once_with(False)
def test_database_sync_forced(pacman: Pacman) -> None:
""" """
must sync databases with force flag must sync databases with force flag
""" """
handle_mock = MagicMock() handle_mock = MagicMock()
core_mock = MagicMock() handle_mock.get_syncdbs.return_value = [1]
handle_mock.get_syncdbs.return_value = [core_mock]
sync_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync")
pacman.database_sync(handle_mock, force=True)
sync_mock.assert_called_once_with(force=True)
def test_files(pacman: Pacman, package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must load files from databases
"""
handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock
tarball = resource_path_root / "core" / "arcanisrepo.files.tar.gz"
mocker.patch("pathlib.Path.is_file", return_value=True)
open_mock = mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=tarfile.open(tarball, "r:gz"))
files = pacman.files()
assert len(files) == 2
assert package_ahriman.base in files
assert Path("usr/bin/ahriman") in files[package_ahriman.base]
open_mock.assert_called_once_with(pytest.helpers.anyvar(int), "r:gz")
def test_files_package(pacman: Pacman, package_ahriman: Package, mocker: MockerFixture,
resource_path_root: Path) -> None:
"""
must load files only for the specified package
"""
handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock pacman.handle = handle_mock
pacman.database_sync(pacman.handle, force=True) tarball = resource_path_root / "core" / "arcanisrepo.files.tar.gz"
handle_mock.init_transaction.assert_called_once_with()
core_mock.update.assert_called_once_with(True) mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=tarfile.open(tarball, "r:gz"))
files = pacman.files(package_ahriman.base)
assert len(files) == 1
assert package_ahriman.base in files
def test_package_get(pacman: Pacman) -> None: def test_files_skip(pacman: Pacman, mocker: MockerFixture) -> None:
"""
must return empty list if no database found
"""
handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock
mocker.patch("pathlib.Path.is_file", return_value=False)
assert not pacman.files()
def test_files_no_content(pacman: Pacman, mocker: MockerFixture) -> None:
"""
must skip package if no content can be loaded
"""
handle_mock = MagicMock()
handle_mock.get_syncdbs.return_value = [MagicMock()]
pacman.handle = handle_mock
tar_mock = MagicMock()
tar_mock.getmembers.return_value = [MagicMock()]
tar_mock.extractfile.return_value = None
open_mock = MagicMock()
open_mock.__enter__.return_value = tar_mock
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=open_mock)
assert not pacman.files()
def test_package(pacman: Pacman) -> None:
""" """
must retrieve package must retrieve package
""" """
assert list(pacman.package_get("pacman")) assert list(pacman.package("pacman"))
def test_package_get_empty(pacman: Pacman) -> None: def test_package_empty(pacman: Pacman) -> None:
""" """
must return empty packages list without exception must return empty packages list without exception
""" """
assert not list(pacman.package_get("some-random-name")) assert not list(pacman.package("some-random-name"))
def test_packages(pacman: Pacman) -> None: def test_packages(pacman: Pacman) -> None:

View File

@ -0,0 +1,203 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock, call as MockCall
from ahriman.core.alpm.pacman_database import PacmanDatabase
from ahriman.core.exceptions import PacmanError
def test_copy(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must copy loca database file
"""
copy_mock = mocker.patch("shutil.copy")
pacman_database.copy(Path("remote"), Path("local"))
copy_mock.assert_called_once_with(Path("remote"), Path("local"))
def test_download(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must download database by remote url
"""
response_obj = MagicMock()
response_obj.headers = {pacman_database.LAST_MODIFIED_HEADER: "Fri, 09 Feb 2024 00:25:55 GMT"}
response_obj.iter_content.return_value = ["chunk".encode("utf8")]
path = Path("local")
url = "url"
file_mock = MagicMock()
request_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request",
return_value=response_obj)
open_mock = mocker.patch("pathlib.Path.open")
open_mock.return_value.__enter__.return_value = file_mock
mtime_mock = mocker.patch("os.utime")
pacman_database.download(url, path)
request_mock.assert_called_once_with("GET", url, stream=True)
open_mock.assert_called_once_with("wb")
file_mock.write.assert_called_once_with("chunk".encode("utf8"))
mtime_mock.assert_called_once_with(path, (1707438355.0, 1707438355.0))
def test_download_no_header(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must raise exception in case if no last modified head found
"""
response_obj = MagicMock()
response_obj.headers = {}
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request", return_value=response_obj)
with pytest.raises(PacmanError):
pacman_database.download("url", Path("local"))
def test_is_outdated(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must correctly check if file is outdated
"""
response_obj = MagicMock()
response_obj.headers = {pacman_database.LAST_MODIFIED_HEADER: "Fri, 09 Feb 2024 00:25:55 GMT"}
stat_mock = MagicMock()
stat_mock.st_mtime = 1707438354
path = Path("local")
url = "url"
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request", return_value=response_obj)
mocker.patch("pathlib.Path.stat", return_value=stat_mock)
assert pacman_database.is_outdated(url, path)
stat_mock.st_mtime += 1
assert not pacman_database.is_outdated(url, path)
stat_mock.st_mtime += 1
assert not pacman_database.is_outdated(url, path)
def test_is_outdated_not_a_file(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must mark as outdated if no file was found
"""
mocker.patch("pathlib.Path.is_file", return_value=False)
assert pacman_database.is_outdated("url", Path("local"))
def test_is_outdated_no_header(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must raise exception in case if no last modified head found during timestamp check
"""
response_obj = MagicMock()
response_obj.headers = {}
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request", return_value=response_obj)
mocker.patch("pathlib.Path.is_file", return_value=True)
with pytest.raises(PacmanError):
pacman_database.is_outdated("url", Path("local"))
def test_sync(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must sync database
"""
sync_db_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_packages")
sync_files_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_files")
pacman_database.sync(force=True)
pacman_database.sync(force=False)
sync_db_mock.assert_has_calls([MockCall(force=True), MockCall(force=False)])
sync_files_mock.assert_not_called()
def test_sync_with_files(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must sync database and files
"""
sync_db_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_packages")
sync_files_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_files")
pacman_database.sync_files_database = True
pacman_database.sync(force=True)
pacman_database.sync(force=False)
sync_db_mock.assert_has_calls([MockCall(force=True), MockCall(force=False)])
sync_files_mock.assert_has_calls([MockCall(force=True), MockCall(force=False)])
def test_sync_exception(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must suppress all exceptions on failure
"""
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_packages", side_effect=Exception())
pacman_database.sync(force=True)
def test_sync_files(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must sync files database
"""
outdated_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.is_outdated", return_value=True)
download_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.download")
pacman_database.sync_files(force=False)
outdated_mock.assert_called_once_with(
"https://geo.mirror.pkgbuild.com/core/os/x86_64/core.files.tar.gz", pytest.helpers.anyvar(int))
download_mock.assert_called_once_with(
"https://geo.mirror.pkgbuild.com/core/os/x86_64/core.files.tar.gz", pytest.helpers.anyvar(int))
def test_sync_files_not_outdated(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must skip files sync if up-to-date
"""
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.is_outdated", return_value=False)
download_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.download")
pacman_database.sync_files(force=False)
download_mock.assert_not_called()
def test_sync_files_force(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must sync up-to-date files if force flag is set
"""
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.is_outdated", return_value=False)
download_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.download")
pacman_database.sync_files(force=True)
download_mock.assert_called_once_with(
"https://geo.mirror.pkgbuild.com/core/os/x86_64/core.files.tar.gz", pytest.helpers.anyvar(int))
def test_sync_files_local(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
"""
must copy local files instead of downloading them
"""
pacman_database.database.servers = ["file:///var"]
copy_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.copy")
pacman_database.sync_files(force=False)
copy_mock.assert_called_once_with(Path("/var/core.files.tar.gz"), pytest.helpers.anyvar(int))
def test_sync_files_unknown_source(pacman_database: PacmanDatabase) -> None:
"""
must raise an exception in case if server scheme is unsupported
"""
pacman_database.database.servers = ["some random string"]
with pytest.raises(PacmanError):
pacman_database.sync_files(force=False)
def test_sync_packages(pacman_database: PacmanDatabase) -> None:
"""
must sync packages by using pyalpm method
"""
pacman_database.database = MagicMock()
pacman_database.sync_packages(force=True)
pacman_database.sync_packages(force=False)
pacman_database.database.update.assert_has_calls([MockCall(True), MockCall(False)])

View File

@ -26,13 +26,28 @@ def test_repo_add(repo: Repo, mocker: MockerFixture) -> None:
def test_repo_init(repo: Repo, mocker: MockerFixture) -> None: def test_repo_init(repo: Repo, mocker: MockerFixture) -> None:
""" """
must call repo-add with empty package list on repo initializing must create empty database files
""" """
check_output_mock = mocker.patch("ahriman.core.alpm.repo.check_output") mocker.patch("pathlib.Path.exists", return_value=False)
touch_mock = mocker.patch("pathlib.Path.touch")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
repo.init() repo.init()
check_output_mock.assert_called_once() # it will be checked later touch_mock.assert_called_once_with(exist_ok=True)
assert check_output_mock.call_args[0][0] == "repo-add" symlink_mock.assert_called_once_with(repo.repo_path)
def test_repo_init_skip(repo: Repo, mocker: MockerFixture) -> None:
"""
must do not create files if database already exists
"""
mocker.patch("pathlib.Path.exists", return_value=True)
touch_mock = mocker.patch("pathlib.Path.touch")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
repo.init()
touch_mock.assert_not_called()
symlink_mock.assert_not_called()
def test_repo_remove(repo: Repo, mocker: MockerFixture) -> None: def test_repo_remove(repo: Repo, mocker: MockerFixture) -> None:

View File

@ -1,5 +1,8 @@
import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
@ -9,9 +12,83 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
""" """
must build package must build package
""" """
local = Path("local")
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output") check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
task_ahriman.build(Path("ahriman"))
check_output_mock.assert_called() task_ahriman.build(local)
check_output_mock.assert_has_calls([
MockCall(
"extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
user=task_ahriman.uid,
environment={},
),
MockCall(
"makepkg", "--packagelist",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
environment={},
),
])
def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must build package with environment variables set
"""
local = Path("local")
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
environment = {"variable": "value"}
task_ahriman.build(local, **environment, empty=None)
check_output_mock.assert_has_calls([
MockCall(
"extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
user=task_ahriman.uid,
environment=environment,
),
MockCall(
"makepkg", "--packagelist",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
environment=environment,
),
])
def test_build_no_debug(task_ahriman: Task, mocker: MockerFixture) -> None:
"""
must filter debug packages from result
"""
local = Path("local")
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
task_ahriman.include_debug_packages = False
task_ahriman.build(local)
check_output_mock.assert_has_calls([
MockCall(
"extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
user=task_ahriman.uid,
environment={},
),
MockCall(
"makepkg", "--packagelist", "OPTIONS=(!debug)",
exception=pytest.helpers.anyvar(int),
cwd=local,
logger=task_ahriman.logger,
environment={},
),
])
def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> None: def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> None:

View File

@ -0,0 +1,8 @@
from ahriman.core.database.migrations.m013_dependencies import steps
def test_migration_dependencies() -> None:
"""
migration must not be empty
"""
assert steps

View File

@ -0,0 +1,61 @@
from pathlib import Path
from ahriman.core.database import SQLite
from ahriman.models.dependencies import Dependencies
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
def test_dependencies_insert_get(database: SQLite, package_ahriman: Package) -> None:
"""
must insert and get dependencies
"""
dependencies = Dependencies(package_ahriman.base, {Path("usr/lib/python3.11/site-packages"): ["python"]})
database.dependencies_insert(dependencies)
assert database.dependencies_get(package_ahriman.base) == [dependencies]
dependencies2 = Dependencies(package_ahriman.base, {Path("usr/lib/python3.11/site-packages"): ["python3"]})
database.dependencies_insert(dependencies2, RepositoryId("i686", database._repository_id.name))
assert database.dependencies_get() == [dependencies]
assert database.dependencies_get(package_ahriman.base) == [dependencies]
assert database.dependencies_get(
package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == [dependencies2]
def test_dependencies_insert_remove(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must remove dependencies for the package
"""
dependencies1 = Dependencies(package_ahriman.base, {Path("usr"): ["python"]})
database.dependencies_insert(dependencies1)
dependencies2 = Dependencies(package_python_schedule.base, {Path("usr"): ["filesystem"]})
database.dependencies_insert(dependencies2)
dependencies3 = Dependencies(package_ahriman.base, {Path("usr"): ["python3"]})
database.dependencies_insert(dependencies3, RepositoryId("i686", database._repository_id.name))
assert database.dependencies_get() == [dependencies1, dependencies2]
database.dependencies_remove(package_ahriman.base)
assert database.dependencies_get(package_ahriman.base) == []
assert database.dependencies_get(package_python_schedule.base) == [dependencies2]
# insert null
database.dependencies_remove(package_ahriman.base, RepositoryId("i686", database._repository_id.name))
assert database.dependencies_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == []
assert database.dependencies_get(package_python_schedule.base) == [dependencies2]
def test_dependencies_insert_remove_full(database: SQLite, package_ahriman: Package,
package_python_schedule: Package) -> None:
"""
must remove all dependencies for the repository
"""
database.dependencies_insert(Dependencies(package_ahriman.base, {Path("usr"): ["python"]}))
database.dependencies_insert(Dependencies(package_python_schedule.base, {Path("usr"): ["filesystem"]}))
database.dependencies_insert(Dependencies(package_ahriman.base, {Path("usr"): ["python3"]}),
RepositoryId("i686", database._repository_id.name))
database.dependencies_remove(None)
assert database.dependencies_get() == []
assert database.dependencies_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name))

View File

@ -33,3 +33,21 @@ def test_init_skip_migration(database: SQLite, configuration: Configuration, moc
database.init(configuration) database.init(configuration)
migrate_schema_mock.assert_not_called() migrate_schema_mock.assert_not_called()
def test_package_clear(database: SQLite, mocker: MockerFixture) -> None:
"""
must clear package data
"""
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_clear")
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove")
changes_mock = mocker.patch("ahriman.core.database.SQLite.changes_remove")
dependencies_mock = mocker.patch("ahriman.core.database.SQLite.dependencies_remove")
database.package_clear("package")
build_queue_mock.assert_called_once_with("package")
patches_mock.assert_called_once_with("package", [])
logs_mock.assert_called_once_with("package", None)
changes_mock.assert_called_once_with("package")
dependencies_mock.assert_called_once_with("package")

View File

@ -82,34 +82,40 @@ def test_make_request(mocker: MockerFixture) -> None:
auth = client.auth = ("username", "password") auth = client.auth = ("username", "password")
assert client.make_request("GET", "url9") is not None assert client.make_request("GET", "url9") is not None
client.auth = None
assert client.make_request("GET", "url10", stream=True) is not None
request_mock.assert_has_calls([ request_mock.assert_has_calls([
MockCall("GET", "url1", params=None, data=None, headers=None, files=None, json=None, MockCall("GET", "url1", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("GET", "url2", params=[("param", "value")], data=None, headers=None, files=None, json=None, MockCall("GET", "url2", params=[("param", "value")], data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("POST", "url3", params=None, data=None, headers=None, files=None, json=None, MockCall("POST", "url3", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("POST", "url4", params=None, data=None, headers=None, files=None, json={"param": "value"}, MockCall("POST", "url4", params=None, data=None, headers=None, files=None, json={"param": "value"},
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("POST", "url5", params=None, data={"param": "value"}, headers=None, files=None, json=None, MockCall("POST", "url5", params=None, data={"param": "value"}, headers=None, files=None, json=None,
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("POST", "url6", params=None, data=None, headers=None, files={"file": "tuple"}, json=None, MockCall("POST", "url6", params=None, data=None, headers=None, files={"file": "tuple"}, json=None,
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("DELETE", "url7", params=None, data=None, headers=None, files=None, json=None, MockCall("DELETE", "url7", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("GET", "url8", params=None, data=None, headers={"user-agent": "ua"}, files=None, json=None, MockCall("GET", "url8", params=None, data=None, headers={"user-agent": "ua"}, files=None, json=None,
auth=None, timeout=client.timeout), stream=None, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
MockCall("GET", "url9", params=None, data=None, headers=None, files=None, json=None, MockCall("GET", "url9", params=None, data=None, headers=None, files=None, json=None,
auth=auth, timeout=client.timeout), stream=None, auth=auth, timeout=client.timeout),
MockCall().raise_for_status(),
MockCall("GET", "url10", params=None, data=None, headers=None, files=None, json=None,
stream=True, auth=None, timeout=client.timeout),
MockCall().raise_for_status(), MockCall().raise_for_status(),
]) ])
@ -151,4 +157,4 @@ def test_make_request_session() -> None:
client.make_request("GET", "url", session=session_mock) client.make_request("GET", "url", session=session_mock)
session_mock.request.assert_called_once_with( session_mock.request.assert_called_once_with(
"GET", "url", params=None, data=None, headers=None, files=None, json=None, "GET", "url", params=None, data=None, headers=None, files=None, json=None,
auth=None, timeout=client.timeout) stream=None, auth=None, timeout=client.timeout)

View File

@ -85,7 +85,7 @@ def test_remote_update(remote_call: RemoteCall, mocker: MockerFixture) -> None:
"aur": False, "aur": False,
"local": False, "local": False,
"manual": True, "manual": True,
}) })
def test_remote_wait(remote_call: RemoteCall, mocker: MockerFixture) -> None: def test_remote_wait(remote_call: RemoteCall, mocker: MockerFixture) -> None:

View File

@ -2,28 +2,37 @@ import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import call as MockCall from unittest.mock import call as MockCall
from ahriman.core.repository.executor import Executor from ahriman.core.repository.executor import Executor
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.packagers import Packagers from ahriman.models.packagers import Packagers
from ahriman.models.user import User from ahriman.models.user import User
def test_process_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any, mocker: MockerFixture) -> None:
""" """
must run build process must run build process
""" """
dependencies = Dependencies(package_ahriman.base)
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)]) mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init", return_value="sha") init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init", return_value="sha")
move_mock = mocker.patch("shutil.move") move_mock = mocker.patch("shutil.move")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
commit_sha_mock = mocker.patch("ahriman.core.status.client.Client.package_changes_set") commit_sha_mock = mocker.patch("ahriman.core.status.client.Client.package_changes_set")
depends_on_mock = mocker.patch("ahriman.models.package_archive.PackageArchive.depends_on",
return_value=dependencies)
dependencies_mock = mocker.patch("ahriman.core.database.SQLite.dependencies_insert")
executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=False) executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=False)
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None) init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
depends_on_mock.assert_called_once_with()
dependencies_mock.assert_called_once_with(dependencies)
# must move files (once) # must move files (once)
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base) move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
# must update status # must update status
@ -70,10 +79,7 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman]) mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear") tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear")
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove") repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_clear") database_mock = mocker.patch("ahriman.core.database.SQLite.package_clear")
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove")
commit_sha_mock = mocker.patch("ahriman.core.database.SQLite.changes_remove")
status_client_mock = mocker.patch("ahriman.core.status.client.Client.package_remove") status_client_mock = mocker.patch("ahriman.core.status.client.Client.package_remove")
executor.process_remove([package_ahriman.base]) executor.process_remove([package_ahriman.base])
@ -82,11 +88,30 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath) package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath)
# must update status and remove package files # must update status and remove package files
tree_clear_mock.assert_called_once_with(package_ahriman.base) tree_clear_mock.assert_called_once_with(package_ahriman.base)
build_queue_mock.assert_called_once_with(package_ahriman.base) database_mock.assert_called_once_with(package_ahriman.base)
patches_mock.assert_called_once_with(package_ahriman.base, [])
logs_mock.assert_called_once_with(package_ahriman.base, None)
status_client_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base)
commit_sha_mock.assert_called_once_with(package_ahriman.base)
def test_process_remove_with_debug(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run remove debug packages too
"""
package_ahriman.packages = {
package_ahriman.base: package_ahriman.packages[package_ahriman.base],
f"{package_ahriman.base}-debug": package_ahriman.packages[package_ahriman.base],
}
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear")
mocker.patch("ahriman.core.database.SQLite.package_clear")
mocker.patch("ahriman.core.status.client.Client.package_remove")
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
executor.process_remove([package_ahriman.base])
# must remove via alpm wrapper
repo_remove_mock.assert_has_calls([
MockCall(package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath),
MockCall(f"{package_ahriman.base}-debug", package_ahriman.packages[package_ahriman.base].filepath),
])
def test_process_remove_base_multiple(executor: Executor, package_python_schedule: Package, def test_process_remove_base_multiple(executor: Executor, package_python_schedule: Package,

View File

@ -96,17 +96,30 @@ def test_package_changes_skip(package_info: PackageInfo, package_ahriman: Packag
changes_mock.assert_not_called() changes_mock.assert_not_called()
def test_packages(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None: def test_packages(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
""" """
must return repository packages must return repository packages
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman]) mocker.patch("pathlib.Path.iterdir")
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives") load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
package_info.packages() return_value=[package_ahriman, package_python_schedule])
assert package_info.packages() == [package_ahriman, package_python_schedule]
# it uses filter object, so we cannot verify argument list =/ # it uses filter object, so we cannot verify argument list =/
load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_packages_filter(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must filter result by bases
"""
mocker.patch("pathlib.Path.iterdir")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_ahriman, package_python_schedule])
assert package_info.packages([package_ahriman.base]) == [package_ahriman]
def test_packages_built(package_info: PackageInfo, mocker: MockerFixture) -> None: def test_packages_built(package_info: PackageInfo, mocker: MockerFixture) -> None:
""" """
must return build packages must return build packages

View File

@ -6,6 +6,7 @@ from typing import Any
from ahriman.core.exceptions import UnknownPackageError from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.repository.update_handler import UpdateHandler from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.models.dependencies import Dependencies
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
@ -16,12 +17,14 @@ def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package,
""" """
must provide updates with status updates must provide updates with status updates
""" """
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) packages_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman)
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
assert update_handler.updates_aur([], vcs=True) == [package_ahriman] assert update_handler.updates_aur([], vcs=True) == [package_ahriman]
packages_mock.assert_called_once_with([])
status_client_mock.assert_called_once_with(package_ahriman.base) status_client_mock.assert_called_once_with(package_ahriman.base)
package_is_outdated_mock.assert_called_once_with( package_is_outdated_mock.assert_called_once_with(
package_ahriman, update_handler.paths, package_ahriman, update_handler.paths,
@ -70,17 +73,17 @@ def test_updates_aur_local(update_handler: UpdateHandler, package_ahriman: Packa
package_load_mock.assert_not_called() package_load_mock.assert_not_called()
def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Package, package_python_schedule: Package, def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None:
mocker: MockerFixture) -> None:
""" """
must provide updates only for filtered packages must provide updates only for filtered packages
""" """
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", packages_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
return_value=[package_ahriman, package_python_schedule]) return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman) package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman)
assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman] assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman]
packages_mock.assert_called_once_with([package_ahriman.base])
package_load_mock.assert_called_once_with(package_ahriman.base, None) package_load_mock.assert_called_once_with(package_ahriman.base, None)
@ -131,7 +134,7 @@ def test_updates_aur_load_by_package(update_handler: UpdateHandler, package_pyth
assert update_handler.updates_aur([], vcs=True) == [package_python_schedule] assert update_handler.updates_aur([], vcs=True) == [package_python_schedule]
def test_updates_load_by_package_aur_failed(update_handler: UpdateHandler, package_ahriman: Package, def test_updates_aur_load_by_package_failed(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must update status via client for failed load must update status via client for failed load
@ -143,6 +146,56 @@ def test_updates_load_by_package_aur_failed(update_handler: UpdateHandler, packa
update_handler.updates_aur([], vcs=True) update_handler.updates_aur([], vcs=True)
def test_updates_dependencies(update_handler: UpdateHandler, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must define updates with broken dependencies
"""
packages_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
return_value=[package_ahriman, package_python_schedule])
dependencies = [
Dependencies(package_ahriman.base, {Path("usr/lib/python3.11/site-packages"): ["python"]}),
Dependencies(package_python_schedule.base, {Path("usr/lib/python3.12/site-packages"): ["python"]}),
]
mocker.patch("ahriman.core.database.SQLite.dependencies_get", return_value=dependencies)
mocker.patch("ahriman.core.alpm.pacman.Pacman.files",
return_value={"python": {Path("usr/lib/python3.12/site-packages")}})
assert update_handler.updates_dependencies(["filter"]) == [package_ahriman]
packages_mock.assert_called_once_with(["filter"])
def test_updates_dependencies_skip_unknown(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must skip unknown package dependencies
"""
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
mocker.patch("ahriman.core.database.SQLite.dependencies_get", return_value=[])
mocker.patch("ahriman.core.alpm.pacman.Pacman.files",
return_value={"python": {Path("usr/lib/python3.12/site-packages")}})
assert update_handler.updates_dependencies(["filter"]) == []
def test_updates_dependencies_partial(update_handler: UpdateHandler, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must skip broken dependencies update if at least one package provides file
"""
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
dependencies = [
Dependencies(package_ahriman.base, {Path("usr"): ["filesystem", "python"]}),
]
mocker.patch("ahriman.core.database.SQLite.dependencies_get", return_value=dependencies)
mocker.patch("ahriman.core.alpm.pacman.Pacman.files", return_value={
"filesystem": {Path("usr")},
"python": {Path("usr")},
})
assert update_handler.updates_dependencies(["filter"]) == []
def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must check for updates for locally stored packages must check for updates for locally stored packages

View File

@ -6,7 +6,6 @@ from pytest_mock import MockerFixture
from unittest.mock import MagicMock, call as MockCall from unittest.mock import MagicMock, call as MockCall
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
from ahriman.core.util import utcnow
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -38,7 +37,7 @@ def test_pkgver(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) ->
must implement default version as current date must implement default version as current date
""" """
mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.utcnow", return_value=datetime.datetime(2002, 3, 11)) mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.utcnow", return_value=datetime.datetime(2002, 3, 11))
assert pkgbuild_generator.pkgver == utcnow().strftime("20020311") assert pkgbuild_generator.pkgver == "20020311"
def test_url(pkgbuild_generator: PkgbuildGenerator) -> None: def test_url(pkgbuild_generator: PkgbuildGenerator) -> None:

View File

@ -480,6 +480,7 @@ def test_walk(resource_path_root: Path) -> None:
""" """
expected = sorted([ expected = sorted([
resource_path_root / "core" / "ahriman.ini", resource_path_root / "core" / "ahriman.ini",
resource_path_root / "core" / "arcanisrepo.files.tar.gz",
resource_path_root / "core" / "logging.ini", resource_path_root / "core" / "logging.ini",
resource_path_root / "models" / "aur_error", resource_path_root / "models" / "aur_error",
resource_path_root / "models" / "big_file_checksum", resource_path_root / "models" / "big_file_checksum",
@ -487,6 +488,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "models" / "official_error", resource_path_root / "models" / "official_error",
resource_path_root / "models" / "package_ahriman_aur", resource_path_root / "models" / "package_ahriman_aur",
resource_path_root / "models" / "package_akonadi_aur", resource_path_root / "models" / "package_akonadi_aur",
resource_path_root / "models" / "package_ahriman_files",
resource_path_root / "models" / "package_ahriman_srcinfo", resource_path_root / "models" / "package_ahriman_srcinfo",
resource_path_root / "models" / "package_gcc10_srcinfo", resource_path_root / "models" / "package_gcc10_srcinfo",
resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo", resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo",

View File

@ -1,7 +1,9 @@
import datetime import datetime
import pytest import pytest
from typing import Any
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
from pytest_mock import MockerFixture
from ahriman import __version__ from ahriman import __version__
from ahriman.core.alpm.remote import AUR from ahriman.core.alpm.remote import AUR
@ -10,9 +12,11 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.counters import Counters from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_archive import PackageArchive
from ahriman.models.package_description import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths
@pytest.fixture @pytest.fixture
@ -60,6 +64,25 @@ def internal_status(counters: Counters) -> InternalStatus:
repository="aur-clone") repository="aur-clone")
@pytest.fixture
def package_archive_ahriman(package_ahriman: Package, repository_paths: RepositoryPaths,
passwd: Any, mocker: MockerFixture) -> PackageArchive:
"""
package archive fixture
Args:
package_ahriman(Package): package test instance
repository_paths(RepositoryPaths): repository paths test instance
passwd(Any): passwd structure test instance
mocker(MockerFixture): mocker object
Returns:
PackageArchive: package archive test instance
"""
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
return PackageArchive(repository_paths.build_directory, package_ahriman)
@pytest.fixture @pytest.fixture
def package_tpacpi_bat_git() -> Package: def package_tpacpi_bat_git() -> Package:
""" """

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