OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / tools / build-windows-bundle.sh

162 lines · bash ref:
#!/usr/bin/env bash
# Assemble the standalone Windows bundle.
#
# This is the CI recipe's stage-fat-tree step, laid out flat so a tester unzips
# and runs: no content store, no launcher script, no OTA.
#
# pgh is NOT a requirement here and never was — index.js's resolvePghBin() returns
# null when it is absent and the launcher logs "pgh CLI not found, skipping OTA"
# and carries on. What is lost is only the background update, and pgh-runtime can
# supply that later without a pgh install: it takes an injected fetcher and store
# rather than reaching for the sidecar.
#
# Prerequisites, none of which this script builds (each is minutes of compiling
# and belongs to its own repo):
#   nvidia-rtx-godot   scons platform=windows target=template_release use_mingw=yes \
#                            d3d12=no winrt=no accesskit=no angle=no
#   dc-terrain         scons platform=windows target=template_release use_mingw=yes
#   hyperswarm-peer    tools/build-mingw-deps.sh, then the same scons line
#   hyper-tps          --export-release "Windows" build/windows/hypertps.exe
set -euo pipefail

# THE REPO THIS SCRIPT IS IN -- never a fixed path. CI runs it from a clean
# `git archive` checkout holding CI's own fresh export and keet-daemon; a
# hard-coded /storage/.../hyper-tps made every CI bundle package the DEV TREE
# instead: v0.1206.0 shipped a 17:27 local export (.pck 661,238,224 B, untracked
# files and pre-TCP GDScript included) and the dev tree's stale keet-daemon 1.1.8.
GAME="$(cd "$(dirname "$0")/.." && pwd)"
DCT=/storage/projects/code/godot/dc-terrain
OUT="${1:-$GAME/distribute/hypertps-win64}"

cd "$GAME"
test -f build/windows/hypertps.exe || { echo "FATAL: export first (build/windows/hypertps.exe)"; exit 1; }
test -f build/windows/hypertps.pck || { echo "FATAL: no .pck beside the exe"; exit 1; }

mkdir -p "$OUT"

