Compare commits

..

15 Commits

23 changed files with 289 additions and 161 deletions

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=0.11.0 pkgver=0.11.6
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=('02a586de3253908022ec0d8329d217dc52c922ab091d3c6ed91bb45fbc521de08f9163c17dfb7589422f43e7114b0b3dd6d35357c4cd82e564251a0f7f908d18' sha512sums=('89860693f9a3e6aeb675f571f19366712d65d124879745d89696f8c3d642ef3a468dd0f4a508800dead1c14705c52324c61dad09b7d9e6b9030baa6ee0f86a11'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075' '13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4') '55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini' backup=('etc/ahriman.ini'

View File

@ -3,84 +3,40 @@
<head> <head>
<title>{{ repository|e }}</title> <title>{{ repository|e }}</title>
<style> {% include "style.jinja2" %}
:root {
--color-building: 250, 255, 146;
--color-failed: 255, 94, 94;
--color-pending: 250, 255, 146;
--color-success: 121, 255, 94;
--color-unknown: 197, 197, 197;
}
@keyframes blink-building { {% include "sorttable.jinja2" %}
0% { background-color: rgba(var(--color-building), 1.0); } {% include "search.jinja2" %}
10% { background-color: rgba(var(--color-building), 0.9); }
20% { background-color: rgba(var(--color-building), 0.8); }
30% { background-color: rgba(var(--color-building), 0.7); }
40% { background-color: rgba(var(--color-building), 0.6); }
50% { background-color: rgba(var(--color-building), 0.5); }
60% { background-color: rgba(var(--color-building), 0.4); }
70% { background-color: rgba(var(--color-building), 0.3); }
80% { background-color: rgba(var(--color-building), 0.2); }
90% { background-color: rgba(var(--color-building), 0.1); }
100% { background-color: rgba(var(--color-building), 0.0); }
}
table, th, td {
padding: 5px;
}
td.package {
font-weight: bolder;
}
td.package-unknown {
background-color: rgba(var(--color-unknown), 1.0);
}
td.package-pending {
background-color: rgba(var(--color-pending), 1.0);
}
td.package-building {
background-color: rgba(var(--color-building), 1.0);
animation-name: blink-building;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
td.package-failed {
background-color: rgba(var(--color-failed), 1.0);
}
td.package-success {
background-color: rgba(var(--color-success), 1.0);
}
</style>
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>
</head> </head>
<body> <body>
<h1>ahriman {{ version|e }}</h1> <div class="root">
<h1>ahriman {{ version|e }} ({{ architecture|e }})</h1>
<table class="sortable" id="builds"> {% include "search-line.jinja2" %}
<tr>
<th>package base</th>
<th>packages</th>
<th>version</th>
<th>architecture</th>
<th>timestamp</th>
<th>status</th>
</tr>
{% for package in packages %} <section class="element">
<tr> <table class="sortable search-table">
<td class="package"><a href="{{ package.web_url|e }}" title="{{ package.base|e }}">{{ package.base|e }}</a></td> <tr class="header">
<td>{{ package.packages|join("<br>"|safe) }}</td> <th>package base</th>
<td>{{ package.version|e }}</td> <th>packages</th>
<td>{{ architecture|e }}</td> <th>version</th>
<td>{{ package.timestamp|e }}</td> <th>last update</th>
<td class="package-{{ package.status|e }}">{{ package.status|e }}</td> <th>status</th>
</tr> </tr>
{% endfor %}
</table> {% for package in packages %}
<tr class="package">
<td class="include-search"><a href="{{ package.web_url|e }}" title="{{ package.base|e }}">{{ package.base|e }}</a></td>
<td class="include-search">{{ package.packages|join("<br>"|safe) }}</td>
<td>{{ package.version|e }}</td>
<td>{{ package.timestamp|e }}</td>
<td class="package-{{ package.status|e }}">{{ package.status|e }}</td>
</tr>
{% endfor %}
</table>
</section>
</div>
</body> </body>
</html> </html>

View File

@ -2,31 +2,51 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>{{ repository|e }}</title> <title>{{ repository|e }}</title>
{% include "style.jinja2" %}
{% include "sorttable.jinja2" %}
{% include "search.jinja2" %}
</head> </head>
<body> <body>
<h1>Archlinux custom repository</h1> <div class="root">
<h1>Archlinux user repository</h1>
{% if pgp_key is not none %} <section class="element">
<p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}" title="key search">{{ pgp_key|e }}</a>.</p> {% if pgp_key is not none %}
{% endif %} <p>This repository is signed with <a href="http://keys.gnupg.net/pks/lookup?search=0x{{ pgp_key|e }}&fingerprint=on&op=index" title="key search">{{ pgp_key|e }}</a> by default.</p>
{% endif %}
<code> <code>
$ cat /etc/pacman.conf<br> $ cat /etc/pacman.conf<br>
[{{ repository|e }}]<br> [{{ repository|e }}]<br>
Server = {{ link_path|e }}<br> Server = {{ link_path|e }}<br>
SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
</code> </code>
</section>
<p>Packages:</p> {% include "search-line.jinja2" %}
<ul>
{% for package, package_url in packages.items() %}
<li><a href="{{ package_url|e }}" title="{{ package|e }}">{{ package|e }}</a></li>
{% endfor %}
</ul>
{% if homepage is not none %} <section class="element">
<footer><a href="{{ homepage|e }}" title="homepage">Homepage</a></footer> <table class="sortable search-table">
{% endif %} <tr class="header">
<th>package</th>
<th>version</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>
</tr>
{% endfor %}
</table>
</section>
{% if homepage is not none %}
<footer><a href="{{ homepage|e }}" title="homepage">Homepage</a></footer>
{% endif %}
</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,3 @@
<section class="element">
<input type="search" id="search" onkeyup="searchInTable()" placeholder="search for package" title="search for package"/>
</section>

View File

@ -0,0 +1,25 @@
<script type="text/javascript">
function searchInTable() {
const input = document.getElementById("search");
const filter = input.value.toLowerCase();
const tables = document.getElementsByClassName("search-table");
for (let i = 0; i < tables.length; i++) {
const tr = tables[i].getElementsByTagName("tr");
// from 1 coz of header
for (let i = 1; i < tr.length; i++) {
let td = tr[i].getElementsByClassName("include-search");
let display = "none";
for (let j = 0; j < td.length; j++) {
if (td[j].tagName.toLowerCase() === "td") {
if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) {
display = "";
break;
}
}
}
tr[i].style.display = display;
}
}
}
</script>

View File

@ -0,0 +1 @@
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script>

View File

@ -0,0 +1,78 @@
<style>
:root {
--color-building: 255, 255, 146;
--color-failed: 255, 94, 94;
--color-pending: 255, 255, 146;
--color-success: 94, 255, 94;
--color-unknown: 225, 225, 225;
}
@keyframes blink-building {
0% { background-color: rgba(var(--color-building), 1.0); }
10% { background-color: rgba(var(--color-building), 0.9); }
20% { background-color: rgba(var(--color-building), 0.8); }
30% { background-color: rgba(var(--color-building), 0.7); }
40% { background-color: rgba(var(--color-building), 0.6); }
50% { background-color: rgba(var(--color-building), 0.5); }
60% { background-color: rgba(var(--color-building), 0.4); }
70% { background-color: rgba(var(--color-building), 0.3); }
80% { background-color: rgba(var(--color-building), 0.2); }
90% { background-color: rgba(var(--color-building), 0.1); }
100% { background-color: rgba(var(--color-building), 0.0); }
}
div.root {
width: 70%;
padding: 15px 15% 0;
}
section.element {
width: 100%;
padding: 10px 0;
}
code, input, table {
width: inherit;
}
th, td {
padding: 5px;
}
tr.package:nth-child(odd) {
background-color: rgba(255, 255, 255, 1);
}
tr.package:nth-child(even) {
background-color: rgba(235, 235, 255, 1);
}
tr.package:hover {
background-color: rgba(255, 255, 225, 1);
}
tr.header{
background-color: rgba(200, 200, 255, 1);
}
td.package-unknown {
background-color: rgba(var(--color-unknown), 1.0);
}
td.package-pending {
background-color: rgba(var(--color-pending), 1.0);
}
td.package-building {
background-color: rgba(var(--color-building), 1.0);
animation-name: blink-building;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
td.package-failed {
background-color: rgba(var(--color-failed), 1.0);
}
td.package-success {
background-color: rgba(var(--color-success), 1.0);
}
</style>

View File

@ -57,6 +57,10 @@ setup(
('share/ahriman', [ ('share/ahriman', [
'package/share/ahriman/build-status.jinja2', 'package/share/ahriman/build-status.jinja2',
'package/share/ahriman/repo-index.jinja2', 'package/share/ahriman/repo-index.jinja2',
'package/share/ahriman/search.jinja2',
'package/share/ahriman/search-line.jinja2',
'package/share/ahriman/sorttable.jinja2',
'package/share/ahriman/style.jinja2',
]), ]),
], ],

View File

@ -51,7 +51,7 @@ def sync(args: argparse.Namespace) -> None:
def update(args: argparse.Namespace) -> None: def update(args: argparse.Namespace) -> None:
app = Application.from_args(args) app = Application.from_args(args)
log_fn = lambda line: print(line) if args.dry_run else app.logger.info(line) log_fn = lambda line: print(line) if args.dry_run else app.logger.info(line)
packages = app.get_updates(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
app.update(packages) app.update(packages)
@ -79,6 +79,7 @@ if __name__ == '__main__':
add_parser.set_defaults(fn=add) add_parser.set_defaults(fn=add)
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.set_defaults(fn=update, no_aur=False, no_manual=True, no_vcs=False, dry_run=True) check_parser.set_defaults(fn=update, no_aur=False, no_manual=True, no_vcs=False, dry_run=True)
rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository') rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository')
@ -97,6 +98,7 @@ if __name__ == '__main__':
sync_parser.set_defaults(fn=sync) sync_parser.set_defaults(fn=sync)
update_parser = subparsers.add_parser('update', description='run updates') update_parser = subparsers.add_parser('update', description='run updates')
update_parser.add_argument('package', help='filter check by packages', nargs='*')
update_parser.add_argument('--dry-run', help='just perform check for updates, same as check command', action='store_true') update_parser.add_argument('--dry-run', help='just perform check for updates, same as check command', action='store_true')
update_parser.add_argument('--no-aur', help='do not check for AUR updates', action='store_true') update_parser.add_argument('--no-aur', help='do not check for AUR updates', action='store_true')
update_parser.add_argument('--no-manual', help='do not include manual updates', action='store_true') update_parser.add_argument('--no-manual', help='do not include manual updates', action='store_true')
@ -111,5 +113,5 @@ if __name__ == '__main__':
parser.print_help() parser.print_help()
exit(1) exit(1)
with Lock(args.lock, args.force): with Lock(args.lock, args.architecture, args.force):
args.fn(args) args.fn(args)

View File

@ -47,10 +47,10 @@ class Application:
return cls(args.architecture, config) return cls(args.architecture, config)
def _known_packages(self) -> Set[str]: def _known_packages(self) -> Set[str]:
known_packages = set() known_packages: Set[str] = set()
# local set # local set
for package in self.repository.packages(): for package in self.repository.packages():
known_packages.update(package.packages) known_packages.update(package.packages.keys())
known_packages.update(self.repository.pacman.all_packages()) known_packages.update(self.repository.pacman.all_packages())
return known_packages return known_packages
@ -58,12 +58,12 @@ class Application:
self.report() self.report()
self.sync() self.sync()
def get_updates(self, no_aur: bool, no_manual: bool, no_vcs: bool, def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]: log_fn: Callable[[str], None]) -> List[Package]:
updates = [] updates = []
if not no_aur: if not no_aur:
updates.extend(self.repository.updates_aur(no_vcs)) updates.extend(self.repository.updates_aur(filter_packages, no_vcs))
if not no_manual: if not no_manual:
updates.extend(self.repository.updates_manual()) updates.extend(self.repository.updates_manual())
@ -114,13 +114,18 @@ class Application:
self.repository.process_sync(targets) self.repository.process_sync(targets)
def update(self, updates: Iterable[Package]) -> None: def update(self, updates: Iterable[Package]) -> None:
def process_single(portion: Iterable[Package]): def process_update(paths: Iterable[str]) -> None:
packages = self.repository.process_build(portion) self.repository.process_update(paths)
self.repository.process_update(packages)
self._finalize() self._finalize()
# process built packages
packages = self.repository.packages_built()
process_update(packages)
# process manual packages
tree = Tree() tree = Tree()
tree.load(updates) tree.load(updates)
for num, level in enumerate(tree.levels()): for num, level in enumerate(tree.levels()):
self.logger.info(f'processing level #{num} {[package.base for package in level]}') self.logger.info(f'processing level #{num} {[package.base for package in level]}')
process_single(level) packages = self.repository.process_build(level)
process_update(packages)

