Files
NX2navi/tools/vulkan_pin.py
chrg6 61cd4784f8 Build the Linux kit against pinned Vulkan headers and without #embed, and try the compiler before the long steps.
A player's AppImage build of 0.7.1 stopped in its compile step after eight minutes of RomFS work: four
files did not compile, because the kit builds with the system's compiler and headers and theirs were
older than the runtime needs. vk_pipeline.c and vk_device.c name VK_KHR_pipeline_binary,
VK_KHR_dynamic_rendering_local_read and VK_KHR_shader_maximal_reconvergence, which their Vulkan headers
did not declare, and ui/menu.cpp takes its font in with #embed, which clang has from version 19.

- Vulkan headers: tools/vulkan_pin.py holds the release the other builds use (1.4.363, the archive and
  hash tools/make_release_linux.py already pinned, which now reads them from there). The AppImage carries
  its C headers in toolchain/vulkan-headers and builds against them whatever the system has; a kit or a
  source checkout uses the system's when they are at least that release and fetches the pinned ones
  otherwise (build_game.py vulkan_include). Their folder goes first on the include path of the runtime's
  sources only, so the lifted code's objects keep their keys, and a runtime object compiled before
  against the system's headers is taken over instead of compiled again. AppRun no longer asks for the
  system's Vulkan headers.
- #embed: tools/build_shaders.py also writes the font's bytes as runtime/ui/roboto_ttf.h (ignored by git,
  carried by a kit), and menu.cpp includes that where the compiler has no __has_embed. With #embed
  nothing changes.
- Before its long steps a Linux build that will compile now checks every runtime source that needs no
  header of the install with the compile step's own command (clang -fsyntax-only) and links an empty
  program against the game's libraries (make_game.py preflight): an old compiler, an old header or a
  missing development package stops it in the first seconds with the compiler's own errors. A failed
  compile names every file that failed, not only the three whose errors it prints.

Checked on Windows: tests/launcher_test.py passes with four new tests (117); the header of bytes and
#embed give the same 162,588 bytes, and both branches of menu.cpp compile; the early check passes on the
Windows file set with clang 23 and stops on files made to fail; the 34 local headers of release 363 are
the upstream tag's files by git blob id. Not run: anything on Linux, the pinned archive's download, a
compiler older than 23.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 12:42:17 -05:00

110 lines
5.4 KiB
Python

"""The Vulkan headers the runtime is compiled against where the system's are older.
The runtime names structures and constants of Vulkan extensions as they come out (runtime/vk_device.c asks for
VK_KHR_pipeline_binary, VK_KHR_dynamic_rendering_local_read and VK_KHR_shader_maximal_reconvergence when the driver
has them), so it needs headers at least as new as the ones it was written against. On Windows MSYS2 supplies them and
the Linux release (tools/make_release_linux.py) carries them in runtime/deps. A build with the system's own compiler
and headers (tools/make_appimage.py's kit, a source checkout) met distributions whose packaged headers are years
older: every file that names a newer extension then fails to compile. Such a build takes the headers from here
instead: the release the others use, from Khronos's Vulkan-Headers repository, checked against its SHA-256.
Only the C headers are kept (include/vulkan/*.h and include/vk_video/*.h, which vulkan_core.h includes) with the
repository's licence note. The headers are Apache-2.0 OR MIT (THIRD_PARTY_NOTICES.md). Standard library only: the
build kit's Python runs this.
python tools/vulkan_pin.py [--dest toolchain/vulkan-headers] fetch and unpack the pinned headers
"""
import argparse
import hashlib
import io
import re
import shutil
import sys
import tarfile
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
VERSION = '1.4.363'
HEADER_VERSION = 363 # VK_HEADER_VERSION of that release; the numbers only grow, across 1.3 and 1.4 alike
# (address, SHA-256): the archive GitHub makes of the release's tag
SOURCE = (f'https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/v{VERSION}.tar.gz',
'cbaf687d3c59b9666fe080e7f8396b6b4f4d344a768ee6b337c59852f4526b68')
TOP = f'Vulkan-Headers-{VERSION}/' # the archive's top folder
FOLDER = Path('toolchain') / 'vulkan-headers' # below a checkout's or a kit's root: include/ and LICENSE.md
STAMP = 'pinned.txt' # in the folder: the release it holds, written last
KEEP = re.compile(r'include/(vulkan|vk_video)/[A-Za-z0-9_]+\.h|LICENSE\.md')
def have(folder):
"""True when folder holds the pinned headers, complete (the stamp is written after the last file)."""
try:
return (folder / STAMP).read_text(encoding='ascii').strip() == VERSION and \
(folder / 'include' / 'vulkan' / 'vulkan_core.h').is_file()
except OSError:
return False
def unpack(archive, folder):
"""The C headers and the licence note of the release's archive (bytes) into folder, replacing what is there."""
shutil.rmtree(folder, ignore_errors=True)
n = 0
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
for m in tar:
rel = m.name[len(TOP):] if m.name.startswith(TOP) else ''
if not m.isfile() or not KEEP.fullmatch(rel): # a name the pattern takes has no '..' and no other folder
continue
dst = folder / rel
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_bytes(tar.extractfile(m).read())
n += 1
if not (folder / 'include' / 'vulkan' / 'vulkan_core.h').is_file() or not (folder / 'LICENSE.md').is_file():
shutil.rmtree(folder, ignore_errors=True)
sys.exit(f'vulkan_pin: the archive of Vulkan-Headers {VERSION} lacks vulkan_core.h or LICENSE.md')
(folder / STAMP).write_text(VERSION + '\n', encoding='ascii')
return n
def resolve(folder=None, archive=None):
"""The folder holding the pinned headers (default <root>/toolchain/vulkan-headers), fetched and unpacked there
when it does not hold them yet. archive: the release's archive on disk instead of the download; it must be the
pinned file either way. Returns the folder; its include/ goes on the include path."""
folder = Path(folder) if folder else ROOT / FOLDER
if have(folder):
return folder
url, want = SOURCE
if archive:
data = Path(archive).read_bytes()
else:
print(f'vulkan_pin: fetching the Vulkan headers {VERSION} into {folder}', flush=True)
try:
data = urllib.request.urlopen(url, timeout=120).read()
except OSError as e:
sys.exit(f'vulkan_pin: fetching {url} failed ({e}). Install Vulkan headers {VERSION} or newer, or put the '
f'archive on disk and run tools/vulkan_pin.py --archive <file>')
if hashlib.sha256(data).hexdigest() != want:
sys.exit(f'vulkan_pin: {archive or url} is not the pinned archive of Vulkan-Headers {VERSION} (SHA-256 differs)')
unpack(data, folder)
return folder
def header_version(text):
"""VK_HEADER_VERSION as vulkan_core.h defines it in text, 0 when it does not."""
m = re.search(r'^[ \t]*#[ \t]*define[ \t]+VK_HEADER_VERSION[ \t]+(\d+)', text, re.M)
return int(m.group(1)) if m else 0
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--dest', type=Path, default=ROOT / FOLDER, help='where the headers go (default toolchain/vulkan-headers)')
ap.add_argument('--archive', type=Path, help=f'the archive of Vulkan-Headers {VERSION} on disk instead of the download')
a = ap.parse_args()
folder = resolve(a.dest, a.archive)
n = sum(1 for _ in (folder / 'include').rglob('*.h'))
print(f'{folder}: Vulkan headers {VERSION}, {n} files')
return 0
if __name__ == '__main__':
sys.exit(main())