Compare commits

..

9 Commits

Author SHA1 Message Date
310eba694f Release 0.14.1 2021-03-17 03:35:38 +03:00
22d2057c3a replace script by makefile 2021-03-17 03:35:16 +03:00
47fc5bca57 Release 0.14.0 2021-03-16 20:11:56 +03:00
d4222eca25 add dump config option, change all timestamp objects to int, check git
directory
2021-03-16 05:24:07 +03:00
b5046b787c some improvements
* handle exceptions in multiprocessing
* readme update
* safe logger handler implementation (uses either stderr or
  rotatingfiles)
* user UID check
2021-03-16 04:25:58 +03:00
75c0cc970e Release 0.13.0 2021-03-16 01:40:48 +03:00
504d57b2f5 more package propertieis 2021-03-16 01:39:16 +03:00
4c20d0241a add clean subcommand 2021-03-15 23:34:50 +03:00
db0a6bf34e smart fetch & vcs cache 2021-03-15 23:28:08 +03:00
25 changed files with 412 additions and 163 deletions

46
Makefile Normal file
View File

@ -0,0 +1,46 @@
.PHONY: archive archive_directory archlinux clean directory push version
.DEFAULT_GOAL := archlinux
PROJECT := ahriman
FILES := COPYING CONFIGURING.md README.md package src setup.py
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache
ifndef VERSION
$(error VERSION is not set)
endif
$(TARGET_FILES) : $(addprefix $(PROJECT), %) : $(addprefix ., %) directory version
@cp -rp $< $@
archive: archive_directory
tar cJf "$(PROJECT)-$(VERSION)-src.tar.xz" "$(PROJECT)"
rm -rf "$(PROJECT)"
archive_directory: $(TARGET_FILES)
rm -fr $(addprefix $(PROJECT)/, $(IGNORE_FILES))
find $(PROJECT) -type f -name '*.pyc' -delete
find $(PROJECT) -depth -type d -name '__pycache__' -execdir rm -rf {} +
find $(PROJECT) -depth -type d -name '*.egg-info' -execdir rm -rf {} +
archlinux: archive
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$$(sha512sum $(PROJECT)-$(VERSION)-src.tar.xz | awk '{print $$1}')'/" package/archlinux/PKGBUILD
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
clean:
find . -type f -name '$(PROJECT)-*-src.tar.xz' -delete
rm -rf "$(PROJECT)"
directory: clean
mkdir "$(PROJECT)"
push: archlinux
git add package/archlinux/PKGBUILD src/ahriman/version.py
git commit -m "Release $(VERSION)"
git push
git tag "$(VERSION)"
git push --tags
version:
sed -i "/__version__ = '[0-9.]*/s/[^'][^)]*/__version__ = '$(VERSION)'/" src/ahriman/version.py

View File

