290 lines
18 KiB
Python
290 lines
18 KiB
Python
"""Prepared Windows workspace gates. No default candidate; never launches artifacts."""
|
|
from pathlib import Path
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tomllib
|
|
|
|
sys.dont_write_bytecode = True
|
|
from run import clean, environment, inputs, now, read, record, save, verify_environment
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
TOOLCHAIN = ROOT.parent / 'toolchain'
|
|
HOST = 'x86_64-pc-windows-gnullvm'
|
|
TOOLCHAIN_NAME = 'stable-' + HOST
|
|
RUNTIME = TOOLCHAIN / 'llvm-mingw-20260908-ucrt-x86_64/bin/libunwind.dll'
|
|
RUNTIME_SHA = 'aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a'
|
|
|
|
|
|
def manifest(path):
|
|
with path.open('rb') as stream:
|
|
return tomllib.load(stream)
|
|
|
|
|
|
def example(repo, name):
|
|
core = repo / 'crates/oa-core'
|
|
cfg = manifest(core / 'Cargo.toml')
|
|
declared = [x for x in cfg.get('example', []) if x.get('name') == name]
|
|
if len(declared) > 1:
|
|
raise RuntimeError('Duplicate example declaration')
|
|
if declared:
|
|
path = core / declared[0].get('path', 'examples/' + name + '.rs')
|
|
elif cfg['package'].get('autoexamples', True):
|
|
choices = [p for p in (core / ('examples/' + name + '.rs'), core / ('examples/' + name + '/main.rs')) if p.is_file()]
|
|
if len(choices) != 1:
|
|
return None
|
|
path = choices[0]
|
|
else:
|
|
return None
|
|
if not path.resolve().is_relative_to(repo) or not path.is_file():
|
|
raise RuntimeError('Example source missing or outside candidate')
|
|
return record(path)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--repo', required=True, type=Path)
|
|
parser.add_argument('--source-commit', required=True)
|
|
parser.add_argument('--attempt', required=True)
|
|
parser.add_argument('--profile', required=True, choices=('dev', 'release'))
|
|
parser.add_argument('--snapshot-example', choices=('attachment_snapshot',), help='Build only when explicitly requested and present in the candidate')
|
|
parser.add_argument('--source-converter-example', help='Explicit reviewed oa-core example name; never reuse an older executable')
|
|
args = parser.parse_args()
|
|
repo, pin = args.repo.resolve(), args.source_commit
|
|
if not re.fullmatch('[0-9a-f]{40}', pin) or not re.fullmatch('final-[0-9]{2}', args.attempt):
|
|
raise RuntimeError('Full explicit source pin and fresh final-NN required')
|
|
if args.source_converter_example and not re.fullmatch('[A-Za-z0-9_-]+', args.source_converter_example):
|
|
raise RuntimeError('Invalid converter example name')
|
|
out = ROOT / args.attempt
|
|
if out.exists() or not repo.is_dir() or os.name != 'nt':
|
|
raise RuntimeError('Fresh output and existing Windows candidate required')
|
|
clean(repo, pin)
|
|
for package, binary in (('oa-desktop', 'arcadia-studio'), ('oa-server', 'arcadia-server')):
|
|
cfg = manifest(repo / ('crates/' + package + '/Cargo.toml'))
|
|
if cfg['package']['name'] != package or not any(x.get('name') == binary for x in cfg.get('bin', [])):
|
|
raise RuntimeError('Candidate executable contract differs: ' + binary)
|
|
target_text = os.environ.get('CARGO_TARGET_DIR', '')
|
|
if not target_text or not Path(target_text).is_absolute():
|
|
raise RuntimeError('Explicit absolute CARGO_TARGET_DIR required')
|
|
target = Path(target_text).resolve()
|
|
if os.environ.get('CARGO_BUILD_TARGET'):
|
|
raise RuntimeError('Host-native cache required; CARGO_BUILD_TARGET must be unset')
|
|
compiler_keys = ('RUSTC', 'RUSTDOC', 'RUSTC_WRAPPER', 'RUSTC_WORKSPACE_WRAPPER',
|
|
'CARGO_BUILD_RUSTC', 'CARGO_BUILD_RUSTDOC',
|
|
'CARGO_BUILD_RUSTC_WRAPPER', 'CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER')
|
|
if any(os.environ.get(key) for key in compiler_keys):
|
|
raise RuntimeError('Unreviewed Cargo compiler override; use the verified pinned toolchain')
|
|
required_env = {'CARGO_HOME': str((TOOLCHAIN / 'cargo').resolve()),
|
|
'RUSTUP_HOME': str((TOOLCHAIN / 'rustup').resolve()),
|
|
'RUSTUP_TOOLCHAIN': TOOLCHAIN_NAME, 'CARGO_BUILD_JOBS': '1', 'CARGO_INCREMENTAL': '0'}
|
|
for key, value in required_env.items():
|
|
actual = os.environ.get(key, '')
|
|
if (Path(actual).resolve() != Path(value).resolve()) if key.endswith('_HOME') else actual != value:
|
|
raise RuntimeError('Required environment differs: ' + key)
|
|
if record(RUNTIME)['sha256'] != RUNTIME_SHA:
|
|
raise RuntimeError('Admitted MinGW runtime differs')
|
|
tools = [TOOLCHAIN / 'env.ps1', RUNTIME, ROOT / 'run.py', ROOT / 'final.py', ROOT / 'README.md']
|
|
actual_bin = TOOLCHAIN / 'rustup/toolchains' / TOOLCHAIN_NAME / 'bin'
|
|
for name in ('cargo.exe', 'rustc.exe', 'rustdoc.exe', 'cargo-clippy.exe', 'clippy-driver.exe'):
|
|
tools.append(actual_bin / name)
|
|
tools.extend(actual_bin.glob('rustc_driver-*.dll'))
|
|
for name in ('cargo', 'rustc', 'clang.exe'):
|
|
resolved = shutil.which(name)
|
|
if not resolved:
|
|
raise RuntimeError('Required command unavailable: ' + name)
|
|
tools.append(Path(resolved))
|
|
rc = os.environ.get('RC')
|
|
if not rc:
|
|
raise RuntimeError('Explicit windres path required')
|
|
tools.append(Path(rc))
|
|
configs = []
|
|
cargo_home = TOOLCHAIN / 'cargo'
|
|
for folder in dict.fromkeys([repo, *repo.parents, cargo_home.resolve()]):
|
|
for name in ('config', 'config.toml'):
|
|
path = folder / name if folder == cargo_home.resolve() else folder / '.cargo' / name
|
|
configs.append(dict(path=str(path), present=path.is_file(), record=record(path) if path.is_file() else None))
|
|
if path.is_file():
|
|
build = manifest(path).get('build', {})
|
|
if 'target' in build:
|
|
raise RuntimeError('Host-native cache required; Cargo build.target must be absent: ' + str(path))
|
|
if any(key in build for key in ('rustc', 'rustdoc', 'rustc-wrapper', 'rustc-workspace-wrapper')):
|
|
raise RuntimeError('Unreviewed Cargo compiler configuration: ' + str(path))
|
|
frozen = dict(environment=environment(), target=HOST, profile=args.profile,
|
|
profile_scope='selected build profile only', target_mode='implicit verified rustc host',
|
|
target_directory=str(target), cargo_configs=configs,
|
|
bound_files=[record(p) for p in dict.fromkeys(p.resolve() for p in tools)])
|
|
before = inputs(repo, pin)
|
|
common = ['--locked', '--offline']
|
|
build_flags = [*common, *(['--release'] if args.profile == 'release' else [])]
|
|
commands = [('toolchain', ['rustc', '--version', '--verbose'], None),
|
|
('cargo-version', ['cargo', '--version', '--verbose'], None),
|
|
('workspace-test-build', ['cargo', 'test', '--locked', '--offline', '--workspace', '--no-run', '--message-format=json-render-diagnostics'], None),
|
|
('workspace-tests', ['cargo', 'test', '--locked', '--offline', '--workspace', '--no-fail-fast', '--', '--test-threads=1'], None),
|
|
('workspace-clippy', ['cargo', 'clippy', '--locked', '--offline', '--workspace', '--all-targets', '--', '-D', 'warnings'], None)]
|
|
identified = {}
|
|
for package, binary in (('oa-desktop', 'arcadia-studio'), ('oa-server', 'arcadia-server')):
|
|
commands.append((binary + '-build', ['cargo', 'build', '-p', package, '--bin', binary, *build_flags, '--message-format=json-render-diagnostics'], binary))
|
|
snapshot = example(repo, 'attachment_snapshot') if args.snapshot_example else None
|
|
if args.snapshot_example and snapshot is None:
|
|
raise RuntimeError('Requested attachment_snapshot example is absent')
|
|
if snapshot:
|
|
identified['attachment_snapshot'] = snapshot
|
|
if args.source_converter_example:
|
|
converter = example(repo, args.source_converter_example)
|
|
if converter is None or args.source_converter_example in ('attachment_snapshot', 'arcadia-studio', 'arcadia-server'):
|
|
raise RuntimeError('Explicit distinct converter example not present')
|
|
identified[args.source_converter_example] = converter
|
|
for name in identified:
|
|
commands.append((name + '-build', ['cargo', 'build', '-p', 'oa-core', '--example', name, *build_flags, '--message-format=json-render-diagnostics'], name))
|
|
out.mkdir()
|
|
(out / 'artifacts').mkdir()
|
|
save(out / 'inputs-before.json', before)
|
|
save(out / 'environment.json', frozen)
|
|
save(out / 'plan.json', dict(source_commit=pin, repo=str(repo), target=HOST, profile=args.profile,
|
|
profile_scope='selected build profile only', target_mode='implicit verified rustc host',
|
|
test_profile='default test', clippy_profile='default dev',
|
|
commands=commands, examples=identified,
|
|
attachment_snapshot_requested=args.snapshot_example,
|
|
source_converter_requested=args.source_converter_example,
|
|
no_prior_gate_or_binary_reuse=True))
|
|
started = now()
|
|
steps, outputs, relocations = [], [], []
|
|
failure = None
|
|
unchanged = False
|
|
|
|
def preserve(path, build_record=None):
|
|
original = record(path)
|
|
dest = out / 'artifacts' / path.name
|
|
with path.open('rb') as src, dest.open('xb') as dst:
|
|
shutil.copyfileobj(src, dst)
|
|
copied = record(dest)
|
|
if record(path) != original or any(original[k] != copied[k] for k in ('bytes', 'sha256')):
|
|
raise RuntimeError('Artifact changed during preservation')
|
|
outputs.append(copied)
|
|
relocations.append(dict(original=original, preserved=copied, cargo_artifact=build_record))
|
|
|
|
try:
|
|
for name, argv, executable in commands:
|
|
command_profile = args.profile if executable else ('test' if name in ('workspace-test-build', 'workspace-tests') else 'dev' if name == 'workspace-clippy' else 'none')
|
|
verify_environment(frozen)
|
|
if inputs(repo, pin) != before:
|
|
raise RuntimeError('Source changed before ' + name)
|
|
wrapper = out / (name + '.orchestration.log')
|
|
inventory_args = ['--test-inventory', str(out / 'test-artifact-inventory.json')] if name == 'workspace-tests' else []
|
|
with wrapper.open('xb') as stream:
|
|
proc = subprocess.run([sys.executable, str(ROOT / 'run.py'), '--repo', str(repo),
|
|
'--source-commit', pin, '--profile', args.profile, '--command-profile', command_profile, '--output', str(out / name),
|
|
'--environment', str(out / 'environment.json'), *inventory_args, '--', *argv], stdout=stream, stderr=subprocess.STDOUT)
|
|
result_path = out / name / 'result.json'
|
|
result = read(result_path) if result_path.is_file() else None
|
|
steps.append(dict(name=name, argv=argv, command_profile=command_profile, runner_exit_code=proc.returncode,
|
|
orchestration_log=record(wrapper), result=record(result_path) if result else None))
|
|
if proc.returncode or not result or result['exit_code'] != 0 or result['failure'] or not result['inputs_unchanged']:
|
|
raise RuntimeError('Gate failed: ' + name)
|
|
if result['source_commit'] != pin or result['argv'] != argv or Path(result['cwd']) != repo or result['profile'] != args.profile or result['command_profile'] != command_profile or result['target'] != HOST:
|
|
raise RuntimeError('Command attribution differs')
|
|
for phase in ('inputs-before.json', 'inputs-after.json'):
|
|
if read(out / name / phase) != before:
|
|
raise RuntimeError('Source inventory differs in ' + name)
|
|
verify_environment(frozen)
|
|
if name == 'toolchain':
|
|
host_lines = [line for line in (out / name / 'output.log').read_text().splitlines() if line.startswith('host:')]
|
|
if host_lines != ['host: ' + HOST]:
|
|
raise RuntimeError('Actual rustc host differs')
|
|
if name == 'workspace-test-build':
|
|
tests = {}
|
|
source_by_repo_path = {item['repository_path']: item for item in before}
|
|
for line in (out / name / 'output.log').read_text(encoding='utf-8').splitlines():
|
|
artifact = json.loads(line)
|
|
if artifact.get('reason') != 'compiler-artifact' or artifact.get('profile', {}).get('test') is not True or not artifact.get('executable'):
|
|
continue
|
|
path = Path(artifact['executable']).resolve()
|
|
source = Path(artifact['target']['src_path']).resolve()
|
|
if not path.is_relative_to(target) or path.suffix.lower() != '.exe' or not source.is_relative_to(repo):
|
|
raise RuntimeError('Unexpected native test artifact/source')
|
|
package_manifest = Path(artifact['manifest_path']).resolve()
|
|
if not package_manifest.is_relative_to(repo) or not package_manifest.is_file():
|
|
raise RuntimeError('Test package manifest unavailable')
|
|
admitted_inputs = []
|
|
for input_path in (source, package_manifest):
|
|
relative_path = input_path.relative_to(repo).as_posix()
|
|
admitted = source_by_repo_path.get(relative_path)
|
|
if admitted is None or record(input_path) != {key: admitted[key] for key in ('path', 'bytes', 'sha256')}:
|
|
raise RuntimeError('Test source/manifest absent from or changed since frozen input map: ' + relative_path)
|
|
admitted_inputs.append(admitted)
|
|
cfg = manifest(package_manifest)
|
|
kind = artifact['target']['kind']
|
|
declarations = [cfg.get('lib', {})] if 'lib' in kind else [item for section in ('bin', 'test', 'example', 'bench') for item in cfg.get(section, []) if item.get('name') == artifact['target']['name']]
|
|
if any(item.get('harness') is False for item in declarations):
|
|
raise RuntimeError('Custom test harness requires separate reviewed listing contract')
|
|
item = dict(binary=record(path), cargo_artifact=artifact,
|
|
source_input=admitted_inputs[0], package_manifest=admitted_inputs[1])
|
|
if str(path) in tests and tests[str(path)] != item:
|
|
raise RuntimeError('Conflicting test executable attribution')
|
|
tests[str(path)] = item
|
|
if not tests:
|
|
raise RuntimeError('No native test harness artifacts identified')
|
|
save(out / 'test-artifact-inventory.json', dict(source_commit=pin, target=HOST,
|
|
build_result=record(result_path), artifacts=[tests[p] for p in sorted(tests)],
|
|
scope='Native test harnesses from the no-run build; rustdoc tests remain in the literal Cargo test log. Binaries are hashed, not copied.'))
|
|
if executable:
|
|
matches = []
|
|
for line in (out / name / 'output.log').read_text(encoding='utf-8').splitlines():
|
|
item = json.loads(line)
|
|
if item.get('reason') == 'compiler-artifact' and item.get('target', {}).get('name') == executable and item.get('executable'):
|
|
matches.append(item)
|
|
if len(matches) != 1:
|
|
raise RuntimeError('One exact Cargo executable artifact required')
|
|
artifact = matches[0]
|
|
path = Path(artifact['executable']).resolve()
|
|
if not path.is_relative_to(target) or path.suffix.lower() != '.exe' or artifact.get('profile', {}).get('test') is not False:
|
|
raise RuntimeError('Unexpected executable location/profile')
|
|
preserve(path, artifact)
|
|
preserve(RUNTIME)
|
|
except Exception as error:
|
|
failure = str(error)
|
|
finally:
|
|
errors = []
|
|
try:
|
|
after = inputs(repo, pin)
|
|
save(out / 'inputs-after.json', after)
|
|
unchanged = after == before
|
|
if not unchanged:
|
|
errors.append('Final source differs')
|
|
except Exception as error:
|
|
errors.append('Final source verification failed: ' + str(error))
|
|
try:
|
|
verify_environment(frozen)
|
|
except Exception as error:
|
|
errors.append('Final environment verification failed: ' + str(error))
|
|
for item in outputs:
|
|
try:
|
|
if record(item['path']) != item:
|
|
errors.append('Preserved artifact changed')
|
|
except Exception as error:
|
|
errors.append('Final artifact verification failed: ' + str(error))
|
|
failure = '; '.join(filter(None, [failure, *errors])) or None
|
|
save(out / 'artifact-relocation.json', dict(source_commit=pin, target=HOST, profile=args.profile, artifacts=relocations))
|
|
passed = failure is None and unchanged and len(steps) == len(commands) and len(outputs) == 3 + len(identified)
|
|
save(out / 'summary.json', dict(source_commit=pin, repo=str(repo), target=HOST, profile=args.profile,
|
|
profile_scope='selected build profile only', target_mode='implicit verified rustc host',
|
|
passed=passed, failure=failure, inputs_unchanged=unchanged, started_utc=started, finished_utc=now(),
|
|
steps=steps, outputs=outputs, source_manifest=record(out / 'inputs-before.json'),
|
|
environment=record(out / 'environment.json'), plan=record(out / 'plan.json'),
|
|
relocation=record(out / 'artifact-relocation.json'),
|
|
test_artifact_inventory=record(out / 'test-artifact-inventory.json') if (out / 'test-artifact-inventory.json').exists() else None,
|
|
test_name_inventory=record(out / 'workspace-tests/test-name-inventory.json') if (out / 'workspace-tests/test-name-inventory.json').exists() else None,
|
|
observed_ignores=record(out / 'workspace-tests/observed-ignores.json') if (out / 'workspace-tests/observed-ignores.json').exists() else None,
|
|
scope='Fresh literal workspace tests and strict all-target workspace lint; identified builds only. Existing ignored tests remain ignored; native/package/server-launch acceptance is separate.'))
|
|
print(json.dumps(dict(summary=str(out / 'summary.json'), passed=passed, failure=failure)))
|
|
if not passed:
|
|
raise SystemExit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|