mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
initial import
This commit is contained in:
commit
53d21d6496
96
.gitignore
vendored
Normal file
96
.gitignore
vendored
Normal file
@ -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
|
35
make_release.sh
Executable file
35
make_release.sh
Executable file
@ -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
|
38
package/archlinux/PKGBUILD
Normal file
38
package/archlinux/PKGBUILD
Normal file
@ -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"
|
||||||
|
}
|
4
package/archlinux/ahriman.sudoers
Normal file
4
package/archlinux/ahriman.sudoers
Normal file
@ -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
|
1
package/archlinux/ahriman.sysusers
Normal file
1
package/archlinux/ahriman.sysusers
Normal file
@ -0,0 +1 @@
|
|||||||
|
u ahriman 643 "ArcHlinux ReposItory MANager" /var/lib/ahriman
|
2
package/archlinux/ahriman.tmpfiles
Normal file
2
package/archlinux/ahriman.tmpfiles
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
d /var/lib/ahriman 0775 ahriman log
|
||||||
|
d /var/log/ahriman 0755 ahriman ahriman
|
3
package/bin/ahriman
Executable file
3
package/bin/ahriman
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
exec python -B -m ahriman.application.ahriman "$@"
|
20
package/etc/ahriman.ini
Normal file
20
package/etc/ahriman.ini
Normal file
@ -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 =
|
47
package/etc/ahriman.ini.d/logging.ini
Normal file
47
package/etc/ahriman.ini.d/logging.ini
Normal file
@ -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
|
7
package/lib/systemd/system/ahriman.service
Normal file
7
package/lib/systemd/system/ahriman.service
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ArcHlinux ReposItory MANager
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/ahriman update
|
||||||
|
User=ahriman
|
||||||
|
Group=ahriman
|
9
package/lib/systemd/system/ahriman.timer
Normal file
9
package/lib/systemd/system/ahriman.timer
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=ArcHlinux ReposItory MANager timer
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=daily
|
||||||
|
RandomizedDelaySec=3600
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
56
setup.py
Normal file
56
setup.py
Normal file
@ -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'],
|
||||||
|
},
|
||||||
|
)
|
0
src/ahriman/__init__.py
Normal file
0
src/ahriman/__init__.py
Normal file
0
src/ahriman/application/__init__.py
Normal file
0
src/ahriman/application/__init__.py
Normal file
76
src/ahriman/application/ahriman.py
Normal file
76
src/ahriman/application/ahriman.py
Normal file
@ -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)
|
||||||
|
|
29
src/ahriman/application/application.py
Normal file
29
src/ahriman/application/application.py
Normal file
@ -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)
|
0
src/ahriman/core/__init__.py
Normal file
0
src/ahriman/core/__init__.py
Normal file
40
src/ahriman/core/configuration.py
Normal file
40
src/ahriman/core/configuration.py
Normal file
@ -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'))
|
21
src/ahriman/core/exceptions.py
Normal file
21
src/ahriman/core/exceptions.py
Normal file
@ -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')
|
40
src/ahriman/core/repo_wrapper.py
Normal file
40
src/ahriman/core/repo_wrapper.py
Normal file
@ -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)
|
123
src/ahriman/core/repository.py
Normal file
123
src/ahriman/core/repository.py
Normal file
@ -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
|
44
src/ahriman/core/sign.py
Normal file
44
src/ahriman/core/sign.py
Normal file
@ -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)
|
54
src/ahriman/core/task.py
Normal file
54
src/ahriman/core/task.py
Normal file
@ -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)
|
20
src/ahriman/core/util.py
Normal file
20
src/ahriman/core/util.py
Normal file
@ -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
|
0
src/ahriman/models/__init__.py
Normal file
0
src/ahriman/models/__init__.py
Normal file
65
src/ahriman/models/package.py
Normal file
65
src/ahriman/models/package.py
Normal file
@ -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
|
35
src/ahriman/models/repository_paths.py
Normal file
35
src/ahriman/models/repository_paths.py
Normal file
@ -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)
|
22
src/ahriman/models/sign_settings.py
Normal file
22
src/ahriman/models/sign_settings.py
Normal file
@ -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)
|
1
src/ahriman/version.py
Normal file
1
src/ahriman/version.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__version__ = '0.1.0'
|
Loading…
Reference in New Issue
Block a user