#!/usr/bin/env python3 # Adélie Linux architecture package tester # Ensure all packages depend on latest versions of each other # # Copyright © 2019-2020 Adélie Linux team. All rights reserved. # NCSA license. # import argparse import collections import functools import os import sys from apkkit.base.index import Index from version import is_older, is_same, verkey, ver_is, APK_OPS from output import FORMATTERS, Yes, No, Partial, PARTIAL_MISSING def atomize(spec): if not any(i in spec for i in APK_OPS): return spec, None for op in APK_OPS: try: name, ver = spec.split(op, maxsplit=1) except ValueError: continue return name, functools.partial(ver_is, op=op, b=ver) # Not reached assert False def analyze(opts): pkgs = {i: collections.defaultdict(dict) for i in opts.arches} newest = {i: dict() for i in opts.arches} providers = {i: collections.defaultdict(list) for i in opts.arches} for arch in opts.arches: print("Loading " + arch + "...", file=sys.stderr) index = [] for repo in opts.repos: url = f"{opts.url}/{repo}/{arch}/APKINDEX.tar.gz" index.extend(Index(url=url).packages) for pkg in index: new = pkg.version pkgs[arch][pkg.name][new] = pkg curr = newest[arch].get(pkg.name, None) if curr is None: newest[arch][pkg.name] = new elif is_older(curr, new): newest[arch][pkg.name] = new providers[arch][pkg.name].append((pkg.name, new)) for i in pkg.provides: i = i.split("=", maxsplit=1) if len(i) == 2: i, pver = i else: i, pver = i[0], new curr = newest[arch].get(i, None) if curr is None: newest[arch][i] = pver elif is_older(curr, pver): newest[arch][i] = pver providers[arch][i].append((pkg.name, pver)) return pkgs, newest, providers def trace_deps(providers, newest, pkg): for dep in pkg.depends: if dep.startswith("!"): continue spec = dep dep, constraint = atomize(dep) if dep in pkg.provides: continue if dep not in providers: yield No(f"missing {spec}") continue if constraint: for provider, pver in providers[dep]: if constraint(pver): break else: yield No(f"missing {spec}") for _, pver in providers[dep]: if is_same(pver, newest[dep]): break else: yield No(f"old {spec}") def order_arch(opts): pkgs, newest, providers = analyze(opts) yield ["arch", "package", "version", "issue"] for arch in opts.arches: for name in sorted(pkgs[arch].keys()): # DON'T use newest[] here. It is possible that a package # provides= another package's name with a newer version ver = sorted(pkgs[arch][name].keys(), key=verkey)[-1] pkg = pkgs[arch][name][ver] for problem in trace_deps(providers[arch], newest[arch], pkg): yield [arch, name, ver, problem] def order_pkg(opts): pkgs, newest, providers = analyze(opts) all_pkgs = set() for arch in pkgs: all_pkgs.update(pkgs[arch].keys()) yield ["package", "problem", *opts.arches] for name in sorted(all_pkgs): all_problems = set() problems = {} for arch in opts.arches: vers = pkgs[arch].get(name, {}) if not vers: problems[arch] = [PARTIAL_MISSING] # Don't add PARTIAL_MISSING to all_problems: we don't # care if "name" is missing, use pkgver_test.py for that continue ver = sorted(vers.keys(), key=verkey)[-1] pkg = vers[ver] problems[arch] = list( trace_deps(providers[arch], newest[arch], pkg) ) all_problems.update(problems[arch]) if not all_problems: continue for problem in sorted(all_problems): row = [name, problem.content] for arch in opts.arches: if problem in problems[arch]: row.append(type(problem)("yes")) elif PARTIAL_MISSING in problems[arch]: row.append(Yes("n/a")) else: row.append(Yes("no")) yield row ORDERS = { "arch": order_arch, "pkg": order_pkg, } if __name__ == "__main__": opts = argparse.ArgumentParser( description="""Scan the REPO/ARCH repositories under URL and look for outdated or missing dependencies as required by the most recent version of each package. This script examines both main packages and subpackages. Be sure to include any repositories on which the repository of interest depends, otherwise a lot of dependencies will be incorrectly marked as missing.""", ) opts.add_argument( "-f", "--format", choices=FORMATTERS.keys(), default="pretty" if os.isatty(sys.stdout.fileno()) else "tab", help="display format (default: guess pretty or tab)", ) opts.add_argument( "-o", "--order", choices=ORDERS.keys(), default="pkg", help="display order (default: pkg)", ) opts.add_argument( "url", metavar="URL", help="base URL (no repository or arch)", ) opts.add_argument( "repos", metavar="REPOS", help="repositories (comma separated)", ) opts.add_argument( "arches", metavar="ARCHES", help="architectures (comma separated)", ) opts = opts.parse_args() opts.repos = opts.repos.split(",") opts.arches = opts.arches.split(",") FORMATTERS[opts.format]( opts, ORDERS[opts.order](opts), )