View File

@ -17,28 +17,33 @@
# 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 os import os
from typing import Optional from types import TracebackType
from typing import Literal, Optional, Type
from ahriman.core.exceptions import DuplicateRun from ahriman.core.exceptions import DuplicateRun
class Lock: class Lock:
def __init__(self, path: Optional[str], force: bool) -> None: def __init__(self, path: Optional[str], architecture: str, force: bool) -> None:
self.path = path self.path = f'{path}_{architecture}' if path is not None else None
self.force = force self.force = force
def __enter__(self): def __enter__(self) -> Lock:
if self.force: if self.force:
self.remove() self.remove()
self.check() self.check()
self.create() self.create()
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception],
exc_tb: TracebackType) -> Literal[False]:
self.remove() self.remove()
return False
def check(self) -> None: def check(self) -> None:
if self.path is None: if self.path is None:

View File

@ -17,7 +17,7 @@
# 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 pyalpm import Handle from pyalpm import Handle # type: ignore
from typing import List, Set from typing import List, Set
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration

View File

@ -20,11 +20,11 @@
import jinja2 import jinja2
import os import os
from typing import Dict 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 package_like from ahriman.models.package import Package
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
@ -34,35 +34,39 @@ class HTML(Report):
Report.__init__(self, architecture, config) Report.__init__(self, architecture, config)
section = config.get_section_name('html', architecture) section = config.get_section_name('html', architecture)
self.report_path = config.get(section, 'path') self.report_path = config.get(section, 'path')
self.link_path = config.get(section, 'link_path') self.link_path = config.get(section, 'link_path')
self.template_path = config.get(section, 'template_path') self.template_path = config.get(section, 'template_path')
# base template vars # base template vars
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist('sign', 'target')]
self.pgp_key = config.get('sign', 'key', fallback=None)
self.homepage = config.get(section, 'homepage', fallback=None) self.homepage = config.get(section, 'homepage', fallback=None)
self.repository = config.get('repository', 'name') self.repository = config.get('repository', 'name')
def generate(self, path: str) -> None: sign_section = config.get_section_name('sign', architecture)
self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist(sign_section, 'target')]
self.pgp_key = config.get(sign_section, 'key') if self.sign_targets else None
def generate(self, packages: Iterable[Package]) -> None:
# idea comes from https://stackoverflow.com/a/38642558 # idea comes from https://stackoverflow.com/a/38642558
templates_dir, template_name = os.path.split(self.template_path) templates_dir, template_name = os.path.split(self.template_path)
loader = jinja2.FileSystemLoader(searchpath=templates_dir) loader = jinja2.FileSystemLoader(searchpath=templates_dir)
environment = jinja2.Environment(loader=loader) environment = jinja2.Environment(loader=loader)
template = environment.get_template(template_name) template = environment.get_template(template_name)
packages: Dict[str, str] = {} content = [
for fn in sorted(os.listdir(path)): {
if not package_like(fn): 'filename': filename,
continue 'name': package,
packages[fn] = f'{self.link_path}/{fn}' 'version': base.version
} for base in packages for package, filename in base.packages.items()
]
comparator: Callable[[Dict[str, str]], str] = lambda item: item['filename']
html = template.render( html = template.render(
homepage=self.homepage, homepage=self.homepage,
link_path=self.link_path, link_path=self.link_path,
has_package_signed=SignSettings.SignPackages in self.sign_targets, has_package_signed=SignSettings.SignPackages in self.sign_targets,
has_repo_signed=SignSettings.SignRepository in self.sign_targets, has_repo_signed=SignSettings.SignRepository in self.sign_targets,
packages=packages, packages=sorted(content, key=comparator),
pgp_key=self.pgp_key, pgp_key=self.pgp_key,
repository=self.repository) repository=self.repository)

