92 lines
4 KiB
Python
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())
|