diff --git a/docs/ahriman.application.handlers.rst b/docs/ahriman.application.handlers.rst
index 4cf98974..342280a6 100644
--- a/docs/ahriman.application.handlers.rst
+++ b/docs/ahriman.application.handlers.rst
@@ -92,6 +92,14 @@ ahriman.application.handlers.patch module
:no-undoc-members:
:show-inheritance:
+ahriman.application.handlers.pkgbuild module
+--------------------------------------------
+
+.. automodule:: ahriman.application.handlers.pkgbuild
+ :members:
+ :no-undoc-members:
+ :show-inheritance:
+
ahriman.application.handlers.rebuild module
-------------------------------------------
diff --git a/docs/ahriman.core.formatters.rst b/docs/ahriman.core.formatters.rst
index 85f7b329..36f23094 100644
--- a/docs/ahriman.core.formatters.rst
+++ b/docs/ahriman.core.formatters.rst
@@ -76,6 +76,14 @@ ahriman.core.formatters.patch\_printer module
:no-undoc-members:
:show-inheritance:
+ahriman.core.formatters.pkgbuild\_printer module
+------------------------------------------------
+
+.. automodule:: ahriman.core.formatters.pkgbuild_printer
+ :members:
+ :no-undoc-members:
+ :show-inheritance:
+
ahriman.core.formatters.printer module
--------------------------------------
diff --git a/src/ahriman/application/handlers/change.py b/src/ahriman/application/handlers/change.py
index 2dcf7e8e..89396ce9 100644
--- a/src/ahriman/application/handlers/change.py
+++ b/src/ahriman/application/handlers/change.py
@@ -54,7 +54,7 @@ class Change(Handler):
case Action.List:
changes = client.package_changes_get(args.package)
ChangesPrinter(changes)(verbose=True, separator="")
- Change.check_status(args.exit_code, not changes.is_empty)
+ Change.check_status(args.exit_code, changes.changes is not None)
case Action.Remove:
client.package_changes_update(args.package, Changes())
diff --git a/src/ahriman/application/handlers/pkgbuild.py b/src/ahriman/application/handlers/pkgbuild.py
new file mode 100644
index 00000000..9f42244f
--- /dev/null
+++ b/src/ahriman/application/handlers/pkgbuild.py
@@ -0,0 +1,101 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import argparse
+
+from dataclasses import replace
+
+from ahriman.application.application import Application
+from ahriman.application.handlers.handler import Handler, SubParserAction
+from ahriman.core.configuration import Configuration
+from ahriman.core.formatters import PkgbuildPrinter
+from ahriman.models.action import Action
+from ahriman.models.repository_id import RepositoryId
+
+
+class Pkgbuild(Handler):
+ """
+ package pkgbuild handler
+ """
+
+ ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
+
+ @classmethod
+ def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
+ report: bool) -> None:
+ """
+ callback for command line
+
+ Args:
+ args(argparse.Namespace): command line args
+ repository_id(RepositoryId): repository unique identifier
+ configuration(Configuration): configuration instance
+ report(bool): force enable or disable reporting
+ """
+ application = Application(repository_id, configuration, report=True)
+ client = application.repository.reporter
+
+ match args.action:
+ case Action.List:
+ changes = client.package_changes_get(args.package)
+ PkgbuildPrinter(changes)(verbose=True, separator="")
+ Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
+ case Action.Remove:
+ changes = client.package_changes_get(args.package)
+ client.package_changes_update(args.package, replace(changes, pkgbuild=None))
+
+ @staticmethod
+ def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
+ """
+ add parser for package pkgbuild subcommand
+
+ Args:
+ root(SubParserAction): subparsers for the commands
+
+ Returns:
+ argparse.ArgumentParser: created argument parser
+ """
+ parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
+ description="retrieve package PKGBUILD stored in database",
+ epilog="This command requests package status from the web interface "
+ "if it is available.")
+ parser.add_argument("package", help="package base")
+ parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
+ action="store_true")
+ parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
+ return parser
+
+ @staticmethod
+ def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
+ """
+ add parser for package pkgbuild remove subcommand
+
+ Args:
+ root(SubParserAction): subparsers for the commands
+
+ Returns:
+ argparse.ArgumentParser: created argument parser
+ """
+ parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
+ description="remove the package PKGBUILD stored remotely")
+ parser.add_argument("package", help="package base")
+ parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
+ return parser
+
+ arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
diff --git a/src/ahriman/core/formatters/__init__.py b/src/ahriman/core/formatters/__init__.py
index 1b4f8bf9..55bf6995 100644
--- a/src/ahriman/core/formatters/__init__.py
+++ b/src/ahriman/core/formatters/__init__.py
@@ -26,6 +26,7 @@ from ahriman.core.formatters.event_stats_printer import EventStatsPrinter
from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
from ahriman.core.formatters.patch_printer import PatchPrinter
+from ahriman.core.formatters.pkgbuild_printer import PkgbuildPrinter
from ahriman.core.formatters.printer import Printer
from ahriman.core.formatters.repository_printer import RepositoryPrinter
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
diff --git a/src/ahriman/core/formatters/changes_printer.py b/src/ahriman/core/formatters/changes_printer.py
index 9fba4fa8..e28da064 100644
--- a/src/ahriman/core/formatters/changes_printer.py
+++ b/src/ahriman/core/formatters/changes_printer.py
@@ -45,7 +45,7 @@ class ChangesPrinter(Printer):
Returns:
list[Property]: list of content properties
"""
- if self.changes.is_empty:
+ if self.changes.changes is None:
return []
return [Property("", self.changes.changes, is_required=True, indent=0)]
@@ -57,6 +57,6 @@ class ChangesPrinter(Printer):
Returns:
str | None: content title if it can be generated and ``None`` otherwise
"""
- if self.changes.is_empty:
+ if self.changes.changes is None:
return None
return self.changes.last_commit_sha
diff --git a/src/ahriman/core/formatters/pkgbuild_printer.py b/src/ahriman/core/formatters/pkgbuild_printer.py
new file mode 100644
index 00000000..d16ffc83
--- /dev/null
+++ b/src/ahriman/core/formatters/pkgbuild_printer.py
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2021-2026 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from ahriman.core.formatters.printer import Printer
+from ahriman.models.changes import Changes
+from ahriman.models.property import Property
+
+
+class PkgbuildPrinter(Printer):
+ """
+ print content of the pkgbuild stored in changes
+
+ Attributes:
+ changes(Changes): package changes
+ """
+
+ def __init__(self, changes: Changes) -> None:
+ """
+ Args:
+ changes(Changes): package changes
+ """
+ Printer.__init__(self)
+ self.changes = changes
+
+ def properties(self) -> list[Property]:
+ """
+ convert content into printable data
+
+ Returns:
+ list[Property]: list of content properties
+ """
+ if self.changes.pkgbuild is None:
+ return []
+ return [Property("", self.changes.pkgbuild, is_required=True, indent=0)]
+
+ # pylint: disable=redundant-returns-doc
+ def title(self) -> str | None:
+ """
+ generate entry title from content
+
+ Returns:
+ str | None: content title if it can be generated and ``None`` otherwise
+ """
+ if self.changes.pkgbuild is None:
+ return None
+ return self.changes.last_commit_sha
diff --git a/src/ahriman/models/changes.py b/src/ahriman/models/changes.py
index 423f8a89..7b08f2e3 100644
--- a/src/ahriman/models/changes.py
+++ b/src/ahriman/models/changes.py
@@ -38,16 +38,6 @@ class Changes:
changes: str | None = None
pkgbuild: str | None = None
- @property
- def is_empty(self) -> bool:
- """
- validate that changes are not empty
-
- Returns:
- bool: ``True`` in case if changes are not set and ``False`` otherwise
- """
- return self.changes is None
-
@classmethod
def from_json(cls, dump: dict[str, Any]) -> Self:
"""
diff --git a/tests/ahriman/application/handlers/test_handler_pkgbuild.py b/tests/ahriman/application/handlers/test_handler_pkgbuild.py
new file mode 100644
index 00000000..d6a2b8bd
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_pkgbuild.py
@@ -0,0 +1,100 @@
+import argparse
+import pytest
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers.pkgbuild import Pkgbuild
+from ahriman.core.configuration import Configuration
+from ahriman.core.database import SQLite
+from ahriman.core.repository import Repository
+from ahriman.models.action import Action
+from ahriman.models.changes import Changes
+
+
+def _default_args(args: argparse.Namespace) -> argparse.Namespace:
+ """
+ default arguments for these test cases
+
+ Args:
+ args(argparse.Namespace): command line arguments fixture
+
+ Returns:
+ argparse.Namespace: generated arguments for these test cases
+ """
+ args.action = Action.List
+ args.exit_code = False
+ args.package = "package"
+ return args
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
+ mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args = _default_args(args)
+ mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
+ application_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
+ return_value=Changes("sha", "change", "pkgbuild content"))
+ check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
+ print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
+
+ _, repository_id = configuration.check_loaded()
+ Pkgbuild.run(args, repository_id, configuration, report=False)
+ application_mock.assert_called_once_with(args.package)
+ check_mock.assert_called_once_with(False, True)
+ print_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator="")
+
+
+def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
+ mocker: MockerFixture) -> None:
+ """
+ must raise ExitCode exception on empty pkgbuild result
+ """
+ args = _default_args(args)
+ args.exit_code = True
+ mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
+ mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=Changes())
+ check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
+
+ _, repository_id = configuration.check_loaded()
+ Pkgbuild.run(args, repository_id, configuration, report=False)
+ check_mock.assert_called_once_with(True, False)
+
+
+def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
+ mocker: MockerFixture) -> None:
+ """
+ must remove package pkgbuild
+ """
+ args = _default_args(args)
+ args.action = Action.Remove
+ mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
+ changes = Changes("sha", "change", "pkgbuild content")
+ mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
+ update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
+
+ _, repository_id = configuration.check_loaded()
+ Pkgbuild.run(args, repository_id, configuration, report=False)
+ update_mock.assert_called_once_with(args.package, Changes("sha", "change", None))
+
+
+def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite,
+ mocker: MockerFixture) -> None:
+ """
+ must create application object with native reporting
+ """
+ args = _default_args(args)
+ mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
+ load_mock = mocker.patch("ahriman.core.repository.Repository.load")
+
+ _, repository_id = configuration.check_loaded()
+ Pkgbuild.run(args, repository_id, configuration, report=False)
+ load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0)
+
+
+def test_disallow_multi_architecture_run() -> None:
+ """
+ must not allow multi architecture run
+ """
+ assert not Pkgbuild.ALLOW_MULTI_ARCHITECTURE_RUN
diff --git a/tests/ahriman/core/formatters/conftest.py b/tests/ahriman/core/formatters/conftest.py
index 4f76a69d..6e993d65 100644
--- a/tests/ahriman/core/formatters/conftest.py
+++ b/tests/ahriman/core/formatters/conftest.py
@@ -2,24 +2,26 @@ import pytest
from pathlib import Path
-from ahriman.core.formatters import \
- AurPrinter, \
- ChangesPrinter, \
- ConfigurationPathsPrinter, \
- ConfigurationPrinter, \
- EventStatsPrinter, \
- PackagePrinter, \
- PackageStatsPrinter, \
- PatchPrinter, \
- RepositoryPrinter, \
- RepositoryStatsPrinter, \
- StatusPrinter, \
- StringPrinter, \
- TreePrinter, \
- UpdatePrinter, \
- UserPrinter, \
- ValidationPrinter, \
+from ahriman.core.formatters import (
+ AurPrinter,
+ ChangesPrinter,
+ ConfigurationPathsPrinter,
+ ConfigurationPrinter,
+ EventStatsPrinter,
+ PackagePrinter,
+ PackageStatsPrinter,
+ PatchPrinter,
+ PkgbuildPrinter,
+ RepositoryPrinter,
+ RepositoryStatsPrinter,
+ StatusPrinter,
+ StringPrinter,
+ TreePrinter,
+ UpdatePrinter,
+ UserPrinter,
+ ValidationPrinter,
VersionPrinter
+)
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus
from ahriman.models.changes import Changes
@@ -55,6 +57,17 @@ def changes_printer() -> ChangesPrinter:
return ChangesPrinter(Changes("sha", "changes"))
+@pytest.fixture
+def pkgbuild_printer() -> PkgbuildPrinter:
+ """
+ fixture for pkgbuild printer
+
+ Returns:
+ PkgbuildPrinter: pkgbuild printer test instance
+ """
+ return PkgbuildPrinter(Changes("sha", "changes", "pkgbuild content"))
+
+
@pytest.fixture
def configuration_paths_printer() -> ConfigurationPathsPrinter:
"""
diff --git a/tests/ahriman/core/formatters/test_pkgbuild_printer.py b/tests/ahriman/core/formatters/test_pkgbuild_printer.py
new file mode 100644
index 00000000..db3a3b5a
--- /dev/null
+++ b/tests/ahriman/core/formatters/test_pkgbuild_printer.py
@@ -0,0 +1,32 @@
+from ahriman.core.formatters import PkgbuildPrinter
+from ahriman.models.changes import Changes
+
+
+def test_properties(pkgbuild_printer: PkgbuildPrinter) -> None:
+ """
+ must return non-empty properties list
+ """
+ assert pkgbuild_printer.properties()
+
+
+def test_properties_empty() -> None:
+ """
+ must return empty properties list if pkgbuild is empty
+ """
+ assert not PkgbuildPrinter(Changes()).properties()
+ assert not PkgbuildPrinter(Changes("sha", "changes")).properties()
+
+
+def test_title(pkgbuild_printer: PkgbuildPrinter) -> None:
+ """
+ must return non-empty title
+ """
+ assert pkgbuild_printer.title()
+
+
+def test_title_empty() -> None:
+ """
+ must return empty title if change is empty
+ """
+ assert not PkgbuildPrinter(Changes()).title()
+ assert not PkgbuildPrinter(Changes("sha")).title()
diff --git a/tests/ahriman/models/test_changes.py b/tests/ahriman/models/test_changes.py
index decfd429..8eb6e919 100644
--- a/tests/ahriman/models/test_changes.py
+++ b/tests/ahriman/models/test_changes.py
@@ -1,17 +1,6 @@
from ahriman.models.changes import Changes
-def test_is_empty() -> None:
- """
- must check if changes are empty
- """
- assert Changes().is_empty
- assert Changes("sha").is_empty
-
- assert not Changes("sha", "change").is_empty
- assert not Changes(None, "change").is_empty # well, ok
-
-
def test_changes_from_json_view() -> None:
"""
must construct same object from json