#!/usr/bin/env python3 """sim-approved.py — READ-ONLY. Against throwaway apt indexes (no system list touched), classify this machine's pending updates against an approved list: what the fast lane WOULD install (exact approved version, downloadable now), what is approved but not downloadable, and what is NOT COVERED.""" import subprocess, sys, tempfile, os, re, shutil approved_path, layer = sys.argv[1], sys.argv[2] approved = {} for l in open(approved_path): if l.startswith('#') or not l.strip(): continue lay, pkg, ver, origin = l.rstrip('\n').split('\t') if lay == layer: approved[pkg] = (ver, origin) env = dict(os.environ, LC_ALL='C') t = tempfile.mkdtemp(prefix='simapproved.') os.makedirs(t + '/lists/partial'); os.makedirs(t + '/cache/archives/partial') A = ['-o', 'Dir::State::Lists=' + t + '/lists', '-o', 'Dir::Cache=' + t + '/cache', '-o', 'Debug::NoLocking=1'] subprocess.run(['apt-get'] + A + ['-q', 'update'], env=env, capture_output=True) sim = subprocess.run(['apt-get'] + A + ['-s', 'dist-upgrade'], env=env, capture_output=True, text=True).stdout pending = {} for l in sim.splitlines(): m = re.match(r'^Inst (\S+) (?:\[([^]]*)\] )?\((\S+) (.*?) \[[a-z0-9]+\]\)', l) if m: pending[m.group(1)] = (m.group(2) or '(new)', m.group(3), m.group(4)) def downloadable(pkg, ver): out = subprocess.run(['apt-cache'] + A + ['madison', pkg], env=env, capture_output=True, text=True).stdout return any(f.split('|')[1].strip() == ver for f in out.splitlines() if f.count('|') >= 2) def installed(pkg): r = subprocess.run(['dpkg-query', '-W', '-f=${Version}', pkg], env=env, capture_output=True, text=True) return r.stdout if r.returncode == 0 else None def newer(a, b): # a > b ? return subprocess.run(['dpkg', '--compare-versions', a, 'gt', b]).returncode == 0 would, gone, already, notinst = [], [], [], [] for pkg, (ver, origin) in sorted(approved.items()): inst = installed(pkg) if inst is None: notinst.append(pkg); continue if not newer(ver, inst): already.append(pkg); continue (would if downloadable(pkg, ver) else gone).append((pkg, inst, ver)) notcov = [(p, v) for p, v in sorted(pending.items()) if p not in approved] shutil.rmtree(t) print(f"# layer {layer} on {os.uname().nodename}: {len(approved)} approved, {len(pending)} pending here") print(f"WOULD INSTALL (exact approved version, downloadable now): {len(would)}") for p, i, v in would: print(f" {p} {i} -> {v}") print(f"APPROVED BUT NOT DOWNLOADABLE NOW: {len(gone)}") for p, i, v in gone: print(f" {p} {i} -> {v} (gone)") print(f"approved, already at or above: {len(already)} approved, not installed here: {len(notinst)} {' '.join(notinst)}") by = {} for p, (old, new, origin) in notcov: by.setdefault(origin.split(',')[0].split(':')[0], []).append(f"{p} {old} -> {new}") print(f"NOT COVERED (pending here, no approved entry): {len(notcov)}") for o, ps in sorted(by.items()): print(f" [{o}] {len(ps)}") for x in ps: print(" " + x)