Compare commits

..

10 Commits

20 changed files with 291 additions and 132 deletions

View File

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

View File

@ -11,7 +11,11 @@
<body> <body>
<div class="root"> <div class="root">
<h1>ahriman {{ version|e }} ({{ architecture|e }})<sup class="service-{{ service.status|e }}" title="{{ service.timestamp }}">{{ service.status|e }}</sup></h1> <h1>ahriman
<img src="https://img.shields.io/badge/version-{{ version|e }}-informational" alt="{{ version|e }}">
<img src="https://img.shields.io/badge/architecture-{{ architecture|e }}-informational" alt="{{ architecture|e }}">
<img src="https://img.shields.io/badge/service%20status-{{ service.status|e }}-{{ service.status_color|e }}" alt="{{ service.status|e }}" title="{{ service.timestamp|e }}">
</h1>
{% include "search-line.jinja2" %} {% include "search-line.jinja2" %}
@ -31,11 +35,19 @@
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td> <td class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.version|e }}</td> <td>{{ package.version|e }}</td>
<td>{{ package.timestamp|e }}</td> <td>{{ package.timestamp|e }}</td>
<td class="package-{{ package.status|e }}">{{ package.status|e }}</td> <td class="status package-{{ package.status|e }}">{{ package.status|e }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</section> </section>
<footer>
<ul class="navigation">
<li><a href="https://github.com/arcan1s/ahriman" title="sources">ahriman</a></li>
<li><a href="https://github.com/arcan1s/ahriman/releases" title="releases list">releases</a></li>
<li><a href="https://github.com/arcan1s/ahriman/issues" title="issues tracker">report a bug</a></li>
</ul>
</footer>
</div> </div>
</body> </body>

View File

@ -33,22 +33,30 @@
<tr class="header"> <tr class="header">
<th>package</th> <th>package</th>
<th>version</th> <th>version</th>
<th>archive size</th>
<th>installed size</th> <th>installed size</th>
<th>build date</th>
</tr> </tr>
{% for package in packages %} {% for package in packages %}
<tr class="package"> <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 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.version|e }}</td>
<td>{{ package.archive_size|e }}</td>
<td>{{ package.installed_size|e }}</td> <td>{{ package.installed_size|e }}</td>
<td>{{ package.build_date|e }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</section> </section>
{% if homepage is not none %} <footer>
<footer><a href="{{ homepage|e }}" title="homepage">Homepage</a></footer> <ul class="navigation">
{% endif %} {% if homepage is not none %}
<li><a href="{{ homepage|e }}" title="homepage">Homepage</a></li>
{% endif %}
</ul>
</footer>
</div> </div>
</body> </body>
</html> </html>

View File

@ -5,6 +5,11 @@
--color-pending: 255, 255, 146; --color-pending: 255, 255, 146;
--color-success: 94, 255, 94; --color-success: 94, 255, 94;
--color-unknown: 225, 225, 225; --color-unknown: 225, 225, 225;
--color-header: 200, 200, 255;
--color-hover: 255, 255, 225;
--color-line-blue: 235, 235, 255;
--color-line-white: 255, 255, 255;
} }
@keyframes blink-building { @keyframes blink-building {
@ -26,7 +31,7 @@
padding: 15px 15% 0; padding: 15px 15% 0;
} }
section.element { section.element, footer {
width: 100%; width: 100%;
padding: 10px 0; padding: 10px 0;
} }
@ -40,19 +45,23 @@
} }
tr.package:nth-child(odd) { tr.package:nth-child(odd) {
background-color: rgba(255, 255, 255, 1); background-color: rgba(var(--color-line-white), 1.0);
} }
tr.package:nth-child(even) { tr.package:nth-child(even) {
background-color: rgba(235, 235, 255, 1); background-color: rgba(var(--color-line-blue), 1.0);
} }
tr.package:hover { tr.package:hover {
background-color: rgba(255, 255, 225, 1); background-color: rgba(var(--color-hover), 1.0);
} }
tr.header{ tr.header{
background-color: rgba(200, 200, 255, 1); background-color: rgba(var(--color-header), 1.0);
}
td.status {
text-align: center;
} }
td.package-unknown { td.package-unknown {
@ -76,12 +85,10 @@
background-color: rgba(var(--color-success), 1.0); background-color: rgba(var(--color-success), 1.0);
} }
sup.service-unknown { li.service-unknown {
font-weight: lighter;
background-color: rgba(var(--color-unknown), 1.0); background-color: rgba(var(--color-unknown), 1.0);
} }
sup.service-building { li.service-building {
font-weight: lighter;
background-color: rgba(var(--color-building), 1.0); background-color: rgba(var(--color-building), 1.0);
animation-name: blink-building; animation-name: blink-building;
animation-duration: 1s; animation-duration: 1s;
@ -89,12 +96,41 @@
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-direction: alternate; animation-direction: alternate;
} }
sup.service-failed { li.service-failed {
font-weight: lighter;
background-color: rgba(var(--color-failed), 1.0); background-color: rgba(var(--color-failed), 1.0);
} }
sup.service-success { li.service-success {
font-weight: lighter;
background-color: rgba(var(--color-success), 1.0); background-color: rgba(var(--color-success), 1.0);
} }
ul.navigation {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: rgba(var(--color-header), 1.0);
}
ul.navigation li {
float: left;
}
ul.navigation li.status {
display: block;
text-align: center;
text-decoration: none;
padding: 14px 16px;
}
ul.navigation li a {
display: block;
color: black;
text-align: center;
text-decoration: none;
padding: 14px 16px;
}
ul.navigation li a:hover {
background-color: rgba(var(--color-hover), 1.0);
}
</style> </style>

