f45e3f1237
Compile and validate original exporter bundles during release builds, cache development compilation, and reuse initialized JVMs and collision data during preparation. Keep legacy preparers compatible while current releases need only a Java runtime. Co-Authored-By: Codex <noreply@openai.com>
146 lines
6.4 KiB
Python
146 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare two isolated preparations with a batched, runtime-only release preparation.
|
|
|
|
Uses only locally imported pinned Minecraft files. Run after compile-exporters and copy its
|
|
server/, client/ and manifest.json to exporters/. Byte differences are accepted only where
|
|
both isolated runs also differ and normalize identically (known exporter noise).
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def hashes(folder):
|
|
return {str(p.relative_to(folder)): hashlib.sha256(p.read_bytes()).hexdigest()
|
|
for p in folder.rglob('*') if p.is_file() and p.suffix != '.log'
|
|
and not {'classes', 'client-classes'} & set(p.relative_to(folder).parts)
|
|
and p.name != 'horizon-resources.json'}
|
|
|
|
|
|
def normalize(path):
|
|
value = json.loads(path.read_text())
|
|
if path.name == 'block-behaviours.json':
|
|
# JVM-assigned lambda class addresses are printed as opaque field values.
|
|
def clean(node):
|
|
if isinstance(node, str):
|
|
return re.sub(r'\$\$Lambda/0x[0-9a-fA-F]+', '$$Lambda/<address>', node)
|
|
if isinstance(node, list):
|
|
return [clean(item) for item in node]
|
|
if isinstance(node, dict):
|
|
return {key: clean(item) for key, item in node.items()}
|
|
return node
|
|
value = clean(value)
|
|
elif path.name == 'creative-inventory.json':
|
|
for tab in value['tabs']:
|
|
if 'tags' in tab:
|
|
tab['tags'] = [sorted(tags) for tags in tab['tags']]
|
|
return value
|
|
|
|
|
|
def verify(folders):
|
|
a, b, batched = [hashes(folder) for folder in folders]
|
|
if a.keys() != b.keys() or a.keys() != batched.keys():
|
|
raise SystemExit('Resource file sets differ')
|
|
known = {'render-definitions.json', 'block-behaviours.json', 'creative-inventory.json'}
|
|
noise = [name for name in a if a[name] != b[name] or a[name] != batched[name]]
|
|
differences = []
|
|
for name in noise:
|
|
if name not in known:
|
|
differences.append(name)
|
|
else:
|
|
x, y, z = [normalize(folder / name) for folder in folders]
|
|
if x != y or x != z:
|
|
differences.append(name)
|
|
manifests = []
|
|
for folder, raw in zip(folders, [a, b, batched]):
|
|
replacements = {raw[name]: f'normalized:{name}' for name in noise}
|
|
def clean(node):
|
|
if isinstance(node, str):
|
|
return replacements.get(node, node)
|
|
if isinstance(node, list):
|
|
return [clean(item) for item in node]
|
|
if isinstance(node, dict):
|
|
return {key: clean(item) for key, item in node.items()}
|
|
return node
|
|
manifests.append(clean(json.loads((folder / 'horizon-resources.json').read_text())))
|
|
if manifests[0] != manifests[1] or manifests[0] != manifests[2]:
|
|
differences.append('horizon-resources.json')
|
|
print(f'Normalized exporter noise: {noise}')
|
|
if differences:
|
|
raise SystemExit(f'Resources differ: {differences}')
|
|
print(f'PASS: all {len(a)} resource files match, with only known noise normalized')
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--binary', type=Path, required=True)
|
|
parser.add_argument('--work', type=Path)
|
|
args = parser.parse_args()
|
|
binary = args.binary.resolve()
|
|
with tempfile.TemporaryDirectory(prefix='prepare-exporters-') as temporary:
|
|
work = args.work.resolve() if args.work else Path(temporary)
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
# Freeze the tested executable even if a developer rebuilds concurrently.
|
|
frozen = work / binary.name
|
|
shutil.copy2(binary, frozen)
|
|
binary = frozen
|
|
results = []
|
|
times = []
|
|
for name in ['isolated-1', 'isolated-2', 'batched']:
|
|
folder = work / name
|
|
folder.mkdir()
|
|
shutil.copyfile(ROOT / 'research-lock.json', folder / 'research-lock.json')
|
|
shutil.copytree(ROOT / 'mods/horizon/src/test/java', folder / 'mods/horizon/src/test/java')
|
|
shutil.copytree(ROOT / 'exporters', folder / 'exporters')
|
|
(folder / '.local').mkdir()
|
|
(folder / '.local/minecraft').symlink_to((ROOT / '.local/minecraft').resolve(), target_is_directory=True)
|
|
env = dict(os.environ)
|
|
env.pop('HORIZON_PREPARE_ISOLATED', None)
|
|
if name.startswith('isolated'):
|
|
env['HORIZON_PREPARE_ISOLATED'] = '1'
|
|
else:
|
|
# An actual runtime-only home: no javac in bin. Java resolves its own libraries.
|
|
java = Path(shutil.which('java')).resolve()
|
|
runtime = folder / 'runtime'
|
|
(runtime / 'bin').mkdir(parents=True)
|
|
trace = folder / 'java-invocations.txt'
|
|
wrapper = runtime / 'bin' / java.name
|
|
wrapper.write_text('#!/bin/sh\nprintf "%s\\n" "$*" >> '
|
|
+ shlex.quote(str(trace)) + '\nexec '
|
|
+ shlex.quote(str(java)) + ' "$@"\n')
|
|
wrapper.chmod(0o755)
|
|
env['JAVA_HOME'] = str(runtime)
|
|
env['PATH'] = str(runtime / 'bin')
|
|
started = time.monotonic()
|
|
with (work / f'{name}.log').open('w') as log:
|
|
subprocess.run([str(binary), '--root', str(folder)], cwd=folder, env=env,
|
|
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=1200)
|
|
if name == 'batched':
|
|
launches = [line for line in trace.read_text().splitlines() if line != '-version']
|
|
if len(launches) != 5:
|
|
raise SystemExit(f'Expected 5 exporter/data-generator JVMs, got {len(launches)}')
|
|
print(f'Runtime-only preparation: {len(launches)} JVM launches', flush=True)
|
|
text = (work / f'{name}.log').read_text()
|
|
if 'Compiling ' in text:
|
|
raise SystemExit(f'{name} unexpectedly needed a compiler')
|
|
resources = next((folder / '.local/horizon-client').iterdir())
|
|
results.append(resources)
|
|
times.append(round(time.monotonic() - started, 2))
|
|
print(f'{name}: {times[-1]}s, prepared', flush=True)
|
|
verify(results)
|
|
print(f'Isolated {times[:2]}s, batched {times[2]}s')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|