Fiw-Gentoo-Dots/tools/audit-packages.py

92 lines
4 KiB
Python

#!/usr/bin/env python3
"""Compare explicit world/set selections with main and optional package lists."""
import argparse
import json
import re
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
def lines(path):
if not path.is_file():
return []
return [line.split('#', 1)[0].strip() for line in path.read_text().splitlines()
if line.split('#', 1)[0].strip()]
def package(atom):
value = atom.split('::', 1)[0].split('[', 1)[0].lstrip('<>=~')
base, _, slot = value.partition(':')
base = re.sub(r'-[0-9].*$', '', base)
if not re.fullmatch(r'[A-Za-z0-9+_.-]+/[A-Za-z0-9+_.-]+', base):
raise ValueError('Invalid explicit package atom.')
return base + (':' + slot if slot else '')
def explicit(world, world_sets, sets):
selections, unresolved = {}, set()
def read(items, origin, stack=()):
for atom in items:
if atom.startswith('@'):
name = atom[1:]
if not re.fullmatch(r'[A-Za-z0-9+_.-]+', name):
raise ValueError('Invalid selected set name.')
if name in stack:
raise ValueError('Cyclic selected set: ' + name)
source = sets / name
if not source.is_file():
unresolved.add(name)
else:
read(lines(source), '@' + name, stack + (name,))
else:
selections.setdefault(package(atom), set()).add(origin)
read(lines(world), 'world')
read(lines(world_sets), 'world_sets')
return {atom: sorted(origins) for atom, origins in sorted(selections.items())}, sorted(unresolved)
def audit(world, world_sets, sets, repo=REPO):
selected, unresolved = explicit(world, world_sets, sets)
coverage = {}
for path in sorted((repo / 'packages').rglob('*.list')):
if path.name == 'source.list':
continue
category = path.relative_to(repo / 'packages').with_suffix('').as_posix()
for atom in lines(path):
coverage.setdefault(package(atom), set()).add(category)
for atom, category in [('sys-kernel/gentoo-kernel', 'kernel/custom'),
('sys-kernel/gentoo-kernel-bin', 'kernel/binary')]:
coverage.setdefault(atom, set()).add(category)
exceptions = json.loads((repo / 'packages/audit-exceptions.json').read_text())
matched = {atom: sorted(coverage[atom]) for atom in selected if atom in coverage}
manual = {atom: exceptions[atom] for atom in selected if atom in exceptions and atom not in coverage}
missing = sorted(set(selected) - set(matched) - set(manual))
return {'explicit_count': len(selected), 'covered_count': len(matched),
'coverage': matched, 'sources': selected, 'manual_helpers': manual,
'missing': missing, 'unresolved_sets': unresolved}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--world', type=Path, default=Path('/var/lib/portage/world'))
parser.add_argument('--world-sets', type=Path, default=Path('/var/lib/portage/world_sets'))
parser.add_argument('--sets', type=Path, default=Path('/etc/portage/sets'))
parser.add_argument('--json', action='store_true')
args = parser.parse_args()
if not args.world.is_file() and not args.world_sets.is_file():
parser.error('No Portage explicit selection files found.')
result = audit(args.world, args.world_sets, args.sets)
if args.json:
print(json.dumps(result, indent=2))
else:
print(str(result['covered_count']) + '/' + str(result['explicit_count']) + ' explicit selections covered by package lists/kernel choices.')
for atom, reason in result['manual_helpers'].items():
print('Manual helper: ' + atom + ' — ' + reason)
print('Missing main packages: ' + (', '.join(result['missing']) or 'none'))
print('Unresolved selected sets: ' + (', '.join(result['unresolved_sets']) or 'none'))
return 1 if result['missing'] or result['unresolved_sets'] else 0
if __name__ == '__main__':
raise SystemExit(main())