143 lines
7.9 KiB
Python
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))
|