87 lines
4.4 KiB
Python
87 lines
4.4 KiB
Python
#!/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())
|