#!/usr/bin/env python3 """Check recorded Arch patches on selected pristine release sources, without building.""" import argparse import hashlib import json from pathlib import Path, PurePosixPath import shutil import subprocess ROOT = Path(__file__).resolve().parents[2] def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest() def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--output', type=Path, required=True) a = p.parse_args(); output = a.output.resolve() if not output.is_relative_to(ROOT): p.error('Use an output directory inside the workspace') inspection = ROOT / 'tests/results/source-archives/qt-inspection' record = json.loads((inspection / 'report.json').read_text()) if not record['success'] or sha(inspection / 'files.jsonl') != record['inventorySha256']: raise SystemExit('A verified source inventory is required') inventory = {row['path']: row for line in (inspection / 'files.jsonl').open() if (row := json.loads(line))} source = ROOT / record['sourceTree'] evidence = ROOT / 'tests/results/source-correspondence/arch' patches = [ ('qtbase', evidence / 'packages/qt6-base/qt6-base-cflags.patch'), ('qtbase', evidence / 'packages/qt6-base/qt6-base-nostrip.patch'), ('qtbase', evidence / 'upstream-metadata/qtbase-e80e3f0.patch'), ('qtdeclarative', evidence / 'upstream-metadata/qtdeclarative-2efb7c6.patch')] output.mkdir(parents=True, exist_ok=False) inputs, steps = {}, [] for module, patch in patches: for line in patch.read_text().splitlines(): if not line.startswith('+++ b/'): continue name = line[len('+++ b/'):].split('\t', 1)[0] path = PurePosixPath(name) if path.is_absolute() or '..' in path.parts: raise ValueError('Unsafe patch target') relative = module + '/' + name target = output / 'tree' / relative target.parent.mkdir(parents=True, exist_ok=True) if relative in inputs: continue before = None if (source / relative).exists(): before = sha(source / relative) if before != inventory[relative]['sha256']: raise ValueError('Selected release file changed') shutil.copyfile(source / relative, target) inputs[relative] = before success = True for module, patch in patches: # The two local Arch patches use GNU patch's default fuzz tolerance. # Keep exact context for the separately recorded upstream changes. fuzz = 2 if patch.name.startswith('qt6-base-') else 0 step = {'module': module, 'patch': str(patch.relative_to(ROOT)), 'patchSha256': sha(patch), 'fuzzLimit': fuzz} for label, extra in [('dryRun', ['--dry-run']), ('apply', [])]: command = ['patch', '--batch', '--forward', '--fuzz=' + str(fuzz), '-p1', '-d', str(output / 'tree' / module), '-i', str(patch), *extra] run = subprocess.run(command, capture_output=True, text=True, timeout=20) step[label] = {'exitCode': run.returncode, 'stdout': run.stdout, 'stderr': run.stderr} if run.returncode: success = False; break steps.append(step) if not success: break after = {name: sha(output / 'tree' / name) if (output / 'tree' / name).is_file() else None for name in inputs} report = {'success': success, 'sourceArchiveSha256': record['archiveSha256'], 'beforeSha256': inputs, 'afterSha256': after, 'steps': steps, 'pristineFilesUnchanged': all(sha(source / name) == digest for name, digest in inputs.items() if digest), 'scope': 'Recorded Arch patches checked on a private selected-file copy with explicit tolerances; no upstream build scripts run and no resulting binaries rebuilt'} report['installedMkspecComparisons'] = { name: sha(output / 'tree/qtbase/mkspecs/common' / name) == sha(Path('/usr/lib/qt6/mkspecs/common') / name) for name in ['g++-unix.conf', 'gcc-base.conf']} (output / 'report.json').write_text(json.dumps(report, indent=2) + '\n') print(json.dumps({'success': success, 'patches': len(steps), 'targetFiles': len(inputs), 'pristineFilesUnchanged': report['pristineFilesUnchanged']})) return 0 if success else 1 if __name__ == '__main__': raise SystemExit(main())