Files
ARCADIA-Studio/harness/experiments/navigation-440-windows-20260913/raw/.local/build-navigation-palette-artifacts.py
T

143 lines
7.9 KiB
Python

"""Build identified Windows binaries and preserve Cargo-reported artifacts."""
from pathlib import Path
from datetime import datetime, timezone
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
root = Path(__file__).resolve().parent.parent
source, name = sys.argv[1:3]
assert re.fullmatch(r'[a-zA-Z0-9-]+', name)
assert subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root).decode().strip() == source
folder = root / '.local/navigation-palette-gates-20260913' / name
folder.mkdir(exist_ok=False)
def digest(path):
raw = path.read_bytes()
return {'bytes': len(raw), 'sha256': hashlib.sha256(raw).hexdigest()}
def write(name, value):
with (folder / name).open('x', encoding='utf-8', newline='\n') as stream:
stream.write(json.dumps(value, indent=2) + '\n')
# A failed spawn, malformed output or artifact admission must also close a receipt.
inputs, artifacts, changed = [], [], []
command, head_after, result = [], None, None
started = datetime.now(timezone.utc).isoformat()
phase, failure, status_after = 'input/tool admission', None, None
try:
scopes = ['Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo', 'build',
'crates', 'assets', 'docs', 'examples', 'harness/fixtures', 'scripts']
paths = subprocess.check_output(['git', 'ls-tree', '-r', '--name-only', source, *scopes], cwd=root).decode().splitlines()
assert not subprocess.check_output(['git', 'status', '--porcelain', '--untracked-files=all', '--', *scopes], cwd=root)
blobs = subprocess.check_output(['git', 'cat-file', '--batch'], cwd=root,
input=''.join(f'{source}:{p}\n' for p in paths).encode())
offset, inputs = 0, []
for path in paths:
end = blobs.index(b'\n', offset)
_, kind, size = blobs[offset:end].split()
assert kind == b'blob'
size = int(size)
raw = blobs[end + 1:end + 1 + size]
assert (root / path).read_bytes() == raw, path
inputs.append({'path': path, 'bytes': size, 'sha256': hashlib.sha256(raw).hexdigest()})
offset = end + size + 2
assert offset == len(blobs)
write('inputs.json', {'source': source, 'inputs': inputs})
environment = {k: v for k, v in os.environ.items() if k in (
'CARGO_HOME', 'RUSTUP_HOME', 'CARGO_TARGET_DIR', 'CARGO_BUILD_JOBS',
'RUSTFLAGS', 'CARGO_ENCODED_RUSTFLAGS', 'RUSTC', 'RUSTC_WRAPPER', 'RUSTC_WORKSPACE_WRAPPER',
'CARGO_BUILD_TARGET', 'CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER')}
tools = {}
for tool, args in [('rustc', ['-Vv']), ('cargo', ['--version']), ('clang', ['--version'])]:
requested = os.environ.get('RUSTC', 'rustc') if tool == 'rustc' else tool
resolved = shutil.which(requested)
assert resolved, f'Required tool unavailable: {requested}'
path = Path(resolved)
tools[tool] = {'path': str(path), **digest(path),
'version': subprocess.check_output([str(path), *args], cwd=root).decode()}
assert 'host: x86_64-pc-windows-gnullvm' in tools['rustc']['version']
write('environment.json', {'environment': environment, 'tools': tools,
'launcher': digest(Path(__file__)), 'profile': 'unoptimized development',
'target_mode': 'Cargo default/configured target; no explicit --target; artifacts require Windows x64 PE'})
command = [tools['cargo']['path'], 'build', '--locked', '--offline', '-p', 'oa-desktop', '-p', 'oa-server',
'--bins', '--message-format=json-render-diagnostics']
started = datetime.now(timezone.utc).isoformat()
print(json.dumps({'start': started, 'source': source, 'command': command}), flush=True)
phase = 'cargo invocation'
with (folder / 'cargo.jsonl').open('xb') as stdout, (folder / 'cargo.stderr.log').open('xb') as stderr:
result = subprocess.run(command, cwd=root, stdout=stdout, stderr=stderr)
ended = datetime.now(timezone.utc).isoformat()
changed = [p['path'] for p in inputs if digest(root / p['path']) != {k:p[k] for k in ('bytes', 'sha256')}]
head_after = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root).decode().strip()
phase = 'Cargo JSON/artifact admission'
messages = [json.loads(line) for line in (folder / 'cargo.jsonl').read_bytes().splitlines()]
finished = [m for m in messages if m.get('reason') == 'build-finished']
artifacts = []
if result.returncode == 0 and not changed and head_after == source:
assert len(finished) == 1 and finished[0]['success'] is True
selected = [m for m in messages if m.get('reason') == 'compiler-artifact'
and m['target']['name'] in ('arcadia-studio', 'arcadia-server')
and m.get('executable') and m['target']['kind'] == ['bin']]
assert len(selected) == 2 and {m['target']['name'] for m in selected} == {'arcadia-studio', 'arcadia-server'}
manifests = {'arcadia-studio': root / 'crates/oa-desktop/Cargo.toml',
'arcadia-server': root / 'crates/oa-server/Cargo.toml'}
for item in selected:
manifest = Path(item['manifest_path'])
if not manifest.is_absolute():
manifest = root / manifest
assert manifest.resolve() == manifests[item['target']['name']].resolve()
assert not item['profile']['test'] and item['profile']['opt_level'] == '0'
assert item['profile']['debug_assertions'], 'Expected a development-profile binary'
origin = Path(item['executable'])
if not origin.is_absolute():
origin = root / origin
origin = origin.resolve()
raw = origin.read_bytes()
assert raw[:2] == b'MZ' and len(raw) >= 64
pe = int.from_bytes(raw[60:64], 'little')
assert raw[pe:pe+4] == b'PE\0\0', 'Expected a PE executable'
assert int.from_bytes(raw[pe+4:pe+6], 'little') == 0x8664, 'Expected Windows x64 PE'
destination = folder / origin.name
with destination.open('xb') as stream:
stream.write(raw)
assert digest(destination) == digest(origin)
artifacts.append({'name': item['target']['name'], 'origin': str(origin),
'preserved': str(destination), **digest(destination), 'cargo_artifact': item})
except Exception as error:
failure = {'phase': phase, 'type': type(error).__name__, 'message': str(error)}
# Recheck after artifact preservation as well, including added/untracked inputs.
try:
changed = [item['path'] for item in inputs
if not (root / item['path']).is_file()
or digest(root / item['path']) != {key: item[key] for key in ('bytes', 'sha256')}]
head_after = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=root).decode().strip()
status_after = subprocess.check_output(
['git', 'status', '--porcelain', '--untracked-files=all', '--', *scopes], cwd=root).decode()
except Exception as error:
postflight = {'phase': 'final input admission', 'type': type(error).__name__, 'message': str(error)}
if failure is None:
failure = postflight
else:
failure['postflight'] = postflight
code = result.returncode if result is not None else None
passed = code == 0 and failure is None and not changed and head_after == source and status_after == ''
record = {'source': source, 'head_after': head_after, 'start': started,
'end': datetime.now(timezone.utc).isoformat(),
'exit': code, 'outcome': 'passed' if passed else 'failed', 'failure': failure,
'command': command, 'changed_inputs': changed, 'status_after': status_after, 'artifacts': artifacts,
'launcher': digest(Path(__file__))}
for key, filename in [('inputs', 'inputs.json'), ('environment', 'environment.json'),
('stdout', 'cargo.jsonl'), ('stderr', 'cargo.stderr.log')]:
path = folder / filename
record[key] = digest(path) if path.is_file() else None
write('result.json', record)
print(json.dumps(record))
sys.exit(0 if passed else (code or 1))