1b74ddc0c9
gates / gates (push) Successful in 33s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
62 lines
3.0 KiB
Python
62 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""sim-approved.py <approved.tsv> <layer> — 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)
|