@ -15,13 +15,54 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
## Installation and run
* Install package as usual.
* Change settings if required, see `CONFIGURING.md` for more details.
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`).
* Configure build tools (it might be required if your package will use any custom repositories):
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/custom-x86_64-build` (you can choose any name for command);
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,custom}.conf`;
* change configuration file, add your own repository, add multilib repository etc;
* set `build.build_command` setting to point to your command;
* configure `/etc/sudoers.d/ahriman` to allow running command without password.
* Start and enable `ahriman.timer` via `systemctl`.
* Add packages by using `ahriman add {package}` command.
* Change settings if required, see [CONFIGURING](CONFIGURING.md) for more details.
* Create `/var/lib/ahriman/.makepkg.conf` with `makepkg.conf` overrides if required (at least you might want to set `PACKAGER`):
```shell
echo 'PACKAGES="John Doe <john@doe.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
```
* Configure build tools (it is required for correct dependency management system):
* create build command, e.g. `ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build` (you can choose any name for command, basically it should be `{name}-{arch}-build`);
* create configuration file, e.g. `cp /usr/share/devtools/pacman-{extra,ahriman}.conf` (same as previous `pacman-{name}.conf`);
* change configuration file, add your own repository, add multilib repository etc. Hint: you can use `Include` option as well;
* set `build_command` option to point to your command;
* configure `/etc/sudoers.d/ahriman` to allow running command without a password.
```shell
ln -s /usr/bin/archbuild /usr/local/bin/ahriman-x86_64-build
cp /usr/share/devtools/pacman-{extra,ahriman}.conf
echo '[multilib]' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Include = /etc/pacman.d/mirrorlist' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo '[aur-clone]' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'SigLevel = Optional TrustAll' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo 'Server = file:///var/lib/ahriman/repository/$arch' | tee -a /usr/share/devtools/pacman-ahriman.conf
echo '[build]' | tee -a /etc/ahriman.ini.d/build.ini
echo 'build_command = ahriman-x86_64-build' | tee -a /etc/ahriman.ini.d/build.ini
echo 'Cmnd_Alias CARCHBUILD_CMD = /usr/local/bin/ahriman-x86_64-build *' | tee -a /etc/sudoers.d/ahriman
echo 'ahriman ALL=(ALL) NOPASSWD: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman
```
* Start and enable `ahriman@.timer` via `systemctl`:
```shell
systemctl enable --now ahriman@x86_64.timer
```
* Start and enable status page:
```shell
systemctl enable --now ahriman-web@x86_64
```
* Add packages by using `ahriman add {package}` command:
```shell
sudo -u ahriman ahriman -a x86_64 add yay
```

View File

@ -1,35 +0,0 @@
#!/bin/bash
set -e
VERSION="$1"
ARCHIVE="ahriman"
FILES="COPYING CONFIGURING.md README.md 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 -f {} \;
read -p "Publish release? [Ny] " -n 1 -r
if [[ $REPLY =~ ^[Yy]$ ]]; then
git add package/archlinux/PKGBUILD src/ahriman/version.py
git commit -m "Release $VERSION" && git push
git tag "$VERSION" && git push --tags
fi

View File

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

View File

@ -2,17 +2,11 @@
keys = root,builder,build_details,http
[handlers]
keys = console_handler,build_file_handler,file_handler,http_handler
keys = build_file_handler,file_handler,http_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

View File

@ -33,14 +33,18 @@
<tr class="header">
<th>package</th>
<th>version</th>
<th>archive size</th>
<th>installed size</th>
<th>build date</th>
</tr>
{% for package in packages %}
<tr class="package">
<td class="include-search"><a href="{{ link_path|e }}/{{ package.filename|e }}" title="{{ package.name|e }}">{{ package.name|e }}</a></td>
<td>{{ package.version|e }}</td>
<td>{{ package.archive_size|e }}</td>
<td>{{ package.installed_size|e }}</td>
<td>{{ package.build_date|e }}</td>
</tr>
{% endfor %}
</table>

View File

@ -18,6 +18,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
import logging
import sys
from multiprocessing import Pool
@ -28,15 +30,21 @@ from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
def _call(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
def _call(args: argparse.Namespace, architecture: str, config: Configuration) -> bool:
'''
additional function to wrap all calls for multiprocessing library
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:return: True on success, False otherwise
'''
with Lock(args.lock, architecture, args.force, config):
args.fn(args, architecture, config)
try:
with Lock(args.lock, architecture, args.force, args.unsafe, config):
args.fn(args, architecture, config)
return True
except Exception:
logging.getLogger('root').exception('process exception', exc_info=True)
return False
def add(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
@ -49,6 +57,32 @@ def add(args: argparse.Namespace, architecture: str, config: Configuration) -> N
Application(architecture, config).add(args.package, args.without_dependencies)
def clean(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
clean caches callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages)
def dump_config(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
configuration dump callback
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
result = config.dump(architecture)
for section, values in sorted(result.items()):
print(f'[{section}]')
for key, value in sorted(values.items()):
print(f'{key} = {value}')
print()
def rebuild(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
world rebuild callback
@ -100,14 +134,14 @@ def update(args: argparse.Namespace, architecture: str, config: Configuration) -
'''
# typing workaround
def log_fn(line: str) -> None:
return print(line) if args.dry_run else app.logger.info(line)
return print(line) if args.dry_run else application.logger.info(line)
app = Application(architecture, config)
packages = app.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
application = Application(architecture, config)
packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
if args.dry_run:
return
app.update(packages)
application.update(packages)
def web(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
@ -118,16 +152,21 @@ def web(args: argparse.Namespace, architecture: str, config: Configuration) -> N
:param config: configuration instance
'''
from ahriman.web.web import run_server, setup_service
app = setup_service(architecture, config)
run_server(app, architecture)
application = setup_service(architecture, config)
run_server(application, architecture)
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager')
parser.add_argument('-a', '--architecture', help='target architectures', action='append')
parser.add_argument(
'-a',
'--architecture',
help='target architectures (can be used multiple times)',
action='append')
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('--unsafe', help='allow to run ahriman as non-ahriman user', action='store_true')
parser.add_argument('-v', '--version', action='version', version=version.__version__)
subparsers = parser.add_subparsers(title='command')
@ -141,6 +180,20 @@ if __name__ == '__main__':
check_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
check_parser.set_defaults(fn=update, no_aur=False, no_manual=True, dry_run=True)
clean_parser = subparsers.add_parser('clean', description='clear all local caches')
clean_parser.add_argument('--no-build', help='do not clear directory with package sources', action='store_true')
clean_parser.add_argument('--no-cache', help='do not clear directory with package caches', action='store_true')
clean_parser.add_argument('--no-chroot', help='do not clear build chroot', action='store_true')
clean_parser.add_argument(
'--no-manual',
help='do not clear directory with manually added packages',
action='store_true')
clean_parser.add_argument('--no-packages', help='do not clear directory with built packages', action='store_true')
clean_parser.set_defaults(fn=clean)
config_parser = subparsers.add_parser('config', description='dump configuration for specified architecture')
config_parser.set_defaults(fn=dump_config)
rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository')
rebuild_parser.set_defaults(fn=rebuild)
@ -175,4 +228,6 @@ if __name__ == '__main__':
config = Configuration.from_path(args.config)
with Pool(len(args.architecture)) as pool:
pool.starmap(_call, [(args, architecture, config) for architecture in args.architecture])
result = pool.starmap(_call, [(args, architecture, config) for architecture in args.architecture])
sys.exit(0 if all(result) else 1)

View File

@ -17,13 +17,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
import logging
import os
import shutil
from typing import Callable, Iterable, List, Optional, Set, Type
from typing import Callable, Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
@ -128,6 +126,26 @@ class Application:
for name in names:
process_single(name)
def clean(self, no_build: bool, no_cache: bool, no_chroot: bool, no_manual: bool, no_packages: bool) -> None:
'''
run all clean methods. Warning: some functions might not be available under non-root
:param no_build: do not clear directory with package sources
:param no_cache: do not clear directory with package caches
:param no_chroot: do not clear build chroot
:param no_manual: do not clear directory with manually added packages
:param no_packages: do not clear directory with built packages
'''
if not no_build:
self.repository._clear_build()
if not no_cache:
self.repository._clear_cache()
if not no_chroot:
self.repository._clear_chroot()
if not no_manual:
self.repository._clear_manual()
if not no_packages:
self.repository._clear_packages()
def remove(self, names: Iterable[str]) -> None:
'''
remove packages from repository

View File

@ -25,7 +25,7 @@ from types import TracebackType
from typing import Literal, Optional, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRun
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
from ahriman.core.watcher.client import Client
from ahriman.models.build_status import BuildStatusEnum
@ -36,30 +36,38 @@ class Lock:
:ivar force: remove lock file on start if any
:ivar path: path to lock file if any
:ivar reporter: build status reporter instance
:ivar root: repository root (i.e. ahriman home)
:ivar unsafe: skip user check
'''
def __init__(self, path: Optional[str], architecture: str, force: bool, config: Configuration) -> None:
def __init__(self, path: Optional[str], architecture: str, force: bool, unsafe: bool,
config: Configuration) -> None:
'''
default constructor
:param path: optional path to lock file, if empty no file lock will be used
:param architecture: repository architecture
:param force: remove lock file on start if any
:param unsafe: skip user check
:param config: configuration instance
'''
self.path = f'{path}_{architecture}' if path is not None else None
self.force = force
self.unsafe = unsafe
self.root = config.get('repository', 'root')
self.reporter = Client.load(architecture, config)
def __enter__(self) -> Lock:
'''
default workflow is the following:
check user UID
remove lock file if force flag is set
check if there is lock file
create lock file
report to web if enabled
'''
self.check_user()
if self.force:
self.remove()
self.check()
@ -90,6 +98,17 @@ class Lock:
if os.path.exists(self.path):
raise DuplicateRun()
def check_user(self) -> None:
'''
check if current user is actually owner of ahriman root
'''
if self.unsafe:
return
current_uid = os.getuid()
root_uid = os.stat(self.root).st_uid
if current_uid != root_uid:
raise UnsafeRun(current_uid, root_uid)
def create(self) -> None:
'''
create lock file

View File

@ -58,6 +58,13 @@ class Task:
self.makepkg_flags = config.getlist(section, 'makepkg_flags')
self.makechrootpkg_flags = config.getlist(section, 'makechrootpkg_flags')
@property
def cache_path(self) -> str:
'''
:return: path to cached packages
'''
return os.path.join(self.paths.cache, self.package.base)
@property
def git_path(self) -> str:
'''
@ -66,14 +73,21 @@ class Task:
return os.path.join(self.paths.sources, self.package.base)
@staticmethod
def fetch(local: str, remote: str) -> None:
def fetch(local: str, remote: str, branch: str = 'master') -> None:
'''
fetch package from git
either clone repository or update it to origin/`branch`
:param local: local path to fetch
:param remote: remote target (from where to fetch)
:param branch: branch name to checkout, master by default
'''
shutil.rmtree(local, ignore_errors=True) # remove in case if file exists
check_output('git', 'clone', remote, local, exception=None)
logger = logging.getLogger('build_details')
# local directory exists and there is .git directory
if os.path.isdir(os.path.join(local, '.git')):
check_output('git', 'fetch', 'origin', branch, exception=None, cwd=local, logger=logger)
else:
check_output('git', 'clone', remote, local, exception=None, logger=logger)
# and now force reset to our branch
check_output('git', 'reset', '--hard', f'origin/{branch}', exception=None, cwd=local, logger=logger)
def build(self) -> List[str]:
'''
@ -95,12 +109,16 @@ class Task:
# well it is not actually correct, but we can deal with it
return check_output('makepkg', '--packagelist',
exception=BuildFailed(self.package.base),
cwd=self.git_path).splitlines()
cwd=self.git_path,
logger=self.build_logger).splitlines()
def clone(self, path: Optional[str] = None) -> None:
def init(self, path: Optional[str] = None) -> None:
'''
fetch package from git
:param path: optional local path to fetch. If not set default path will be used
'''
git_path = path or self.git_path
if os.path.isdir(self.cache_path):
# no need to clone whole repository, just copy from cache first
shutil.copytree(self.cache_path, git_path)
return Task.fetch(git_path, self.package.git_url)

View File

@ -20,18 +20,29 @@
from __future__ import annotations
import configparser
import logging
import os
from logging.config import fileConfig
from typing import List, Optional, Type
from typing import Dict, List, Optional, Type
class Configuration(configparser.RawConfigParser):
'''
extension for built-in configuration parser
:ivar path: path to root configuration file
:cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump)
:cvar DEFAULT_LOG_FORMAT: default log format (in case of fallback)
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
:cvar STATIC_SECTIONS: known sections which are not architecture specific (required by dump)
'''
DEFAULT_LOG_FORMAT = '%(asctime)s : %(levelname)s : %(funcName)s : %(message)s'
DEFAULT_LOG_LEVEL = logging.DEBUG
STATIC_SECTIONS = ['alpm', 'report', 'repository', 'settings', 'upload']
ARCHITECTURE_SPECIFIC_SECTIONS = ['build', 'html', 'rsync', 's3', 'sign', 'web']
def __init__(self) -> None:
'''
default constructor
@ -58,6 +69,25 @@ class Configuration(configparser.RawConfigParser):
config.load_logging()
return config
def dump(self, architecture: str) -> Dict[str, Dict[str, str]]:
'''
dump configuration to dictionary
:param architecture: repository architecture
:return: configuration dump for specific architecture
'''
result: Dict[str, Dict[str, str]] = {}
for section in Configuration.STATIC_SECTIONS:
if not self.has_section(section):
continue
result[section] = dict(self[section])
for group in Configuration.ARCHITECTURE_SPECIFIC_SECTIONS:
section = self.get_section_name(group, architecture)
if not self.has_section(section):
continue
result[section] = dict(self[section])
return result
def getlist(self, section: str, key: str) -> List[str]:
'''
get space separated string list option
@ -103,4 +133,9 @@ class Configuration(configparser.RawConfigParser):
'''
setup logging settings from configuration
'''
fileConfig(self.get('settings', 'logging'))
try:
fileConfig(self.get('settings', 'logging'))
except PermissionError:
logging.basicConfig(filename=None, format=Configuration.DEFAULT_LOG_FORMAT,
level=Configuration.DEFAULT_LOG_LEVEL)
logging.error('could not create logfile, fallback to stderr', exc_info=True)

View File

@ -105,3 +105,19 @@ class SyncFailed(Exception):
default constructor
'''
Exception.__init__(self, 'Sync failed')
class UnsafeRun(Exception):
'''
exception which will be raised in case if user is not owner of repository
'''
def __init__(self, current_uid: int, root_uid: int) -> None:
'''
default constructor
'''
Exception.__init__(
self,
f'''Current UID {current_uid} differs from root owner {root_uid}.
Note that for the most actions it is unsafe to run application as different user.
If you are 100% sure that it must be there try --unsafe option''')

View File

@ -24,8 +24,8 @@ from typing import Callable, Dict, Iterable
from ahriman.core.configuration import Configuration
from ahriman.core.report.report import Report
from ahriman.core.util import pretty_size, pretty_datetime
from ahriman.models.package import Package
from ahriman.models.package_desciption import PackageDescription
from ahriman.models.sign_settings import SignSettings
@ -39,7 +39,7 @@ class HTML(Report):
link_path - prefix fo packages to download, string, required
has_package_signed - True in case if package sign enabled, False otherwise, required
has_repo_signed - True in case if repository database sign enabled, False otherwise, required
packages - sorted list of packages properties: filename, installed_size, name, version. Required
packages - sorted list of packages properties: archive_size, build_date, filename, installed_size, name, version. Required
pgp_key - default PGP key ID, string, optional
repository - repository name, string, required
@ -85,8 +85,10 @@ class HTML(Report):
content = [
{
'archive_size': pretty_size(properties.archive_size),
'build_date': pretty_datetime(properties.build_date),
'filename': properties.filename,
'installed_size': PackageDescription.size_to_str(properties.installed_size),
'installed_size': pretty_size(properties.installed_size),
'name': package,
'version': base.version
} for base in packages for package, properties in base.packages.items()

View File

@ -79,6 +79,20 @@ class Repository:
for package in os.listdir(self.paths.sources):
shutil.rmtree(os.path.join(self.paths.sources, package))
def _clear_cache(self) -> None:
'''
clear cache directory
'''
for package in os.listdir(self.paths.cache):
shutil.rmtree(os.path.join(self.paths.cache, package))
def _clear_chroot(self) -> None:
'''
clear cache directory. Warning: this method is architecture independent and will clear every chroot
'''
for chroot in os.listdir(self.paths.chroot):
shutil.rmtree(os.path.join(self.paths.chroot, chroot))
def _clear_manual(self) -> None:
'''
clear directory with manual package updates
@ -130,7 +144,7 @@ class Repository:
def build_single(package: Package) -> None:
self.reporter.set_building(package.base)
task = Task(package, self.architecture, self.config, self.paths)
task.clone()
task.init()
built = task.build()
for src in built:
dst = os.path.join(self.paths.packages, os.path.basename(src))
@ -238,7 +252,7 @@ class Repository:
try:
remote = Package.load(local.base, self.pacman, self.aur_url)
if local.is_outdated(remote):
if local.is_outdated(remote, self.paths):
result.append(remote)
self.reporter.set_pending(local.base)
except Exception:

View File

@ -17,11 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import datetime
import subprocess
from logging import Logger
from typing import Optional
from ahriman.core.exceptions import InvalidOption
def check_output(*args: str, exception: Optional[Exception],
cwd: Optional[str] = None, stderr: int = subprocess.STDOUT,
@ -55,3 +58,37 @@ def package_like(filename: str) -> bool:
:return: True in case if name contains `.pkg.` and not signature, False otherwise
'''
return '.pkg.' in filename and not filename.endswith('.sig')
def pretty_datetime(timestamp: Optional[int]) -> str:
'''
convert datetime object to string
:param timestamp: datetime to convert
:return: pretty printable datetime as string
'''
return '' if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
def pretty_size(size: Optional[float], level: int = 0) -> str:
'''
convert size to string
:param size: size to convert
:param level: represents current units, 0 is B, 1 is KiB etc
:return: pretty printable size as string
'''
def str_level() -> str:
if level == 0:
return 'B'
elif level == 1:
return 'KiB'
elif level == 2:
return 'MiB'
elif level == 3:
return 'GiB'
raise InvalidOption(level) # I hope it will not be more than 1024 GiB
if size is None:
return ''
elif size < 1024:
return f'{round(size, 2)} {str_level()}'
return pretty_size(size / 1024, level + 1)

View File

@ -63,18 +63,11 @@ class BuildStatus:
'''
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
timestamp: Optional[datetime.datetime] = None) -> None:
timestamp: Optional[int] = None) -> None:
'''
default constructor
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
:param timestamp: build status timestamp. Current timestamp will be used if not set
'''
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
self._timestamp = timestamp or datetime.datetime.utcnow()
@property
def timestamp(self) -> str:
'''
:return: string representation of build status timestamp
'''
return self._timestamp.strftime('%Y-%m-%d %H:%M:%S')
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())

View File

@ -19,10 +19,10 @@
#
from __future__ import annotations
import logging
import aur # type: ignore
import os
import shutil
import tempfile
from dataclasses import dataclass
from pyalpm import vercmp # type: ignore
@ -33,6 +33,7 @@ from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output
from ahriman.models.package_desciption import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths
@dataclass
@ -76,30 +77,30 @@ class Package:
'''
return f'{self.aur_url}/packages/{self.base}'
def actual_version(self) -> str:
def actual_version(self, paths: RepositoryPaths) -> str:
'''
additional method to handle VCS package versions
:param paths: repository paths instance
:return: package version if package is not VCS and current version according to VCS otherwise
'''
if not self.is_vcs:
return self.version
from ahriman.core.build_tools.task import Task
clone_dir = tempfile.mkdtemp()
try:
Task.fetch(clone_dir, self.git_url)
# update pkgver first
check_output('makepkg', '--nodeps', '--nobuild',
exception=None, cwd=clone_dir)
# generate new .SRCINFO and put it to parser
src_info_source = check_output('makepkg', '--printsrcinfo',
exception=None, cwd=clone_dir)
src_info, errors = parse_srcinfo(src_info_source)
if errors:
raise InvalidPackageInfo(errors)
return self.full_version(src_info.get('epoch'), src_info['pkgver'], src_info['pkgrel'])
finally:
shutil.rmtree(clone_dir, ignore_errors=True)
clone_dir = os.path.join(paths.cache, self.base)
logger = logging.getLogger('build_details')
Task.fetch(clone_dir, self.git_url)
# update pkgver first
check_output('makepkg', '--nodeps', '--nobuild', exception=None, cwd=clone_dir, logger=logger)
# generate new .SRCINFO and put it to parser
src_info_source = check_output('makepkg', '--printsrcinfo', exception=None, cwd=clone_dir, logger=logger)
src_info, errors = parse_srcinfo(src_info_source)
if errors:
raise InvalidPackageInfo(errors)
return self.full_version(src_info.get('epoch'), src_info['pkgver'], src_info['pkgrel'])
@classmethod
def from_archive(cls: Type[Package], path: str, pacman: Pacman, aur_url: str) -> Package:
@ -111,7 +112,7 @@ class Package:
:return: package properties
'''
package = pacman.handle.load_pkg(path)
properties = PackageDescription(os.path.basename(path), package.isize)
properties = PackageDescription(package.size, package.builddate, os.path.basename(path), package.isize)
return cls(package.base, package.version, aur_url, {package.name: properties})
@classmethod
@ -196,12 +197,13 @@ class Package:
except Exception as e:
raise InvalidPackageInfo(str(e))
def is_outdated(self, remote: Package) -> bool:
def is_outdated(self, remote: Package, paths: RepositoryPaths) -> bool:
'''
check if package is out-of-dated
:param remote: package properties from remote source
:param paths: repository paths instance. Required for VCS packages cache
:return: True if the package is out-of-dated and False otherwise
'''
remote_version = remote.actual_version() # either normal version or updated VCS
remote_version = remote.actual_version(paths) # either normal version or updated VCS
result: int = vercmp(self.version, remote_version)
return result < 0

View File

@ -20,38 +20,18 @@
from dataclasses import dataclass
from typing import Optional
from ahriman.core.exceptions import InvalidOption
@dataclass
class PackageDescription:
'''
package specific properties
:ivar archive_size: package archive size
:ivar build_date: package build date
:ivar filename: package archive name
:ivar installed_size: package installed size
'''
archive_size: Optional[int] = None
build_date: Optional[int] = None
filename: Optional[str] = None
installed_size: Optional[int] = None
@staticmethod
def size_to_str(size: Optional[float], level: int = 0) -> str:
'''
convert size to string
:param size: size to convert
:param level: represents current units, 0 is B, 1 is KiB etc
:return: pretty printable size as string
'''
def str_level() -> str:
if level == 0:
return 'B'
elif level == 1:
return 'KiB'
elif level == 2:
return 'MiB'
elif level == 3:
return 'GiB'
raise InvalidOption(level)
if size is None:
return ''
elif size < 1024:
return f'{round(size, 2)} {str_level()}'
return PackageDescription.size_to_str(size / 1024, level + 1)

View File

@ -27,7 +27,7 @@ from ahriman.core.exceptions import InvalidOption
class ReportSettings(Enum):
'''
report targets enumeration
:ivar HTML: html report generation
:cvar HTML: html report generation
'''
HTML = auto()

View File

@ -33,6 +33,13 @@ class RepositoryPaths:
root: str
architecture: str
@property
def cache(self) -> str:
'''
:return: directory for packages cache (mainly used for VCS packages)
'''
return os.path.join(self.root, 'cache')
@property
def chroot(self) -> str:
'''
@ -73,6 +80,7 @@ class RepositoryPaths:
'''
create ahriman working tree
'''
os.makedirs(self.cache, mode=0o755, exist_ok=True)
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)

View File

@ -27,8 +27,8 @@ from ahriman.core.exceptions import InvalidOption
class SignSettings(Enum):
'''
sign targets enumeration
:ivar SignPackages: sign each package
:ivar SignRepository: sign repository database file
:cvar SignPackages: sign each package
:cvar SignRepository: sign repository database file
'''
SignPackages = auto()

View File

@ -27,8 +27,8 @@ from ahriman.core.exceptions import InvalidOption
class UploadSettings(Enum):
'''
remote synchronization targets enumeration
:ivar Rsync: sync via rsync
:ivar S3: sync to Amazon S3
:cvar Rsync: sync via rsync
:cvar S3: sync to Amazon S3
'''
Rsync = auto()

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = '0.12.2'
__version__ = '0.14.1'

View File

@ -23,6 +23,7 @@ from typing import Any, Dict
import ahriman.version as version
from ahriman.core.util import pretty_datetime
from ahriman.web.views.base import BaseView
@ -52,7 +53,7 @@ class IndexView(BaseView):
'base': package.base,
'packages': [p for p in sorted(package.packages)],
'status': status.status.value,
'timestamp': status.timestamp,
'timestamp': pretty_datetime(status.timestamp),
'version': package.version,
'web_url': package.web_url
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
@ -60,7 +61,7 @@ class IndexView(BaseView):
service = {
'status': self.service.status.status.value,
'status_color': self.service.status.status.badges_color(),
'timestamp': self.service.status.timestamp
'timestamp': pretty_datetime(self.service.status.timestamp)
}
return {

View File

@ -63,7 +63,8 @@ def run_server(application: web.Application, architecture: str) -> None:
host = application['config'].get(section, 'host')
port = application['config'].getint(section, 'port')
web.run_app(application, host=host, port=port, handle_signals=False)
web.run_app(application, host=host, port=port, handle_signals=False,
access_log=logging.getLogger('http'))
def setup_service(architecture: str, config: Configuration) -> web.Application:
@ -73,22 +74,22 @@ def setup_service(architecture: str, config: Configuration) -> web.Application:
:param config: configuration instance
:return: web application instance
'''
app = web.Application(logger=logging.getLogger('http'))
app.on_shutdown.append(on_shutdown)
app.on_startup.append(on_startup)
application = web.Application(logger=logging.getLogger('http'))
application.on_shutdown.append(on_shutdown)
application.on_startup.append(on_startup)
app.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
app.middlewares.append(exception_handler(app.logger))
application.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
application.middlewares.append(exception_handler(application.logger))
app.logger.info('setup routes')
setup_routes(app)
app.logger.info('setup templates')
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(config.get('web', 'templates')))
application.logger.info('setup routes')
setup_routes(application)
application.logger.info('setup templates')
aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.get('web', 'templates')))
app.logger.info('setup configuration')
app['config'] = config
application.logger.info('setup configuration')
application['config'] = config
app.logger.info('setup watcher')
app['watcher'] = Watcher(architecture, config)
application.logger.info('setup watcher')
application['watcher'] = Watcher(architecture, config)
return app
return application