mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
3.6 KiB
Python
80 lines
3.6 KiB
Python
"""End-to-end run of the installer in dev mode with the game files named in config.local.toml.
|
|
|
|
Reads input.base, the 1.2.1 entry of input.updates (the one build/extracted/v121 was extracted from, else the only
|
|
one) and input.keys, and passes them to `python -m installer` started the way the release launcher starts it: the
|
|
embeddable Python (build/py314-embed) with this checkout on sys.path. The paths are never printed. Events go to
|
|
<events> (JSON lines); stdout gets each step's state and time, warnings and errors.
|
|
|
|
python tests/installer_e2e.py --dest build/install-test --lang en-US [--events FILE] [--no-inputs] [-- extra args]
|
|
|
|
--no-inputs leaves the game files out (update mode on an existing folder).
|
|
"""
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
EMBED = ROOT / 'build' / 'py314-embed' / 'py' / 'python.exe'
|
|
BOOT = ('import runpy, sys; sys.path.insert(0, sys.argv.pop(1)); '
|
|
"runpy.run_module('installer', run_name='__main__', alter_sys=True)")
|
|
|
|
|
|
def game_inputs():
|
|
config = tomllib.loads((ROOT / 'config.local.toml').read_text(encoding='utf-8'))['input']
|
|
updates = [Path(u) for u in config.get('updates', [])]
|
|
manifest = ROOT / 'build' / 'extracted' / 'v121' / 'extraction-manifest.json'
|
|
update = None
|
|
if manifest.is_file():
|
|
source = json.loads(manifest.read_text())['source']
|
|
update = next((u for u in updates if str(u.resolve()) == source), None)
|
|
if update is None and len(updates) == 1:
|
|
update = updates[0]
|
|
if update is None:
|
|
sys.exit('installer_e2e: cannot tell which input.updates entry is 1.2.1')
|
|
return ['--base', config['base'], '--update', str(update), '--keys', config['keys']]
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
ap.add_argument('--dest', type=Path, required=True)
|
|
ap.add_argument('--lang')
|
|
ap.add_argument('--events', type=Path)
|
|
ap.add_argument('--no-inputs', action='store_true')
|
|
ap.add_argument('extra', nargs='*')
|
|
a = ap.parse_args()
|
|
python = EMBED if EMBED.is_file() else Path(sys.executable)
|
|
command = [str(python), '-c', BOOT, str(ROOT), '--dest', str(a.dest.resolve())]
|
|
if a.lang:
|
|
command += ['--lang', a.lang]
|
|
if not a.no_inputs:
|
|
command += game_inputs()
|
|
command += a.extra
|
|
events = a.events or a.dest.resolve().parent / f'{a.dest.name}-events.jsonl'
|
|
with open(events, 'w', encoding='utf-8') as log:
|
|
proc = subprocess.Popen(command, stdout=subprocess.PIPE, text=True, encoding='utf-8', cwd=ROOT)
|
|
for line in proc.stdout:
|
|
log.write(line)
|
|
log.flush()
|
|
try:
|
|
e = json.loads(line)
|
|
except ValueError:
|
|
print('non-JSON line on stdout:', line.rstrip())
|
|
continue
|
|
if e['type'] == 'step' and e['state'] != 'started':
|
|
extra = f'{e["seconds"]:.1f} s' if 'seconds' in e else e.get('reason', '')
|
|
print(f'[{e["t"]:8.1f}] {e["step"]:8} {e["state"]} {extra}', flush=True)
|
|
elif e['type'] == 'step':
|
|
print(f'[{e["t"]:8.1f}] {e["step"]:8} started', flush=True)
|
|
elif e['type'] in ('warning', 'error', 'plan', 'end'):
|
|
print(f'[{e["t"]:8.1f}] {json.dumps({k: v for k, v in e.items() if k != "t"})}', flush=True)
|
|
elif e['type'] == 'start':
|
|
print(f'[{e["t"]:8.1f}] start mode={e["mode"]} release={e["release"]}', flush=True)
|
|
return proc.wait()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|