"""The eight setup steps of docs/installer.md: check, fonts, extract, lift, compile, prepare, dlss, cleanup. Extraction runs in this process through tools/extract.py (the package's sibling tools/, which ships with it); fonts runs the vendored extract_shared_fonts.py on the firmware folder; lift, compile and prepare run the bundled Python on recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and tools/prepare_shaders.py, whose output goes to /logs/-.log and is read for progress. No Win32 call here: the host check is a small C program built with the bundled clang (hostcheck.c). Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), nx2navi.exe and its DLLs, nvngx_dlss.dll (with --dlss), romfs-index.bin, romfs-patches/, romfs-patches-xbox/, romfs-patches-xbox-label/, romfs-patches-pc/, nx2navi_xlate.exe, the shader state of nx2navi.exe --prepare-shaders, nx2navi.ini, nx2navi-install.json. Temp folder (/-): check/ (ExeFS of both inputs, the host check), unpacked/ (the NSP of an NSZ or XCZ input until the extraction is done), gen/ (C), build/ (objects), logs/. The cleanup keeps gen/, build/ and logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites only the chunks whose C changed, and Ninja in build/ recompiles only those. A change of the mods' code cheats then costs the lift (about a minute) and the chunks it touches, not a full compile. Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status (tools/exefs_patches.py) and rebuild.txt, the command that starts this setup in its rebuild mode on the install folder (installer/gui.py --rebuild), one argument per line; nx2navi.exe runs it at start when an enabled mod has code cheats the build lacks (runtime/cheats.c). """ import hashlib import http.client import json import os import re import shutil import signal import subprocess import sys import threading import time import urllib.request from pathlib import Path from . import ini from .layout import EXE, SUPPORTED, version_tag from .runner import InstallError, Step sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'tools')) import extract # noqa: E402 import ncz # noqa: E402 import shader_list # noqa: E402 from extract_exefs import (common_tickets, container_entries, load_keys, nca_ctr_key, nca_inventory, # noqa: E402 program_nca) GiB = 1 << 30 DEST_EXTRA = 4 * GiB # beside the RomFS: shader and pipeline caches (about 2 GB), exefs, nx2navi.exe, seed files TEMP_NEED = 3 * GiB # generated C (about 0.9 GB) and objects HOSTCHECK = Path(__file__).resolve().parent / 'hostcheck.c' STAMP = '.install-key' # ---- helpers def file_sha256(path, progress=None): """SHA-256 hex of a file; progress(done, total) after each block.""" h = hashlib.sha256() total = path.stat().st_size done = 0 with open(path, 'rb', buffering=0) as f: buf = bytearray(8 << 20) view = memoryview(buf) while n := f.readinto(buf): h.update(view[:n]) done += n if progress: progress(done, total) return h.hexdigest() def fingerprint(*parts): """SHA-256 over strings and the contents of files and directory trees (a missing path counts as missing).""" h = hashlib.sha256() for p in parts: if isinstance(p, Path): if p.is_dir(): for f in sorted(p.rglob('*')): if f.is_file() and '__pycache__' not in f.parts: h.update(f.relative_to(p).as_posix().encode() + b'\0' + bytes.fromhex(file_sha256(f))) elif p.is_file(): h.update(p.name.encode() + b'\0' + bytes.fromhex(file_sha256(p))) else: h.update(b'missing\0' + p.name.encode()) else: h.update(str(p).encode()) h.update(b'\1') return h.hexdigest() def remove(path): if path.is_dir() and not path.is_symlink(): shutil.rmtree(path) elif path.exists() or path.is_symlink(): path.unlink() def replace_file(src, dst): """Copy src over dst through a temporary name, so a cancelled copy never leaves half a file under dst's name.""" tmp = dst.with_name(dst.name + '.tmp') shutil.copy2(src, tmp) os.replace(tmp, dst) def kill_tree(proc): if proc.poll() is not None: return if os.name == 'nt': subprocess.run(['taskkill', '/T', '/F', '/PID', str(proc.pid)], capture_output=True) return try: # run_tool starts each tool in a session of its own: the group holds ninja, clang and the game os.killpg(proc.pid, signal.SIGKILL) except OSError: proc.kill() def linux_glibc(): """(major, minor) of the C library this Linux process runs on, None elsewhere or when it is not glibc.""" try: name = os.confstr('CS_GNU_LIBC_VERSION') if sys.platform.startswith('linux') else None except (ValueError, OSError): return None m = re.match(r'glibc (\d+)\.(\d+)', name or '') return (int(m.group(1)), int(m.group(2))) if m else None # The bundled clang, lld and the cryptography wheel are built against glibc 2.34 (tools/make_release_linux.py # GLIBC_FLOOR); on an older one they cannot start GLIBC_NEEDED = (2, 34) def linux_kernel(): """(major, minor) of the running Linux kernel, None elsewhere.""" if not sys.platform.startswith('linux'): return None m = re.match(r'(\d+)\.(\d+)', os.uname().release) return (int(m.group(1)), int(m.group(2))) if m else None def run_tool(ctx, step, script, args, parse=None, cwd=None): """Run a tool script with the bundled Python. Its output goes to /logs/-