echo "=== engine + extensions ==="
cp build/windows/hypertps.exe build/windows/hypertps.pck "$OUT/"
cp build/windows/*.dll "$OUT/"
# The compute shaders go BESIDE THE DLL, which is what _shader_dir resolves to
# (GetModuleHandleEx on the extension, not the exe). Without them dc_terrain
# silently CPU-falls-back to dual-contouring meshing.
mkdir -p "$OUT/shaders"
cp -rL "$DCT"/shaders/. "$OUT/shaders/"

echo "=== bare runtime ==="
# bare-runtime delivers per-platform binaries as optional dependencies, so the
# Windows interpreter is its own package rather than a prebuilds/ dir.
#
# ⛔ FATAL, not a WARN. hypertps.cmd is `bare.exe index.js`, so a bundle without
# bare.exe cannot start at all. It used to look in one agent session's scratchpad,
# which stopped existing, and only WARNed -- v0.1117.0 shipped without bare.exe
# and every Windows release since would have too (found 2026-09-29).
# The version is the one the Linux installs run (pgh's runtime/bare --version),
# so both platforms execute index.js on the same interpreter.
BARE_VER="${BARE_VER:-1.28.7}"
BARE_CACHE="${BARE_CACHE:-$HOME/.cache/hyper-tps}"
BARE_TGZ="$BARE_CACHE/bare-runtime-win32-x64-$BARE_VER.tgz"
if [ ! -f "$BARE_TGZ" ]; then
  mkdir -p "$BARE_CACHE"
  (cd "$BARE_CACHE" && npm pack "bare-runtime-win32-x64@$BARE_VER" --silent) \
    || { echo "FATAL: could not fetch bare-runtime-win32-x64@$BARE_VER"; exit 1; }
fi
tar xzf "$BARE_TGZ" -C "$OUT" --strip-components=2 package/bin/bare.exe
test -f "$OUT/bare.exe" || { echo "FATAL: bare.exe missing from the bundle"; exit 1; }
echo "  bare.exe $BARE_VER ($(stat -c%s "$OUT/bare.exe") bytes)"

echo "=== NVIDIA Streamline (DLSS / ray reconstruction) ==="
# On Windows the engine reaches DLSS ONLY through Streamline: it LoadLibrary's
# sl.interposer.dll by bare name while creating the Vulkan context
# (drivers/vulkan/rendering_context_driver_vulkan.cpp:924), and Streamline loads
# its plugins from beside the interposer (no pathsToPlugins is set). The Windows
# template has no NGX-direct path (use_ngx_dlss is linuxbsd-only), so without
# these files a bundle runs path tracing with NO ray reconstruction and logs
# "Streamline failed to initialize (sl.interposer.dll not found)" -- which is
# exactly how v0.1207.0 shipped.
#
# FATAL, not a WARN, for the same reason as bare.exe: silence is how it shipped.
#
# SL_VER MUST match the headers the engine was built against
# (nvidia-rtx-godot-v3/thirdparty/streamline/include/sl_version.h = 2.10.0) --
# slInit() is passed sl::kSDKVersion. Bump the version, the zip hash and every
# DLL hash together. NVIDIA publishes only the full SDK zip, so the zip is cached
# once; nothing is vendored into git.
SL_VER="${SL_VER:-2.10.0}"
SL_ZIP_SHA256=b0cc810c86b0335d4fd76a02d35fff53e528d2fb66c29860565130f76c9383b0
SL_ZIP="$BARE_CACHE/streamline-sdk-v$SL_VER.zip"
if [ ! -f "$SL_ZIP" ]; then
  mkdir -p "$BARE_CACHE"
  curl -sfL -o "$SL_ZIP.part" \
    "https://github.com/NVIDIA-RTX/Streamline/releases/download/v$SL_VER/streamline-sdk-v$SL_VER.zip" \
    && mv "$SL_ZIP.part" "$SL_ZIP" \
    || { echo "FATAL: could not fetch streamline-sdk-v$SL_VER.zip"; exit 1; }
fi
echo "$SL_ZIP_SHA256  $SL_ZIP" | sha256sum -c --quiet - \
  || { echo "FATAL: $SL_ZIP does not match the pinned SHA-256"; exit 1; }

# Production (bin/x64, not bin/x64/development) builds of exactly what
# streamline_context.cpp loads for a game: PCL, Reflex, DLSS-G, DLSS, DLSS-RR,
# NIS. ImGUI is development-only and loads only under a debug setting.
SL_FILES="
c2397e96e35c1e78c0b351b0d2b070c6529ccc990f4cc73f04722db24dc2acce sl.interposer.dll
be50a4c89177abb3695de92c57cc41662bfb7598ec447707b0d6efb77256d977 sl.common.dll
d6cd931f8db74978aa9159a41af288ae1480ffbeae59e64f4ea447074d861bc0 sl.pcl.dll
42c45991b8f2972d4f341d379bd9be093c408b59d01f22b338f505f476617cc5 sl.reflex.dll
2a77dc3e1c724b7eea5755be0ae7423752e79a2459fae72181a9f00e3507e5d6 NvLowLatencyVk.dll
2dbee8cb45af3de640f98ee68b4955d1fd90e772784e843f00951ac53ab908f6 sl.dlss.dll
22c4660249da5b352cde0ce4d95178c3761223dd6ac096d0b88e5c03d7a5cdd1 nvngx_dlss.dll
1a65ae1656d7aab4cb1025e14bc14a92330a3a3008a05aafae803c4fe508c260 sl.dlss_d.dll
4d53f6114edc61bd41fc293008aa818ab092bb8286830e61c98453da92d936d0 nvngx_dlssd.dll
e07fd38310805e34265929d1999f33ed5566cf30176c082ca84ef63992f6c7d7 sl.dlss_g.dll
5685c065e6c048f673d70f5dda5b23570c8570642e4e284216f59c2e85583016 nvngx_dlssg.dll
4629c778ea65cf0676583936a0da61204c035bc4e1994e7736bc07e0114e42cb sl.nis.dll
"
# NVIDIA's redistribution terms travel with the binaries.
SL_LICENSES="nvngx_dlss.license.txt reflex.license.txt nis.license.txt"
sl_names=$(echo "$SL_FILES" | awk 'NF {print "bin/x64/" $2}')
unzip -oqj "$SL_ZIP" $sl_names $(for l in $SL_LICENSES; do echo "bin/x64/$l"; done) -d "$OUT" \
  || { echo "FATAL: extracting Streamline runtime from $SL_ZIP"; exit 1; }
echo "$SL_FILES" | awk -v d="$OUT" 'NF {print $1 "  " d "/" $2}' | sha256sum -c --quiet - \
  || { echo "FATAL: a Streamline DLL in the bundle does not match its pinned SHA-256"; exit 1; }
echo "  streamline $SL_VER: $(echo "$SL_FILES" | awk 'NF' | wc -l) DLLs + $(echo $SL_LICENSES | wc -w) licenses ($(du -cb $(echo "$SL_FILES" | awk -v d="$OUT" 'NF {print d "/" $2}') | tail -1 | cut -f1) bytes)"

echo "=== app + node_modules ==="
cp index.js package.json "$OUT/"
[ -f version.txt ] && cp version.txt "$OUT/"
for d in daemon lib scripts; do [ -d "$d" ] && cp -r "$d" "$OUT/"; done
# FOREIGN PREBUILDS ARE EXCLUDED AT COPY TIME, not deleted afterwards. Every one
# of the 73 packages that ships prebuilds has a win32-x64 build (measured, not
# assumed), so the other twelve architectures are pure weight in a zip — and
# filtering here means the bundle is never briefly 1.9GB on disk, and there is no
# rm -rf over a tree that also contains the thing we just built.
#
# CI's equivalent step tries to delete afterwards and silently does nothing:
# `find <root> -path "*/prebuilds/*" -mindepth 1 -maxdepth 1` cannot match, since
# those depths are relative to <root>. Its Linux releases carry all thirteen.
rsync -a --exclude ".git" \
      --include="prebuilds/" --include="prebuilds/win32-x64/***" --exclude="prebuilds/*/" \
      node_modules/ "$OUT/node_modules/"

echo "=== prebuild coverage ==="
# A REPORT, not a strip: the copy above already filtered. This asserts the
# assumption that filtering is safe -- every package that ships prebuilds has a
# win32-x64 one. A package that does not would be a native module this bundle
# cannot run, and silence about it is how that ships.
tot=0; miss=0
for d in $(find "$OUT/node_modules" -type d -name prebuilds); do
  tot=$((tot+1))
  if [ ! -d "$d/win32-x64" ]; then miss=$((miss+1)); echo "  MISSING win32-x64: $(basename "$(dirname "$d")")"; fi
done
echo "  $tot package(s) with prebuilds, $miss missing win32-x64"
[ "$miss" -eq 0 ] || echo "  WARN: those modules cannot load on Windows"

echo "=== launcher ==="
# CRLF, because a .cmd with LF endings fails in ways that read as a corrupt file.
printf '@echo off\r\nstart "" "%%~dp0bare.exe" "%%~dp0index.js" %%*\r\n' > "$OUT/hypertps.cmd"

echo
echo "=== bundle: $OUT ==="
du -sh "$OUT"
ls "$OUT"