View File

@ -19,8 +19,11 @@
# #
import logging import logging
from typing import Iterable
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ReportFailed from ahriman.core.exceptions import ReportFailed
from ahriman.models.package import Package
from ahriman.models.report_settings import ReportSettings from ahriman.models.report_settings import ReportSettings
@ -32,7 +35,7 @@ class Report:
self.config = config self.config = config
@staticmethod @staticmethod
def run(architecture: str, config: Configuration, target: str, path: str) -> None: def run(architecture: str, config: Configuration, target: str, packages: Iterable[Package]) -> None:
provider = ReportSettings.from_option(target) provider = ReportSettings.from_option(target)
if provider == ReportSettings.HTML: if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML from ahriman.core.report.html import HTML
@ -41,10 +44,10 @@ class Report:
report = Report(architecture, config) report = Report(architecture, config)
try: try:
report.generate(path) report.generate(packages)
except Exception: except Exception:
report.logger.exception('report generation failed', exc_info=True) report.logger.exception('report generation failed', exc_info=True)
raise ReportFailed() raise ReportFailed()
def generate(self, path: str) -> None: def generate(self, packages: Iterable[Package]) -> None:
pass pass

View File

@ -63,8 +63,8 @@ class Repository:
shutil.rmtree(os.path.join(self.paths.manual, package)) shutil.rmtree(os.path.join(self.paths.manual, package))
def _clear_packages(self) -> None: def _clear_packages(self) -> None:
for package in os.listdir(self.paths.packages): for package in self.packages_built():
os.remove(os.path.join(self.paths.packages, package)) os.remove(package)
def packages(self) -> List[Package]: def packages(self) -> List[Package]:
result: Dict[str, Package] = {} result: Dict[str, Package] = {}
@ -80,6 +80,12 @@ class Repository:
continue continue
return list(result.values()) return list(result.values())
def packages_built(self) -> List[str]:
return [
os.path.join(self.paths.packages, fn)
for fn in os.listdir(self.paths.packages)
]
def process_build(self, updates: Iterable[Package]) -> List[str]: def process_build(self, updates: Iterable[Package]) -> List[str]:
def build_single(package: Package) -> None: def build_single(package: Package) -> None:
self.web.set_building(package.base) self.web.set_building(package.base)
@ -99,10 +105,7 @@ class Repository:
continue continue
self._clear_build() self._clear_build()
return [ return self.packages_built()
os.path.join(self.paths.packages, fn)
for fn in os.listdir(self.paths.packages)
]
def process_remove(self, packages: Iterable[str]) -> str: def process_remove(self, packages: Iterable[str]) -> str:
def remove_single(package: str) -> None: def remove_single(package: str) -> None:
@ -111,11 +114,12 @@ class Repository:
except Exception: except Exception:
self.logger.exception(f'could not remove {package}', exc_info=True) self.logger.exception(f'could not remove {package}', exc_info=True)
requested = set(packages)
for local in self.packages(): for local in self.packages():
if local.base in packages: if local.base in packages:
to_remove = local.packages to_remove = set(local.packages.keys())
elif local.packages.intersection(packages): elif requested.intersection(local.packages.keys()):
to_remove = local.packages.intersection(packages) to_remove = requested.intersection(local.packages.keys())
else: else:
to_remove = set() to_remove = set()
self.web.remove(local.base, to_remove) self.web.remove(local.base, to_remove)
@ -128,7 +132,7 @@ class Repository:
if targets is None: if targets is None:
targets = self.config.getlist('report', 'target') targets = self.config.getlist('report', 'target')
for target in targets: for target in targets:
Report.run(self.architecture, self.config, target, self.paths.repository) Report.run(self.architecture, self.config, target, self.packages())
def process_sync(self, targets: Optional[Iterable[str]]) -> None: def process_sync(self, targets: Optional[Iterable[str]]) -> None:
if targets is None: if targets is None:
@ -154,7 +158,7 @@ class Repository:
return self.repo.repo_path return self.repo.repo_path
def updates_aur(self, no_vcs: bool) -> List[Package]: def updates_aur(self, filter_packages: Iterable[str], no_vcs: bool) -> List[Package]:
result: List[Package] = [] result: List[Package] = []
build_section = self.config.get_section_name('build', self.architecture) build_section = self.config.get_section_name('build', self.architecture)
@ -165,6 +169,8 @@ class Repository:
continue continue
if local.is_vcs and no_vcs: if local.is_vcs and no_vcs:
continue continue
if filter_packages and local.base not in filter_packages:
continue
try: try:
remote = Package.load(local.base, self.pacman, self.aur_url) remote = Package.load(local.base, self.pacman, self.aur_url)

