332 lines
20 KiB
Python
332 lines
20 KiB
Python
"""Prepare isolated exact sources/binaries only after recorded GUI/full gates.
|
|
|
|
Do not execute without root's preparation authorization. Never invokes Cargo,
|
|
launches applications, publishes, or writes the main repository's index/refs.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import traceback
|
|
import sys
|
|
import tomllib
|
|
import zipfile
|
|
|
|
BASE = Path(__file__).resolve().parent
|
|
ROOT = BASE.parents[1]
|
|
SOURCE = BASE / 'source'
|
|
COMMIT = '33c6d83e9ba9a5b1310b1b627d7c40affcbcf15c'
|
|
BRANCH = 'codex/navigation-repair-prerelease-source-20260911'
|
|
VERSION = '0.2.0-preview.navigation.20260911'
|
|
TARGET = 'x86_64-pc-windows-gnullvm'
|
|
GATES = ROOT / '.local/text-repair-save-20260911'
|
|
HISTORY = ROOT / '.local/contextual-prerelease-20260911/evidence'
|
|
INPUT_SHA = '75a282fd3d4cb2dec6763046ea45d0caa015b998281fbdd6f222d8e3a175b13f'
|
|
ZIP_SHA = '213fdb39ea7c3601b4baac674c3ab3ecc863ac44db1b092d922d3e24f26887b7'
|
|
SERVER_SHA = 'efeeb89c0d2e0f93df4472cb3e9f48b2b722a37ae1043c90a5a56f2e845fe665'
|
|
DLL_SHA = 'f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4'
|
|
HISTORICAL = {
|
|
'server-original-gates.json': 'b7fdfc55ed4c964d276f20f1c57c81c1e924b8da315171089740ab8335ac9f0d',
|
|
'server-original-binary-hashes.json': '110a03e323133efb48d50ad065d41414599aa85e0ac420e7d622aa04df4d54f8',
|
|
'server-prior-input-comparisons.json': '3c78dea4e93dae186fd24ab826c6680178a2c7849c270a22deae2ff732f1857b',
|
|
'prior-dependency-notices.json': '98fab15c1a2bb3aa727c6d52d5c69435b15efa56b6668ed717e7a04cf86f9b52',
|
|
'server-source-snapshot.tar.gz': 'cbb371a93a562e1e641d7629fe351e016d63e66fc9112499b3fed6d5cd171f78',
|
|
}
|
|
SCRIPT_HASHES = {
|
|
'scripts/package-windows.ps1': '28997ff78565619ed9c9b185f66dc922808f1f0be17f522b430c6c415448663d',
|
|
'scripts/package-common.ps1': 'd859453a8230b4f305d822c8742b371a71e35df698109a5d55c948f6dfaf0640',
|
|
'scripts/windows-notices.ps1': 'a5328c05579e1b9ca09142ee99e87c12994d12764f7c69448e392883d718ef01',
|
|
}
|
|
|
|
COMMANDS = {
|
|
'desktop-tests': ['cargo', 'test', '--locked', '--offline', '-p', 'oa-desktop', '--bin', 'arcadia-studio', '--', '--test-threads=1'],
|
|
'desktop-clippy': ['cargo', 'clippy', '--locked', '--offline', '-p', 'oa-desktop', '--all-targets', '--', '-D', 'warnings'],
|
|
'scoped-fmt': ['rustfmt', '--check', '--edition', '2024', '--config', 'skip_children=true', 'crates/oa-desktop/src/app.rs', 'crates/oa-desktop/src/document.rs', 'crates/oa-desktop/src/app/repair_save_state_tests.rs'],
|
|
'diff-check': ['git', 'diff', '--check'],
|
|
'gui-build': ['cargo', 'build', '--locked', '--offline', '-p', 'oa-desktop', '--bin', 'arcadia-studio', '--message-format=json-render-diagnostics'],
|
|
}
|
|
|
|
|
|
def now():
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def ordinary(path):
|
|
path = Path(path).absolute()
|
|
for part in (path, *path.parents):
|
|
if part.exists():
|
|
assert not part.is_symlink() and not (getattr(part.lstat(), 'st_file_attributes', 0) & 0x400), str(part)
|
|
return path
|
|
|
|
|
|
def sha(path):
|
|
with ordinary(path).open('rb') as stream:
|
|
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
|
|
|
|
def load(path):
|
|
return json.loads(ordinary(path).read_text(encoding='utf-8-sig'))
|
|
|
|
|
|
def record(path, value):
|
|
ordinary(path)
|
|
with Path(path).open('x', encoding='utf-8', newline='\n') as stream:
|
|
json.dump(value, stream, ensure_ascii=False, indent=2)
|
|
stream.write('\n')
|
|
|
|
|
|
_git_call = 0
|
|
|
|
|
|
def git(cwd, *args, input=None):
|
|
global _git_call
|
|
_git_call += 1
|
|
result = subprocess.run(['git', *args], cwd=cwd, capture_output=True, input=input)
|
|
if result.returncode:
|
|
prefix = BASE / f'git-failure-{_git_call:03d}'
|
|
with prefix.with_suffix('.stdout.log').open('xb') as out:
|
|
out.write(result.stdout)
|
|
with prefix.with_suffix('.stderr.log').open('xb') as err:
|
|
err.write(result.stderr)
|
|
record(prefix.with_suffix('.json'), {'command': result.args, 'cwd': str(cwd), 'exitCode': result.returncode,
|
|
'stdout_sha256': hashlib.sha256(result.stdout).hexdigest(),
|
|
'stderr_sha256': hashlib.sha256(result.stderr).hexdigest(), 'atUtc': now()})
|
|
result.check_returncode()
|
|
return result.stdout
|
|
|
|
|
|
def relative(name):
|
|
p = PurePosixPath(name)
|
|
assert name == p.as_posix() and not p.is_absolute() and p.parts and all(x not in ('..', '.') for x in p.parts)
|
|
assert '\\' not in name and ':' not in name and '\n' not in name and '\r' not in name
|
|
return name
|
|
|
|
|
|
def blobs(cwd, revision, paths):
|
|
paths = list(paths)
|
|
raw = git(cwd, 'cat-file', '--batch', input=''.join(f'{revision}:{p}\n' for p in paths).encode())
|
|
cursor, out = 0, {}
|
|
for path in paths:
|
|
end = raw.index(b'\n', cursor)
|
|
header = raw[cursor:end].split()
|
|
assert len(header) == 3 and header[1] == b'blob', path
|
|
size = int(header[2])
|
|
out[path] = raw[end + 1:end + 1 + size]
|
|
cursor = end + size + 2
|
|
assert cursor == len(raw)
|
|
return out
|
|
|
|
|
|
def root_identity():
|
|
index = Path(git(ROOT, 'rev-parse', '--git-path', 'index').decode().strip())
|
|
if not index.is_absolute():
|
|
index = ROOT / index
|
|
return {'head': git(ROOT, 'rev-parse', 'HEAD').decode().strip(), 'index_sha256': sha(index),
|
|
'tracked_status': git(ROOT, 'status', '--porcelain', '-uno').decode()}
|
|
|
|
|
|
def historical_inputs():
|
|
for name, checksum in HISTORICAL.items():
|
|
assert sha(HISTORY / name) == checksum, name
|
|
inventory = load(HISTORY / 'server-prior-input-comparisons.json')
|
|
entries = inventory['server_source_file_comparisons']
|
|
expected = {relative(e['path']): e['recorded_sha256'] for e in entries}
|
|
assert len(expected) == len(entries) and expected
|
|
found = {}
|
|
with tarfile.open(HISTORY / 'server-source-snapshot.tar.gz', 'r:gz') as archive:
|
|
for member in archive.getmembers():
|
|
name = member.name.removeprefix('./')
|
|
if name in expected:
|
|
assert member.isfile() and name not in found, name
|
|
with archive.extractfile(member) as stream:
|
|
found[name] = hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
assert found == expected
|
|
assert any(e['Hash'].lower() == SERVER_SHA and Path(e['Path']).name == 'arcadia-server.exe'
|
|
for e in load(HISTORY / 'server-original-binary-hashes.json'))
|
|
return expected
|
|
|
|
|
|
def admission():
|
|
assert sha(GATES / 'inputs1.json') == INPUT_SHA
|
|
assert sha(GATES / 'inputs1.zip') == ZIP_SHA
|
|
manifest = load(GATES / 'inputs1.json')
|
|
assert manifest['archiveSha256'] == ZIP_SHA
|
|
expected = {relative(e['path']): e for e in manifest['files']}
|
|
assert len(expected) == len(manifest['files']) and expected
|
|
assert len({p.casefold() for p in expected}) == len(expected)
|
|
with zipfile.ZipFile(GATES / 'inputs1.zip') as archive:
|
|
assert len(archive.namelist()) == len(expected) and set(archive.namelist()) == set(expected)
|
|
frozen = {name: archive.read(name) for name in expected}
|
|
for name, raw in frozen.items():
|
|
assert len(raw) == expected[name]['bytes'] and hashlib.sha256(raw).hexdigest() == expected[name]['sha256'], name
|
|
assert len(expected) == 500
|
|
complete = load(GATES / 'complete.json')
|
|
assert complete['archiveSha256'] == ZIP_SHA and complete['inputsUnchanged'] is True
|
|
assert complete['gates'] == list(COMMANDS) and complete['sourceHead'] == manifest['head']
|
|
# The gates captured eeeaa126 plus the repair delta; COMMIT commits those
|
|
# exact raw inputs. Never relabel the captured sourceHead as a clean build.
|
|
assert manifest['head'] == 'eeeaa1262748800c4c9aaa1eca79a7de96757358'
|
|
for name, command in COMMANDS.items():
|
|
receipt = load(GATES / (name + '-result.json'))
|
|
assert receipt['exitCode'] == 0 and receipt['inputsUnchanged'] is True and receipt['command'] == command
|
|
assert sha(GATES / (name + '-stdout.log')) == receipt['stdoutSha256']
|
|
assert sha(GATES / (name + '-stderr.log')) == receipt['stderrSha256']
|
|
build = load(GATES / 'gui-build-result.json')['binary']
|
|
artifact = build['artifact']
|
|
assert artifact['target']['name'] == 'arcadia-studio' and artifact['profile']['test'] is False
|
|
assert artifact['profile']['opt_level'] == '0' and artifact['profile']['debug_assertions'] is True
|
|
assert Path(artifact['executable']).resolve() == Path(build['path']).resolve()
|
|
assert Path(build['path']).resolve().is_relative_to(ROOT / '.local/target-gnullvm')
|
|
gui = GATES / 'gui-repair.exe'
|
|
assert sha(gui) == build['sha256'] == 'b183ef31f16b3986538b3f2aba8435eab45978635c47f99ca01be6e72b168848'
|
|
assert gui.stat().st_size == build['bytes'] == 113206272
|
|
summaries = re.findall(r'test result: ok\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;',
|
|
(GATES / 'desktop-tests-stdout.log').read_text(encoding='utf-8'))
|
|
assert summaries == [('755', '0', '1', '0', '0')]
|
|
counts = dict(zip(('passed', 'failed', 'ignored', 'measured', 'filtered'), map(int, summaries[0])))
|
|
server = ROOT / '.local/contextual-prerelease-20260911/bin/arcadia-server.exe'
|
|
dll = ROOT / '.local/contextual-prerelease-20260911/bin/libunwind.dll'
|
|
assert sha(server) == SERVER_SHA and sha(dll) == DLL_SHA
|
|
old = historical_inputs()
|
|
return manifest, frozen, build, gui, counts, server, dll, old
|
|
|
|
|
|
def main():
|
|
ordinary(BASE)
|
|
for name in ('source', 'source-roundtrip', 'bin', 'evidence', 'preparation.json', 'committed-roundtrip.json', 'build-evidence.json', 'evidence-manifest.json'):
|
|
assert not (BASE / name).exists(), f'Refusing existing output: {name}'
|
|
before = root_identity()
|
|
assert not before['tracked_status'], 'Root tracked edits must be frozen before preparation.'
|
|
assert git(ROOT, 'rev-parse', COMMIT).decode().strip() == COMMIT
|
|
# Later root commits may record evidence only. Do not silently package later
|
|
# production/docs/dependency or instruction changes under the tested source.
|
|
git(ROOT, 'merge-base', '--is-ancestor', COMMIT, before['head'])
|
|
later = git(ROOT, 'diff', '--name-only', '-z', COMMIT, before['head']).decode().split('\0')[:-1]
|
|
assert all(p.startswith('harness/') and not p.startswith('harness/fixtures/') for p in later), later
|
|
manifest, frozen, build, gui, counts, server, dll, old = admission()
|
|
for path, raw in frozen.items():
|
|
assert ordinary(ROOT / path).read_bytes() == raw, f'Current frozen input changed: {path}'
|
|
selectors = ['crates', '.cargo', 'Cargo.toml', 'Cargo.lock', 'docs', 'examples', 'harness/fixtures']
|
|
current_paths = git(ROOT, 'ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', *selectors).decode().split('\0')[:-1]
|
|
assert sorted(current_paths) == sorted(frozen), 'Current input file set differs from the successful freeze.'
|
|
committed_paths = git(ROOT, 'ls-tree', '-r', '--name-only', '-z', COMMIT, '--', *selectors).decode().split('\0')[:-1]
|
|
assert sorted(committed_paths) == sorted(frozen), 'Committed input file set differs from the successful gate freeze.'
|
|
assert shutil.disk_usage(BASE).free >= 2 * 1024 ** 3
|
|
original = blobs(ROOT, COMMIT, frozen)
|
|
for path, raw in frozen.items():
|
|
assert original[path].replace(b'\r\n', b'\n') == raw.replace(b'\r\n', b'\n'), f'Non-newline source mismatch: {path}'
|
|
notice = load(HISTORY / 'prior-dependency-notices.json')
|
|
shared = {p: h for p, h in notice['workspace_build_inputs_sha256'].items() if p not in ('Cargo.toml', 'Cargo.lock')}
|
|
for p, raw in blobs(ROOT, COMMIT, shared).items():
|
|
assert hashlib.sha256(raw).hexdigest() == shared[p] or raw.replace(b'\r\n', b'\n') == (ROOT / p).read_bytes().replace(b'\r\n', b'\n')
|
|
assert sha(ROOT / p) == shared[p], p
|
|
if p in frozen:
|
|
assert frozen[p] == (ROOT / p).read_bytes(), p
|
|
frozen[p] = (ROOT / p).read_bytes()
|
|
registry = {(x['name'], x['version'], x['source']): x['registry_checksum']
|
|
for x in notice['packages'] if x['source']}
|
|
locked = {(x['name'], x['version'], x.get('source')): x.get('checksum')
|
|
for x in tomllib.loads(frozen['Cargo.lock'].decode())['package']}
|
|
assert registry and all(locked.get(k) == v for k, v in registry.items()), 'Historical registry coverage differs.'
|
|
# Local clone has its own index/refs and does not create a worktree in ROOT.
|
|
git(ROOT, 'clone', '--no-hardlinks', '--no-checkout', str(ROOT), str(SOURCE))
|
|
git(SOURCE, 'checkout', '-b', BRANCH, COMMIT)
|
|
checks = {p: hashlib.sha256(raw).hexdigest() for p, raw in frozen.items()}
|
|
original = blobs(SOURCE, COMMIT, checks)
|
|
restores = []
|
|
for p, raw in frozen.items():
|
|
assert original[p].replace(b'\r\n', b'\n') == raw.replace(b'\r\n', b'\n'), p
|
|
if hashlib.sha256(original[p]).hexdigest() != checks[p] or sha(SOURCE / p) != checks[p]:
|
|
assert re.fullmatch(r'[A-Za-z0-9_./-]+', p), 'Attribute path needs explicit quoting review.'
|
|
ordinary(SOURCE / p).write_bytes(raw)
|
|
restores.append({'path': p, 'sha256': checks[p], 'base_git_sha256': hashlib.sha256(original[p]).hexdigest(),
|
|
'bytes': len(raw), 'crlf': raw.count(b'\r\n'), 'lf': raw.count(b'\n')})
|
|
if restores:
|
|
with (SOURCE / '.gitattributes').open('ab') as out:
|
|
out.write(b'\n# Exact frozen navigation GUI input bytes; historical server source is separate.\n')
|
|
for item in restores:
|
|
out.write((item['path'] + ' -text\n').encode())
|
|
git(SOURCE, 'add', '--renormalize', '--', '.gitattributes', *[x['path'] for x in restores])
|
|
git(SOURCE, '-c', 'user.name=Codex-9e8r6t', '-c', 'user.email=codex-9e8r6t@local.invalid',
|
|
'commit', '-m', 'Preserve exact navigation prerelease GUI inputs')
|
|
reconstruction = git(SOURCE, 'rev-parse', 'HEAD').decode().strip()
|
|
committed = blobs(SOURCE, 'HEAD', checks)
|
|
fresh = BASE / 'source-roundtrip'
|
|
fresh.mkdir()
|
|
git(SOURCE, '-c', 'core.longpaths=true', 'checkout-index', '--all', '--prefix=' + fresh.as_posix() + '/')
|
|
for p, checksum in checks.items():
|
|
assert hashlib.sha256(committed[p]).hexdigest() == sha(SOURCE / p) == sha(fresh / p) == checksum, p
|
|
for p, checksum in SCRIPT_HASHES.items():
|
|
assert sha(SOURCE / p) == checksum, p
|
|
assert not git(SOURCE, 'status', '--porcelain').strip()
|
|
comparison = {'historical_file_count': len(old), 'changed_current_inputs': [p for p, h in old.items() if sha(SOURCE / p) != h],
|
|
'added_current_files': sorted(p.relative_to(SOURCE).as_posix() for crate in ('oa-core', 'oa-server')
|
|
for p in (SOURCE / 'crates' / crate).rglob('*') if p.is_file() and p.relative_to(SOURCE).as_posix() not in old),
|
|
'scope': 'Historical server inputs verified against their own preserved tar, not required to equal the new GUI source.'}
|
|
assert comparison['changed_current_inputs'] == ['crates/oa-core/src/language.rs']
|
|
assert comparison['added_current_files'] == ['crates/oa-core/src/language_outline_tests.rs']
|
|
(BASE / 'bin').mkdir()
|
|
artifacts = []
|
|
for source, name, checksum in ((gui, 'arcadia-studio.exe', build['sha256']), (server, 'arcadia-server.exe', SERVER_SHA), (dll, 'libunwind.dll', DLL_SHA)):
|
|
assert sha(source) == checksum
|
|
target = BASE / 'bin' / name
|
|
with source.open('rb') as src, target.open('xb') as dst:
|
|
shutil.copyfileobj(src, dst)
|
|
assert sha(source) == sha(target) == checksum
|
|
artifacts.append({'source': str(source), 'destination': 'bin/' + name, 'sha256': checksum, 'bytes': target.stat().st_size})
|
|
provenance = {name: HISTORY / name for name in HISTORICAL}
|
|
provenance.update({'gui-inputs1.json': GATES / 'inputs1.json', 'gui-complete.json': GATES / 'complete.json',
|
|
'gui-gate-run.py': GATES / 'run.py'})
|
|
for name in COMMANDS:
|
|
for suffix in ('-result.json', '-stdout.log', '-stderr.log'):
|
|
provenance['gui-' + name + suffix] = GATES / (name + suffix)
|
|
(BASE / 'evidence').mkdir()
|
|
evidence = []
|
|
for name, source in provenance.items():
|
|
checksum = sha(source)
|
|
destination = BASE / 'evidence' / name
|
|
with source.open('rb') as src, destination.open('xb') as dst:
|
|
shutil.copyfileobj(src, dst)
|
|
assert sha(source) == sha(destination) == checksum
|
|
evidence.append({'source': str(source), 'destination': 'evidence/' + name, 'sha256': checksum, 'bytes': destination.stat().st_size})
|
|
record(BASE / 'evidence-manifest.json', evidence)
|
|
record(BASE / 'committed-roundtrip.json', {'base_commit': COMMIT, 'reconstruction_commit': reconstruction, 'checks': checks, 'clean': True})
|
|
record(BASE / 'build-evidence.json', {
|
|
'source_identity': f'GUI exact-byte reconstruction {reconstruction} from {COMMIT}; {len(manifest["files"])} frozen GUI gate inputs plus separately verified shared build resources. This is not the original build commit. EFEE server retains its separate September 7 source snapshot; its old language.rs differs from current GUI core. No current-source server claim.',
|
|
'profile': f'Prebuilt Windows development GUI, actual full desktop result {counts}. Retained EFEE server is the historical development artifact, not rebuilt. Native/package acceptance remains pending.',
|
|
'compiler': 'Original GUI Cargo artifact/profile and server build records retained separately; package collector records current compiler/sysroot facts. No reproducible build or new server build claimed.',
|
|
'target': TARGET, 'binary_sha256': {'arcadia-studio.exe': build['sha256'], 'arcadia-server.exe': SERVER_SHA},
|
|
'provenance_reference_base': 'evidence/ references are external preparation records, not files inside the ZIP.',
|
|
'per_binary_provenance': {
|
|
'arcadia-studio.exe': {'build': 'evidence/gui-gui-build-result.json', 'inputs': 'evidence/gui-inputs1.json', 'full_gate': 'evidence/gui-desktop-tests-result.json', 'complete': 'evidence/gui-complete.json', 'captured_head': manifest['head'], 'scope': 'Built from captured eeeaa126 plus exact repair input delta; those bytes are committed at base_commit, then reconstructed without changing code.'},
|
|
'arcadia-server.exe': {'build': 'evidence/server-original-gates.json', 'hashes': 'evidence/server-original-binary-hashes.json', 'source_tar': 'evidence/server-source-snapshot.tar.gz', 'source_inventory': 'evidence/server-prior-input-comparisons.json', 'notices': 'evidence/prior-dependency-notices.json', 'scope': 'Unchanged historical EFEE; separate source, not rebuilt from navigation source.'}}})
|
|
after = root_identity()
|
|
assert after == before, 'Root HEAD/index/tracked state changed during preparation; inspect before proceeding.'
|
|
record(BASE / 'preparation.json', {'version': VERSION, 'target': TARGET, 'base_commit': COMMIT, 'reconstruction_commit': reconstruction,
|
|
'branch': BRANCH, 'record_only_root_paths_after_base': later, 'historical_registry_count': len(registry), 'source_repository': str(SOURCE), 'restorations': restores, 'gui_inputs': {e['path']: e['sha256'] for e in manifest['files']},
|
|
'shared_non_cargo_inputs': shared, 'historical_server_inputs': old, 'historical_server_comparison': comparison,
|
|
'artifacts': artifacts, 'full_test_counts': counts, 'root_before': before, 'root_after': after, 'prepared': now(),
|
|
'scope': 'Isolated preparation only; package integrity and native acceptance pending.'})
|
|
print(json.dumps({'success': True, 'reconstruction': reconstruction, 'gui_inputs': len(manifest['files']), 'restorations': len(restores), 'historical_server_inputs': len(old)}))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
sys.stderr.reconfigure(encoding='utf-8')
|
|
assert not (BASE / 'prepare-result.json').exists()
|
|
status = {'started': now(), 'scope': 'Preparation only; no application/Cargo/package execution.'}
|
|
try:
|
|
main()
|
|
status['success'] = True
|
|
except BaseException as error:
|
|
status.update(success=False, error=repr(error))
|
|
traceback.print_exc()
|
|
raise
|
|
finally:
|
|
status['finished'] = now()
|
|
record(BASE / 'prepare-result.json', status)
|