Preserve navigation Windows package preparation and exact source provenance
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""Read back the produced ZIP/stage/source without launching either executable."""
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import sys
|
||||
import zipfile
|
||||
from prepare import BASE, SOURCE, VERSION, TARGET, COMMIT, SERVER_SHA, DLL_SHA, ordinary, sha, load, record, git, relative, historical_inputs, HISTORY
|
||||
|
||||
|
||||
def main():
|
||||
assert not (BASE / 'package-readback.json').exists()
|
||||
preparation = load(BASE / 'preparation.json')
|
||||
terminal = load(BASE / 'package2-result.json')
|
||||
assert terminal['success'] is True and terminal['metadata_calls'] == 1
|
||||
assert preparation['base_commit'] == COMMIT and preparation['version'] == VERSION and preparation['target'] == TARGET
|
||||
name = f'ArcadiaStudio-{VERSION}-{TARGET}'
|
||||
archive_path = BASE / 'packages' / (name + '.zip')
|
||||
stage = BASE / 'packages' / name
|
||||
assert Path(terminal['archive']).resolve() == archive_path.resolve()
|
||||
assert terminal['archive_sha256'] == sha(archive_path) and terminal['archive_bytes'] == archive_path.stat().st_size
|
||||
actual = {}
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
entries = archive.infolist()
|
||||
assert entries and len({e.filename.casefold() for e in entries}) == len(entries)
|
||||
for entry in entries:
|
||||
assert entry.filename.startswith(name + '/') and not entry.is_dir()
|
||||
path = relative(entry.filename[len(name) + 1:])
|
||||
assert not (entry.flag_bits & 1) and not stat.S_ISLNK(entry.external_attr >> 16)
|
||||
assert entry.date_time == (2000, 1, 1, 0, 0, 0)
|
||||
with archive.open(entry) as stream:
|
||||
checksum = hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
assert sha(stage / path) == checksum and (stage / path).stat().st_size == entry.file_size
|
||||
actual[path] = {'sha256': checksum, 'bytes': entry.file_size}
|
||||
files = {ordinary(p).relative_to(stage).as_posix() for p in stage.rglob('*') if p.is_file()}
|
||||
assert set(actual) == files
|
||||
sums = {}
|
||||
for line in (stage / 'SHA256SUMS.txt').read_text(encoding='utf-8').splitlines():
|
||||
checksum, path = line.split(' ', 1)
|
||||
assert relative(path) not in sums
|
||||
sums[path] = checksum
|
||||
assert set(sums) == set(actual) - {'SHA256SUMS.txt'}
|
||||
assert all(actual[p]['sha256'] == h for p, h in sums.items())
|
||||
companion = (archive_path.with_suffix('.zip.sha256')).read_text(encoding='utf-8').strip().split()
|
||||
assert companion == [terminal['archive_sha256'], archive_path.name]
|
||||
for item in preparation['artifacts']:
|
||||
p = Path(item['destination']).name
|
||||
assert actual[p]['sha256'] == sha(BASE / item['destination']) == item['sha256']
|
||||
assert actual['arcadia-server.exe']['sha256'] == SERVER_SHA and actual['libunwind.dll']['sha256'] == DLL_SHA
|
||||
assert git(SOURCE, 'rev-parse', 'HEAD').decode().strip() == preparation['reconstruction_commit']
|
||||
assert not git(SOURCE, 'status', '--porcelain').strip()
|
||||
for mapping in (preparation['gui_inputs'], preparation['shared_non_cargo_inputs']):
|
||||
for path, checksum in mapping.items():
|
||||
assert sha(SOURCE / path) == checksum
|
||||
# This comparison deliberately uses the old server tar, never GUI source.
|
||||
assert historical_inputs() == preparation['historical_server_inputs']
|
||||
for item in load(BASE / 'evidence-manifest.json'):
|
||||
assert sha(BASE / item['destination']) == item['sha256'] and (BASE / item['destination']).stat().st_size == item['bytes']
|
||||
package = load(stage / 'package.json')
|
||||
assert package['version'] == VERSION and package['target'] == TARGET
|
||||
assert package['build_evidence']['evidence'] == load(BASE / 'build-evidence.json')
|
||||
for path, checksum in package['input_sha256'].items():
|
||||
assert actual[path]['sha256'] == checksum
|
||||
notices = load(stage / 'dependency-notices.json')
|
||||
previous = load(HISTORY / 'prior-dependency-notices.json')
|
||||
def registry_key(item):
|
||||
return item['name'], item['version'], item['source'], item['registry_checksum']
|
||||
collected = {registry_key(x): x for x in notices['packages'] if x['source']}
|
||||
covered = 0
|
||||
for old in previous['packages']:
|
||||
if not old['source']:
|
||||
continue
|
||||
current = collected[registry_key(old)]
|
||||
assert old['license'] == current['license']
|
||||
assert set(old['features']).issubset(current['features'])
|
||||
# Exact recorded notice files must remain in the actual ZIP. Current
|
||||
# metadata does not substitute for the historical server's provenance.
|
||||
new_notices = {(n['path'], n['sha256']) for n in current['notices']}
|
||||
for item in old['notices']:
|
||||
assert (item['path'], item['sha256']) in new_notices
|
||||
assert actual[item['path']]['sha256'] == item['sha256']
|
||||
assert old['embedded_fonts'] == current['embedded_fonts']
|
||||
for font in old['embedded_fonts']:
|
||||
for item in font['notices']:
|
||||
assert actual[item['path']]['sha256'] == item['sha256']
|
||||
covered += 1
|
||||
assert covered == preparation['historical_registry_count']
|
||||
docs = load(stage / 'documentation-check.json')
|
||||
result = {'atUtc': datetime.now(timezone.utc).isoformat(), 'status': 'PASS', 'archive_sha256': sha(archive_path),
|
||||
'archive_bytes': archive_path.stat().st_size, 'file_count': len(actual), 'files': actual,
|
||||
'manifest_entries': len(sums), 'source_commit': preparation['reconstruction_commit'],
|
||||
'gui_inputs_verified': len(preparation['gui_inputs']), 'historical_server_inputs_verified': len(preparation['historical_server_inputs']),
|
||||
'historical_server_comparison': preparation['historical_server_comparison'],
|
||||
'historical_registry_packages_covered': covered, 'notice_roots': notices['root_packages'], 'notice_package_count': len(notices['packages']),
|
||||
'documentation_check': docs, 'binary_imports': package['imports'],
|
||||
'scope': 'Producer packaging readback only; no application launch or independent/native acceptance.'}
|
||||
record(BASE / 'package-readback.json', result)
|
||||
print(json.dumps({k: v for k, v in result.items() if k != 'files'}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
main()
|
||||
Reference in New Issue
Block a user