Compare commits

...

12 Commits

64 changed files with 881 additions and 391 deletions

View File

@ -1,6 +1,6 @@
# ahriman configuration # ahriman configuration
Some groups can be specified for each architecture separately. E.g. if there are `build` and `build_x86_64` groups it will use the `build_x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). Some groups can be specified for each architecture separately. E.g. if there are `build` and `build_x86_64` groups it will use the option from `build_x86_64` for the `x86_64` architecture and `build` for any other (architecture specific group has higher priority). In case if both groups are presented, architecture specific options will be merged into global ones overriding them.
## `settings` group ## `settings` group
@ -68,12 +68,14 @@ Remote synchronization settings.
Group name must refer to architecture, e.g. it should be `rsync_x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`. Group name must refer to architecture, e.g. it should be `rsync_x86_64` for x86_64 architecture. Requires `rsync` package to be installed. Do not forget to configure ssh for user `ahriman`.
* `command` - rsync command to run, space separated list of string, required.
* `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required. * `remote` - remote server to rsync (e.g. `1.2.3.4:5678:path/to/sync`), string, required.
### `s3_*` groups ### `s3_*` groups
Group name must refer to architecture, e.g. it should be `s3_x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`. Group name must refer to architecture, e.g. it should be `s3_x86_64` for x86_64 architecture. Requires `aws-cli` package to be installed. Do not forget to configure it for user `ahriman`.
* `command` - s3 command to run, space separated list of string, required.
* `bucket` - bucket name (e.g. `s3://bucket/path`), string, required. * `bucket` - bucket name (e.g. `s3://bucket/path`), string, required.
## `web_*` groups ## `web_*` groups

View File

@ -24,7 +24,7 @@ archlinux: archive
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$$(sha512sum $(PROJECT)-$(VERSION)-src.tar.xz | awk '{print $$1}')'/" package/archlinux/PKGBUILD sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$$(sha512sum $(PROJECT)-$(VERSION)-src.tar.xz | awk '{print $$1}')'/" package/archlinux/PKGBUILD
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
check: check: clean
cd src && mypy --implicit-reexport --strict -p "$(PROJECT)" cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
find "src/$(PROJECT)" tests -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} + find "src/$(PROJECT)" tests -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} +
cd src && pylint --rcfile=../.pylintrc "$(PROJECT)" cd src && pylint --rcfile=../.pylintrc "$(PROJECT)"
@ -43,7 +43,7 @@ push: archlinux
git tag "$(VERSION)" git tag "$(VERSION)"
git push --tags git push --tags
tests: tests: clean
python setup.py test python setup.py test
version: version:

View File