View File

@ -19,6 +19,8 @@
# #
import argparse import argparse
from multiprocessing import Pool
import ahriman.version as version import ahriman.version as version
from ahriman.application.application import Application from ahriman.application.application import Application
@ -26,64 +28,91 @@ from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
def add(args: argparse.Namespace, config: Configuration) -> None: def _call(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
'''
additional function to wrap all calls for multiprocessing library
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
'''
with Lock(args.lock, architecture, args.force, config):
args.fn(args, architecture, config)
def add(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
''' '''
add packages callback add packages callback
:param args: command line args :param args: command line args
:param architecture: repository architecture
:param config: configuration instance :param config: configuration instance
''' '''
Application.from_args(args, config).add(args.package, args.without_dependencies) Application(architecture, config).add(args.package, args.without_dependencies)
def rebuild(args: argparse.Namespace, config: Configuration) -> None: 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()
def rebuild(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
''' '''
world rebuild callback world rebuild callback
:param args: command line args :param args: command line args
:param architecture: repository architecture
:param config: configuration instance :param config: configuration instance
''' '''
app = Application.from_args(args, config) app = Application(architecture, config)
packages = app.repository.packages() packages = app.repository.packages()
app.update(packages) app.update(packages)
def remove(args: argparse.Namespace, config: Configuration) -> None: def remove(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
''' '''
remove packages callback remove packages callback
:param args: command line args :param args: command line args
:param architecture: repository architecture
:param config: configuration instance :param config: configuration instance
''' '''
Application.from_args(args, config).remove(args.package) Application(architecture, config).remove(args.package)
def report(args: argparse.Namespace, config: Configuration) -> None: def report(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
''' '''
generate report callback generate report callback
:param args: command line args :param args: command line args
:param architecture: repository architecture
:param config: configuration instance :param config: configuration instance
''' '''
Application.from_args(args, config).report(args.target) Application(architecture, config).report(args.target)
def sync(args: argparse.Namespace, config: Configuration) -> None: def sync(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
''' '''
sync to remote server callback sync to remote server callback
:param args: command line args :param args: command line args
:param architecture: repository architecture
:param config: configuration instance :param config: configuration instance
''' '''
Application.from_args(args, config).sync(args.target) Application(architecture, config).sync(args.target)
def update(args: argparse.Namespace, config: Configuration) -> None: def update(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
''' '''
update packages callback update packages callback
:param args: command line args :param args: command line args
:param architecture: repository architecture
:param config: configuration instance :param config: configuration instance
''' '''
# typing workaround # typing workaround
def log_fn(line: str) -> None: def log_fn(line: str) -> None:
return print(line) if args.dry_run else app.logger.info(line) return print(line) if args.dry_run else app.logger.info(line)
app = Application.from_args(args, config) app = Application(architecture, config)
packages = app.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn) packages = app.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn)
if args.dry_run: if args.dry_run:
return return
@ -91,20 +120,21 @@ def update(args: argparse.Namespace, config: Configuration) -> None:
app.update(packages) app.update(packages)
def web(args: argparse.Namespace, config: Configuration) -> None: def web(args: argparse.Namespace, architecture: str, config: Configuration) -> None:
''' '''
web server callback web server callback
:param args: command line args :param args: command line args
:param architecture: repository architecture
:param config: configuration instance :param config: configuration instance
''' '''
from ahriman.web.web import run_server, setup_service from ahriman.web.web import run_server, setup_service
app = setup_service(args.architecture, config) app = setup_service(architecture, config)
run_server(app, args.architecture) run_server(app, architecture)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager') parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager')
parser.add_argument('-a', '--architecture', help='target architecture', required=True) parser.add_argument('-a', '--architecture', help='target architectures', action='append')
parser.add_argument('-c', '--config', help='configuration path', default='/etc/ahriman.ini') 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('--force', help='force run, remove file lock', action='store_true')
parser.add_argument('--lock', help='lock file', default='/tmp/ahriman.lock') parser.add_argument('--lock', help='lock file', default='/tmp/ahriman.lock')
@ -118,7 +148,11 @@ if __name__ == '__main__':
check_parser = subparsers.add_parser('check', description='check for updates') check_parser = subparsers.add_parser('check', description='check for updates')
check_parser.add_argument('package', help='filter check by packages', nargs='*') check_parser.add_argument('package', help='filter check by packages', nargs='*')
check_parser.set_defaults(fn=update, no_aur=False, no_manual=True, no_vcs=False, dry_run=True) 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.set_defaults(fn=clean)
rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository') rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository')
rebuild_parser.set_defaults(fn=rebuild) rebuild_parser.set_defaults(fn=rebuild)
@ -153,5 +187,5 @@ if __name__ == '__main__':
exit(1) exit(1)
config = Configuration.from_path(args.config) config = Configuration.from_path(args.config)
with Lock(args.lock, args.architecture, args.force, config): with Pool(len(args.architecture)) as pool:
args.fn(args, config) pool.starmap(_call, [(args, architecture, config) for architecture in args.architecture])

View File

@ -17,14 +17,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from __future__ import annotations
import argparse
import logging import logging
import os import os
import shutil 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.build_tools.task import Task
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -53,16 +50,6 @@ class Application:
self.architecture = architecture self.architecture = architecture
self.repository = Repository(architecture, config) self.repository = Repository(architecture, config)
@classmethod
def from_args(cls: Type[Application], args: argparse.Namespace, config: Configuration) -> Application:
'''
constructor which has to be used to build instance from command line args
:param args: command line args
:param config: configuration instance
:return: application instance
'''
return cls(args.architecture, config)
def _known_packages(self) -> Set[str]: def _known_packages(self) -> Set[str]:
''' '''
load packages from repository and pacman repositories load packages from repository and pacman repositories
@ -139,6 +126,16 @@ class Application:
for name in names: for name in names:
process_single(name) process_single(name)
def clean(self) -> None:
'''
run all clean methods
'''
self.repository._clear_build()
self.repository._clear_cache()
self.repository._clear_chroot()
self.repository._clear_manual()
self.repository._clear_packages()
def remove(self, names: Iterable[str]) -> None: def remove(self, names: Iterable[str]) -> None:
''' '''
remove packages from repository remove packages from repository

View File

@ -58,6 +58,13 @@ class Task:
self.makepkg_flags = config.getlist(section, 'makepkg_flags') self.makepkg_flags = config.getlist(section, 'makepkg_flags')
self.makechrootpkg_flags = config.getlist(section, 'makechrootpkg_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 @property
def git_path(self) -> str: def git_path(self) -> str:
''' '''
@ -66,14 +73,19 @@ class Task:
return os.path.join(self.paths.sources, self.package.base) return os.path.join(self.paths.sources, self.package.base)
@staticmethod @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 local: local path to fetch
:param remote: remote target (from where 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 if os.path.isdir(local):
check_output('git', 'clone', remote, local, exception=None) check_output('git', 'fetch', 'origin', branch, cwd=local, exception=None)
else:
check_output('git', 'clone', remote, local, exception=None)
# and now force reset to our branch
check_output('git', 'reset', '--hard', f'origin/{branch}', cwd=local, exception=None)
def build(self) -> List[str]: def build(self) -> List[str]:
''' '''
@ -97,10 +109,13 @@ class Task:
exception=BuildFailed(self.package.base), exception=BuildFailed(self.package.base),
cwd=self.git_path).splitlines() cwd=self.git_path).splitlines()
def clone(self, path: Optional[str] = None) -> None: def init(self, path: Optional[str] = None) -> None:
''' '''
fetch package from git fetch package from git
:param path: optional local path to fetch. If not set default path will be used :param path: optional local path to fetch. If not set default path will be used
''' '''
git_path = path or self.git_path 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) return Task.fetch(git_path, self.package.git_url)

View File

@ -24,8 +24,8 @@ from typing import Callable, Dict, Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.report.report import Report from ahriman.core.report.report import Report
from ahriman.core.util import pretty_size, pretty_datetime
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.package_desciption import PackageDescription
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
@ -39,7 +39,7 @@ class HTML(Report):
link_path - prefix fo packages to download, string, required link_path - prefix fo packages to download, string, required
has_package_signed - True in case if package sign enabled, False otherwise, 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 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 pgp_key - default PGP key ID, string, optional
repository - repository name, string, required repository - repository name, string, required
@ -85,8 +85,10 @@ class HTML(Report):
content = [ content = [
{ {
'archive_size': pretty_size(properties.archive_size),
'build_date': pretty_datetime(properties.build_date),
'filename': properties.filename, 'filename': properties.filename,
'installed_size': PackageDescription.size_to_str(properties.installed_size), 'installed_size': pretty_size(properties.installed_size),
'name': package, 'name': package,
'version': base.version 'version': base.version
} for base in packages for package, properties in base.packages.items() } 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): for package in os.listdir(self.paths.sources):
shutil.rmtree(os.path.join(self.paths.sources, package)) 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: def _clear_manual(self) -> None:
''' '''
clear directory with manual package updates clear directory with manual package updates
@ -130,7 +144,7 @@ class Repository:
def build_single(package: Package) -> None: def build_single(package: Package) -> None:
self.reporter.set_building(package.base) self.reporter.set_building(package.base)
task = Task(package, self.architecture, self.config, self.paths) task = Task(package, self.architecture, self.config, self.paths)
task.clone() task.init()
built = task.build() built = task.build()
for src in built: for src in built:
dst = os.path.join(self.paths.packages, os.path.basename(src)) dst = os.path.join(self.paths.packages, os.path.basename(src))
@ -238,7 +252,7 @@ class Repository:
try: try:
remote = Package.load(local.base, self.pacman, self.aur_url) 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) result.append(remote)
self.reporter.set_pending(local.base) self.reporter.set_pending(local.base)
except Exception: except Exception:

View File

@ -17,11 +17,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import datetime
import subprocess import subprocess
from logging import Logger from logging import Logger
from typing import Optional from typing import Optional
from ahriman.core.exceptions import InvalidOption
def check_output(*args: str, exception: Optional[Exception], def check_output(*args: str, exception: Optional[Exception],
cwd: Optional[str] = None, stderr: int = subprocess.STDOUT, 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: True in case if name contains `.pkg.` and not signature, False otherwise
''' '''
return '.pkg.' in filename and not filename.endswith('.sig') return '.pkg.' in filename and not filename.endswith('.sig')
def pretty_datetime(timestamp: Optional[datetime.datetime]) -> str:
'''
convert datetime object to string
:param timestamp: datetime to convert
:return: pretty printable datetime as string
'''
return '' if timestamp is None else 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

@ -39,6 +39,21 @@ class BuildStatusEnum(Enum):
Failed = 'failed' Failed = 'failed'
Success = 'success' Success = 'success'
def badges_color(self) -> str:
'''
convert itself to shield.io badges color
:return: shields.io color
'''
if self == BuildStatusEnum.Pending:
return 'yellow'
elif self == BuildStatusEnum.Building:
return 'yellow'
elif self == BuildStatusEnum.Failed:
return 'critical'
elif self == BuildStatusEnum.Success:
return 'success'
return 'inactive'
class BuildStatus: class BuildStatus:
''' '''
@ -55,11 +70,4 @@ class BuildStatus:
:param timestamp: build status timestamp. Current timestamp 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.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
self._timestamp = timestamp or datetime.datetime.utcnow() 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')

View File

@ -20,9 +20,8 @@
from __future__ import annotations from __future__ import annotations
import aur # type: ignore import aur # type: ignore
import datetime
import os import os
import shutil
import tempfile
from dataclasses import dataclass from dataclasses import dataclass
from pyalpm import vercmp # type: ignore from pyalpm import vercmp # type: ignore
@ -33,6 +32,7 @@ from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.exceptions import InvalidPackageInfo
from ahriman.core.util import check_output from ahriman.core.util import check_output
from ahriman.models.package_desciption import PackageDescription from ahriman.models.package_desciption import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths
@dataclass @dataclass
@ -76,30 +76,28 @@ class Package:
''' '''
return f'{self.aur_url}/packages/{self.base}' 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 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 :return: package version if package is not VCS and current version according to VCS otherwise
''' '''
if not self.is_vcs: if not self.is_vcs:
return self.version return self.version
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
clone_dir = tempfile.mkdtemp() clone_dir = os.path.join(paths.cache, self.base)
try:
Task.fetch(clone_dir, self.git_url) Task.fetch(clone_dir, self.git_url)
# update pkgver first # update pkgver first
check_output('makepkg', '--nodeps', '--nobuild', check_output('makepkg', '--nodeps', '--nobuild', exception=None, cwd=clone_dir)
exception=None, cwd=clone_dir) # generate new .SRCINFO and put it to parser
# generate new .SRCINFO and put it to parser src_info_source = check_output('makepkg', '--printsrcinfo', exception=None, cwd=clone_dir)
src_info_source = check_output('makepkg', '--printsrcinfo', src_info, errors = parse_srcinfo(src_info_source)
exception=None, cwd=clone_dir) if errors:
src_info, errors = parse_srcinfo(src_info_source) raise InvalidPackageInfo(errors)
if errors:
raise InvalidPackageInfo(errors) return self.full_version(src_info.get('epoch'), src_info['pkgver'], src_info['pkgrel'])
return self.full_version(src_info.get('epoch'), src_info['pkgver'], src_info['pkgrel'])
finally:
shutil.rmtree(clone_dir, ignore_errors=True)
@classmethod @classmethod
def from_archive(cls: Type[Package], path: str, pacman: Pacman, aur_url: str) -> Package: def from_archive(cls: Type[Package], path: str, pacman: Pacman, aur_url: str) -> Package:
@ -111,7 +109,8 @@ class Package:
:return: package properties :return: package properties
''' '''
package = pacman.handle.load_pkg(path) package = pacman.handle.load_pkg(path)
properties = PackageDescription(os.path.basename(path), package.isize) build_date = datetime.datetime.fromtimestamp(package.builddate)
properties = PackageDescription(package.size, build_date, os.path.basename(path), package.isize)
return cls(package.base, package.version, aur_url, {package.name: properties}) return cls(package.base, package.version, aur_url, {package.name: properties})
@classmethod @classmethod
@ -196,12 +195,13 @@ class Package:
except Exception as e: except Exception as e:
raise InvalidPackageInfo(str(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 check if package is out-of-dated
:param remote: package properties from remote source :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 :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) result: int = vercmp(self.version, remote_version)
return result < 0 return result < 0

View File

@ -17,41 +17,23 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import datetime
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from ahriman.core.exceptions import InvalidOption
@dataclass @dataclass
class PackageDescription: class PackageDescription:
''' '''
package specific properties 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[datetime.datetime] = None
filename: Optional[str] = None filename: Optional[str] = None
installed_size: Optional[int] = 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): class ReportSettings(Enum):
''' '''
report targets enumeration report targets enumeration
:ivar HTML: html report generation :cvar HTML: html report generation
''' '''
HTML = auto() HTML = auto()

View File

@ -25,7 +25,7 @@ from dataclasses import dataclass
@dataclass @dataclass
class RepositoryPaths: class RepositoryPaths:
''' '''
repository paths holder repository paths holder. For the most operations with paths you want to use this object
:ivar root: repository root (i.e. ahriman home) :ivar root: repository root (i.e. ahriman home)
:ivar architecture: repository architecture :ivar architecture: repository architecture
''' '''
@ -33,11 +33,19 @@ class RepositoryPaths:
root: str root: str
architecture: 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 @property
def chroot(self) -> str: def chroot(self) -> str:
''' '''
:return: directory for devtools chroot :return: directory for devtools chroot
''' '''
# for the chroot directory devtools will create own tree and we don't have to specify architecture here
return os.path.join(self.root, 'chroot') return os.path.join(self.root, 'chroot')
@property @property
@ -45,14 +53,14 @@ class RepositoryPaths:
''' '''
:return: directory for manual updates (i.e. from add command) :return: directory for manual updates (i.e. from add command)
''' '''
return os.path.join(self.root, 'manual') return os.path.join(self.root, 'manual', self.architecture)
@property @property
def packages(self) -> str: def packages(self) -> str:
''' '''
:return: directory for built packages :return: directory for built packages
''' '''
return os.path.join(self.root, 'packages') return os.path.join(self.root, 'packages', self.architecture)
@property @property
def repository(self) -> str: def repository(self) -> str:
@ -66,12 +74,13 @@ class RepositoryPaths:
''' '''
:return: directory for downloaded PKGBUILDs for current build :return: directory for downloaded PKGBUILDs for current build
''' '''
return os.path.join(self.root, 'sources') return os.path.join(self.root, 'sources', self.architecture)
def create_tree(self) -> None: def create_tree(self) -> None:
''' '''
create ahriman working tree 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.chroot, mode=0o755, exist_ok=True)
os.makedirs(self.manual, 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.packages, mode=0o755, exist_ok=True)

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@
# #
from aiohttp.web import Application from aiohttp.web import Application
from ahriman.web.views.ahriman import AhrimanView
from ahriman.web.views.index import IndexView from ahriman.web.views.index import IndexView
from ahriman.web.views.package import PackageView from ahriman.web.views.package import PackageView
from ahriman.web.views.packages import PackagesView from ahriman.web.views.packages import PackagesView
@ -45,6 +46,8 @@ def setup_routes(application: Application) -> None:
application.router.add_get('/', IndexView) application.router.add_get('/', IndexView)
application.router.add_get('/index.html', IndexView) application.router.add_get('/index.html', IndexView)
application.router.add_post('/api/v1/ahriman', AhrimanView)
application.router.add_post('/api/v1/packages', PackagesView) application.router.add_post('/api/v1/packages', PackagesView)
application.router.add_delete('/api/v1/packages/{package}', PackageView) application.router.add_delete('/api/v1/packages/{package}', PackageView)

View File

@ -23,6 +23,7 @@ from typing import Any, Dict
import ahriman.version as version import ahriman.version as version
from ahriman.core.util import pretty_datetime
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -36,7 +37,7 @@ class IndexView(BaseView):
packages - sorted list of packages properties: base, packages (sorted list), status, packages - sorted list of packages properties: base, packages (sorted list), status,
timestamp, version, web_url. Required timestamp, version, web_url. Required
repository - repository name, string, required repository - repository name, string, required
service - service status properties: status, timestamp. Required service - service status properties: status, status_color, timestamp. Required
version - ahriman version, string, required version - ahriman version, string, required
''' '''
@ -52,14 +53,15 @@ class IndexView(BaseView):
'base': package.base, 'base': package.base,
'packages': [p for p in sorted(package.packages)], 'packages': [p for p in sorted(package.packages)],
'status': status.status.value, 'status': status.status.value,
'timestamp': status.timestamp, 'timestamp': pretty_datetime(status.timestamp),
'version': package.version, 'version': package.version,
'web_url': package.web_url 'web_url': package.web_url
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base) } for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
] ]
service = { service = {
'status': self.service.status.status.value, 'status': self.service.status.status.value,
'timestamp': self.service.status.timestamp 'status_color': self.service.status.status.badges_color(),
'timestamp': pretty_datetime(self.service.status.timestamp)
} }
return { return {