Build Ocarina natively on Linux with tools/make_game.py, and pack the kit as an AppImage.

make_game.py only knew MSYS2. On Linux it now does what tools/build_game.py's Linux path does:
- clang, clang++ and lld from PATH (checked at start), the system's headers and libraries, vk_mem_alloc.h from the
  system or the pinned copy build_game.py fetches into toolchain/vma;
- the runtime's *_sdl.* files and platform/linux.c in place of the Win32 ones; no shim.c (ui/menu_sdl.cpp defines
  menu_sdl_event), no windres, no DLLs to copy;
- -fdeclspec for C++, as third_party/ffx_api/ffx_api.h declares __declspec(dllexport) unconditionally;
- build_game.py's Linux link flags (-no-pie, noexecstack, rpath $ORIGIN, libstdc++ static) and libraries, plus
  dav1d (the AV1 movies), libatomic and libunwind (the previous commit's plat_frame_capture);
- ocarina and nx2navi_xlate without .exe; the translator from the kit's bin/ or a checkout's build/xlate.
Windows is unchanged: every difference is under os.name.

tools/make_appimage.py packs the kit (tools, recomp, runtime, the built translator, VMA) with a relocatable CPython 3.14
and numpy, lz4, zstandard and fonttools into NX2navi-Linux-<version>-x86_64.AppImage. Its AppRun checks for the
compiler and headers and names the packages for Arch, Fedora and Debian/Ubuntu, installs zbic (GPL-2.0, not bundled)
with pip into ~/.local/share/nx2navi/pydeps on the first run, picks --jobs from the memory, builds and writes play.sh
beside the game. README-BuildKit.md has a Linux section.

Tested on CachyOS (kernel 7.3, clang 23, RTX 3080, NVIDIA 620.70.06, 16 GB): make_game.py --nup ... --jobs 4 builds
ocarina in 9.9 minutes from a 1.1.1 dump, which passes --probe-cfg; a build with the same sources and flags
(compiled by hand before this script could) starts, loads a save and plays. With one compile per thread on 16 GB
the build filled memory and swap and the kernel stopped desktop processes, hence the --jobs advice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
loteran
2026-11-08 14:48:31 +01:00
committed by chrg6
parent 844940c6fb
commit f3daf7e2b4
3 changed files with 307 additions and 29 deletions

View File

@@ -60,6 +60,46 @@ meanwhile. If it stops, run the same command again: finished steps are skipped.
When it says `finished`, start `C:\Games\Ocarina\ocarina.exe`. You can delete `C:\Games\Ocarina\build-cache` (about
5 GB) afterwards.
## Linux
The same command builds a native Linux `ocarina` on an x86-64 Linux 6.7 or later (the write watch needs
`PAGEMAP_SCAN`) with a Vulkan 1.3 driver that has `VK_EXT_external_memory_host` and `VK_EXT_mesh_shader` (NVIDIA,
RADV). It takes clang, clang++ and lld from `PATH` and links against the system's libraries; `--msys` is not used.
1. Packages, on Arch and its derivatives (other distributions: the same libraries and their `-dev` headers):
```sh
sudo pacman -S --needed clang lld sdl3 opus libvpx dav1d zstd libunwind vulkan-headers vulkan-icd-loader \
python fonttools python-numpy python-lz4 python-zstandard dotnet-sdk
```
Python 3.14 or later (the RomFS step and the PlayStation prompts use its `compression.zstd`), then `zbic`, which the
kit only calls (GPL-2.0): `pip install --user zbic`, or in a virtual environment with the packages above.
Vulkan Memory Allocator is one header: the system's (AUR `vulkan-memory-allocator`), else the build fetches the
pinned 3.4.0 into `toolchain/vma/` and checks its hash.
2. The shader translator: a kit with `bin/nx2navi_xlate` has it; from a source checkout, build it once with
`python tools/build_xlate.py` (needs the .NET 10 SDK). The build copies it beside the game.
3. Build, then play:
```sh
python3 -I tools/make_game.py --nup "/path/to/your game.nup" --out ~/Games/Ocarina
~/Games/Ocarina/ocarina
```
With 16 GB of RAM, add `--jobs 4`: the compiles run beside the 26 GB RomFS image, and one clang per logical
processor can fill memory and swap until the kernel stops processes of the desktop session (about 10 minutes on a
12-thread Ryzen with 4 jobs). Do not play or prepare shaders while it builds.
On NVIDIA, start the game with `NX2NAVI_SHADOW=0` if the 3D world stays black while the HUD draws (issue #72). The
profile, logs and settings are in the `--out` folder as on Windows.
`tools/make_appimage.py` packs this kit into one `NX2navi-Linux-<version>-x86_64.AppImage` with its own Python, for
players without a source checkout: it checks for the packages above, installs zbic with pip on its first run, picks
`--jobs` from the memory, builds into `~/Games/Ocarina` (or the folder given after the dump) and writes `play.sh`
beside the game.
## Playing
- **F1** opens the settings menu. Settings are saved in `nx2navi.ini` beside the game; saves go to `profile\`.
@@ -87,5 +127,7 @@ When it says `finished`, start `C:\Games\Ocarina\ocarina.exe`. You can delete `C
| `not game version 1.1.1` | Install the 1.1.1 update and dump the game again |
| `the Python package ... is missing` | Run the `pip install` line above |
| `...\clang.exe is missing` | Run the `pacman` line above in MSYS2 UCRT64 |
| `clang is not on PATH` (Linux) | Install the packages under "Linux" |
| `nx2navi_xlate is missing` (Linux) | Build the translator with `python tools/build_xlate.py` |
| `needs ... GB free` | Free space on the `--out` disk, or build to another one |
| The game opens and closes | Update your graphics driver, then send `logs\nx2navi.log` |

187
tools/make_appimage.py Normal file
View File

@@ -0,0 +1,187 @@
"""Build NX2navi-Linux-<version>-x86_64.AppImage: the Linux build kit as one file, which builds Ocarina of Time on the
player's PC from their own dump with tools/make_game.py, as NX2navi-Setup.exe does on Windows.
python3 tools/make_appimage.py --python <standalone CPython 3.14> --appimagetool <appimagetool> [--out dist]
The AppImage holds no game files: the kit's tools/, recomp/, runtime/ and docs, the shader translator built by
tools/build_xlate.py (bin/nx2navi_xlate), the pinned Vulkan Memory Allocator header, and a relocatable CPython 3.14
(python-build-standalone, as `uv python install 3.14` puts in ~/.local/share/uv/python) with numpy, lz4, zstandard and
fonttools. zbic, the GPL package that unpacks the game's program files, is not bundled: the AppImage installs it with
pip into ~/.local/share/nx2navi/pydeps the first time it runs, as the Windows launcher downloads it.
The compiler and the libraries come from the system (README-BuildKit.md, "Linux"): the AppImage checks for clang,
clang++, lld and the headers before it starts, and names the packages for Arch, Fedora and Debian/Ubuntu.
"""
import argparse
import os
import shutil
import stat
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
KIT_DIRS = ('recomp', 'runtime')
KIT_FILES = ('VERSION', 'LICENSE', 'LICENSE-ADDITIONS', 'THIRD_PARTY_NOTICES.md', 'nx2navi.ini.example')
TOOLS_SKIP = ('vendor', 'xlate', '__pycache__') # the translator ships built (bin/); its sources stay out
PACKAGES = ('numpy==2.5.4', 'lz4==4.4.5', 'zstandard==0.25.0', 'fonttools==4.68.0')
APPRUN = r'''#!/bin/bash
# NX2navi build kit for Linux: builds Ocarina of Time (Switch 2, game 1.1.1) from the player's own dump.
HERE="$(dirname "$(readlink -f "$0")")"
PY="$HERE/usr/python/bin/python3"
KIT="$HERE/usr/kit"
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/nx2navi"
PYDEPS="$DATA/pydeps"
unset PYTHONPATH PYTHONHOME
usage() {
cat <<EOF
NX2navi $(cat "$KIT/VERSION") - build Ocarina of Time (Switch 2, game 1.1.1) natively for Linux from your own dump.
$(basename "${APPIMAGE:-$0}") <your game.nup> [game folder] build (default folder: ~/Games/Ocarina)
$(basename "${APPIMAGE:-$0}") --check <your game.nup> only check the dump's version
$(basename "${APPIMAGE:-$0}") --make-game <make_game.py options> run tools/make_game.py directly
The game folder then holds 'ocarina' and 'play.sh', which starts it. Nothing of the game is in this file:
everything the build writes comes from your dump, so keep it to yourself.
EOF
}
case "$1" in ""|-h|--help) usage; exit 0 ;; esac
missing=()
for tool in clang clang++ ld.lld; do command -v "$tool" >/dev/null || missing+=("$tool"); done
for h in SDL3/SDL.h vulkan/vulkan.h opus/opus.h vpx/vpx_decoder.h dav1d/dav1d.h zstd.h libunwind.h; do
[ -e "/usr/include/$h" ] || [ -e "/usr/local/include/$h" ] || missing+=("$h")
done
if [ ${#missing[@]} -gt 0 ]; then
cat >&2 <<EOF
Missing: ${missing[*]}
Install the compiler and libraries, then run this again:
Arch, CachyOS, Manjaro, SteamOS (developer mode):
sudo pacman -S --needed clang lld sdl3 opus libvpx dav1d zstd libunwind vulkan-headers vulkan-icd-loader
Fedora:
sudo dnf install clang lld SDL3-devel opus-devel libvpx-devel libdav1d-devel libzstd-devel libunwind-devel \\
vulkan-headers vulkan-loader-devel libstdc++-static libatomic
Debian, Ubuntu:
sudo apt install clang lld libsdl3-dev libopus-dev libvpx-dev libdav1d-dev libzstd-dev libunwind-dev libvulkan-dev
EOF
exit 1
fi
if ! PYTHONPATH="$PYDEPS" "$PY" -c 'import zbic' 2>/dev/null; then
echo "Installing zbic (GPL-2.0, unpacks the game's program files) into $PYDEPS ..."
"$PY" -m pip install --quiet --disable-pip-version-check --target "$PYDEPS" zbic || {
echo "error: pip could not install zbic (it needs the internet once)" >&2; exit 1; }
fi
# One compile per 4 GB of RAM at most: with 16 GB, one per thread beside the RomFS image fills memory and swap.
mem_gb=$(awk '/MemTotal/ {print int($2 / 1048576)}' /proc/meminfo)
jobs=$(( mem_gb / 4 )); [ "$jobs" -lt 2 ] && jobs=2; [ "$jobs" -gt "$(nproc)" ] && jobs=$(nproc)
run_make_game() {
"$PY" -I -c 'import sys; sys.path[:0] = [sys.argv[1], sys.argv[2] + "/tools"]; s = sys.argv[2] + "/tools/make_game.py"; sys.argv = [s] + sys.argv[3:]; import runpy; runpy.run_path(s, run_name="__main__")' \
"$PYDEPS" "$KIT" "$@"
}
case "$1" in
--make-game) shift; run_make_game "$@"; exit $? ;;
--check) run_make_game --nup "$2" --check; exit $? ;;
esac
NUP="$1"
OUT="${2:-$HOME/Games/Ocarina}"
[ -f "$NUP" ] || { echo "error: $NUP is not a file" >&2; usage; exit 1; }
echo "Building into $OUT with $jobs compiles at a time (${mem_gb} GB of RAM); this takes 10 to 25 minutes."
run_make_game --nup "$NUP" --out "$OUT" --jobs "$jobs" || exit $?
# play.sh: the environment the game wants on this machine, then the game
{
echo '#!/bin/bash'
echo 'cd "$(dirname "$(readlink -f "$0")")"'
if [ -e /proc/driver/nvidia/version ]; then
echo '# NVIDIA: the default shadow storage leaves the 3D world black (issue #72)'
echo 'export NX2NAVI_SHADOW="${NX2NAVI_SHADOW:-0}"'
fi
echo 'exec ./ocarina "$@"'
} > "$OUT/play.sh"
chmod +x "$OUT/play.sh"
echo
echo "Done. Start the game with: $OUT/play.sh"
'''
DESKTOP = '''[Desktop Entry]
Type=Application
Name=NX2navi
Comment=Build Ocarina of Time natively for Linux from your own dump
Exec=AppRun
Icon=nx2navi
Terminal=true
Categories=Game;
'''
def copy_tree(src, dst, skip=()):
shutil.copytree(src, dst, ignore=lambda d, names: [n for n in names if n in skip or n == '__pycache__'])
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--python', type=Path, required=True,
help='a relocatable CPython 3.14 folder (python-build-standalone, ~/.local/share/uv/python/cpython-3.14*)')
ap.add_argument('--appimagetool', type=Path, required=True, help='appimagetool-x86_64.AppImage')
ap.add_argument('--out', type=Path, default=ROOT / 'dist', help='where the AppImage goes (default dist/)')
a = ap.parse_args()
version = (ROOT / 'VERSION').read_text(encoding='utf-8').strip()
xlate = ROOT / 'build' / 'xlate'
for need in (xlate / 'nx2navi_xlate', xlate / 'nx2navi_xlate.id', ROOT / 'toolchain' / 'vma' / 'vk_mem_alloc.h',
a.python / 'bin' / 'python3'):
if not need.exists():
sys.exit(f'{need} is missing (tools/build_xlate.py builds the translator; tools/build_game.py fetches VMA)')
app = ROOT / 'build' / 'appimage' / 'NX2navi.AppDir'
shutil.rmtree(app, ignore_errors=True)
kit = app / 'usr' / 'kit'
kit.mkdir(parents=True)
for d in KIT_DIRS:
copy_tree(ROOT / d, kit / d)
copy_tree(ROOT / 'tools', kit / 'tools', TOOLS_SKIP)
for f in KIT_FILES:
shutil.copy2(ROOT / f, kit / f)
(kit / 'docs').mkdir()
shutil.copy2(ROOT / 'docs' / 'README-BuildKit.md', kit / 'docs' / 'README-BuildKit.md')
(kit / 'bin').mkdir()
for f in ('nx2navi_xlate', 'nx2navi_xlate.id'):
shutil.copy2(xlate / f, kit / 'bin' / f)
(kit / 'toolchain' / 'vma').mkdir(parents=True)
shutil.copy2(ROOT / 'toolchain' / 'vma' / 'vk_mem_alloc.h', kit / 'toolchain' / 'vma' / 'vk_mem_alloc.h')
py = app / 'usr' / 'python'
shutil.copytree(a.python, py, symlinks=True)
for marker in py.glob('lib/python3*/EXTERNALLY-MANAGED'): # uv marks its Pythons; this copy is the AppImage's own
marker.unlink()
subprocess.run([py / 'bin' / 'python3', '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
'--no-warn-script-location', *PACKAGES], check=True)
for cache in py.rglob('__pycache__'):
shutil.rmtree(cache, ignore_errors=True)
(app / 'AppRun').write_text(APPRUN, encoding='utf-8')
(app / 'AppRun').chmod(0o755)
(app / 'nx2navi.desktop').write_text(DESKTOP, encoding='utf-8')
from PIL import Image # the window icon of runtime/nx2navi.ico, as PNG
with Image.open(ROOT / 'runtime' / 'nx2navi.ico') as ico:
ico.size = max(ico.ico.sizes())
ico.convert('RGBA').save(app / 'nx2navi.png')
a.out.mkdir(parents=True, exist_ok=True)
target = a.out / f'NX2navi-Linux-{version}-x86_64.AppImage'
env = dict(os.environ, ARCH='x86_64')
subprocess.run([a.appimagetool, '--no-appstream', app, target], check=True, env=env)
target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
print(f'{target} ({target.stat().st_size / 1e6:.0f} MB)')
if __name__ == '__main__':
main()

View File

@@ -41,6 +41,11 @@ version (fast_clang: the same objects, about a fifth faster).
python tools/make_game.py --make-runtime-lib <kit>/lib --msys <ucrt64> # the release's prebuilt runtime
On Linux the same steps make <out>/ocarina with the clang, clang++ and lld on PATH and the system's libraries
(Vulkan, SDL3, Opus, libvpx, dav1d, zstd, libunwind; libstdc++ static, rpath $ORIGIN), as tools/build_game.py's Linux
build does: the runtime's *_sdl.* and platform/linux.c take the place of its *_win.*, platform/win32.c and
nx2navi.rc, and --msys is not used. README-BuildKit.md, "Linux", lists the packages.
Nothing is written outside --out (but for --make-runtime-lib DIR, which writes DIR). The game folder then holds
only what this PC built from the player's files: never
share it. Needs Python 3.10+ with numpy, lz4, zstandard and zbic (pip), with fonttools and pillow too when the kit has
@@ -101,10 +106,17 @@ LIFT_FIXES = [
' { uint64_t ea = x8 + 0x1f0ull; uint64_t v = LD64(ea); x8 = (uint64_t)(v); }\n'
' if (x8) c->tpidr = x8; /* zero slot: keep the host\'s TLS (make_game.py) */\n'),
]
LINK_LIBS = ['-lvulkan-1', '-lSDL3', '-lopus', '-lvpx', '-l:libdav1d.a', '-lzstd', '-l:libatomic.a', '-lbcrypt',
'-limm32', '-luuid', '-lole32', '-luser32', '-lshell32', '-lgdi32', '-ladvapi32', '-lwinmm']
WIN = os.name == 'nt'
if WIN:
LINK_LIBS = ['-lvulkan-1', '-lSDL3', '-lopus', '-lvpx', '-l:libdav1d.a', '-lzstd', '-l:libatomic.a', '-lbcrypt',
'-limm32', '-luuid', '-lole32', '-luser32', '-lshell32', '-lgdi32', '-ladvapi32', '-lwinmm']
else: # tools/build_game.py's Linux libraries, plus dav1d (the AV1 movies), libatomic and libunwind (platform/linux.c's
# plat_frame_capture walks the host stack with it, issue #72)
LINK_LIBS = ['-lvulkan', '-lSDL3', '-lopus', '-lvpx', '-ldav1d', '-lzstd', '-l:libstdc++.a', '-latomic', '-lunwind',
'-lm', '-lpthread', '-ldl']
SYSTEM_DLLS_SKIP = {'vulkan-1.dll'} # the graphics driver's loader, never a copy
EXE_NAME = 'ocarina.exe'
EXE_NAME = 'ocarina.exe' if WIN else 'ocarina'
XLATE_NAME = 'nx2navi_xlate.exe' if WIN else 'nx2navi_xlate'
# Headers this build writes per install (gen.py, romfs_index.py): a runtime source that includes one, directly or
# through a runtime header, is compiled on the player's PC; every other runtime source can come prebuilt.
GENERATED_HEADERS = ('layout.h', 'patches.h', 'romfs_config.h')
@@ -456,8 +468,9 @@ def modules_c(lift, out, gen):
f'const char rt_port_version[] = {json.dumps(kit_version())};']
p = gen / 'modules.c'
write_if_changed(p, '\n'.join(L) + '\n')
write_if_changed(gen / 'shim.c', '/* Defined by the SDL window path only; this build uses the Win32 one. */\n'
'void menu_sdl_event(void *e) { (void)e; }\n')
if WIN: # on Linux ui/menu_sdl.cpp defines it
write_if_changed(gen / 'shim.c', '/* Defined by the SDL window path only; this build uses the Win32 one. */\n'
'void menu_sdl_event(void *e) { (void)e; }\n')
return p
@@ -724,12 +737,13 @@ def compile_all(srcs, cmd_for, cache, jobs, env, progress=None, salt='', stop=No
def runtime_sources():
"""(source, object name) of every runtime file the Windows build compiles."""
"""(source, object name) of every runtime file this OS's build compiles: the Win32 files on Windows, the SDL
window's (*_sdl.*) and platform/linux.c elsewhere (tools/build_game.py's foreign_os)."""
rt = KIT / 'runtime'
out = []
for pat in RUNTIME_PATTERNS:
for p in sorted(rt.glob(pat)):
if p.stem.endswith('_sdl') or p.name == 'linux.c':
if (p.stem.endswith('_sdl') or p.name == 'linux.c') if WIN else (p.stem.endswith('_win') or p.name == 'win32.c'):
continue
out.append((p, 'rt_' + p.relative_to(rt).as_posix().replace('/', '_').rsplit('.', 1)[0] + '.o'))
return out
@@ -800,6 +814,8 @@ def fast_clang(msys, clang, env):
built with profile-guided optimisation, about a fifth faster on the lifted code. With UCRT64's headers
(--sysroot) it writes the same objects as UCRT64's clang, byte for byte but the COFF timestamp; it is used only
when it reports the same version and target. None otherwise."""
if not WIN:
return None
fast = Path(msys).parent / 'clang64' / 'bin' / 'clang.exe'
if not fast.is_file():
return None
@@ -818,8 +834,10 @@ def runtime_cmd(clang, clangxx, inc, level=0, fast=None, msys=None):
guest = dst.name.startswith(('main_', 'sdk_', 'subsdk0_'))
march = [f'-march=x86-64-v{level}'] if guest and level else []
cc = [str(fast), f'--sysroot={msys}'] if guest and fast else [str(clangxx if cpp else clang)]
# -fdeclspec: third_party/ffx_api/ffx_api.h declares its entry points __declspec(dllexport) unconditionally
declspec = ['-fdeclspec'] if cpp and not WIN else []
return [*cc, '-c', '-std=gnu++17' if cpp else '-std=gnu11', '-O1' if guest else '-O2',
*march, '-fno-strict-aliasing', '-w', *inc, '-o', str(dst), str(src)]
*march, '-fno-strict-aliasing', '-w', *declspec, *inc, '-o', str(dst), str(src)]
return cmd_for
@@ -923,6 +941,26 @@ def compile_resources(msys, rt, obj, env):
return rco
def toolchain(msys):
"""(clang, clang++, environment, the folder holding vk_mem_alloc.h): MSYS2 UCRT64's on Windows, the ones on PATH
elsewhere, with tools/build_game.py's VMA lookup (the system's header, else the pinned one fetched into
toolchain/vma)."""
if WIN:
return (msys / 'bin' / 'clang.exe', msys / 'bin' / 'clang++.exe',
dict(os.environ, PATH=str(msys / 'bin') + os.pathsep + os.environ.get('PATH', '')), msys / 'include' / 'vma')
from build_game import vma_include
return Path(shutil.which('clang')), Path(shutil.which('clang++')), dict(os.environ), vma_include()
def xlate_source():
"""The folder of the shader translator to copy beside the game: the kit's bin/, else a source checkout's
build/xlate/ (tools/build_xlate.py)."""
for d in (KIT / 'bin', KIT / 'build' / 'xlate'):
if (d / XLATE_NAME).is_file():
return d
fail(f'{XLATE_NAME} is missing: build it with tools/build_xlate.py')
def step_compile(out, cache_dir, msys, jobs, romfs_job=None):
"""romfs_job: the RomFS step still running beside this one (a first build). Everything that does not include
romfs_config.h compiles meanwhile; the RomFS index and the few files that include it follow once it is done."""
@@ -930,8 +968,7 @@ def step_compile(out, cache_dir, msys, jobs, romfs_job=None):
gen.mkdir(parents=True, exist_ok=True)
obj.mkdir(parents=True, exist_ok=True)
rt = KIT / 'runtime'
clang, clangxx = msys / 'bin' / 'clang.exe', msys / 'bin' / 'clang++.exe'
env = dict(os.environ, PATH=str(msys / 'bin') + os.pathsep + os.environ.get('PATH', ''))
clang, clangxx, env, vma = toolchain(msys)
if not romfs_job:
romfs_index(out, gen)
srcs = [(p, obj / f'{mod}_{p.stem}.o') for mod in ('main', 'sdk', 'subsdk0') for p in sorted((lift / mod).glob('*.c'))]
@@ -941,9 +978,10 @@ def step_compile(out, cache_dir, msys, jobs, romfs_job=None):
runtime = [(p, o) for p, o in runtime if o.name not in prebuilt[1]]
say(f' the runtime comes prebuilt; {len(runtime)} runtime files are compiled here')
srcs += runtime
srcs += [(modules_c(lift, out, gen), obj / 'modules.o'), (gen / 'shim.c', obj / 'shim.o')]
inc = ['-I', str(rt), '-I', str(lift), '-I', str(gen), '-I', str(rt / 'third_party' / 'imgui'),
'-I', str(msys / 'include' / 'vma')]
srcs.append((modules_c(lift, out, gen), obj / 'modules.o'))
if WIN:
srcs.append((gen / 'shim.c', obj / 'shim.o'))
inc = ['-I', str(rt), '-I', str(lift), '-I', str(gen), '-I', str(rt / 'third_party' / 'imgui'), '-I', str(vma)]
cid = compiler_id(clang, env)
level = cpu_level(clang, env)
say(f' the lifted code is built for x86-64-v{level}, this processor\'s level' if level else
@@ -970,7 +1008,7 @@ def step_compile(out, cache_dir, msys, jobs, romfs_job=None):
late = []
if romfs_job: # the lifted chunks include only cpu.h and simd.h: only the runtime and generated files are read
tainted = per_install_names(('romfs_config.h',))
late = [(s, o) for s, o in runtime + srcs[-2:] if per_install(s, tainted)]
late = [(s, o) for s, o in runtime + srcs[-(2 if WIN else 1):] if per_install(s, tainted)]
say(f' {len(late)} files wait for the RomFS index: {", ".join(s.name for s, _ in late)}')
early = [j for j in srcs if j not in late]
compiled, reused, bad = compile_all(early, cmd_for, cache, jobs, env, lambda d, t: progress(d, len(srcs)),
@@ -990,14 +1028,16 @@ def step_compile(out, cache_dir, msys, jobs, romfs_job=None):
print(f'--- {src}\n{err}', file=sys.stderr)
fail(f'{len(bad)} files did not compile')
rco = compile_resources(msys, rt, obj, env)
objs = [o for _, o in srcs] + [rco]
objs = [o for _, o in srcs] + ([compile_resources(msys, rt, obj, env)] if WIN else [])
exe = out / EXE_NAME
rsp = cache_dir / 'link.rsp'
rsp.write_text('\n'.join('"%s"' % o.as_posix() for o in objs), encoding='utf-8')
# lld links several times faster than GNU ld; the stack matches tools/build_game.py's build
# 16 MiB for the main thread with either linker (GNU ld takes the same flag): the default 2 MiB is too little
ld = (['-fuse-ld=lld'] if (msys / 'bin' / 'ld.lld.exe').is_file() else []) + ['-Wl,--stack,16777216']
if WIN:
ld = (['-fuse-ld=lld'] if (msys / 'bin' / 'ld.lld.exe').is_file() else []) + ['-Wl,--stack,16777216']
else: # tools/build_game.py's Linux flags; the main thread's stack is the process's (ulimit -s), not the linker's
ld = (['-fuse-ld=lld'] if shutil.which('ld.lld') else []) + ['-no-pie', '-Wl,-z,noexecstack', '-Wl,-rpath,$ORIGIN']
# the link map feeds tools/prof_report.py (symbols for an NX2NAVI_PROFILE sampling profile); both linkers write one
ld.append('-Wl,--Map=' + (cache_dir / 'ocarina.map').as_posix())
lib = ['-Wl,--whole-archive', str(prebuilt[0]), '-Wl,--no-whole-archive'] if prebuilt else []
@@ -1007,13 +1047,14 @@ def step_compile(out, cache_dir, msys, jobs, romfs_job=None):
link_key = sha256_bytes(json.dumps([LINK_LIBS, ld, cid] + stamp).encode())
state_link = cache_dir / 'link.key'
if exe.is_file() and state_link.is_file() and state_link.read_text(encoding='utf-8') == link_key:
say(' ocarina.exe is current, not linked again')
say(f' {EXE_NAME} is current, not linked again')
else:
emit('phase', step='compile', phase='link')
say(' linking')
tmp = out / (EXE_NAME + '.new')
p = subprocess.run([str(clangxx), *ld, '-o', str(tmp), '@' + str(rsp), *lib, '-L', str(msys / 'lib'),
*LINK_LIBS], capture_output=True, text=True, env=env, errors='replace')
lib_dir = ['-L', str(msys / 'lib')] if WIN else []
p = subprocess.run([str(clangxx), *ld, '-o', str(tmp), '@' + str(rsp), *lib, *lib_dir, *LINK_LIBS],
capture_output=True, text=True, env=env, errors='replace')
if p.returncode:
print(p.stderr[-4000:], file=sys.stderr)
tmp.unlink(missing_ok=True)
@@ -1025,14 +1066,16 @@ def step_compile(out, cache_dir, msys, jobs, romfs_job=None):
fail(f'{exe} is in use: close the game and try again')
state_link.write_text(link_key, encoding='utf-8')
copy_if_changed(gen / 'romfs-index.bin', out / 'romfs-index.bin')
for name in ('nx2navi_xlate.exe', 'nx2navi_xlate.id'):
copy_if_changed(KIT / 'bin' / name, out / name)
xlate = xlate_source()
for name in (XLATE_NAME, 'nx2navi_xlate.id'):
copy_if_changed(xlate / name, out / name)
if (KIT / 'bin' / 'amd_fidelityfx_vk.dll').is_file(): # FSR 3.1 (runtime/vk_ffx.cpp)
copy_if_changed(KIT / 'bin' / 'amd_fidelityfx_vk.dll', out / 'amd_fidelityfx_vk.dll')
kept = place_dlss(out) # NVIDIA DLSS (runtime/vk_dlaa.c)
kept = place_dlss(out) if WIN else None # NVIDIA DLSS (runtime/vk_dlaa.c): its library is a Windows DLL
if kept:
say(f' {kept}')
say(f' {EXE_NAME} {exe.stat().st_size / 1e6:.0f} MB; beside it: {", ".join(copy_dlls(exe, msys / "bin", out))}')
beside = copy_dlls(exe, msys / 'bin', out) if WIN else []
say(f' {EXE_NAME} {exe.stat().st_size / 1e6:.0f} MB' + (f'; beside it: {", ".join(beside)}' if beside else ''))
(out / 'profile').mkdir(exist_ok=True)
@@ -1067,7 +1110,7 @@ def step_shaders(out):
Its output is read as it comes: the counts nx2navi.exe prints become progress events, the other lines go on to
the log, and while nothing moves an event every few seconds keeps saying the step is running and for how long."""
cmd = [sys.executable, '-u', str(KIT / 'tools' / 'prepare_shaders.py'), str(out), '--romfs', str(out / 'romfs'),
'--exefs', str(out / 'exefs'), '--no-list', '--exe', 'ocarina.exe']
'--exefs', str(out / 'exefs'), '--no-list', '--exe', EXE_NAME]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding='utf-8',
errors='replace', bufsize=1)
t0 = time.monotonic()
@@ -1238,7 +1281,8 @@ def main():
help='build again from the game files the last run unpacked into --out (after a kit update)')
ap.add_argument('--out', help='the folder for the finished game')
ap.add_argument('--check', action='store_true', help='only check the game\'s version; builds nothing')
ap.add_argument('--msys', default=r'C:\msys64\ucrt64', help='MSYS2 UCRT64 folder (default C:\\msys64\\ucrt64)')
ap.add_argument('--msys', default=r'C:\msys64\ucrt64',
help='MSYS2 UCRT64 folder (default C:\\msys64\\ucrt64; Windows only: Linux takes clang and lld from PATH)')
ap.add_argument('--jobs', type=int, default=os.cpu_count() or 4, help='parallel compiles')
ap.add_argument('--events', action='store_true', help='machine-readable progress lines on stdout ("@@" + JSON)')
ap.add_argument('--make-runtime-lib', metavar='DIR', help='build the prebuilt runtime library of a release into DIR')
@@ -1270,9 +1314,14 @@ def main():
if not out:
ap.error('--out is needed to build')
msys = Path(a.msys)
for need in ('bin/clang.exe', 'bin/clang++.exe', 'bin/SDL3.dll', 'lib/libSDL3.dll.a', 'include/vma'):
if not (msys / need).exists():
fail(f'{msys / need} is missing: install MSYS2 and the packages README-BuildKit.md lists')
if WIN:
for need in ('bin/clang.exe', 'bin/clang++.exe', 'bin/SDL3.dll', 'lib/libSDL3.dll.a', 'include/vma'):
if not (msys / need).exists():
fail(f'{msys / need} is missing: install MSYS2 and the packages README-BuildKit.md lists')
else:
for tool in ('clang', 'clang++', 'ld.lld'):
if not shutil.which(tool):
fail(f'{tool} is not on PATH: install the packages README-BuildKit.md lists under "Linux"')
packages = {'numpy': 'numpy', 'lz4': 'lz4', **({} if a.rebuild else {'zbic': 'zbic'}),
**(PROMPT_PACKAGES if prompt_art_dir() else {})}
for pip_name, mod in packages.items():