View File

@ -56,13 +56,17 @@ class Leaf:
self.package = package self.package = package
self.dependencies: Set[str] = set() self.dependencies: Set[str] = set()
@property
def items(self) -> Iterable[str]:
return self.package.packages.keys()
def is_root(self, packages: Iterable[Leaf]) -> bool: def is_root(self, packages: Iterable[Leaf]) -> bool:
''' '''
:param packages: :param packages:
:return: true if any of packages is dependency of the leaf, false otherwise :return: true if any of packages is dependency of the leaf, false otherwise
''' '''
for package in packages: for leaf in packages:
if package.package.packages.intersection(self.dependencies): if self.dependencies.intersection(leaf.items):
return False return False
return True return True

View File

@ -24,7 +24,7 @@ from typing import Optional
def check_output(*args: str, exception: Optional[Exception], def check_output(*args: str, exception: Optional[Exception],
cwd = None, stderr: int = subprocess.STDOUT, cwd: Optional[str] = None, stderr: int = subprocess.STDOUT,
logger: Optional[Logger] = None) -> str: logger: Optional[Logger] = None) -> str:
try: try:
result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode('utf8').strip() result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode('utf8').strip()

View File

@ -19,15 +19,15 @@
# #
from __future__ import annotations from __future__ import annotations
import aur import aur # type: ignore
import os import os
import shutil import shutil
import tempfile import tempfile
from dataclasses import dataclass, field from dataclasses import dataclass
from pyalpm import Handle from pyalpm import vercmp # type: ignore
from srcinfo.parse import parse_srcinfo from srcinfo.parse import parse_srcinfo # type: ignore
from typing import List, Set, Type from typing import Dict, List, Optional, Set, Type
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo from ahriman.core.exceptions import InvalidPackageInfo
@ -39,7 +39,7 @@ class Package:
base: str base: str
version: str version: str
aur_url: str aur_url: str
packages: Set[str] = field(default_factory=set) packages: Dict[str, str] # map of package name to archive name
@property @property
def git_url(self) -> str: def git_url(self) -> str:
@ -68,7 +68,7 @@ class Package:
try: 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', '--noprepare', '--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', src_info_source = check_output('makepkg', '--printsrcinfo',
@ -76,19 +76,19 @@ class Package:
src_info, errors = parse_srcinfo(src_info_source) src_info, errors = parse_srcinfo(src_info_source)
if errors: if errors:
raise InvalidPackageInfo(errors) raise InvalidPackageInfo(errors)
return f'{src_info["pkgver"]}-{src_info["pkgrel"]}' return self.full_version(src_info.get('epoch'), src_info['pkgver'], src_info['pkgrel'])
finally: finally:
shutil.rmtree(clone_dir, ignore_errors=True) 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:
package = pacman.handle.load_pkg(path) package = pacman.handle.load_pkg(path)
return cls(package.base, package.version, aur_url, {package.name}) return cls(package.base, package.version, aur_url, {package.name: os.path.basename(path)})
@classmethod @classmethod
def from_aur(cls: Type[Package], name: str, aur_url: str)-> Package: def from_aur(cls: Type[Package], name: str, aur_url: str)-> Package:
package = aur.info(name) package = aur.info(name)
return cls(package.package_base, package.version, aur_url, {package.name}) return cls(package.package_base, package.version, aur_url, {package.name: ''})
@classmethod @classmethod
def from_build(cls: Type[Package], path: str, aur_url: str) -> Package: def from_build(cls: Type[Package], path: str, aur_url: str) -> Package:
@ -96,9 +96,10 @@ class Package:
src_info, errors = parse_srcinfo(fn.read()) src_info, errors = parse_srcinfo(fn.read())
if errors: if errors:
raise InvalidPackageInfo(errors) raise InvalidPackageInfo(errors)
packages = set(src_info['packages'].keys()) packages = {key: '' for key in src_info['packages'].keys()}
version = cls.full_version(src_info.get('epoch'), src_info['pkgver'], src_info['pkgrel'])
return cls(src_info['pkgbase'], f'{src_info["pkgver"]}-{src_info["pkgrel"]}', aur_url, packages) return cls(src_info['pkgbase'], version, aur_url, packages)
@staticmethod @staticmethod
def dependencies(path: str) -> Set[str]: def dependencies(path: str) -> Set[str]:
@ -115,6 +116,11 @@ class Package:
packages = set(src_info['packages'].keys()) packages = set(src_info['packages'].keys())
return set(depends + makedepends) - packages return set(depends + makedepends) - packages
@staticmethod
def full_version(epoch: Optional[str], pkgver: str, pkgrel: str) -> str:
prefix = f'{epoch}:' if epoch else ''
return f'{prefix}{pkgver}-{pkgrel}'
@staticmethod @staticmethod
def load(path: str, pacman: Pacman, aur_url: str) -> Package: def load(path: str, pacman: Pacman, aur_url: str) -> Package:
try: try:
@ -132,5 +138,5 @@ class Package:
def is_outdated(self, remote: Package) -> bool: def is_outdated(self, remote: Package) -> bool:
remote_version = remote.actual_version() # either normal version or updated VCS remote_version = remote.actual_version() # either normal version or updated VCS
result = check_output('vercmp', self.version, remote_version, exception=None) result: int = vercmp(self.version, remote_version)
return True if int(result) < 0 else False return result < 0

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.11.0' __version__ = '0.11.6'

