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