mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
The check identifies the update by hash among layout.SUPPORTED and records it in totk-install.json; an install that records none is 1.2.1. Extraction, lifting, compiling and preparing take the version from there: the patch table and expected hashes per version, build_game.py --version (rt_game_version), make_romfs_patches.py and prepare_shaders.py --version. Step keys of a 1.2.1 install stay as they were, so updating such an install does not extract again. Another update file than the recorded one plans the extraction again (a copy of the same file is recognised by its hash), and an identified version change re-plans every step after the check. The disk space estimate takes the larger RomFS. Catalog strings name both versions and take the version as a parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.4 KiB
Python
79 lines
3.4 KiB
Python
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR]
|
|
[--temp DIR] [--verify]
|
|
|
|
A first install needs every option. On a folder with totk-install.json (resume after a cancel or failure, or update
|
|
with a newer release) only --dest is needed: --lang defaults to the recorded language, and the game files are needed
|
|
only when the extraction has to run again. Prints JSON events on stdout (events.py). Exit code: 0 done, 1 failed,
|
|
2 bad command line, 3 cancelled (Ctrl+C or Ctrl+Break).
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import signal
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from . import layout as layouts
|
|
from . import state as statefile
|
|
from .events import Reporter
|
|
from .runner import Context, run_steps
|
|
from .steps import all_steps
|
|
|
|
EXIT = {'ok': 0, 'failed': 1, 'cancelled': 3}
|
|
|
|
|
|
class Parser(argparse.ArgumentParser):
|
|
def error(self, message):
|
|
Reporter().error('cli.usage', {'detail': message})
|
|
sys.exit(2)
|
|
|
|
|
|
def temp_folder(base, dest):
|
|
"""One temp folder per install folder, so two installs never share generated files."""
|
|
tag = hashlib.sha256(str(dest).lower().encode()).hexdigest()[:8]
|
|
return base / f'{dest.name or "root"}-{tag}'
|
|
|
|
|
|
def cancel(signum, frame):
|
|
raise KeyboardInterrupt
|
|
|
|
|
|
def main(argv=None):
|
|
ap = Parser(prog='python -m installer', description=__doc__.splitlines()[0])
|
|
ap.add_argument('--base', type=Path, help='base game, XCI or NSP')
|
|
ap.add_argument('--update', type=Path, help='1.2.1 or 1.4.3 update NSP')
|
|
ap.add_argument('--keys', type=Path, help='prod.keys')
|
|
ap.add_argument('--lang', help='game language, a Switch language code (en-US, en-GB, ja, de, ru, ...)')
|
|
ap.add_argument('--dest', type=Path, required=True, help='install folder')
|
|
ap.add_argument('--root', type=Path, help='release folder or repository (default: the folder above installer/)')
|
|
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release, build/install-temp in a '
|
|
'repository)')
|
|
ap.add_argument('--verify', action='store_true', help='hash every kept file, also those this release made')
|
|
args = ap.parse_args(argv)
|
|
|
|
# The window cancels with Ctrl+Break to the installer's process group (CREATE_NEW_PROCESS_GROUP), which also reaches
|
|
# the tools it runs; Python's default for it ends the process without recording anything.
|
|
for name in ('SIGBREAK', 'SIGTERM'):
|
|
if hasattr(signal, name):
|
|
signal.signal(getattr(signal, name), cancel)
|
|
layout = layouts.find(args.root)
|
|
dest = args.dest.resolve()
|
|
temp = temp_folder((args.temp or layout.temp).resolve(), dest)
|
|
log = temp / 'logs' / time.strftime('install-%Y%m%d-%H%M%S.log')
|
|
rep = Reporter(log_path=log)
|
|
t0 = time.monotonic()
|
|
try:
|
|
state = statefile.load(dest)
|
|
except statefile.StateError as e:
|
|
rep.error(e.id, {'path': str(e.path)})
|
|
rep.emit('end', result='failed', seconds=0)
|
|
return 1
|
|
ctx = Context(layout, dest, temp, rep, args, state=state, verify=args.verify)
|
|
steps = all_steps()
|
|
rep.emit('start', release=layout.release, mode=ctx.mode, dev=layout.dev, dest=str(dest), log=str(log),
|
|
steps=[s.id for s in steps])
|
|
result = run_steps(steps, ctx)
|
|
rep.emit('end', result=result, seconds=round(time.monotonic() - t0, 1))
|
|
rep.close()
|
|
return EXIT[result]
|