View File

@ -17,15 +17,19 @@
# 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 aiohttp.web import middleware, Request, Response from aiohttp.web import middleware, Request
from aiohttp.web_exceptions import HTTPClientError from aiohttp.web_exceptions import HTTPClientError
from aiohttp.web_response import StreamResponse
from logging import Logger from logging import Logger
from typing import Callable from typing import Awaitable, Callable
def exception_handler(logger: Logger) -> Callable: HandlerType = Callable[[Request], Awaitable[StreamResponse]]
def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaitable[StreamResponse]]:
@middleware @middleware
async def handle(request: Request, handler: Callable) -> Response: async def handle(request: Request, handler: HandlerType) -> StreamResponse:
try: try:
return await handler(request) return await handler(request)
except HTTPClientError: except HTTPClientError:

View File

@ -27,4 +27,5 @@ class BaseView(View):
@property @property
def service(self) -> Watcher: def service(self) -> Watcher:
return self.request.app['watcher'] watcher: Watcher = self.request.app['watcher']
return watcher

View File

@ -17,7 +17,8 @@
# 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 aiohttp_jinja2 import template import aiohttp_jinja2 # type: ignore
from typing import Any, Dict from typing import Any, Dict
import ahriman.version as version import ahriman.version as version
@ -27,7 +28,7 @@ from ahriman.web.views.base import BaseView
class IndexView(BaseView): class IndexView(BaseView):
@template("build-status.jinja2") @aiohttp_jinja2.template("build-status.jinja2") # type: ignore
async def get(self) -> Dict[str, Any]: async def get(self) -> Dict[str, Any]:
# some magic to make it jinja-friendly # some magic to make it jinja-friendly
packages = [ packages = [

View File

@ -17,7 +17,7 @@
# 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 aiohttp_jinja2 import aiohttp_jinja2 # type: ignore
import jinja2 import jinja2
import logging import logging