"""Build a release of the NX2navi launcher (docs/launcher.md, "Releases"). python tools/make_launcher.py --xlate build/xlate [--ffx amd_fidelityfx_vk.dll] [--dlss nvngx_dlss.dll | --no-dlss] [--ffx4 FOLDER] [--prompt-art FOLDER [FOLDER ...]] [--out build/launcher-release] [--pgo build/pgo/pgo-v111.profdata] [--feed owner/name] [--notes notes.md] [--publish] Needs Python 3.14 (tarfile and zstd for the MSYS2 packages), uv on PATH, network access on the first run, and either Windows (msiexec, and the C compiler of MSYS2 it downloads itself) or Linux with clang, lld and llvm-windres (it cross-compiles against the same MSYS2 packages) plus msitools. Downloads are cached in toolchain/launcher-dl/ and checked against the sha256 in PINS. The release folder (--out) holds: NX2navi/ the launcher folder exactly as NX2navi-Setup.exe installs it: NX2navi.exe launcher/native/nx2navi.c with the Navi icon (launcher/ui/art.py) python/ python.org's embeddable Python 3.14.9, tkinter and Tcl/Tk 9.0 from its tcltk.msi, the packages of tools/requirements-launcher.txt, runtime-id.txt app/current.txt, app// launcher/ (without native/), runtime/, recomp/, the tools make_game.py uses, bin/nx2navi_xlate.exe and .id (and amd_fidelityfx_vk.dll with --ffx, AMD's two FSR 4 libraries with --ffx4), bin/nvngx_dlss.dll and its notice (NVIDIA DLSS at the version tools/dlss_pin.py pins, downloaded from NVIDIA's DLSS SDK repository and checked, or --dlss; not with --no-dlss), bin/dlssg/ with --dlssg (DLSS Frame Generation: nx2navi_dlssg.dll, Streamline's runtime DLLs and nvngx_dlssg.dll at the version tools/streamline_pin.py pins, downloaded from NVIDIA's Streamline SDK release and checked, or --streamline; their licences and a notice; the release stops first on a DLL that lacks an export runtime/dlssg/nx2_sl.h declares, tools/build_dlssg.py check), lib/libnx2navirt.a (the runtime, prebuilt with the release's compiler), data/pgo-v111.profdata and .json with --pgo (the execution profile make_game.py compiles the lifted code and the runtime with, the prebuilt runtime included, and its record: tools/pgo.py merge), prompt-art/ with --prompt-art (the button art make_game.py draws the Xbox, PlayStation, Steam Deck and keyboard and mouse prompts from, tools/prompt_art.py art_files), toolchain.lock.json (the MSYS2 packages of today, pinned), feed.json, VERSION, the licence files NX2navi-Setup-.exe launcher/native/setup.c with the folder above appended as a zip: the one file players download NX2navi-app-.zip app/ alone: what the launcher downloads to update itself NX2navi-runtime-.zip python/ alone: downloaded only when a release changes the Python NX2navi-update.json the manifest the launcher reads (launcher/feed.py) --prompt-art names the button art: folders that hold XGamepad, P5Gamepad and Keyboard_Mouse, or those three folders themselves. Only the pictures tools/pad_prompts.py reads are copied; the art stays out of the repository. Without it the release has no prompt-art folder and builds the game with the game's own prompts only. --pgo names the execution profile of a play session (docs/building.md, "Profile-guided build"). It is derived from the game's code and stays out of the repository; its record must name this checkout's recomp/, or the release stops (tools/pgo.py fitting). Without it the release builds the game as before, the lifted code at -O1. The profile is part of NX2navi-app-.zip, so an update brings it and the launcher's next build of the game compiles with it. --publish uploads the four files as GitHub release v of the --feed repository with the gh CLI (a draft first is wiser: add --draft). check_content runs over everything before it is packed: the release holds no game data. """ import argparse import hashlib import json import os import shutil import subprocess import sys import time import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / 'tools')) import build_shaders # noqa: E402 import check_content # noqa: E402 import dlss_pin # noqa: E402 import pgo # noqa: E402 import prompt_art # noqa: E402 import streamline_pin # noqa: E402 from launcher import toolchain # noqa: E402 from launcher.paths import Layout # noqa: E402 MAIN_ROOT = (ROOT / 'build').resolve().parent # build/ and toolchain/ are junctions in a worktree DL = MAIN_ROOT / 'toolchain' / 'launcher-dl' WORK = MAIN_ROOT / 'toolchain' / 'launcher-work' PY_VERSION = '3.14.9' PINS = { 'python-embed': (f'https://www.python.org/ftp/python/{PY_VERSION}/python-{PY_VERSION}-embed-amd64.zip', '3c01d7cfc666f746a6f064f60fb2156ab0ec464d3d738ab796371a46a9bb24a2'), # the installer's own Tcl/Tk and tkinter component, published beside it 'tcltk': (f'https://www.python.org/ftp/python/{PY_VERSION}/amd64/tcltk.msi', 'ac87609db3f715404dd41a989c2f1706ea1512f3d86217f938d11c63af8eb505'), } TOOLS = ('make_game.py', 'nup.py', 'romfs_index.py', 'check_content.py', 'hostmem.py', 'prepare_shaders.py', 'shader_census.py', 'shader_list.py', 'pipeline_list.py', 'zstd_compat.py', 'pad_prompts.py', 'prompt_art.py', 'button_icons.py', 'romfs_check.py', 'pack_shader_cache.py', 'mod_files.py', 'cheats.py', 'exefs_patches.py') TOP = ('LICENSE', 'LICENSE-ADDITIONS', 'THIRD_PARTY_NOTICES.md', 'nx2navi.ini.example') SKIP_DIRS = {'__pycache__', '.pytest_cache', 'native'} SKIP_SUFFIXES = {'.o', '.obj', '.pyc', '.exe', '.dll', '.a', '.lib'} TCLTK_FILES = ('_tkinter.pyd', 'tcl90.dll', 'tcl9tk90.dll', 'zlib1.dll', 'libtommath.dll') DEFAULT_FEED = 'tangerineturtle1/NX2navi' def say(msg): print(msg, flush=True) def sha256_file(path): h = hashlib.sha256() with open(path, 'rb') as f: while b := f.read(1 << 20): h.update(b) return h.hexdigest() def fetch(name): url, want = PINS[name] dest = DL / url.rsplit('/', 1)[1] if dest.is_file() and sha256_file(dest) == want: return dest from launcher import net say(f'download {url}') net.download(url, dest, sha256=want) return dest def run(cmd, **kw): say(' ' + ' '.join(str(c) for c in cmd)) subprocess.run([str(c) for c in cmd], check=True, **kw) def copy_tree(src, dst, skip_dirs=SKIP_DIRS): for p in sorted(src.rglob('*')): rel = p.relative_to(src) if any(part in skip_dirs for part in rel.parts) or p.is_dir() or p.suffix.lower() in SKIP_SUFFIXES: continue (dst / rel).parent.mkdir(parents=True, exist_ok=True) shutil.copy2(p, dst / rel) def version(): v = (ROOT / 'VERSION').read_text(encoding='utf-8').strip() r = subprocess.run(['git', '-C', str(ROOT), 'rev-parse', '--short', 'HEAD'], capture_output=True, text=True) return v, f'{v}+{r.stdout.strip()}' if r.returncode == 0 and r.stdout.strip() else v # ---------------------------------------------------------------- python/ def tcltk_files(): """The Tcl/Tk files of the pinned msi, unpacked into WORK/tcltk once. The stamp names the msi they came from and is written last: an unpack from an older pin, or one stopped after _tkinter.pyd was written, is done again.""" dest = WORK / 'tcltk' msi = fetch('tcltk') stamp, want = dest / '.unpacked-from', f'{msi.name} {msi.stat().st_size}' if (dest / 'DLLs' / '_tkinter.pyd').is_file() and stamp.is_file() and stamp.read_text() == want: return dest shutil.rmtree(dest, ignore_errors=True) dest.mkdir(parents=True) if os.name == 'nt': # an administrative image: copies the files, installs nothing run(['msiexec', '/a', msi, '/qn', f'TARGETDIR={dest}']) else: run(['msiextract', '-C', dest, msi], stdout=subprocess.DEVNULL) if not (dest / 'DLLs' / '_tkinter.pyd').is_file(): sys.exit(f'{dest}: no _tkinter.pyd in {msi.name}') stamp.write_text(want) return dest def runtime_id(): req = (ROOT / 'tools' / 'requirements-launcher.txt').read_bytes() return f'py{PY_VERSION}-{hashlib.sha256(req + PINS["tcltk"][1].encode()).hexdigest()[:8]}' def build_python(py): with zipfile.ZipFile(fetch('python-embed')) as z: z.extractall(py) # The embeddable Python's sys.path is exactly this list; ../pydeps holds zbic (launcher/toolchain.py). (py / 'python314._pth').write_text('python314.zip\n.\nLib/site-packages\n../pydeps\n', encoding='utf-8') env = dict(os.environ, UV_CACHE_DIR=str(MAIN_ROOT / 'toolchain' / 'uv-cache')) run(['uv', 'pip', 'install', '--target', py / 'Lib' / 'site-packages', '--python', sys.executable, '--python-platform', 'windows', '--python-version', '3.14', '--only-binary', ':all:', '-q', '-r', ROOT / 'tools' / 'requirements-launcher.txt'], env=env) shutil.rmtree(py / 'Lib' / 'site-packages' / 'bin', ignore_errors=True) tk = tcltk_files() for dll in TCLTK_FILES: if (tk / 'DLLs' / dll).is_file(): shutil.copy2(tk / 'DLLs' / dll, py / dll) copy_tree(tk / 'Lib' / 'tkinter', py / 'tkinter', skip_dirs={'__pycache__', 'test'}) copy_tree(tk / 'tcl', py / 'tcl', skip_dirs={'__pycache__'}) (py / 'runtime-id.txt').write_text(runtime_id() + '\n', encoding='utf-8') # ---------------------------------------------------------------- app// def check_xlate(xlate): """A release carries the translator of its own sources. 0.10.0 and 0.10.1 shipped the 0.9.2 translator (identity 4d051299) after tools/xlate/Program.cs had raised the texture pool to 40960 slots, so their translations still clamped every pool handle to slot 32767 and the ground went see-through after some area loads (issues #569 and #824). A missing translator, or one whose nx2navi_xlate.id differs from this checkout's sources (build_xlate.check_built), stops the build; main runs this before anything is resolved, downloaded or deleted. On Windows the exe itself is asked too, as the .id says nothing of the exe beside it: a translator that declares the texture pool with another length than its sources, or does not run, stops the build (xlate_smoke.pool_check).""" import build_xlate reason = build_xlate.check_built(xlate, 'nx2navi_xlate.exe') if reason: sys.exit(reason) if os.name == 'nt': import xlate_smoke reason = xlate_smoke.pool_check(xlate / 'nx2navi_xlate.exe') if reason: sys.exit(f'{xlate / "nx2navi_xlate.exe"}: {reason}') have = (xlate / 'nx2navi_xlate.id').read_text(encoding='ascii').strip() print(f'translator identity {have[:16]}: built from this checkout\'s sources') def check_dlssg(dll): """A release carries an nx2navi_dlssg.dll that exports every function runtime/dlssg/nx2_sl.h declares. The runtime takes a missing optional export (nx2sl_reflex2, nx2sl_frames_max) for an older DLL and leaves that feature out with one log line, so a DLL built from older sources would ship without an error: with the DLL of 0.8.0, which lacks nx2sl_reflex2, Reflex would never hold the frame limit. Such a DLL, or a missing one, stops the build (build_dlssg.check); main runs this before anything is resolved, downloaded or deleted.""" import build_dlssg build_dlssg.check(dll) print(f'{dll}: exports the {len(build_dlssg.declared_exports())} functions runtime/dlssg/nx2_sl.h declares') def build_app(app, ver, xlate, feed_repo, lock, ffx=None, dlss=None, art=None, dlssg=None, pipelines=None, ffx4=None, pgo_profile=None): # The runtime's own shaders are source in the repository and SPIR-V headers in a release: a player's PC compiles # runtime/ (make_game.py) and has no shader compiler. Build the missing and the stale ones before the copy. build_shaders.build(ROOT / 'runtime') copy_tree(ROOT / 'launcher', app / 'launcher') for d in ('runtime', 'recomp'): copy_tree(ROOT / d, app / d) (app / 'tools').mkdir(parents=True) for t in TOOLS: shutil.copy2(ROOT / 'tools' / t, app / 'tools' / t) for t in TOP: if (ROOT / t).is_file(): shutil.copy2(ROOT / t, app / t) (app / 'VERSION').write_text(ver + '\n', encoding='utf-8') (app / 'bin').mkdir() for n in ('nx2navi_xlate.exe', 'nx2navi_xlate.id'): # main checked them first (check_xlate) shutil.copy2(xlate / n, app / 'bin' / n) if ffx: # FSR 3.1 loads it from beside the game (runtime/vk_ffx.cpp); make_game.py copies it there shutil.copy2(ffx, app / 'bin' / 'amd_fidelityfx_vk.dll') if ffx4: # FSR 4 loads them from beside the game (runtime/vk_ffx4.cpp); make_game.py copies them there for n in ('amd_fidelityfx_loader_vk.dll', 'amd_fidelityfx_upscaler_vk.dll'): shutil.copy2(ffx4 / n, app / 'bin' / n) if (ffx4 / 'amd_fidelityfx_framegeneration_vk.dll').is_file(): # its frame generation (runtime/vk_fg4.cpp) shutil.copy2(ffx4 / 'amd_fidelityfx_framegeneration_vk.dll', app / 'bin' / 'amd_fidelityfx_framegeneration_vk.dll') if dlss: # DLSS: NGX loads it from beside the game (runtime/vk_dlaa.c); make_game.py copies it there dlss_pin.place(dlss, app / 'bin') if dlssg: # DLSS Frame Generation (runtime/vk_dlssg.c): make_game.py place_dlssg copies it beside the game streamline_pin.place(dlssg[0], dlssg[1], app / 'bin' / 'dlssg') if art: # make_game.py draws the button prompts from it (its prompts step) n, size = prompt_art.copy_art(art, app / prompt_art.FOLDER) say(f'button art: {n} pictures, {size / 1e6:.1f} MB') if pipelines: # prepare_shaders.py reads it there (BUNDLED_PIPELINES) (app / 'tools' / 'data').mkdir(parents=True, exist_ok=True) shutil.copy2(pipelines, app / 'tools' / 'data' / 'pipeline_list.bin') say(f'pipeline list: {pipelines.stat().st_size / 1e6:.1f} MB') if pgo_profile: # make_game.py compiles with it (pgo_file); here before the prebuilt runtime is built with it placed = pgo.place(pgo_profile, app / 'data') say(f'execution profile: {placed.stat().st_size / 1e6:.1f} MB') else: say('execution profile: none (--pgo): the game\'s code is built without one, at -O1') (app / 'toolchain.lock.json').write_text(json.dumps(lock, indent=1), encoding='utf-8') (app / 'feed.json').write_text(json.dumps({'type': 'github', 'repo': feed_repo}, indent=1), encoding='utf-8') # ---------------------------------------------------------------- the native programs def native_compiler(lock): """(cc argv prefix, windres argv prefix) for x86_64 Windows: MSYS2's own clang on Windows, else the host's clang against the same MSYS2 packages as a sysroot.""" lay = Layout(root=MAIN_ROOT / 'toolchain' / 'launcher-msys') if not toolchain.is_current(lay, lock): say('MSYS2 packages for the native programs') toolchain.install(lay, lock, log=say) sysroot = lay.msys if os.name == 'nt': return [str(sysroot / 'bin' / 'clang.exe')], [str(sysroot / 'bin' / 'windres.exe')] for tool in ('clang', 'ld.lld'): if not shutil.which(tool): sys.exit(f'{tool} is needed to cross-compile the launcher programs') windres = shutil.which('llvm-windres') or shutil.which('llvm-windres-18') or '/usr/lib/llvm-18/bin/llvm-windres' if not Path(windres).exists(): sys.exit('llvm-windres is needed to compile the launcher programs\' resources') return (['clang', '--target=x86_64-w64-mingw32', f'--sysroot={sysroot}', '-fuse-ld=lld'], [windres, '--target=x86_64-w64-mingw32']) def build_runtime_lib(folder, ver, out, wine): """app//lib/libnx2navirt.a: the runtime compiled once here with the release's MSYS2 clang, so players compile only the few runtime files that include per-install headers (tools/make_game.py --make-runtime-lib). It needs MSYS2's own clang.exe: native on Windows, through Wine with --wine elsewhere; without either players compile runtime/ themselves.""" msys = Layout(root=MAIN_ROOT / 'toolchain' / 'launcher-msys').msys app = folder / 'app' / ver args = ['--make-runtime-lib', app / 'lib', '--obj', out / 'native' / 'runtime-obj', '--msys', msys] if os.name == 'nt': run([sys.executable, app / 'tools' / 'make_game.py', *args]) elif wine: def win(p): return 'Z:' + str(Path(p).resolve()).replace('/', '\\') env = dict(os.environ, WINEDEBUG='-all', NX2NAVI_AR=win(msys / 'bin' / 'ar.exe')) run(['wine', folder / 'python' / 'python.exe', win(app / 'tools' / 'make_game.py'), *[win(a) if isinstance(a, Path) else a for a in args]], env=env) else: say('note: no prebuilt runtime (it is built on Windows, or with --wine): players compile runtime/ themselves') return if not (app / 'lib' / 'libnx2navirt.a').is_file(): sys.exit('the prebuilt runtime was not written') # The release's compiler is the one players build with (toolchain.lock.json): a profile it left out of the # runtime (make_game.py pgo_profile says why in the lines above) their builds would leave out too built = json.loads((app / 'lib' / 'runtime-lib.json').read_text(encoding='utf-8')) if (app / 'data' / 'pgo-v111.profdata').is_file() and not built.get('pgo'): sys.exit('the prebuilt runtime was compiled without the release\'s execution profile (the build above says ' 'why): the players\' builds would leave the profile out as well') def build_native(work, cc, windres): from launcher.ui import art work.mkdir(parents=True, exist_ok=True) (work / 'nx2navi.ico').write_bytes(art.navi_ico()) shutil.copy2(ROOT / 'launcher' / 'native' / 'app.manifest', work / 'app.manifest') shutil.copy2(ROOT / 'launcher' / 'native' / 'res.rc', work / 'res.rc') res = (work / 'res.o').resolve() # MSYS2's GNU windres runs its preprocessor through cmd.exe and, left to find it, names the gcc beside it by full # path, which cmd.exe splits at a space once the .rc's path holds one too. So gcc by bare name, found on PATH (the # toolchain's own bin first; no MSYS2 shell sets it), and the .rc by its name in the work folder, where windres runs. # llvm-windres (Linux) starts its preprocessor without a shell. pp = [] if 'llvm' in Path(windres[0]).name else [ '--preprocessor=gcc', '--preprocessor-arg=-E', '--preprocessor-arg=-xc', '--preprocessor-arg=-DRC_INVOKED'] env = dict(os.environ, PATH=str(Path(windres[0]).parent) + os.pathsep + os.environ.get('PATH', '')) run([*windres, *pp, '-I', '.', 'res.rc', '-O', 'coff', '-o', res], env=env, cwd=work) flags = ['-municode', '-mwindows', '-O2', '-Wall', '-static', '-s'] stub = work / 'NX2navi.exe' setup = work / 'setup-stub.exe' run([*cc, *flags, ROOT / 'launcher' / 'native' / 'nx2navi.c', res, '-o', stub, '-lshell32', '-lole32', '-luuid']) run([*cc, *flags, ROOT / 'launcher' / 'native' / 'setup.c', res, '-o', setup, '-l:libz.a', '-lshell32', '-lole32', '-luuid', '-ladvapi32']) return stub, setup # ---------------------------------------------------------------- packing def zip_dir(src, dest, prefix=''): with zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as z: for p in sorted(src.rglob('*')): if p.is_file(): z.write(p, (Path(prefix) / p.relative_to(src)).as_posix()) return dest def make_setup(stub, payload, dest, ver): """setup-stub.exe + the payload zip + the 48-byte trailer launcher/native/setup.c reads.""" data = stub.read_bytes() off = len(data) z = payload.read_bytes() trailer = b'NX2NSET1' + off.to_bytes(8, 'little') + len(z).to_bytes(8, 'little') + \ ver.encode('utf-8')[:24].ljust(24, b'\0') dest.write_bytes(data + z + trailer) return dest def check_folder(folder): bad = [f'{p.relative_to(folder)}: {m}' for p in sorted(folder.rglob('*')) if p.is_file() for m in check_content.problems_for(p, allow_large=True)] if bad: sys.exit('check_content refuses these release files:\n ' + '\n '.join(bad[:20])) def entry(path): return {'name': path.name, 'size': path.stat().st_size, 'sha256': sha256_file(path)} def main(): ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument('--xlate', type=Path, required=True, help='folder with nx2navi_xlate.exe and nx2navi_xlate.id') ap.add_argument('--ffx', type=Path, help="AMD's signed amd_fidelityfx_vk.dll (FidelityFX SDK 1.1.4, MIT) for FSR 3.1") ap.add_argument('--ffx4', type=Path, help="the folder with AMD's signed amd_fidelityfx_loader_vk.dll and " 'amd_fidelityfx_upscaler_vk.dll (FSR SDK 2.4.0, MIT) for FSR 4, and its ' 'amd_fidelityfx_framegeneration_vk.dll when it is there') ap.add_argument('--dlss', type=Path, help=f"NVIDIA's nvngx_dlss.dll (DLSS {dlss_pin.VERSION}) from disk instead of " "downloading it from NVIDIA's DLSS SDK repository") ap.add_argument('--no-dlss', action='store_true', help='leave NVIDIA DLSS out of the release') ap.add_argument('--dlssg', type=Path, metavar='DLL', help='nx2navi_dlssg.dll (tools/build_dlssg.py) for DLSS Frame Generation') ap.add_argument('--streamline', type=Path, metavar='SDK', help=f'the Streamline {streamline_pin.VERSION} SDK folder whose bin/x64 gives the Streamline and ' 'nvngx_dlssg.dll files, instead of downloading the SDK release with --dlssg') ap.add_argument('--prompt-art', type=Path, nargs='+', metavar='FOLDER', help='the button art: folders holding XGamepad, P5Gamepad and Keyboard_Mouse, or those folders') ap.add_argument('--pipeline-list', type=Path, metavar='FILE', help='the shared pipeline list to carry (tools/pipeline_list.py merge writes it; it is derived from game data and stays out of the repository)') ap.add_argument('--pgo', type=Path, metavar='FILE', help='the execution profile to carry, with its record beside it (tools/pgo.py merge writes ' 'build/pgo/pgo-v111.profdata and .json; derived from the game\'s code, they stay out of the ' 'repository)') ap.add_argument('--out', type=Path, default=MAIN_ROOT / 'build' / 'launcher-release') ap.add_argument('--feed', default=DEFAULT_FEED, help='the GitHub repository the launcher looks for releases in') ap.add_argument('--notes', type=Path, help='release notes (markdown) for the manifest and the GitHub release') ap.add_argument('--lock', type=Path, help='a toolchain.lock.json to reuse instead of resolving today\'s packages') ap.add_argument('--version', help='the release version instead of VERSION\'s (test releases)') ap.add_argument('--wine', action='store_true', help='on Linux: build the prebuilt runtime with MSYS2\'s clang ' 'through Wine') ap.add_argument('--publish', action='store_true', help='upload as GitHub release v with gh') ap.add_argument('--draft', action='store_true', help='with --publish: as a draft') a = ap.parse_args() if sys.version_info < (3, 14): sys.exit('Python 3.14 or later is needed (compression.zstd for the MSYS2 packages)') ver, build = version() if a.version: ver, build = a.version, a.version + build[len(ver):] if a.prompt_art: prompt_art.art_files(a.prompt_art) # the art is all there before anything is downloaded or built if a.pgo: pgo.fitting(a.pgo) # the profile fits this checkout's recomp/ before anything is downloaded or built check_xlate(a.xlate) # the translator is this checkout's before anything is downloaded, built or deleted if a.dlssg: check_dlssg(a.dlssg) # and so is nx2navi_dlssg.dll out = a.out shutil.rmtree(out, ignore_errors=True) folder = out / 'NX2navi' t0 = time.time() lock = (json.loads(a.lock.read_text(encoding='utf-8')) if a.lock else toolchain.make_lock(fast_repo=toolchain.FAST_REPO)) say(f'release {build}: toolchain lock with {len(lock["packages"])} packages' + (f' (CLANG64 clang left out: {lock["fast_skipped"]})' if lock.get('fast_skipped') else '')) dlss = None if a.no_dlss else dlss_pin.resolve(a.dlss, DL) say(f'NVIDIA DLSS {dlss_pin.VERSION}: {dlss}' if dlss else 'NVIDIA DLSS left out (--no-dlss)') if a.streamline and not a.dlssg: sys.exit('--streamline goes with --dlssg') dlssg = (a.dlssg, streamline_pin.resolve(a.streamline, DL)) if a.dlssg else None say(f'DLSS Frame Generation: {a.dlssg}, Streamline {streamline_pin.VERSION}: {dlssg[1]}' if dlssg else 'DLSS Frame Generation left out (no --dlssg)') if a.pipeline_list: import pipeline_list if pipeline_list.unpack(a.pipeline_list.read_bytes()) is None: sys.exit(f'{a.pipeline_list} is not a pipeline list (tools/pipeline_list.py merge writes one)') build_app(folder / 'app' / ver, ver, a.xlate, a.feed, lock, a.ffx, dlss, a.prompt_art, dlssg, a.pipeline_list, a.ffx4, a.pgo) (folder / 'app' / 'current.txt').write_text(ver + '\n', encoding='utf-8') build_python(folder / 'python') cc, windres = native_compiler(lock) build_runtime_lib(folder, ver, out, a.wine) stub, setup_stub = build_native(out / 'native', cc, windres) shutil.copy2(stub, folder / 'NX2navi.exe') check_folder(folder) app_zip = zip_dir(folder / 'app' / ver, out / f'NX2navi-app-{ver}.zip') rt_zip = zip_dir(folder / 'python', out / f'NX2navi-runtime-{runtime_id()}.zip') payload = zip_dir(folder, out / 'native' / 'payload.zip') setup = make_setup(setup_stub, payload, out / f'NX2navi-Setup-{ver}.exe', ver) notes = a.notes.read_text(encoding='utf-8') if a.notes else '' manifest = {'format': 1, 'version': ver, 'build': build, 'notes': notes, 'app': entry(app_zip), 'runtime': {'id': runtime_id(), **entry(rt_zip)}, 'toolchain': toolchain.lock_digest(lock), 'setup': entry(setup), 'made': time.strftime('%Y-%m-%d %H:%M')} man = out / 'NX2navi-update.json' man.write_text(json.dumps(manifest, indent=1), encoding='utf-8') for p in (setup, app_zip, rt_zip, man): say(f'{p.name}: {p.stat().st_size / 1e6:.1f} MB') say(f'release folder {out} in {time.time() - t0:.0f} s') if a.publish: cmd = ['gh', 'release', 'create', f'v{ver}', setup, app_zip, rt_zip, man, '--repo', a.feed, '--title', f'NX2navi {ver}'] cmd += ['--notes-file', a.notes] if a.notes else ['--notes', f'NX2navi {ver}'] if a.draft: cmd.append('--draft') run(cmd) if __name__ == '__main__': main()