@ -1,5 +1,7 @@
# ArcHlinux ReposItory MANager # ArcHlinux ReposItory MANager
![build status](https://github.com/arcan1s/ahriman/actions/workflows/python-app.yml/badge.svg)
Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts). Wrapper for managing custom repository inspired by [repo-scripts](https://github.com/arcan1s/repo-scripts).
## Features ## Features
@ -64,7 +66,7 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* Add packages by using `ahriman add {package}` command: * Add packages by using `ahriman add {package}` command:
```shell ```shell
sudo -u ahriman ahriman -a x86_64 add yay sudo -u ahriman ahriman -a x86_64 add yay --now
``` ```
Note that initial service configuration can be done by running `ahriman setup` with specific arguments. Note that initial service configuration can be done by running `ahriman setup` with specific arguments.

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=0.18.0 pkgver=0.19.0
pkgrel=1 pkgrel=1
pkgdesc="ArcHlinux ReposItory MANager" pkgdesc="ArcHlinux ReposItory MANager"
arch=('any') arch=('any')
@ -23,7 +23,7 @@ optdepends=('aws-cli: sync to s3'
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz" source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sysusers' 'ahriman.sysusers'
'ahriman.tmpfiles') 'ahriman.tmpfiles')
sha512sums=('8acc57f937d587ca665c29092cadddbaf3ba0b80e870b80d1551e283aba8f21306f9030a26fec8c71ab5863316f5f5f061b7ddc63cdff9e6d5a885f28ef1893d' sha512sums=('af644c52c990268f1190632ccd514f351283d5578b161aebd2819d02e9d6a041571d50fe54ca03568bdabecca2e0492222b1a88bffef6bc0eab4e7460193df61'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075' '13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4') '55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini' backup=('etc/ahriman.ini'

View File

@ -21,7 +21,6 @@ root = /var/lib/ahriman
[sign] [sign]
target = target =
key =
[report] [report]
target = target =
@ -36,10 +35,10 @@ template_path = /usr/share/ahriman/repo-index.jinja2
target = target =
[rsync] [rsync]
remote = command = rsync --archive --verbose --compress --partial --delete
[s3] [s3]
bucket = command = aws s3 sync --quiet --delete
[web] [web]
templates = /usr/share/ahriman templates = /usr/share/ahriman

View File

@ -36,10 +36,11 @@ def _parser() -> argparse.ArgumentParser:
command line parser generator command line parser generator
:return: command line parser for the application :return: command line parser for the application
""" """
parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager") parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)", parser.add_argument("-a", "--architecture", help="target architectures (can be used multiple times)",
action="append", required=True) action="append", required=True)
parser.add_argument("-c", "--config", help="configuration path", default="/etc/ahriman.ini") parser.add_argument("-c", "--configuration", help="configuration path", default="/etc/ahriman.ini")
parser.add_argument("--force", help="force run, remove file lock", action="store_true") parser.add_argument("--force", help="force run, remove file lock", action="store_true")
parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock") parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock")
parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true") parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
@ -73,8 +74,10 @@ def _set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("add", description="add package") parser = root.add_parser("add", help="add package", description="add package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="package base/name or archive path", nargs="+") parser.add_argument("package", help="package base/name or archive path", nargs="+")
parser.add_argument("--now", help="run update function after", action="store_true")
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true") parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add) parser.set_defaults(handler=handlers.Add)
return parser return parser
@ -86,7 +89,9 @@ def _set_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("check", description="check for updates. Same as update --dry-run --no-manual") parser = root.add_parser("check", help="check for updates",
description="check for updates. Same as update --dry-run --no-manual",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="filter check by package base", nargs="*") parser.add_argument("package", help="filter check by package base", nargs="*")
parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true") parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True) parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True)
@ -99,7 +104,8 @@ def _set_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("clean", description="clear all local caches") parser = root.add_parser("clean", help="clean local caches", description="clear local caches",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true") parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true")
parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true") parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true")
parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true") parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
@ -115,7 +121,9 @@ def _set_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("config", description="dump configuration for specified architecture") parser = root.add_parser("config", help="dump configuration",
description="dump configuration for specified architecture",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True) parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True)
return parser return parser
@ -126,7 +134,8 @@ def _set_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("rebuild", description="rebuild whole repository") parser = root.add_parser("rebuild", help="rebuild repository", description="rebuild whole repository",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Rebuild) parser.set_defaults(handler=handlers.Rebuild)
return parser return parser
@ -137,7 +146,8 @@ def _set_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("remove", description="remove package") parser = root.add_parser("remove", help="remove package", description="remove package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="package name or base", nargs="+") parser.add_argument("package", help="package name or base", nargs="+")
parser.set_defaults(handler=handlers.Remove) parser.set_defaults(handler=handlers.Remove)
return parser return parser
@ -149,7 +159,8 @@ def _set_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("report", description="generate report") parser = root.add_parser("report", help="generate report", description="generate report",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("target", help="target to generate report", nargs="*") parser.add_argument("target", help="target to generate report", nargs="*")
parser.set_defaults(handler=handlers.Report) parser.set_defaults(handler=handlers.Report)
return parser return parser
@ -161,9 +172,11 @@ def _set_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("setup", description="create initial service configuration, requires root") parser = root.add_parser("setup", help="initial service configuration",
description="create initial service configuration, requires root",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--build-command", help="build command prefix", default="ahriman") parser.add_argument("--build-command", help="build command prefix", default="ahriman")
parser.add_argument("--from-config", help="path to default devtools pacman configuration", parser.add_argument("--from-configuration", help="path to default devtools pacman configuration",
default="/usr/share/devtools/pacman-extra.conf") default="/usr/share/devtools/pacman-extra.conf")
parser.add_argument("--no-multilib", help="do not add multilib repository", action="store_true") parser.add_argument("--no-multilib", help="do not add multilib repository", action="store_true")
parser.add_argument("--packager", help="packager name and email", required=True) parser.add_argument("--packager", help="packager name and email", required=True)
@ -178,7 +191,8 @@ def _set_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("sign", description="(re-)sign packages and repository database") parser = root.add_parser("sign", help="sign packages", description="(re-)sign packages and repository database",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="sign only specified packages", nargs="*") parser.add_argument("package", help="sign only specified packages", nargs="*")
parser.set_defaults(handler=handlers.Sign) parser.set_defaults(handler=handlers.Sign)
return parser return parser
@ -190,7 +204,8 @@ def _set_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("status", description="request status of the package") parser = root.add_parser("status", help="get package status", description="request status of the package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--ahriman", help="get service status itself", action="store_true") parser.add_argument("--ahriman", help="get service status itself", action="store_true")
parser.add_argument("package", help="filter status by package base", nargs="*") parser.add_argument("package", help="filter status by package base", nargs="*")
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, unsafe=True) parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, unsafe=True)
@ -203,7 +218,8 @@ def _set_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("status-update", description="request status of the package") parser = root.add_parser("status-update", help="update package status", description="request status of the package",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument( parser.add_argument(
"package", "package",
help="set status for specified packages. If no packages supplied, service status will be updated", help="set status for specified packages. If no packages supplied, service status will be updated",
@ -220,7 +236,8 @@ def _set_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("sync", description="sync packages to remote server") parser = root.add_parser("sync", help="sync repository", description="sync packages to remote server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("target", help="target to sync", nargs="*") parser.add_argument("target", help="target to sync", nargs="*")
parser.set_defaults(handler=handlers.Sync) parser.set_defaults(handler=handlers.Sync)
return parser return parser
@ -232,7 +249,8 @@ def _set_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("update", description="run updates") parser = root.add_parser("update", help="update packages", description="run updates",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("package", help="filter check by package base", nargs="*") parser.add_argument("package", help="filter check by package base", nargs="*")
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")
parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true") parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
@ -248,7 +266,8 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
:param root: subparsers for the commands :param root: subparsers for the commands
:return: created argument parser :return: created argument parser
""" """
parser = root.add_parser("web", description="start web server") parser = root.add_parser("web", help="start web server", description="start web server",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True) parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
return parser return parser

View File

@ -35,21 +35,21 @@ class Application:
""" """
base application class base application class
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar config: configuration instance :ivar configuration: configuration instance
:ivar logger: application logger :ivar logger: application logger
:ivar repository: repository instance :ivar repository: repository instance
""" """
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
self.logger = logging.getLogger("root") self.logger = logging.getLogger("root")
self.config = config self.configuration = configuration
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, config) self.repository = Repository(architecture, configuration)
def _known_packages(self) -> Set[str]: def _known_packages(self) -> Set[str]:
""" """
@ -106,7 +106,7 @@ class Application:
add_archive(full_path) add_archive(full_path)
def add_manual(src: str) -> Path: def add_manual(src: str) -> Path:
package = Package.load(src, self.repository.pacman, self.config.get("alpm", "aur_url")) package = Package.load(src, self.repository.pacman, self.configuration.get("alpm", "aur_url"))
path = self.repository.paths.manual / package.base path = self.repository.paths.manual / package.base
Task.fetch(path, package.git_url) Task.fetch(path, package.git_url)
return path return path

View File

@ -32,11 +32,17 @@ class Add(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Application(architecture, config).add(args.package, args.without_dependencies) application = Application(architecture, configuration)
application.add(args.package, args.without_dependencies)
if not args.now:
return
packages = application.get_updates(args.package, True, False, True, application.logger.info)
application.update(packages)

View File

@ -32,12 +32,12 @@ class Clean(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot, Application(architecture, configuration).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages) args.no_manual, args.no_packages)

View File

@ -27,20 +27,22 @@ from ahriman.core.configuration import Configuration
class Dump(Handler): class Dump(Handler):
""" """
dump config handler dump configuration handler
""" """
_print = print
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
config_dump = config.dump(architecture) dump = configuration.dump()
for section, values in sorted(config_dump.items()): for section, values in sorted(dump.items()):
print(f"[{section}]") Dump._print(f"[{section}]")
for key, value in sorted(values.items()): for key, value in sorted(values.items()):
print(f"{key} = {value}") Dump._print(f"{key} = {value}")
print() Dump._print()

View File

@ -35,17 +35,17 @@ class Handler:
""" """
@classmethod @classmethod
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> bool: def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
""" """
additional function to wrap all calls for multiprocessing library additional function to wrap all calls for multiprocessing library
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance
:return: True on success, False otherwise :return: True on success, False otherwise
""" """
try: try:
with Lock(args, architecture, config): configuration = Configuration.from_path(args.configuration, architecture, not args.no_log)
cls.run(args, architecture, config) with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration)
return True return True
except Exception: except Exception:
logging.getLogger("root").exception("process exception") logging.getLogger("root").exception("process exception")
@ -58,18 +58,17 @@ class Handler:
:param args: command line args :param args: command line args
:return: 0 on success, 1 otherwise :return: 0 on success, 1 otherwise
""" """
configuration = Configuration.from_path(args.config, not args.no_log)
with Pool(len(args.architecture)) as pool: with Pool(len(args.architecture)) as pool:
result = pool.starmap( result = pool.starmap(
cls._call, [(args, architecture, configuration) for architecture in args.architecture]) cls._call, [(args, architecture) for architecture in set(args.architecture)])
return 0 if all(result) else 1 return 0 if all(result) else 1
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -32,13 +32,13 @@ class Rebuild(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
application = Application(architecture, config) application = Application(architecture, configuration)
packages = application.repository.packages() packages = application.repository.packages()
application.update(packages) application.update(packages)

View File

@ -32,11 +32,11 @@ class Remove(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Application(architecture, config).remove(args.package) Application(architecture, configuration).remove(args.package)

View File

@ -32,11 +32,11 @@ class Report(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Application(architecture, config).report(args.target) Application(architecture, configuration).report(args.target)

View File

@ -44,19 +44,19 @@ class Setup(Handler):
SUDOERS_PATH = Path("/etc/sudoers.d/ahriman") SUDOERS_PATH = Path("/etc/sudoers.d/ahriman")
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
application = Application(architecture, config) application = Application(architecture, configuration)
Setup.create_makepkg_configuration(args.packager, application.repository.paths) Setup.create_makepkg_configuration(args.packager, application.repository.paths)
Setup.create_executable(args.build_command, architecture) Setup.create_executable(args.build_command, architecture)
Setup.create_devtools_configuration(args.build_command, architecture, Path(args.from_config), args.no_multilib, Setup.create_devtools_configuration(args.build_command, architecture, Path(args.from_configuration),
args.repository, application.repository.paths) args.no_multilib, args.repository, application.repository.paths)
Setup.create_ahriman_configuration(args.build_command, architecture, args.repository, config.include) Setup.create_ahriman_configuration(args.build_command, architecture, args.repository, configuration.include)
Setup.create_sudo_configuration(args.build_command, architecture) Setup.create_sudo_configuration(args.build_command, architecture)
@staticmethod @staticmethod
@ -78,17 +78,17 @@ class Setup(Handler):
:param repository: repository name :param repository: repository name
:param include_path: path to directory with configuration includes :param include_path: path to directory with configuration includes
""" """
config = configparser.ConfigParser() configuration = configparser.ConfigParser()
config.add_section("build") configuration.add_section("build")
config.set("build", "build_command", str(Setup.build_command(prefix, architecture))) configuration.set("build", "build_command", str(Setup.build_command(prefix, architecture)))
config.add_section("repository") configuration.add_section("repository")
config.set("repository", "name", repository) configuration.set("repository", "name", repository)
target = include_path / "build-overrides.ini" target = include_path / "build-overrides.ini"
with target.open("w") as ahriman_config: with target.open("w") as ahriman_configuration:
config.write(ahriman_config) configuration.write(ahriman_configuration)
@staticmethod @staticmethod
def create_devtools_configuration(prefix: str, architecture: str, source: Path, def create_devtools_configuration(prefix: str, architecture: str, source: Path,
@ -102,31 +102,31 @@ class Setup(Handler):
:param repository: repository name :param repository: repository name
:param paths: repository paths instance :param paths: repository paths instance
""" """
config = configparser.ConfigParser() configuration = configparser.ConfigParser()
# preserve case # preserve case
# stupid mypy thinks that it is impossible # stupid mypy thinks that it is impossible
config.optionxform = lambda key: key # type: ignore configuration.optionxform = lambda key: key # type: ignore
# load default configuration first # load default configuration first
# we cannot use Include here because it will be copied to new chroot, thus no includes there # we cannot use Include here because it will be copied to new chroot, thus no includes there
config.read(source) configuration.read(source)
# set our architecture now # set our architecture now
config.set("options", "Architecture", architecture) configuration.set("options", "Architecture", architecture)
# add multilib # add multilib
if not no_multilib: if not no_multilib:
config.add_section("multilib") configuration.add_section("multilib")
config.set("multilib", "Include", str(Setup.MIRRORLIST_PATH)) configuration.set("multilib", "Include", str(Setup.MIRRORLIST_PATH))
# add repository itself # add repository itself
config.add_section(repository) configuration.add_section(repository)
config.set(repository, "SigLevel", "Optional TrustAll") # we don't care configuration.set(repository, "SigLevel", "Optional TrustAll") # we don't care
config.set(repository, "Server", f"file://{paths.repository}") configuration.set(repository, "Server", f"file://{paths.repository}")
target = source.parent / f"pacman-{prefix}-{architecture}.conf" target = source.parent / f"pacman-{prefix}-{architecture}.conf"
with target.open("w") as devtools_config: with target.open("w") as devtools_configuration:
config.write(devtools_config) configuration.write(devtools_configuration)
@staticmethod @staticmethod
def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None: def create_makepkg_configuration(packager: str, paths: RepositoryPaths) -> None:

View File

@ -32,11 +32,11 @@ class Sign(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Application(architecture, config).sign(args.package) Application(architecture, configuration).sign(args.package)

View File

@ -34,14 +34,14 @@ class Status(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
application = Application(architecture, config) application = Application(architecture, configuration)
if args.ahriman: if args.ahriman:
ahriman = application.repository.reporter.get_self() ahriman = application.repository.reporter.get_self()
print(ahriman.pretty_print()) print(ahriman.pretty_print())

View File

@ -33,14 +33,14 @@ class StatusUpdate(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
client = Application(architecture, config).repository.reporter client = Application(architecture, configuration).repository.reporter
status = BuildStatusEnum(args.status) status = BuildStatusEnum(args.status)
if args.package: if args.package:
# update packages statuses # update packages statuses

View File

@ -32,11 +32,11 @@ class Sync(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Application(architecture, config).sync(args.target) Application(architecture, configuration).sync(args.target)

View File

@ -32,18 +32,18 @@ class Update(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
# typing workaround # typing workaround
def log_fn(line: str) -> None: def log_fn(line: str) -> None:
return print(line) if args.dry_run else application.logger.info(line) return print(line) if args.dry_run else application.logger.info(line)
application = Application(architecture, config) application = Application(architecture, configuration)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn) packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
if args.dry_run: if args.dry_run:
return return

View File

@ -31,13 +31,13 @@ class Web(Handler):
""" """
@classmethod @classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None: def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
callback for command line callback for command line
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
from ahriman.web.web import run_server, setup_service from ahriman.web.web import run_server, setup_service
application = setup_service(architecture, config) application = setup_service(architecture, configuration)
run_server(application) run_server(application)

View File

@ -42,19 +42,19 @@ class Lock:
:ivar unsafe: skip user check :ivar unsafe: skip user check
""" """
def __init__(self, args: argparse.Namespace, architecture: str, config: Configuration) -> None: def __init__(self, args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param args: command line args :param args: command line args
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None
self.force = args.force self.force = args.force
self.unsafe = args.unsafe self.unsafe = args.unsafe
self.root = Path(config.get("repository", "root")) self.root = Path(configuration.get("repository", "root"))
self.reporter = Client() if args.no_report else Client.load(architecture, config) self.reporter = Client() if args.no_report else Client.load(configuration)
def __enter__(self) -> Lock: def __enter__(self) -> Lock:
""" """

View File

@ -29,15 +29,15 @@ class Pacman:
:ivar handle: pyalpm root `Handle` :ivar handle: pyalpm root `Handle`
""" """
def __init__(self, config: Configuration) -> None: def __init__(self, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param config: configuration instance :param configuration: configuration instance
""" """
root = config.get("alpm", "root") root = configuration.get("alpm", "root")
pacman_root = config.getpath("alpm", "database") pacman_root = configuration.getpath("alpm", "database")
self.handle = Handle(root, str(pacman_root)) self.handle = Handle(root, str(pacman_root))
for repository in config.getlist("alpm", "repositories"): for repository in configuration.getlist("alpm", "repositories"):
self.handle.register_syncdb(repository, 0) # 0 is pgp_level self.handle.register_syncdb(repository, 0) # 0 is pgp_level
def all_packages(self) -> List[str]: def all_packages(self) -> List[str]:

View File

@ -41,12 +41,11 @@ class Task:
_check_output = check_output _check_output = check_output
def __init__(self, package: Package, architecture: str, config: Configuration, paths: RepositoryPaths) -> None: def __init__(self, package: Package, configuration: Configuration, paths: RepositoryPaths) -> None:
""" """
default constructor default constructor
:param package: package definitions :param package: package definitions
:param architecture: repository architecture :param configuration: configuration instance
:param config: configuration instance
:param paths: repository paths instance :param paths: repository paths instance
""" """
self.logger = logging.getLogger("builder") self.logger = logging.getLogger("builder")
@ -54,11 +53,10 @@ class Task:
self.package = package self.package = package
self.paths = paths self.paths = paths
section = config.get_section_name("build", architecture) self.archbuild_flags = configuration.getlist("build", "archbuild_flags")
self.archbuild_flags = config.getlist(section, "archbuild_flags") self.build_command = configuration.get("build", "build_command")
self.build_command = config.get(section, "build_command") self.makepkg_flags = configuration.getlist("build", "makepkg_flags")
self.makepkg_flags = config.getlist(section, "makepkg_flags") self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags")
self.makechrootpkg_flags = config.getlist(section, "makechrootpkg_flags")
@property @property
def cache_path(self) -> Path: def cache_path(self) -> Path:
@ -97,14 +95,14 @@ class Task:
run package build run package build
:return: paths of produced packages :return: paths of produced packages
""" """
cmd = [self.build_command, "-r", str(self.paths.chroot)] command = [self.build_command, "-r", str(self.paths.chroot)]
cmd.extend(self.archbuild_flags) command.extend(self.archbuild_flags)
cmd.extend(["--"] + self.makechrootpkg_flags) command.extend(["--"] + self.makechrootpkg_flags)
cmd.extend(["--"] + self.makepkg_flags) command.extend(["--"] + self.makepkg_flags)
self.logger.info(f"using {cmd} for {self.package.base}") self.logger.info(f"using {command} for {self.package.base}")
Task._check_output( Task._check_output(
*cmd, *command,
exception=BuildFailed(self.package.base), exception=BuildFailed(self.package.base),
cwd=self.git_path, cwd=self.git_path,
logger=self.build_logger) logger=self.build_logger)

View File

@ -34,13 +34,11 @@ class Configuration(configparser.RawConfigParser):
:cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump) :cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump)
:cvar DEFAULT_LOG_FORMAT: default log format (in case of fallback) :cvar DEFAULT_LOG_FORMAT: default log format (in case of fallback)
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback) :cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
:cvar STATIC_SECTIONS: known sections which are not architecture specific (required by dump)
""" """
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s" DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG DEFAULT_LOG_LEVEL = logging.DEBUG
STATIC_SECTIONS = ["alpm", "report", "repository", "settings", "upload"]
ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"] ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
def __init__(self) -> None: def __init__(self) -> None:
@ -57,37 +55,46 @@ class Configuration(configparser.RawConfigParser):
""" """
return self.getpath("settings", "include") return self.getpath("settings", "include")
@property
def logging_path(self) -> Path:
"""
:return: path to logging configuration
"""
return self.getpath("settings", "logging")
@classmethod @classmethod
def from_path(cls: Type[Configuration], path: Path, logfile: bool) -> Configuration: def from_path(cls: Type[Configuration], path: Path, architecture: str, logfile: bool) -> Configuration:
""" """
constructor with full object initialization constructor with full object initialization
:param path: path to root configuration file :param path: path to root configuration file
:param architecture: repository architecture
:param logfile: use log file to output messages :param logfile: use log file to output messages
:return: configuration instance :return: configuration instance
""" """
config = cls() config = cls()
config.load(path) config.load(path, architecture)
config.load_logging(logfile) config.load_logging(logfile)
return config return config
def dump(self, architecture: str) -> Dict[str, Dict[str, str]]: @staticmethod
def section_name(section: str, architecture: str) -> str:
"""
generate section name for architecture specific sections
:param section: section name
:param architecture: repository architecture
:return: correct section name for repository specific section
"""
return f"{section}_{architecture}"
def dump(self) -> Dict[str, Dict[str, str]]:
""" """
dump configuration to dictionary dump configuration to dictionary
:param architecture: repository architecture
:return: configuration dump for specific architecture :return: configuration dump for specific architecture
""" """
result: Dict[str, Dict[str, str]] = {} return {
for section in Configuration.STATIC_SECTIONS: section: dict(self[section])
if not self.has_section(section): for section in self.sections()
continue }
result[section] = dict(self[section])
for group in Configuration.ARCHITECTURE_SPECIFIC_SECTIONS:
section = self.get_section_name(group, architecture)
if not self.has_section(section):
continue
result[section] = dict(self[section])
return result
def getlist(self, section: str, key: str) -> List[str]: def getlist(self, section: str, key: str) -> List[str]:
""" """
@ -113,24 +120,16 @@ class Configuration(configparser.RawConfigParser):
return value return value
return self.path.parent / value return self.path.parent / value
def get_section_name(self, prefix: str, suffix: str) -> str: def load(self, path: Path, architecture: str) -> None:
"""
check if there is `prefix`_`suffix` section and return it on success. Return `prefix` otherwise
:param prefix: section name prefix
:param suffix: section name suffix (e.g. architecture name)
:return: found section name
"""
probe = f"{prefix}_{suffix}"
return probe if self.has_section(probe) else prefix
def load(self, path: Path) -> None:
""" """
fully load configuration fully load configuration
:param path: path to root configuration file :param path: path to root configuration file
:param architecture: repository architecture
""" """
self.path = path self.path = path
self.read(self.path) self.read(self.path)
self.load_includes() self.load_includes()
self.merge_sections(architecture)
def load_includes(self) -> None: def load_includes(self) -> None:
""" """
@ -138,6 +137,8 @@ class Configuration(configparser.RawConfigParser):
""" """
try: try:
for path in sorted(self.include.glob("*.ini")): for path in sorted(self.include.glob("*.ini")):
if path == self.logging_path:
continue # we don't want to load logging explicitly
self.read(path) self.read(path)
except (FileNotFoundError, configparser.NoOptionError): except (FileNotFoundError, configparser.NoOptionError):
pass pass
@ -149,17 +150,39 @@ class Configuration(configparser.RawConfigParser):
""" """
def file_logger() -> None: def file_logger() -> None:
try: try:
config_path = self.getpath("settings", "logging") path = self.logging_path
fileConfig(config_path) fileConfig(path)
except (FileNotFoundError, PermissionError): except (FileNotFoundError, PermissionError):
console_logger() console_logger()
logging.exception("could not create logfile, fallback to stderr") logging.exception("could not create logfile, fallback to stderr")
def console_logger() -> None: def console_logger() -> None:
logging.basicConfig(filename=None, format=Configuration.DEFAULT_LOG_FORMAT, logging.basicConfig(filename=None, format=self.DEFAULT_LOG_FORMAT,
level=Configuration.DEFAULT_LOG_LEVEL) level=self.DEFAULT_LOG_LEVEL)
if logfile: if logfile:
file_logger() file_logger()
else: else:
console_logger() console_logger()
def merge_sections(self, architecture: str) -> None:
"""
merge architecture specific sections into main configuration
:param architecture: repository architecture
"""
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS:
if not self.has_section(section):
self.add_section(section) # add section if not exists
# get overrides
specific = self.section_name(section, architecture)
if self.has_section(specific):
# if there is no such section it means that there is no overrides for this arch
# but we anyway will have to delete sections for others archs
for key, value in self[specific].items():
self.set(section, key, value)
# remove any arch specific section
for foreign in self.sections():
# we would like to use lambda filter here, but pylint is too dumb
if not foreign.startswith(f"{section}_"):
continue
self.remove_section(foreign)

View File

@ -23,6 +23,7 @@ from typing import Callable, Dict, Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.sign.gpg import GPG
from ahriman.core.util import pretty_datetime, pretty_size from ahriman.core.util import pretty_datetime, pretty_size
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
@ -56,31 +57,28 @@ class HTML(Report):
:ivar homepage: homepage link if any (for footer) :ivar homepage: homepage link if any (for footer)
:ivar link_path: prefix fo packages to download :ivar link_path: prefix fo packages to download
:ivar name: repository name :ivar name: repository name
:ivar pgp_key: default PGP key :ivar default_pgp_key: default PGP key
:ivar report_path: output path to html report :ivar report_path: output path to html report
:ivar sign_targets: targets to sign enabled in configuration :ivar sign_targets: targets to sign enabled in configuration
:ivar template_path: path to directory with jinja templates :ivar template_path: path to directory with jinja templates
""" """
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Report.__init__(self, architecture, config) Report.__init__(self, architecture, configuration)
section = config.get_section_name("html", architecture) self.report_path = configuration.getpath("html", "path")
self.report_path = config.getpath(section, "path") self.link_path = configuration.get("html", "link_path")
self.link_path = config.get(section, "link_path") self.template_path = configuration.getpath("html", "template_path")
self.template_path = config.getpath(section, "template_path")
# base template vars # base template vars
self.homepage = config.get(section, "homepage", fallback=None) self.homepage = configuration.get("html", "homepage", fallback=None)
self.name = config.get("repository", "name") self.name = configuration.get("repository", "name")
sign_section = config.get_section_name("sign", architecture) self.sign_targets, self.default_pgp_key = GPG.sign_options(configuration)
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist(sign_section, "target")]
self.pgp_key = config.get(sign_section, "key") if self.sign_targets else None
def generate(self, packages: Iterable[Package]) -> None: def generate(self, packages: Iterable[Package]) -> None:
""" """
@ -115,7 +113,7 @@ class HTML(Report):
has_package_signed=SignSettings.SignPackages in self.sign_targets, has_package_signed=SignSettings.SignPackages in self.sign_targets,
has_repo_signed=SignSettings.SignRepository in self.sign_targets, has_repo_signed=SignSettings.SignRepository in self.sign_targets,
packages=sorted(content, key=comparator), packages=sorted(content, key=comparator),
pgp_key=self.pgp_key, pgp_key=self.default_pgp_key,
repository=self.name) repository=self.name)
self.report_path.write_text(html) self.report_path.write_text(html)

View File

@ -31,35 +31,35 @@ class Report:
""" """
base report generator base report generator
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar config: configuration instance :ivar configuration: configuration instance
:ivar logger: class logger :ivar logger: class logger
""" """
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
self.logger = logging.getLogger("builder") self.logger = logging.getLogger("builder")
self.architecture = architecture self.architecture = architecture
self.config = config self.configuration = configuration
@staticmethod @staticmethod
def run(architecture: str, config: Configuration, target: str, packages: Iterable[Package]) -> None: def run(architecture: str, configuration: Configuration, target: str, packages: Iterable[Package]) -> None:
""" """
run report generation run report generation
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
:param target: target to generate report (e.g. html) :param target: target to generate report (e.g. html)
:param packages: list of packages to generate report :param packages: list of packages to generate report
""" """
provider = ReportSettings.from_option(target) provider = ReportSettings.from_option(target)
if provider == ReportSettings.HTML: if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML from ahriman.core.report.html import HTML
report: Report = HTML(architecture, config) report: Report = HTML(architecture, configuration)
else: else:
report = Report(architecture, config) report = Report(architecture, configuration)
try: try:
report.generate(packages) report.generate(packages)

View File

@ -25,7 +25,7 @@ from typing import Dict, Iterable, List, Optional
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.upload.uploader import Uploader from ahriman.core.upload.upload import Upload
from ahriman.models.package import Package from ahriman.models.package import Package
@ -49,7 +49,7 @@ class Executor(Cleaner):
""" """
def build_single(package: Package) -> None: def build_single(package: Package) -> None:
self.reporter.set_building(package.base) self.reporter.set_building(package.base)
task = Task(package, self.architecture, self.config, self.paths) task = Task(package, self.configuration, self.paths)
task.init() task.init()
built = task.build() built = task.build()
for src in built: for src in built:
@ -106,9 +106,9 @@ class Executor(Cleaner):
:param targets: list of targets to generate reports. Configuration option will be used if it is not set :param targets: list of targets to generate reports. Configuration option will be used if it is not set
""" """
if targets is None: if targets is None:
targets = self.config.getlist("report", "target") targets = self.configuration.getlist("report", "target")
for target in targets: for target in targets:
Report.run(self.architecture, self.config, target, self.packages()) Report.run(self.architecture, self.configuration, target, self.packages())
def process_sync(self, targets: Optional[Iterable[str]]) -> None: def process_sync(self, targets: Optional[Iterable[str]]) -> None:
""" """
@ -116,9 +116,9 @@ class Executor(Cleaner):
:param targets: list of targets to sync. Configuration option will be used if it is not set :param targets: list of targets to sync. Configuration option will be used if it is not set
""" """
if targets is None: if targets is None:
targets = self.config.getlist("upload", "target") targets = self.configuration.getlist("upload", "target")
for target in targets: for target in targets:
Uploader.run(self.architecture, self.config, target, self.paths.repository) Upload.run(self.architecture, self.configuration, target, self.paths.repository)
def process_update(self, packages: Iterable[Path]) -> Path: def process_update(self, packages: Iterable[Path]) -> Path:
""" """

View File

@ -32,7 +32,7 @@ class Properties:
repository internal objects holder repository internal objects holder
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar aur_url: base AUR url :ivar aur_url: base AUR url
:ivar config: configuration instance :ivar configuration: configuration instance
:ivar logger: class logger :ivar logger: class logger
:ivar name: repository name :ivar name: repository name
:ivar pacman: alpm wrapper instance :ivar pacman: alpm wrapper instance
@ -42,18 +42,18 @@ class Properties:
:ivar sign: GPG wrapper instance :ivar sign: GPG wrapper instance
""" """
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
self.logger = logging.getLogger("builder") self.logger = logging.getLogger("builder")
self.architecture = architecture self.architecture = architecture
self.config = config self.configuration = configuration
self.aur_url = config.get("alpm", "aur_url") self.aur_url = configuration.get("alpm", "aur_url")
self.name = config.get("repository", "name") self.name = configuration.get("repository", "name")
self.paths = RepositoryPaths(config.getpath("repository", "root"), architecture) self.paths = RepositoryPaths(configuration.getpath("repository", "root"), architecture)
self.paths.create_tree() self.paths.create_tree()
self.pacman = Pacman(config) self.pacman = Pacman(configuration)
self.sign = GPG(architecture, config) self.sign = GPG(architecture, 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(architecture, config) self.reporter = Client.load(configuration)

View File

@ -44,8 +44,7 @@ class UpdateHandler(Cleaner):
""" """
result: List[Package] = [] result: List[Package] = []
build_section = self.config.get_section_name("build", self.architecture) ignore_list = self.configuration.getlist("build", "ignore_packages")
ignore_list = self.config.getlist(build_section, "ignore_packages")
for local in self.packages(): for local in self.packages():
if local.base in ignore_list: if local.base in ignore_list:

View File

@ -20,7 +20,7 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List from typing import List, Optional, Set, Tuple
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildFailed from ahriman.core.exceptions import BuildFailed
@ -32,37 +32,39 @@ class GPG:
""" """
gnupg wrapper gnupg wrapper
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar config: configuration instance :ivar configuration: configuration instance
:ivar default_key: default PGP key ID to use :ivar default_key: default PGP key ID to use
:ivar logger: class logger :ivar logger: class logger
:ivar target: list of targets to sign (repository, package etc) :ivar targets: list of targets to sign (repository, package etc)
""" """
_check_output = check_output _check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
self.logger = logging.getLogger("build_details") self.logger = logging.getLogger("build_details")
self.config = config self.architecture = architecture
self.section = config.get_section_name("sign", architecture) self.configuration = configuration
self.target = {SignSettings.from_option(opt) for opt in config.getlist(self.section, "target")} self.targets, self.default_key = self.sign_options(configuration)
self.default_key = config.get(self.section, "key") if self.target else ""
@property @property
def repository_sign_args(self) -> List[str]: def repository_sign_args(self) -> List[str]:
""" """
:return: command line arguments for repo-add command to sign database :return: command line arguments for repo-add command to sign database
""" """
if SignSettings.SignRepository not in self.target: if SignSettings.SignRepository not in self.targets:
return []
if self.default_key is None:
self.logger.error("no default key set, skip repository sign")
return [] return []
return ["--sign", "--key", self.default_key] return ["--sign", "--key", self.default_key]
@staticmethod @staticmethod
def sign_cmd(path: Path, key: str) -> List[str]: def sign_command(path: Path, key: str) -> List[str]:
""" """
gpg command to run gpg command to run
:param path: path to file to sign :param path: path to file to sign
@ -71,6 +73,20 @@ class GPG:
""" """
return ["gpg", "-u", key, "-b", str(path)] return ["gpg", "-u", key, "-b", str(path)]
@staticmethod
def sign_options(configuration: Configuration) -> Tuple[Set[SignSettings], Optional[str]]:
"""
extract default sign options from configuration
:param configuration: configuration instance
:return: tuple of sign targets and default PGP key
"""
targets = {
SignSettings.from_option(option)
for option in configuration.getlist("sign", "target")
}
default_key = configuration.get("sign", "key") if targets else None
return targets, default_key
def process(self, path: Path, key: str) -> List[Path]: def process(self, path: Path, key: str) -> List[Path]:
""" """
gpg command wrapper gpg command wrapper
@ -79,7 +95,7 @@ class GPG:
:return: list of generated files including original file :return: list of generated files including original file
""" """
GPG._check_output( GPG._check_output(
*GPG.sign_cmd(path, key), *GPG.sign_command(path, key),
exception=BuildFailed(path.name), exception=BuildFailed(path.name),
logger=self.logger) logger=self.logger)
return [path, path.parent / f"{path.name}.sig"] return [path, path.parent / f"{path.name}.sig"]
@ -91,9 +107,12 @@ class GPG:
:param base: package base required to check for key overrides :param base: package base required to check for key overrides
:return: list of generated files including original file :return: list of generated files including original file
""" """
if SignSettings.SignPackages not in self.target: if SignSettings.SignPackages not in self.targets:
return [path]
key = self.configuration.get("sign", f"key_{base}", fallback=self.default_key)
if key is None:
self.logger.error(f"no default key set, skip package {path} sign")
return [path] return [path]
key = self.config.get(self.section, f"key_{base}", fallback=self.default_key)
return self.process(path, key) return self.process(path, key)
def sign_repository(self, path: Path) -> List[Path]: def sign_repository(self, path: Path) -> List[Path]:
@ -103,6 +122,9 @@ class GPG:
:param path: path to repository database :param path: path to repository database
:return: list of generated files including original file :return: list of generated files including original file
""" """
if SignSettings.SignRepository not in self.target: if SignSettings.SignRepository not in self.targets:
return [path]
if self.default_key is None:
self.logger.error("no default key set, skip repository sign")
return [path] return [path]
return self.process(path, self.default_key) return self.process(path, self.default_key)

View File

@ -111,16 +111,14 @@ class Client:
return self.add(package, BuildStatusEnum.Unknown) return self.add(package, BuildStatusEnum.Unknown)
@staticmethod @staticmethod
def load(architecture: str, config: Configuration) -> Client: def load(configuration: Configuration) -> Client:
""" """
load client from settings load client from settings
:param architecture: repository architecture :param configuration: configuration instance
:param config: configuration instance
:return: client according to current settings :return: client according to current settings
""" """
section = config.get_section_name("web", architecture) host = configuration.get("web", "host", fallback=None)
host = config.get(section, "host", fallback=None) port = configuration.getint("web", "port", fallback=None)
port = config.getint(section, "port", fallback=None)
if host is None or port is None: if host is None or port is None:
return Client() return Client()

View File

@ -40,16 +40,16 @@ class Watcher:
:ivar status: daemon status :ivar status: daemon status
""" """
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
self.logger = logging.getLogger("http") self.logger = logging.getLogger("http")
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, config) self.repository = Repository(architecture, configuration)
self.known: Dict[str, Tuple[Package, BuildStatus]] = {} self.known: Dict[str, Tuple[Package, BuildStatus]] = {}
self.status = BuildStatus() self.status = BuildStatus()

View File

@ -20,41 +20,32 @@
from pathlib import Path from pathlib import Path
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.upload.uploader import Uploader from ahriman.core.upload.upload import Upload
from ahriman.core.util import check_output from ahriman.core.util import check_output
class Rsync(Uploader): class Rsync(Upload):
""" """
rsync wrapper rsync wrapper
:ivar command: command arguments for sync
:ivar remote: remote address to sync :ivar remote: remote address to sync
""" """
_check_output = check_output _check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Uploader.__init__(self, architecture, config) Upload.__init__(self, architecture, configuration)
section = config.get_section_name("rsync", architecture) self.command = configuration.getlist("rsync", "command")
self.remote = config.get(section, "remote") self.remote = configuration.get("rsync", "remote")
def sync(self, path: Path) -> None: def sync(self, path: Path) -> None:
""" """
sync data to remote server sync data to remote server
:param path: local path to sync :param path: local path to sync
""" """
Rsync._check_output( Rsync._check_output(*self.command, str(path), self.remote, exception=None, logger=self.logger)
"rsync",
"--archive",
"--verbose",
"--compress",
"--partial",
"--delete",
str(path),
self.remote,
exception=None,
logger=self.logger)

View File

@ -20,27 +20,28 @@
from pathlib import Path from pathlib import Path
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.upload.uploader import Uploader from ahriman.core.upload.upload import Upload
from ahriman.core.util import check_output from ahriman.core.util import check_output
class S3(Uploader): class S3(Upload):
""" """
aws-cli wrapper aws-cli wrapper
:ivar bucket: full bucket name :ivar bucket: full bucket name
:ivar command: command arguments for sync
""" """
_check_output = check_output _check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
Uploader.__init__(self, architecture, config) Upload.__init__(self, architecture, configuration)
section = config.get_section_name("s3", architecture) self.bucket = configuration.get("s3", "bucket")
self.bucket = config.get(section, "bucket") self.command = configuration.getlist("s3", "command")
def sync(self, path: Path) -> None: def sync(self, path: Path) -> None:
""" """
@ -48,6 +49,4 @@ class S3(Uploader):
:param path: local path to sync :param path: local path to sync
""" """
# TODO rewrite to boto, but it is bullshit # TODO rewrite to boto, but it is bullshit
S3._check_output("aws", "s3", "sync", "--quiet", "--delete", str(path), self.bucket, S3._check_output(*self.command, str(path), self.bucket, exception=None, logger=self.logger)
exception=None,
logger=self.logger)

View File

@ -26,47 +26,47 @@ from ahriman.core.exceptions import SyncFailed
from ahriman.models.upload_settings import UploadSettings from ahriman.models.upload_settings import UploadSettings
class Uploader: class Upload:
""" """
base remote sync class base remote sync class
:ivar architecture: repository architecture :ivar architecture: repository architecture
:ivar config: configuration instance :ivar configuration: configuration instance
:ivar logger: application logger :ivar logger: application logger
""" """
def __init__(self, architecture: str, config: Configuration) -> None: def __init__(self, architecture: str, configuration: Configuration) -> None:
""" """
default constructor default constructor
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
""" """
self.logger = logging.getLogger("builder") self.logger = logging.getLogger("builder")
self.architecture = architecture self.architecture = architecture
self.config = config self.config = configuration
@staticmethod @staticmethod
def run(architecture: str, config: Configuration, target: str, path: Path) -> None: def run(architecture: str, configuration: Configuration, target: str, path: Path) -> None:
""" """
run remote sync run remote sync
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
:param target: target to run sync (e.g. s3) :param target: target to run sync (e.g. s3)
:param path: local path to sync :param path: local path to sync
""" """
provider = UploadSettings.from_option(target) provider = UploadSettings.from_option(target)
if provider == UploadSettings.Rsync: if provider == UploadSettings.Rsync:
from ahriman.core.upload.rsync import Rsync from ahriman.core.upload.rsync import Rsync
uploader: Uploader = Rsync(architecture, config) upload: Upload = Rsync(architecture, configuration)
elif provider == UploadSettings.S3: elif provider == UploadSettings.S3:
from ahriman.core.upload.s3 import S3 from ahriman.core.upload.s3 import S3
uploader = S3(architecture, config) upload = S3(architecture, configuration)
else: else:
uploader = Uploader(architecture, config) upload = Upload(architecture, configuration)
try: try:
uploader.sync(path) upload.sync(path)
except Exception: except Exception:
uploader.logger.exception(f"remote sync failed for {provider.name}") upload.logger.exception(f"remote sync failed for {provider.name}")
raise SyncFailed() raise SyncFailed()
def sync(self, path: Path) -> None: def sync(self, path: Path) -> None:

View File

@ -31,7 +31,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output from ahriman.core.util import check_output
from ahriman.models.package_desciption import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths

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__ = "0.18.0" __version__ = "0.19.0"

View File

@ -58,19 +58,19 @@ def run_server(application: web.Application) -> None:
""" """
application.logger.info("start server") application.logger.info("start server")
section = application["config"].get_section_name("web", application["architecture"]) configuration: Configuration = application["configuration"]
host = application["config"].get(section, "host") host = configuration.get("web", "host")
port = application["config"].getint(section, "port") port = configuration.getint("web", "port")
web.run_app(application, host=host, port=port, handle_signals=False, web.run_app(application, host=host, port=port, handle_signals=False,
access_log=logging.getLogger("http")) access_log=logging.getLogger("http"))
def setup_service(architecture: str, config: Configuration) -> web.Application: def setup_service(architecture: str, configuration: Configuration) -> web.Application:
""" """
create web application create web application
:param architecture: repository architecture :param architecture: repository architecture
:param config: configuration instance :param configuration: configuration instance
:return: web application instance :return: web application instance
""" """
application = web.Application(logger=logging.getLogger("http")) application = web.Application(logger=logging.getLogger("http"))
@ -84,13 +84,12 @@ def setup_service(architecture: str, config: Configuration) -> web.Application:
setup_routes(application) setup_routes(application)
application.logger.info("setup templates") application.logger.info("setup templates")
aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.getpath("web", "templates"))) aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(configuration.getpath("web", "templates")))
application.logger.info("setup configuration") application.logger.info("setup configuration")
application["config"] = config application["configuration"] = configuration
application["architecture"] = architecture
application.logger.info("setup watcher") application.logger.info("setup watcher")
application["watcher"] = Watcher(architecture, config) application["watcher"] = Watcher(architecture, configuration)
return application return application

View File

@ -3,25 +3,27 @@ import argparse
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_call(args: argparse.Namespace, mocker: MockerFixture) -> None:
""" """
must call inside lock must call inside lock
""" """
args.configuration = ""
args.no_log = False
mocker.patch("ahriman.application.handlers.Handler.run") mocker.patch("ahriman.application.handlers.Handler.run")
mocker.patch("ahriman.core.configuration.Configuration.from_path")
enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__") enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__")
exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__") exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__")
assert Handler._call(args, "x86_64", configuration) assert Handler._call(args, "x86_64")
enter_mock.assert_called_once() enter_mock.assert_called_once()
exit_mock.assert_called_once() exit_mock.assert_called_once()
def test_call_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: def test_call_exception(args: argparse.Namespace, mocker: MockerFixture) -> None:
""" """
must process exception must process exception
""" """
mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception()) mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
assert not Handler._call(args, "x86_64", configuration) assert not Handler._call(args, "x86_64")

View File

@ -8,6 +8,7 @@ from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.package = [] args.package = []
args.now = False
args.without_dependencies = False args.without_dependencies = False
return args return args
@ -22,3 +23,19 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
Add.run(args, "x86_64", configuration) Add.run(args, "x86_64", configuration)
application_mock.assert_called_once() application_mock.assert_called_once()
def test_run_with_updates(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with updates after
"""
args = _default_args(args)
args.now = True
mocker.patch("pathlib.Path.mkdir")
mocker.patch("ahriman.application.application.Application.add")
application_mock = mocker.patch("ahriman.application.application.Application.update")
updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
Add.run(args, "x86_64", configuration)
application_mock.assert_called_once()
updates_mock.assert_called_once()

View File

@ -11,7 +11,10 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
must run command must run command
""" """
mocker.patch("pathlib.Path.mkdir") mocker.patch("pathlib.Path.mkdir")
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump") print_mock = mocker.patch("ahriman.application.handlers.dump.Dump._print")
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump",
return_value=configuration.dump())
Dump.run(args, "x86_64", configuration) Dump.run(args, "x86_64", configuration)
application_mock.assert_called_once() application_mock.assert_called_once()
print_mock.assert_called()

View File

@ -11,7 +11,7 @@ from ahriman.models.repository_paths import RepositoryPaths
def _default_args(args: argparse.Namespace) -> argparse.Namespace: def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.build_command = "ahriman" args.build_command = "ahriman"
args.from_config = "/usr/share/devtools/pacman-extra.conf" args.from_configuration = "/usr/share/devtools/pacman-extra.conf"
args.no_multilib = False args.no_multilib = False
args.packager = "John Doe <john@doe.com>" args.packager = "John Doe <john@doe.com>"
args.repository = "aur-clone" args.repository = "aur-clone"
@ -81,8 +81,8 @@ def test_create_devtools_configuration(args: argparse.Namespace, repository_path
add_section_mock = mocker.patch("configparser.RawConfigParser.add_section") add_section_mock = mocker.patch("configparser.RawConfigParser.add_section")
write_mock = mocker.patch("configparser.RawConfigParser.write") write_mock = mocker.patch("configparser.RawConfigParser.write")
Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_config), args.no_multilib, Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_configuration),
args.repository, repository_paths) args.no_multilib, args.repository, repository_paths)
add_section_mock.assert_has_calls([ add_section_mock.assert_has_calls([
mock.call("multilib"), mock.call("multilib"),
mock.call(args.repository) mock.call(args.repository)
@ -101,8 +101,8 @@ def test_create_devtools_configuration_no_multilib(args: argparse.Namespace, rep
add_section_mock = mocker.patch("configparser.RawConfigParser.add_section") add_section_mock = mocker.patch("configparser.RawConfigParser.add_section")
write_mock = mocker.patch("configparser.RawConfigParser.write") write_mock = mocker.patch("configparser.RawConfigParser.write")
Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_config), True, Setup.create_devtools_configuration(args.build_command, "x86_64", Path(args.from_configuration),
args.repository, repository_paths) True, args.repository, repository_paths)
add_section_mock.assert_called_once() add_section_mock.assert_called_once()
write_mock.assert_called_once() write_mock.assert_called_once()

View File

@ -7,7 +7,7 @@ from typing import Any, Type, TypeVar
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.status.watcher import Watcher from ahriman.core.status.watcher import Watcher
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_desciption import PackageDescription from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
T = TypeVar("T") T = TypeVar("T")
@ -27,7 +27,7 @@ def anyvar(cls: Type[T], strict: bool = False) -> T:
@pytest.fixture @pytest.fixture
def configuration(resource_path_root: Path) -> Configuration: def configuration(resource_path_root: Path) -> Configuration:
path = resource_path_root / "core" / "ahriman.ini" path = resource_path_root / "core" / "ahriman.ini"
return Configuration.from_path(path=path, logfile=False) return Configuration.from_path(path=path, architecture="x86_64", logfile=False)
@pytest.fixture @pytest.fixture

View File

@ -1,5 +1,4 @@
import pytest import pytest
import shutil
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture

View File

@ -31,4 +31,4 @@ def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Rep
@pytest.fixture @pytest.fixture
def task_ahriman(package_ahriman: Package, configuration: Configuration, repository_paths: RepositoryPaths) -> Task: def task_ahriman(package_ahriman: Package, configuration: Configuration, repository_paths: RepositoryPaths) -> Task:
return Task(package_ahriman, "x86_64", configuration, repository_paths) return Task(package_ahriman, configuration, repository_paths)

View File

@ -0,0 +1,16 @@
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.report.html import HTML
from ahriman.models.package import Package
def test_generate(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must generate report
"""
write_mock = mocker.patch("pathlib.Path.write_text")
report = HTML("x86_64", configuration)
report.generate([package_ahriman])
write_mock.assert_called_once()

View File

@ -0,0 +1,27 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed
from ahriman.core.report.report import Report
from ahriman.models.report_settings import ReportSettings
def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must raise ReportFailed on errors
"""
mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception())
with pytest.raises(ReportFailed):
Report.run("x86_64", configuration, ReportSettings.HTML.name, Path("path"))
def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate html report
"""
report_mock = mocker.patch("ahriman.core.report.html.HTML.generate")
Report.run("x86_64", configuration, ReportSettings.HTML.name, Path("path"))
report_mock.assert_called_once()

View File

@ -107,20 +107,20 @@ def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None:
""" """
must process report in auto mode if no targets supplied must process report in auto mode if no targets supplied
""" """
config_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist") configuration_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
executor.process_report(None) executor.process_report(None)
config_getlist_mock.assert_called_once() configuration_getlist_mock.assert_called_once()
def test_process_sync_auto(executor: Executor, mocker: MockerFixture) -> None: def test_process_sync_auto(executor: Executor, mocker: MockerFixture) -> None:
""" """
must process sync in auto mode if no targets supplied must process sync in auto mode if no targets supplied
""" """
config_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist") configuration_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
executor.process_sync(None) executor.process_sync(None)
config_getlist_mock.assert_called_once() configuration_getlist_mock.assert_called_once()
def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None: def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -7,3 +7,9 @@ from ahriman.core.sign.gpg import GPG
@pytest.fixture @pytest.fixture
def gpg(configuration: Configuration) -> GPG: def gpg(configuration: Configuration) -> GPG:
return GPG("x86_64", configuration) return GPG("x86_64", configuration)
@pytest.fixture
def gpg_with_key(gpg: GPG) -> GPG:
gpg.default_key = "key"
return gpg

View File

@ -1,61 +0,0 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.sign.gpg import GPG
from ahriman.models.sign_settings import SignSettings
def test_repository_sign_args(gpg: GPG) -> None:
"""
must generate correct sign args
"""
gpg.target = {SignSettings.SignRepository}
assert gpg.repository_sign_args
def test_sign_package(gpg: GPG, mocker: MockerFixture) -> None:
"""
must sign package
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.process", return_value=result)
for target in ({SignSettings.SignPackages}, {SignSettings.SignPackages, SignSettings.SignRepository}):
gpg.target = target
assert gpg.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
def test_sign_package_skip(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.process")
for target in ({}, {SignSettings.SignRepository}):
gpg.target = target
process_mock.assert_not_called()
def test_sign_repository(gpg: GPG, mocker: MockerFixture) -> None:
"""
must sign repository
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.process", return_value=result)
for target in ({SignSettings.SignRepository}, {SignSettings.SignPackages, SignSettings.SignRepository}):
gpg.target = target
assert gpg.sign_repository(Path("a")) == result
process_mock.assert_called_once()
def test_sign_repository_skip(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.process")
for target in ({}, {SignSettings.SignPackages}):
gpg.target = target
process_mock.assert_not_called()

View File

@ -0,0 +1,181 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.sign.gpg import GPG
from ahriman.models.sign_settings import SignSettings
def test_repository_sign_args_1(gpg_with_key: GPG) -> None:
"""
must generate correct sign args
"""
gpg_with_key.targets = {SignSettings.SignRepository}
assert gpg_with_key.repository_sign_args
def test_repository_sign_args_2(gpg_with_key: GPG) -> None:
"""
must generate correct sign args
"""
gpg_with_key.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
assert gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_1(gpg_with_key: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg_with_key.targets = {}
assert not gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_2(gpg_with_key: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg_with_key.targets = {SignSettings.SignPackages}
assert not gpg_with_key.repository_sign_args
def test_repository_sign_args_skip_3(gpg: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg.targets = {SignSettings.SignRepository}
assert not gpg.repository_sign_args
def test_repository_sign_args_skip_4(gpg: GPG) -> None:
"""
must return empty args if it is not set
"""
gpg.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
assert not gpg.repository_sign_args
def test_sign_package_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign package
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignPackages}
assert gpg_with_key.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
def test_sign_package_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign package
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
assert gpg_with_key.sign_package(Path("a"), "a") == result
process_mock.assert_called_once()
def test_sign_package_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {}
gpg_with_key.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_package_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.SignRepository}
gpg_with_key.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_package_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignPackages}
gpg.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_package_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign package if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg.sign_package(Path("a"), "a")
process_mock.assert_not_called()
def test_sign_repository_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign repository
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignRepository}
assert gpg_with_key.sign_repository(Path("a")) == result
process_mock.assert_called_once()
def test_sign_repository_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must sign repository
"""
result = [Path("a"), Path("a.sig")]
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process", return_value=result)
gpg_with_key.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
assert gpg_with_key.sign_repository(Path("a")) == result
process_mock.assert_called_once()
def test_sign_repository_skip_1(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {}
gpg_with_key.sign_repository(Path("a"))
process_mock.assert_not_called()
def test_sign_repository_skip_2(gpg_with_key: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg_with_key.targets = {SignSettings.SignPackages}
gpg_with_key.sign_repository(Path("a"))
process_mock.assert_not_called()
def test_sign_repository_skip_3(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignRepository}
gpg.sign_repository(Path("a"))
process_mock.assert_not_called()
def test_sign_repository_skip_4(gpg: GPG, mocker: MockerFixture) -> None:
"""
must not sign repository if it is not set
"""
process_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process")
gpg.targets = {SignSettings.SignPackages, SignSettings.SignRepository}
gpg.sign_repository(Path("a"))
process_mock.assert_not_called()

View File

@ -104,7 +104,7 @@ def test_load_dummy_client(configuration: Configuration) -> None:
""" """
must load dummy client if no settings set must load dummy client if no settings set
""" """
assert isinstance(Client.load("x86_64", configuration), Client) assert isinstance(Client.load(configuration), Client)
def test_load_full_client(configuration: Configuration) -> None: def test_load_full_client(configuration: Configuration) -> None:
@ -113,4 +113,4 @@ def test_load_full_client(configuration: Configuration) -> None:
""" """
configuration.set("web", "host", "localhost") configuration.set("web", "host", "localhost")
configuration.set("web", "port", "8080") configuration.set("web", "port", "8080")
assert isinstance(Client.load("x86_64", configuration), WebClient) assert isinstance(Client.load(configuration), WebClient)

View File

@ -14,13 +14,20 @@ def test_from_path(mocker: MockerFixture) -> None:
load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging") load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging")
path = Path("path") path = Path("path")
config = Configuration.from_path(path, True) config = Configuration.from_path(path, "x86_64", True)
assert config.path == path assert config.path == path
read_mock.assert_called_with(path) read_mock.assert_called_with(path)
load_includes_mock.assert_called_once() load_includes_mock.assert_called_once()
load_logging_mock.assert_called_once() load_logging_mock.assert_called_once()
def test_section_name(configuration: Configuration) -> None:
"""
must return architecture specific group
"""
assert configuration.section_name("build", "x86_64") == "build_x86_64"
def test_absolute_path_for_absolute(configuration: Configuration) -> None: def test_absolute_path_for_absolute(configuration: Configuration) -> None:
""" """
must not change path for absolute path in settings must not change path for absolute path in settings
@ -46,7 +53,7 @@ def test_dump(configuration: Configuration) -> None:
""" """
dump must not be empty dump must not be empty
""" """
assert configuration.dump("x86_64") assert configuration.dump()
def test_dump_architecture_specific(configuration: Configuration) -> None: def test_dump_architecture_specific(configuration: Configuration) -> None:
@ -54,12 +61,14 @@ def test_dump_architecture_specific(configuration: Configuration) -> None:
dump must contain architecture specific settings dump must contain architecture specific settings
""" """
configuration.add_section("build_x86_64") configuration.add_section("build_x86_64")
configuration.set("build_x86_64", "archbuild_flags", "") configuration.set("build_x86_64", "archbuild_flags", "hello flag")
configuration.merge_sections("x86_64")
dump = configuration.dump("x86_64") dump = configuration.dump()
assert dump assert dump
assert "build" not in dump assert "build" in dump
assert "build_x86_64" in dump assert "build_x86_64" not in dump
assert dump["build"]["archbuild_flags"] == "hello flag"
def test_getlist(configuration: Configuration) -> None: def test_getlist(configuration: Configuration) -> None:
@ -87,23 +96,6 @@ def test_getlist_single(configuration: Configuration) -> None:
assert configuration.getlist("build", "test_list") == ["a"] assert configuration.getlist("build", "test_list") == ["a"]
def test_get_section_name(configuration: Configuration) -> None:
"""
must return architecture specific group
"""
configuration.add_section("build_x86_64")
configuration.set("build_x86_64", "archbuild_flags", "")
assert configuration.get_section_name("build", "x86_64") == "build_x86_64"
def test_get_section_name_missing(configuration: Configuration) -> None:
"""
must return default group if architecture depending group does not exist
"""
assert configuration.get_section_name("prefix", "suffix") == "prefix"
assert configuration.get_section_name("build", "x86_64") == "build"
def test_load_includes_missing(configuration: Configuration) -> None: def test_load_includes_missing(configuration: Configuration) -> None:
""" """
must not fail if not include directory found must not fail if not include directory found
@ -127,3 +119,15 @@ def test_load_logging_stderr(configuration: Configuration, mocker: MockerFixture
logging_mock = mocker.patch("logging.config.fileConfig") logging_mock = mocker.patch("logging.config.fileConfig")
configuration.load_logging(False) configuration.load_logging(False)
logging_mock.assert_not_called() logging_mock.assert_not_called()
def test_merge_sections_missing(configuration: Configuration) -> None:
"""
must merge create section if not exists
"""
configuration.remove_section("build")
configuration.add_section("build_x86_64")
configuration.set("build_x86_64", "key", "value")
configuration.merge_sections("x86_64")
assert configuration.get("build", "key") == "value"

View File

@ -0,0 +1,16 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.upload.rsync import Rsync
def test_sync(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run sync command
"""
check_output_mock = mocker.patch("ahriman.core.upload.rsync.Rsync._check_output")
upload = Rsync("x86_64", configuration)
upload.sync(Path("path"))
check_output_mock.assert_called_once()

View File

@ -0,0 +1,16 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.upload.s3 import S3
def test_sync(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run sync command
"""
check_output_mock = mocker.patch("ahriman.core.upload.s3.S3._check_output")
upload = S3("x86_64", configuration)
upload.sync(Path("path"))
check_output_mock.assert_called_once()

View File

@ -0,0 +1,36 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import SyncFailed
from ahriman.core.upload.upload import Upload
from ahriman.models.upload_settings import UploadSettings
def test_upload_failure(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must raise SyncFailed on errors
"""
mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception())
with pytest.raises(SyncFailed):
Upload.run("x86_64", configuration, UploadSettings.Rsync.name, Path("path"))
def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must upload via rsync
"""
upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync")
Upload.run("x86_64", configuration, UploadSettings.Rsync.name, Path("path"))
upload_mock.assert_called_once()
def test_upload_s3(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must upload via s3
"""
upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync")
Upload.run("x86_64", configuration, UploadSettings.S3.name, Path("path"))
upload_mock.assert_called_once()

View File

@ -1,8 +1,10 @@
import pytest import pytest
from unittest.mock import MagicMock, PropertyMock
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_desciption import PackageDescription from ahriman.models.package_description import PackageDescription
@pytest.fixture @pytest.fixture
@ -17,3 +19,33 @@ def package_tpacpi_bat_git() -> Package:
version="3.1.r12.g4959b52-1", version="3.1.r12.g4959b52-1",
aur_url="https://aur.archlinux.org", aur_url="https://aur.archlinux.org",
packages={"tpacpi-bat-git": PackageDescription()}) packages={"tpacpi-bat-git": PackageDescription()})
@pytest.fixture
def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
mock = MagicMock()
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
return mock
@pytest.fixture
def pyalpm_package_ahriman(package_ahriman: Package) -> MagicMock:
mock = MagicMock()
type(mock).base = PropertyMock(return_value=package_ahriman.base)
type(mock).name = PropertyMock(return_value=package_ahriman.base)
type(mock).version = PropertyMock(return_value=package_ahriman.version)
return mock
@pytest.fixture
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
mock = MagicMock()
type(mock).arch = PropertyMock(return_value=package_description_ahriman.architecture)
type(mock).builddate = PropertyMock(return_value=package_description_ahriman.build_date)
type(mock).desc = PropertyMock(return_value=package_description_ahriman.description)
type(mock).groups = PropertyMock(return_value=package_description_ahriman.groups)
type(mock).isize = PropertyMock(return_value=package_description_ahriman.installed_size)
type(mock).licenses = PropertyMock(return_value=package_description_ahriman.licenses)
type(mock).size = PropertyMock(return_value=package_description_ahriman.archive_size)
type(mock).url = PropertyMock(return_value=package_description_ahriman.url)
return mock

View File

@ -1,6 +1,10 @@
import pytest
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import MagicMock, PropertyMock
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -72,6 +76,44 @@ def test_web_url(package_ahriman: Package) -> None:
assert package_ahriman.base in package_ahriman.web_url assert package_ahriman.base in package_ahriman.web_url
def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must construct package from alpm library
"""
mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
return_value=package_ahriman.packages[package_ahriman.base])
assert Package.from_archive(Path("path"), pyalpm_handle, package_ahriman.aur_url) == package_ahriman
def test_from_aur(package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must construct package from aur
"""
mock = MagicMock()
type(mock).name = PropertyMock(return_value=package_ahriman.base)
type(mock).package_base = PropertyMock(return_value=package_ahriman.base)
type(mock).version = PropertyMock(return_value=package_ahriman.version)
mocker.patch("aur.info", return_value=mock)
package = Package.from_aur(package_ahriman.base, package_ahriman.aur_url)
assert package_ahriman.base == package.base
assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys()
def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must construct package from srcinfo
"""
srcinfo = (resource_path_root / "models" / "package_ahriman_srcinfo").read_text()
mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
package = Package.from_build(Path("path"), package_ahriman.aur_url)
assert package_ahriman.packages.keys() == package.packages.keys()
package_ahriman.packages = package.packages # we are not going to test PackageDescription here
assert package_ahriman == package
def test_from_json_view_1(package_ahriman: Package) -> None: def test_from_json_view_1(package_ahriman: Package) -> None:
""" """
must construct same object from json must construct same object from json
@ -98,12 +140,64 @@ def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Pa
must load correct list of dependencies with version must load correct list of dependencies with version
""" """
srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text()
mocker.patch("pathlib.Path.read_text", return_value=srcinfo) mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
assert Package.dependencies(Path("path")) == {"git", "go", "pacman"} assert Package.dependencies(Path("path")) == {"git", "go", "pacman"}
def test_full_version() -> None:
"""
must construct full version
"""
assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1"
assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1"
def test_load_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from package archive
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_archive")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
def test_load_from_aur(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from AUR
"""
load_mock = mocker.patch("ahriman.models.package.Package.from_aur")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
def test_load_from_build(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must load package from build directory
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
load_mock = mocker.patch("ahriman.models.package.Package.from_build")
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
load_mock.assert_called_once()
def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None:
"""
must raise InvalidPackageInfo on exception
"""
mocker.patch("pathlib.Path.is_dir", side_effect=InvalidPackageInfo("exception!"))
with pytest.raises(InvalidPackageInfo):
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
mocker.patch("pathlib.Path.is_dir", side_effect=Exception())
with pytest.raises(InvalidPackageInfo):
Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url)
def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None: def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
""" """
must return same actual_version as version is must return same actual_version as version is
@ -117,7 +211,6 @@ def test_actual_version_vcs(package_tpacpi_bat_git: Package, repository_paths: R
must return valid actual_version for VCS package must return valid actual_version for VCS package
""" """
srcinfo = (resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo").read_text() srcinfo = (resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo").read_text()
mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo) mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
mocker.patch("ahriman.core.build_tools.task.Task.fetch") mocker.patch("ahriman.core.build_tools.task.Task.fetch")

View File

@ -1,4 +1,6 @@
from ahriman.models.package_desciption import PackageDescription from unittest.mock import MagicMock
from ahriman.models.package_description import PackageDescription
def test_filepath(package_description_ahriman: PackageDescription) -> None: def test_filepath(package_description_ahriman: PackageDescription) -> None:
@ -15,3 +17,13 @@ def test_filepath_empty(package_description_ahriman: PackageDescription) -> None
""" """
package_description_ahriman.filename = None package_description_ahriman.filename = None
assert package_description_ahriman.filepath is None assert package_description_ahriman.filepath is None
def test_from_package(package_description_ahriman: PackageDescription,
pyalpm_package_description_ahriman: MagicMock) -> None:
"""
must construct description from package object
"""
package_description = PackageDescription.from_package(pyalpm_package_description_ahriman,
package_description_ahriman.filepath)
assert package_description_ahriman == package_description

View File

@ -36,10 +36,10 @@ def test_run(application: web.Application, mocker: MockerFixture) -> None:
""" """
host = "localhost" host = "localhost"
port = 8080 port = 8080
application["config"].set("web", "host", host) application["configuration"].set("web", "host", host)
application["config"].set("web", "port", str(port)) application["configuration"].set("web", "port", str(port))
run_app_mock = mocker.patch("aiohttp.web.run_app") run_application_mock = mocker.patch("aiohttp.web.run_app")
run_server(application) run_server(application)
run_app_mock.assert_called_with(application, host=host, port=port, run_application_mock.assert_called_with(application, host=host, port=port,
handle_signals=False, access_log=pytest.helpers.anyvar(int)) handle_signals=False, access_log=pytest.helpers.anyvar(int))

View File

@ -21,7 +21,6 @@ root = /var/lib/ahriman
[sign] [sign]
target = target =
key =
[report] [report]
target = target =
@ -36,10 +35,12 @@ template_path = ../web/templates/repo-index.jinja2
target = target =
[rsync] [rsync]
command = rsync --archive --verbose --compress --partial --delete
remote = remote =
[s3] [s3]
bucket = bucket =
command = aws s3 sync --quiet --delete
[web] [web]
templates = ../web/templates templates = ../web/templates

View File

@ -0,0 +1,34 @@
pkgbase = ahriman
pkgdesc = ArcHlinux ReposItory MANager
pkgver = 0.12.1
pkgrel = 1
url = https://github.com/arcan1s/ahriman
arch = any
license = GPL3
makedepends = python-pip
depends = devtools
depends = git
depends = pyalpm
depends = python-aur
depends = python-srcinfo
optdepends = aws-cli: sync to s3
optdepends = breezy: -bzr packages support
optdepends = darcs: -darcs packages support
optdepends = gnupg: package and repository sign
optdepends = mercurial: -hg packages support
optdepends = python-aiohttp: web server
optdepends = python-aiohttp-jinja2: web server
optdepends = python-jinja: html report generation
optdepends = python-requests: web server
optdepends = rsync: sync by using rsync
optdepends = subversion: -svn packages support
backup = etc/ahriman.ini
backup = etc/ahriman.ini.d/logging.ini
source = https://github.com/arcan1s/ahriman/releases/download/0.12.1/ahriman-0.12.1-src.tar.xz
source = ahriman.sysusers
source = ahriman.tmpfiles
sha512sums = 8acc57f937d587ca665c29092cadddbaf3ba0b80e870b80d1551e283aba8f21306f9030a26fec8c71ab5863316f5f5f061b7ddc63cdff9e6d5a885f28ef1893d
sha512sums = 13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075
sha512sums = 55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4
pkgname = ahriman