commit 53d21d6496ab902b664a678a2f2d3a7f4e96d8d1 Author: Evgeniy Alekseev Date: Tue Mar 2 12:17:01 2021 +0300 initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a4d5b094 --- /dev/null +++ b/.gitignore @@ -0,0 +1,96 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +*.deb + +.idea/ + +.mypy_cache/ + +.venv/ + +*.tar.xz diff --git a/make_release.sh b/make_release.sh new file mode 100755 index 00000000..83969658 --- /dev/null +++ b/make_release.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +VERSION="$1" +ARCHIVE="ahriman" +FILES="package src setup.py" +IGNORELIST="build .idea package/archlinux package/*src.tar.xz" + +# set version +sed -i "/__version__ = '[0-9.]*/s/[^'][^)]*/__version__ = '$VERSION'/" src/ahriman/version.py + +# create archive +[[ -e ${ARCHIVE}-${VERSION}-src.tar.xz ]] && rm -f "${ARCHIVE}-${VERSION}-src.tar.xz" +[[ -d $ARCHIVE ]] && rm -rf "$ARCHIVE" +mkdir "$ARCHIVE" +for FILE in ${FILES[*]}; do cp -r "$FILE" "$ARCHIVE"; done +for FILE in ${IGNORELIST[*]}; do rm -rf "${ARCHIVE}/${FILE}"; done +tar cJf "${ARCHIVE}-${VERSION}-src.tar.xz" "$ARCHIVE" +rm -rf "$ARCHIVE" + +# update checksums +SHA512SUMS=$(sha512sum ${ARCHIVE}-${VERSION}-src.tar.xz | awk '{print $1}') +sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$SHA512SUMS'/" package/archlinux/PKGBUILD +sed -i "s/pkgver=[0-9.]*/pkgver=$VERSION/" package/archlinux/PKGBUILD + +# clear +find . -type f -name '*src.tar.xz' -not -name "*${VERSION}-src.tar.xz" -exec rm -rf {} \; + +exit 0 + +# tag +git add package/archlinux/PKGBUILD +git commit -m "Release $VERSION" && git push +git tag $VERSION && git push --tags diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD new file mode 100644 index 00000000..cb5070ea --- /dev/null +++ b/package/archlinux/PKGBUILD @@ -0,0 +1,38 @@ +# Maintainer: Evgeniy Alekseev + +pkgname='ahriman' +pkgver=0.1.0 +pkgrel=1 +pkgdesc="ArcHlinux ReposItory MANager" +arch=('any') +url="https://github.com/arcan1s/ahriman" +license=('GPL3') +depends=('devtools' 'python-aur' 'python-srcinfo') +makedepends=('python-pip') +optdepends=('gnupg: package and repository sign support') +source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz" + 'ahriman.sudoers' + 'ahriman.sysusers' + 'ahriman.tmpfiles') +sha512sums=('d42d779279493c0de86f8e6880cd644a2d549d61cf6c03c27706a155ca4350158d9a309ac77377de13002071727f2e8532144fb3aa1f2ff95811bd9f3cffd9f3' + '8c9b5b63ac3f7b4d9debaf801a1e9c060877c33d3ecafe18010fcca778e5fa2f2e46909d3d0ff1b229ff8aa978445d8243fd36e1fc104117ed678d5e21901167' + '13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075' + '55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4') +backup=('etc/ahriman.ini' + 'etc/ahriman.ini.d/logging.ini') + +build() { + cd "$pkgname" + + python setup.py build +} + +package() { + cd "$pkgname" + + python setup.py install --root="$pkgdir" + + install -Dm400 "$srcdir/$pkgname.sudoers" "$pkgdir/etc/sudoers.d/$pkgname" + install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf" + install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf" +} diff --git a/package/archlinux/ahriman.sudoers b/package/archlinux/ahriman.sudoers new file mode 100644 index 00000000..d633ca28 --- /dev/null +++ b/package/archlinux/ahriman.sudoers @@ -0,0 +1,4 @@ +# Used by ArcHlinux ReposItory MANager with default settings + +Cmnd_Alias ARCHBUILD_CMD = /usr/bin/extra-x86_64-build *, /usr/bin/multilib-build * +ahriman ALL=(ALL) NOPASSWD: ARCHBUILD_CMD \ No newline at end of file diff --git a/package/archlinux/ahriman.sysusers b/package/archlinux/ahriman.sysusers new file mode 100644 index 00000000..1ab8b466 --- /dev/null +++ b/package/archlinux/ahriman.sysusers @@ -0,0 +1 @@ +u ahriman 643 "ArcHlinux ReposItory MANager" /var/lib/ahriman \ No newline at end of file diff --git a/package/archlinux/ahriman.tmpfiles b/package/archlinux/ahriman.tmpfiles new file mode 100644 index 00000000..b14c1d94 --- /dev/null +++ b/package/archlinux/ahriman.tmpfiles @@ -0,0 +1,2 @@ +d /var/lib/ahriman 0775 ahriman log +d /var/log/ahriman 0755 ahriman ahriman \ No newline at end of file diff --git a/package/bin/ahriman b/package/bin/ahriman new file mode 100755 index 00000000..34fe177f --- /dev/null +++ b/package/bin/ahriman @@ -0,0 +1,3 @@ +#!/bin/sh + +exec python -B -m ahriman.application.ahriman "$@" \ No newline at end of file diff --git a/package/etc/ahriman.ini b/package/etc/ahriman.ini new file mode 100644 index 00000000..4d8a29c1 --- /dev/null +++ b/package/etc/ahriman.ini @@ -0,0 +1,20 @@ +[settings] +include = /etc/ahriman.ini.d +logging = /etc/ahriman.ini.d/logging.ini + +[aur] +url = https://aur.archlinux.org + +[build] +archbuild_flags = -c +extra_build = extra-x86_64-build +makepkg_flags = --skippgpcheck +multilib_build = multilib-build + +[repository] +name = aur-clone +root = /var/lib/ahriman + +[sign] +enabled = disabled +key = diff --git a/package/etc/ahriman.ini.d/logging.ini b/package/etc/ahriman.ini.d/logging.ini new file mode 100644 index 00000000..09c2a770 --- /dev/null +++ b/package/etc/ahriman.ini.d/logging.ini @@ -0,0 +1,47 @@ +[loggers] +keys = root,builder,build_details + +[handlers] +keys = console_handler,build_file_handler,file_handler + +[formatters] +keys = generic_format + +[handler_console_handler] +class = StreamHandler +level = DEBUG +formatter = generic_format +args = (sys.stdout,) + +[handler_file_handler] +class = logging.handlers.RotatingFileHandler +level = DEBUG +formatter = generic_format +args = ('/var/log/ahriman/ahriman.log', 'a', 20971520, 20) + +[handler_build_file_handler] +class = logging.handlers.RotatingFileHandler +level = DEBUG +formatter = generic_format +args = ('/var/log/ahriman/build.log', 'a', 20971520, 20) + +[formatter_generic_format] +format = %(asctime)s : %(levelname)s : %(funcName)s : %(message)s +datefmt = + +[logger_root] +level = DEBUG +handlers = file_handler +qualname = root + +[logger_builder] +level = DEBUG +handlers = file_handler +qualname = builder +propagate = 0 + +[logger_build_details] +level = DEBUG +handlers = build_file_handler +qualname = build_details +propagate = 0 diff --git a/package/lib/systemd/system/ahriman.service b/package/lib/systemd/system/ahriman.service new file mode 100644 index 00000000..3a2c8238 --- /dev/null +++ b/package/lib/systemd/system/ahriman.service @@ -0,0 +1,7 @@ +[Unit] +Description=ArcHlinux ReposItory MANager + +[Service] +ExecStart=/usr/bin/ahriman update +User=ahriman +Group=ahriman \ No newline at end of file diff --git a/package/lib/systemd/system/ahriman.timer b/package/lib/systemd/system/ahriman.timer new file mode 100644 index 00000000..bcee1f2c --- /dev/null +++ b/package/lib/systemd/system/ahriman.timer @@ -0,0 +1,9 @@ +[Unit] +Description=ArcHlinux ReposItory MANager timer + +[Timer] +OnCalendar=daily +RandomizedDelaySec=3600 + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..aa26867e --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +from distutils.util import convert_path +from setuptools import setup, find_packages +from os import path + +here = path.abspath(path.dirname(__file__)) +metadata = dict() +with open(convert_path('src/ahriman/version.py')) as metadata_file: + exec(metadata_file.read(), metadata) + +setup( + name='ahriman', + + version=metadata['__version__'], + zip_safe=False, + + description='ArcHlinux ReposItory MANager', + + author='arcanis', + author_email='', + url='', + + license='GPL3', + + packages=find_packages('src'), + package_dir={'': 'src'}, + + dependency_links=[ + ], + install_requires=[ + 'aur', + 'srcinfo', + ], + setup_requires=[ + 'pytest-runner', + ], + tests_require=[ + 'pytest', + ], + + include_package_data=True, + scripts=[ + 'package/bin/ahriman' + ], + data_files=[ + ('/etc', ['package/etc/ahriman.ini']), + ('/etc/ahriman.ini.d', ['package/etc/ahriman.ini.d/logging.ini']), + ('lib/systemd/system', [ + 'package/lib/systemd/system/ahriman.service', + 'package/lib/systemd/system/ahriman.timer' + ]) + ], + + extras_require={ + 'test': ['coverage', 'pytest'], + }, +) diff --git a/src/ahriman/__init__.py b/src/ahriman/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ahriman/application/__init__.py b/src/ahriman/application/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py new file mode 100644 index 00000000..faebba4a --- /dev/null +++ b/src/ahriman/application/ahriman.py @@ -0,0 +1,76 @@ +import argparse +import os + +import ahriman.version as version + +from ahriman.application.application import Application +from ahriman.core.configuration import Configuration + + +def _get_app(args: argparse.Namespace) -> Application: + config = _get_config(args.config) + return Application(config) + + +def _get_config(config_path: str) -> Configuration: + config = Configuration() + config.load(config_path) + config.load_logging() + return config + + +def _remove_lock(path: str) -> None: + try: + os.remove(path) + except FileNotFoundError: + pass + + +def add(args: argparse.Namespace) -> None: + _get_app(args).add(args.package) + + +def remove(args: argparse.Namespace) -> None: + _get_app(args).remove(args.package) + + +def update(args: argparse.Namespace) -> None: + _get_app(args).update() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='ArcHlinux ReposItory MANager') + parser.add_argument('-c', '--config', help='configuration path', default='/etc/ahriman.ini') + 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('-v', '--version', action='version', version=version.__version__) + subparsers = parser.add_subparsers(title='commands') + + add_parser = subparsers.add_parser('add', description='add package') + add_parser.add_argument('package', help='package name', nargs='+') + add_parser.set_defaults(fn=add) + + remove_parser = subparsers.add_parser('remove', description='remove package') + remove_parser.add_argument('package', help='package name', nargs='+') + remove_parser.set_defaults(fn=remove) + + update_parser = subparsers.add_parser('update', description='run updates') + update_parser.set_defaults(fn=update) + + args = parser.parse_args() + + if args.force: + _remove_lock(args.lock) + if os.path.exists(args.lock): + raise RuntimeError('Another application instance is run') + + if 'fn' not in args: + parser.print_help() + exit(1) + + try: + open(args.lock, 'w').close() + args.fn(args) + finally: + _remove_lock(args.lock) + diff --git a/src/ahriman/application/application.py b/src/ahriman/application/application.py new file mode 100644 index 00000000..33833ab7 --- /dev/null +++ b/src/ahriman/application/application.py @@ -0,0 +1,29 @@ +import os + +from typing import List + +from ahriman.core.configuration import Configuration +from ahriman.core.repository import Repository +from ahriman.core.task import Task +from ahriman.models.package import Package + + +class Application: + + def __init__(self, config: Configuration) -> None: + self.config = config + self.repository = Repository(config) + + def add(self, names: List[str]) -> None: + for name in names: + package = Package.load(name, self.config.get('aur', 'url')) + task = Task(package, self.config, self.repository.paths) + task.fetch(os.path.join(self.repository.paths.manual, package.name)) + + def remove(self, names: List[str]) -> None: + self.repository.process_remove(names) + + def update(self) -> None: + updates = self.repository.updates() + packages = self.repository.process_build(updates) + self.repository.process_update(packages) \ No newline at end of file diff --git a/src/ahriman/core/__init__.py b/src/ahriman/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ahriman/core/configuration.py b/src/ahriman/core/configuration.py new file mode 100644 index 00000000..39faecf5 --- /dev/null +++ b/src/ahriman/core/configuration.py @@ -0,0 +1,40 @@ +import configparser +import os + +from logging.config import fileConfig +from typing import Any, Dict, Optional + +from ahriman.core.exceptions import MissingConfiguration + + +# built-in configparser extension +class Configuration(configparser.RawConfigParser): + + def __init__(self) -> None: + configparser.RawConfigParser.__init__(self, allow_no_value=True) + self.path = None # type: Optional[str] + + @property + def include(self) -> str: + return self.get('settings', 'include') + + def get_section(self, section: str) -> Dict[str, str]: + if not self.has_section(section): + raise MissingConfiguration(section) + return dict(self[section]) + + def load(self, path: str) -> None: + self.path = path + self.read(self.path) + self.load_includes() + + def load_includes(self) -> None: + try: + include_dir = self.include + for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(include_dir))): + self.read(os.path.join(self.include, conf)) + except (FileNotFoundError, configparser.NoOptionError): + pass + + def load_logging(self) -> None: + fileConfig(self.get('settings', 'logging')) diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py new file mode 100644 index 00000000..72f23d0b --- /dev/null +++ b/src/ahriman/core/exceptions.py @@ -0,0 +1,21 @@ +from typing import Any + + +class BuildFailed(Exception): + def __init__(self, package: str) -> None: + Exception.__init__(self, f'Package {package} build failed, check logs for details') + + +class InvalidOptionException(Exception): + def __init__(self, value: Any) -> None: + Exception.__init__(self, f'Invalid or unknown option value `{value}`') + + +class InvalidPackageInfo(Exception): + def __init__(self, details: Any) -> None: + Exception.__init__(self, f'There are errors during reading package information: `{details}`') + + +class MissingConfiguration(Exception): + def __init__(self, name: str) -> None: + Exception.__init__(self, f'No section `{name}` found') diff --git a/src/ahriman/core/repo_wrapper.py b/src/ahriman/core/repo_wrapper.py new file mode 100644 index 00000000..65e70351 --- /dev/null +++ b/src/ahriman/core/repo_wrapper.py @@ -0,0 +1,40 @@ +import logging +import os + +from ahriman.core.exceptions import BuildFailed +from ahriman.core.util import check_output +from ahriman.models.repository_paths import RepositoryPaths + + +class RepoWrapper: + + def __init__(self, name: str, paths: RepositoryPaths) -> None: + self.logger = logging.getLogger('build_details') + self.name = name + self.paths = paths + + @property + def repo_path(self) -> str: + return os.path.join(self.paths.repository, f'{self.name}.db.tar.gz') + + def add(self, path: str) -> None: + check_output( + 'repo-add', '-R', self.repo_path, path, + exception=BuildFailed(path), + cwd=self.paths.repository, + logger=self.logger) + + def remove(self, path: str, package: str) -> None: + try: + os.remove(path) + except FileNotFoundError: + pass + try: + os.remove(f'{path}.sig') # sign if any + except FileNotFoundError: + pass + check_output( + 'repo-remove', self.repo_path, package, + exception=BuildFailed(path), + cwd=self.paths.repository, + logger=self.logger) diff --git a/src/ahriman/core/repository.py b/src/ahriman/core/repository.py new file mode 100644 index 00000000..fd696237 --- /dev/null +++ b/src/ahriman/core/repository.py @@ -0,0 +1,123 @@ +import logging +import os +import shutil + +from typing import List + +from ahriman.core.configuration import Configuration +from ahriman.core.repo_wrapper import RepoWrapper +from ahriman.core.sign import Sign +from ahriman.core.task import Task +from ahriman.models.package import Package +from ahriman.models.repository_paths import RepositoryPaths + + +class Repository: + + def __init__(self, config: Configuration) -> None: + self.logger = logging.getLogger('builder') + self.config = config + + self.aur_url = config.get('aur', 'url') + self.name = config.get('repository', 'name') + + self.paths = RepositoryPaths(config.get('repository', 'root')) + self.paths.create_tree() + + self.sign = Sign(config) + self.wrapper = RepoWrapper(self.name, self.paths) + + def _clear_build(self) -> None: + for package in os.listdir(self.paths.sources): + shutil.rmtree(os.path.join(self.paths.sources, package), ignore_errors=True) + + def _clear_manual(self) -> None: + for package in os.listdir(self.paths.manual): + shutil.rmtree(os.path.join(self.paths.manual, package), ignore_errors=True) + + def _clear_packages(self) -> None: + for package in os.listdir(self.paths.packages): + shutil.rmtree(os.path.join(self.paths.packages, package), ignore_errors=True) + + def process_build(self, updates: List[Package]) -> List[str]: + for package in updates: + try: + task = Task(package, self.config, self.paths) + task.fetch() + built = task.build() + for src in built: + dst = os.path.join(self.paths.packages, os.path.basename(src)) + shutil.move(src, dst) + except Exception: + self.logger.exception(f'{package.name} build exception', exc_info=True) + continue + self._clear_build() + + return [ + os.path.join(self.paths.packages, fn) + for fn in os.listdir(self.paths.packages) + ] + + def process_remove(self, packages: List[str]) -> str: + for fn in os.listdir(self.paths.repository): + if '.pkg.' not in fn: + continue + + full_path = os.path.join(self.paths.repository, fn) + try: + local = Package.load(full_path, self.aur_url) + if local.name not in packages: + continue + self.wrapper.remove(full_path, local.name) + except Exception: + self.logger.exception(f'could not load package from {fn}', exc_info=True) + continue + + self.sign.sign_repository(self.wrapper.repo_path) + return self.wrapper.repo_path + + def process_update(self, packages: List[str]) -> str: + for package in packages: + files = self.sign.sign_package(package) + for src in files: + dst = os.path.join(self.paths.repository, os.path.basename(src)) + shutil.move(src, dst) + package_fn = os.path.join(self.paths.repository, os.path.basename(package)) + self.wrapper.add(package_fn) + self._clear_packages() + + self.sign.sign_repository(self.wrapper.repo_path) + return self.wrapper.repo_path + + def updates(self) -> List[Package]: + result: List[Package] = [] + checked_base: List[str] = [] + + # repository updates + for fn in os.listdir(self.paths.repository): + if '.pkg.' not in fn: + continue + + try: + local = Package.load(os.path.join(self.paths.repository, fn), self.aur_url) + remote = Package.load(local.name, self.aur_url) + except Exception: + self.logger.exception(f'could not load package from {fn}', exc_info=True) + continue + if local.name in checked_base: + continue + + if local.is_outdated(remote): + result.append(remote) + checked_base.append(local.name) + + # manual updates + for fn in os.listdir(self.paths.manual): + local = Package.load(os.path.join(self.paths.manual, fn), self.aur_url) + if local.name in checked_base: + continue + result.append(local) + checked_base.append(local.name) + self._clear_manual() + + return result \ No newline at end of file diff --git a/src/ahriman/core/sign.py b/src/ahriman/core/sign.py new file mode 100644 index 00000000..12112d7f --- /dev/null +++ b/src/ahriman/core/sign.py @@ -0,0 +1,44 @@ +import logging +import os + +from typing import List + +from ahriman.core.configuration import Configuration +from ahriman.core.exceptions import BuildFailed +from ahriman.core.util import check_output +from ahriman.models.sign_settings import SignSettings + + +class Sign: + + def __init__(self, config: Configuration) -> None: + self.logger = logging.getLogger('build_details') + + self.key = config.get('sign', 'key', fallback=None) + self.sign = SignSettings.from_option(config.get('sign', 'enabled')) + + def process(self, path: str) -> List[str]: + cwd = os.path.dirname(path) + check_output( + *self.sign_cmd(path), + exception=BuildFailed(path), + cwd=os.path.dirname(path), + logger=self.logger) + return [path, f'{path}.sig'] + + def sign_cmd(self, path: str) -> List[str]: + cmd = ['gpg'] + if self.key is not None: + cmd.extend(['-u', self.key]) + cmd.extend(['-b', path]) + return cmd + + def sign_package(self, path: str) -> List[str]: + if self.sign != SignSettings.SignPackages: + return [path] + return self.process(path) + + def sign_repository(self, path: str) -> List[str]: + if self.sign != SignSettings.SignRepository: + return [path] + return self.process(path) \ No newline at end of file diff --git a/src/ahriman/core/task.py b/src/ahriman/core/task.py new file mode 100644 index 00000000..f8618c40 --- /dev/null +++ b/src/ahriman/core/task.py @@ -0,0 +1,54 @@ +import os +import logging +import shutil + +from typing import List, Optional + +from ahriman.core.configuration import Configuration +from ahriman.core.exceptions import BuildFailed +from ahriman.core.util import check_output +from ahriman.models.package import Package +from ahriman.models.repository_paths import RepositoryPaths + + +class Task: + + def __init__(self, package: Package, config: Configuration, paths: RepositoryPaths) -> None: + self.logger = logging.getLogger('builder') + self.build_logger = logging.getLogger('build_details') + self.package = package + self.paths = paths + + self.archbuild_flags = config.get('build', 'archbuild_flags').split() + self.extra_build = config.get('build', 'extra_build') + self.makepkg_flags = config.get('build', 'makepkg_flags').split() + self.multilib_build = config.get('build', 'multilib_build') + + @property + def git_path(self) -> str: + return os.path.join(self.paths.sources, self.package.name) + + def build(self) -> List[str]: + build_tool = self.multilib_build if self.package.is_multilib else self.extra_build + + cmd = [build_tool, '-r', self.paths.chroot] + cmd.extend(self.archbuild_flags) + if self.makepkg_flags: + cmd.extend(['--', '--'] + self.makepkg_flags) + self.logger.info(f'using {cmd} for {self.package.name}') + + check_output( + *cmd, + exception=BuildFailed(self.package.name), + cwd=self.git_path, + logger=self.build_logger) + + # well it is not actually correct, but we can deal with it + return check_output('makepkg', '--packagelist', + exception=BuildFailed(self.package.name), + cwd=self.git_path).splitlines() + + def fetch(self, path: Optional[str] = None) -> None: + git_path = path or self.git_path + shutil.rmtree(git_path, ignore_errors=True) + check_output('git', 'clone', self.package.url, git_path, exception=None) diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py new file mode 100644 index 00000000..2cde9164 --- /dev/null +++ b/src/ahriman/core/util.py @@ -0,0 +1,20 @@ +import subprocess + +from logging import Logger +from typing import Optional + + +def check_output(*args: str, exception: Optional[Exception], + cwd = None, stderr: int = subprocess.STDOUT, + logger: Optional[Logger] = None) -> str: + try: + result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode('utf8').strip() + if logger is not None: + for line in result.splitlines(): + logger.debug(line) + except subprocess.CalledProcessError as e: + if e.output is not None and logger is not None: + for line in e.output.decode('utf8').splitlines(): + logger.debug(line) + raise exception or e + return result \ No newline at end of file diff --git a/src/ahriman/models/__init__.py b/src/ahriman/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py new file mode 100644 index 00000000..79aefb4a --- /dev/null +++ b/src/ahriman/models/package.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import aur +import os + +from configparser import RawConfigParser +from dataclasses import dataclass +from srcinfo.parse import parse_srcinfo +from typing import Type + +from ahriman.core.exceptions import InvalidPackageInfo +from ahriman.core.util import check_output + + +@dataclass +class Package: + name: str + version: str + url: str + + @property + def is_multilib(self) -> bool: + return self.name.startswith('lib32-') + + @classmethod + def from_archive(cls: Type[Package], path: str, aur_url: str) -> Package: + name, version = check_output('expac', '-p', '%n %v', path, exception=None).split() + return cls(name, version, f'{aur_url}/{name}.git') + + @classmethod + def from_aur(cls: Type[Package], name: str, aur_url: str)-> Package: + package = aur.info(name) + return cls(package.name, package.version, f'{aur_url}/{name}.git') + + @classmethod + def from_build(cls: Type[Package], path: str) -> Package: + git_config = RawConfigParser() + git_config.read(os.path.join(path, '.git', 'config')) + + with open(os.path.join(path, '.SRCINFO')) as fn: + src_info, errors = parse_srcinfo(fn.read()) + if errors: + raise InvalidPackageInfo(errors) + + return cls(src_info['pkgbase'], f'{src_info["pkgver"]}-{src_info["pkgrel"]}', + git_config.get('remote "origin"', 'url')) + + @classmethod + def load(cls: Type[Package], path: str, aur_url: str) -> Package: + try: + if os.path.isdir(path): + package: Package = cls.from_build(path) + elif os.path.exists(path): + package = cls.from_archive(path, aur_url) + else: + package = cls.from_aur(path, aur_url) + return package + except InvalidPackageInfo: + raise + except Exception as e: + raise InvalidPackageInfo(str(e)) + + def is_outdated(self, remote: Package) -> bool: + result = check_output('vercmp', self.version, remote.version, exception=None) + return True if int(result) < 0 else False \ No newline at end of file diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py new file mode 100644 index 00000000..002caa94 --- /dev/null +++ b/src/ahriman/models/repository_paths.py @@ -0,0 +1,35 @@ +import os + +from dataclasses import dataclass + + +@dataclass +class RepositoryPaths: + root: str + + @property + def chroot(self) -> str: + return os.path.join(self.root, 'chroot') + + @property + def manual(self) -> str: + return os.path.join(self.root, 'manual') + + @property + def packages(self) -> str: + return os.path.join(self.root, 'packages') + + @property + def repository(self) -> str: + return os.path.join(self.root, 'repository') + + @property + def sources(self) -> str: + return os.path.join(self.root, 'sources') + + def create_tree(self) -> None: + os.makedirs(self.chroot, mode=0o755, exist_ok=True) + os.makedirs(self.manual, mode=0o755, exist_ok=True) + os.makedirs(self.packages, mode=0o755, exist_ok=True) + os.makedirs(self.repository, mode=0o755, exist_ok=True) + os.makedirs(self.sources, mode=0o755, exist_ok=True) \ No newline at end of file diff --git a/src/ahriman/models/sign_settings.py b/src/ahriman/models/sign_settings.py new file mode 100644 index 00000000..611f2bfe --- /dev/null +++ b/src/ahriman/models/sign_settings.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from enum import Enum, auto +from typing import Type + +from ahriman.core.exceptions import InvalidOptionException + + +class SignSettings(Enum): + Disabled = auto() + SignPackages = auto() + SignRepository = auto() + + @classmethod + def from_option(cls: Type[SignSettings], value: str) -> SignSettings: + if value.lower() in ('no', 'disabled'): + return cls.Disabled + elif value.lower() in ('package', 'packages', 'sign-package'): + return cls.SignPackages + elif value.lower() in ('repository', 'sign-repository'): + return cls.SignRepository + raise InvalidOptionException(value) \ No newline at end of file diff --git a/src/ahriman/version.py b/src/ahriman/version.py new file mode 100644 index 00000000..541f859d --- /dev/null +++ b/src/ahriman/version.py @@ -0,0 +1 @@ +__version__ = '0.1.0' \ No newline at end of file