#!/usr/bin/env bash # Bundled-release zip packager for PSXRecomp game repos: ships the COMPILED # game, not a kit that compiles it. # # A release built from the title's committed generated/ C is a finished # product. The zip carries the game executable and everything the runtime # reads beside it -- assets, the mod catalog, the bundled OpenBIOS image and # its notice, game.toml / game_options.toml, third-party notices -- plus the # overlay toolchain so overlays outside the compiled static shard can still be # turned native on the player's machine. It carries NO sources, NO emitters at # the root, NO psxrecomp/ or recomp-ui/ tree, NO CLI, NO generated C and NO # BIOS dump. The player supplies a legal disc and plays. # # Usage (from game repo root): # psxrecomp/tools/package_game_release.sh \ # --build-dir build-ci \ # --artifact linux-x64 \ # --zip-prefix bpe \ # --exe-name Bomberman_Party_Edition_Recompiled \ # --display-name "Bomberman Party Edition Recompiled" \ # --recompiler-build build-recompiler \ # [--runtime-file REL]... # extra exe-relative files (keybinds.ini, ...) # [--runtime-dir NAME]... # extra exe-relative dirs (bezels, ...) # [--doc REL]... # extra docs from the repo (DISC.md, ...) # [--overlay-cache-root DIR] # ship a prebuilt overlay shard cache # [--ship-without-overlay-cache-because REASON] # [--no-overlay-toolchain] # do not bundle overlay_toolchain/ # [--stage-only] # validate/stage, without ZIP compression # [--disc-hint "your legally owned disc"] [--version-env RELEASE_VERSION] # # Overlay cache. The runtime's overlay shard cache is compiled FROM THE DISC, # so a CI job -- which has no disc -- cannot build one; the committed static # AOT shard (generated/overlays_static*.c) is compiled into the executable # instead, and the bundled overlay_toolchain/ compiles the rest from the # player's own disc at runtime. A developer packaging locally with a cache # passes --overlay-cache-root and it is staged through release_stage.py # (which owns the cache tag and layout; nothing here knows either). Without a # cache the caller must say why, in words, and the reason is printed: this is # the same deliberately unwieldy switch release_stage.py exposes, because a # packager that quietly staged nothing once looked like a good release twice. # # RELEASE CHANNEL. The build tree must have been configured with # -DPSX_RELEASE_CHANNEL=public|playtest (runtime/psx_release_channel.cmake); # a tree without one is a dev build and is refused. playtest packages are # named --playtest-.zip, public packages always # drop developer-channel mods, and the staged Playtest Diagnostics defaults # must match the channel. Local title releases use # tools/package_title_release.py instead (docs/RELEASING_A_TITLE.md). # # DEVELOPER CHANNEL. mods with channel = "developer" are pruned from public # releases (EXCLUDE_DEV_MODS=1, which CI sets via $CI) and kept in local # exports; --include-dev-mods / --exclude-dev-mods override. Pruning is by # package directory via tools/mod_channel_filter.py, never by editing a # manifest. # # Env: # RELEASE_VERSION / / VERSION file (must match binary stamp) # PSXRECOMP_RUNTIME_BIN_DIR | BPE_RUNTIME_BIN_DIR (Windows MinGW DLL search) # PSX_RELEASE_STAGE_PYTHON (python3 override) # # Lobby pin: the exe must have been built with current runtime.cmake so # $/psx_game_version.txt exists. This script refuses to ship # a VERSION that disagrees with that stamp (netplay list filter bug). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" FRAMEWORK="$(cd "${SCRIPT_DIR}/.." && pwd)" ROOT="$(pwd)" BUILD_DIR="" ARTIFACT="" ZIP_PREFIX="" EXE_NAME="" DISPLAY_NAME="" RECOMPILER_BUILD="build-recompiler" VERSION_ENV="RELEASE_VERSION" DISC_HINT="your legally owned game disc" RUNTIME_TARGET="psx-runtime" RUNTIME_FILES=() RUNTIME_DIRS=() DOCS=() OVERLAY_CACHE_ROOT="" SHIP_WITHOUT_OVERLAY_CACHE_BECAUSE="" SHIP_WITHOUT_OVERLAY_CACHE_KEY_BECAUSE="" STAGE_OVERLAY_TOOLCHAIN=1 STAGE_ONLY=0 if [[ -z "${EXCLUDE_DEV_MODS:-}" ]]; then if [[ -n "${CI:-}" ]]; then EXCLUDE_DEV_MODS=1; else EXCLUDE_DEV_MODS=0; fi fi RUNTIME_BIN_DIR="${PSXRECOMP_RUNTIME_BIN_DIR:-${BPE_RUNTIME_BIN_DIR:-/usr/x86_64-w64-mingw32/bin}}" usage() { sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//' exit "${1:-2}" } while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage 0 ;; --build-dir) BUILD_DIR="${2:?}"; shift 2 ;; --artifact) ARTIFACT="${2:?}"; shift 2 ;; --zip-prefix) ZIP_PREFIX="${2:?}"; shift 2 ;; --exe-name) EXE_NAME="${2:?}"; shift 2 ;; --display-name) DISPLAY_NAME="${2:?}"; shift 2 ;; --recompiler-build) RECOMPILER_BUILD="${2:?}"; shift 2 ;; --runtime-target) RUNTIME_TARGET="${2:?}"; shift 2 ;; --version-env) VERSION_ENV="${2:?}"; shift 2 ;; --disc-hint) DISC_HINT="${2:?}"; shift 2 ;; --runtime-file) RUNTIME_FILES+=("${2:?}"); shift 2 ;; --runtime-dir) RUNTIME_DIRS+=("${2:?}"); shift 2 ;; --doc) DOCS+=("${2:?}"); shift 2 ;; --overlay-cache-root) OVERLAY_CACHE_ROOT="${2:?}"; shift 2 ;; --ship-without-overlay-cache-because) SHIP_WITHOUT_OVERLAY_CACHE_BECAUSE="${2:?}"; shift 2 ;; --ship-without-overlay-cache-key-because) SHIP_WITHOUT_OVERLAY_CACHE_KEY_BECAUSE="${2:?}"; shift 2 ;; --no-overlay-toolchain) STAGE_OVERLAY_TOOLCHAIN=0; shift ;; --stage-only) STAGE_ONLY=1; shift ;; --exclude-dev-mods) EXCLUDE_DEV_MODS=1; shift ;; --include-dev-mods) EXCLUDE_DEV_MODS=0; shift ;; --runtime-bin) RUNTIME_BIN_DIR="${2:?}"; shift 2 ;; --root) ROOT="${2:?}"; shift 2 ;; *) echo "error: unknown arg: $1" >&2 usage 2 ;; esac done if [[ -z "${BUILD_DIR}" || -z "${ARTIFACT}" || -z "${ZIP_PREFIX}" || -z "${EXE_NAME}" ]]; then echo "error: --build-dir, --artifact, --zip-prefix, and --exe-name are required" >&2 usage 2 fi [[ -n "${DISPLAY_NAME}" ]] || DISPLAY_NAME="${EXE_NAME}" ROOT="$(cd "${ROOT}" && pwd)" if [[ -d "${ROOT}/${BUILD_DIR}" ]]; then BUILD_DIR="$(cd "${ROOT}/${BUILD_DIR}" && pwd)" elif [[ -d "${BUILD_DIR}" ]]; then BUILD_DIR="$(cd "${BUILD_DIR}" && pwd)" else echo "error: build dir not found: ${BUILD_DIR}" >&2 exit 1 fi if [[ -d "${ROOT}/${RECOMPILER_BUILD}" ]]; then RECOMPILER_BUILD="$(cd "${ROOT}/${RECOMPILER_BUILD}" && pwd)" elif [[ -d "${RECOMPILER_BUILD}" ]]; then RECOMPILER_BUILD="$(cd "${RECOMPILER_BUILD}" && pwd)" fi # The shared staging surface: cache tag / shards / toolchain / mod catalog are # release_stage.py's, and only its. This script marshals arguments. # shellcheck source=release_overlay_stage.sh . "${SCRIPT_DIR}/release_overlay_stage.sh" psx_release_stage_init "${FRAMEWORK}" PY="${_psx_py}" # --- Version: the compile-time stamp is authoritative ------------------------ REQUESTED="" if [[ -n "${VERSION_ENV}" ]]; then REQUESTED="${!VERSION_ENV:-}"; fi if [[ -z "${REQUESTED}" && -n "${RELEASE_VERSION:-}" ]]; then REQUESTED="${RELEASE_VERSION}"; fi if [[ -z "${REQUESTED}" && -f "${ROOT}/VERSION" ]]; then REQUESTED="$(tr -d '[:space:]' <"${ROOT}/VERSION")"; fi REQUESTED="$(printf '%s' "${REQUESTED}" | tr -d '[:space:]')" REQUESTED="${REQUESTED#v}" EXE="" # Git Bash's -f accepts a Windows PE through its unsuffixed alias. Prefer the # literal .exe path so extension-based DLL bundling and signing cannot be skipped. for cand in "${BUILD_DIR}/${EXE_NAME}.exe" "${BUILD_DIR}/Release/${EXE_NAME}.exe" "${BUILD_DIR}/${EXE_NAME}"; do if [[ -f "${cand}" ]]; then EXE="${cand}"; break; fi done if [[ -z "${EXE}" ]]; then # CMake records the OUTPUT_NAME it chose; prefer it over the wrapper's copy # so a renamed title does not read as "binary missing". marker="${BUILD_DIR}/psxrecomp_exe_name-${RUNTIME_TARGET}.txt" if [[ -f "${marker}" ]]; then chosen="$(tr -d '[:space:]' <"${marker}")" for cand in "${BUILD_DIR}/${chosen}.exe" "${BUILD_DIR}/${chosen}"; do if [[ -f "${cand}" ]]; then EXE="${cand}"; break; fi done if [[ -n "${EXE}" ]]; then echo "note: --exe-name ${EXE_NAME} does not match CMake OUTPUT_NAME ${chosen}; using the marker" >&2 EXE_NAME="${chosen}" fi fi fi if [[ -z "${EXE}" ]]; then echo "error: game executable '${EXE_NAME}' not found under ${BUILD_DIR}" >&2 ls -la "${BUILD_DIR}" >&2 || true exit 1 fi EXE_BASENAME="$(basename "${EXE}")" EXE_DIR="$(dirname "${EXE}")" # A bundled release is a FULL build. The build publishes which it is: the # PSX_HAS_GAME_DISPATCH define only exists when generated C was linked. # Refuse to package a setup host as a game. if [[ -f "${BUILD_DIR}/CMakeCache.txt" ]] && grep -q '^PSXRECOMP_FORCE_SETUP_HOST:BOOL=ON' "${BUILD_DIR}/CMakeCache.txt"; then echo "error: ${BUILD_DIR} was configured with PSXRECOMP_FORCE_SETUP_HOST=ON -- that is a setup host, not a game." >&2 echo " Reconfigure with -DPSXRECOMP_REQUIRE_GAME_C=ON (generated/ committed) and rebuild." >&2 exit 1 fi if ! grep -qs 'PSX_HAS_GAME_DISPATCH' "${BUILD_DIR}/build.ninja" "${BUILD_DIR}/compile_commands.json" \ "${BUILD_DIR}/CMakeFiles/${RUNTIME_TARGET}.dir/flags.make" 2>/dev/null; then echo "error: ${BUILD_DIR} did not compile generated game C (no PSX_HAS_GAME_DISPATCH in the build flags)." >&2 echo " A bundled release links the committed generated/ tree; configure with -DPSXRECOMP_REQUIRE_GAME_C=ON." >&2 exit 1 fi # --- Release channel: from the build, never defaulted ------------------------- CHANNEL="$(sed -n 's/^PSX_RELEASE_CHANNEL:[A-Z]*=//p' "${BUILD_DIR}/CMakeCache.txt" 2>/dev/null | head -n1 || true)" case "${CHANNEL}" in public) CHANNEL_SUFFIX=""; EXCLUDE_DEV_MODS=1 ;; playtest) CHANNEL_SUFFIX="-playtest" ;; *) echo "error: ${BUILD_DIR} has no release channel (PSX_RELEASE_CHANNEL='${CHANNEL}')." >&2 echo " Configure the release build with -DPSX_RELEASE_CHANNEL=public or =playtest." >&2 exit 1 ;; esac echo "release channel: ${CHANNEL}" normalize_ver() { local v; v="$(printf '%s' "${1:-}" | tr -d '[:space:]')"; printf '%s' "${v#v}"; } BUILT="" for stamp in "${EXE_DIR}/psx_game_version.txt" "${BUILD_DIR}/psx_game_version.txt" "${BUILD_DIR}/Release/psx_game_version.txt"; do if [[ -f "${stamp}" ]]; then BUILT="$(normalize_ver "$(cat "${stamp}")")" echo "lobby pin stamp: ${stamp} -> ${BUILT}" break fi done if [[ -z "${BUILT}" ]]; then echo "error: missing psx_game_version.txt next to ${EXE}" >&2 echo " Rebuild with current psxrecomp runtime.cmake so the lobby pin is stamped." >&2 exit 1 fi if [[ -n "${REQUESTED}" && "${REQUESTED}" != "${BUILT}" ]]; then echo "error: RELEASE_VERSION/VERSION=${REQUESTED} but binary stamp=${BUILT}" >&2 echo " Rebuild with -DPSX_GAME_VERSION=${REQUESTED} (or pin VERSION then reconfigure)." >&2 exit 1 fi VERSION="${BUILT}" printf '%s\n' "${VERSION}" >"${ROOT}/VERSION" DIST="${ROOT:?}/dist" STAGE="${DIST:?}/stage-game-${ARTIFACT:?}" ZIP_NAME="${ZIP_PREFIX:?}-${VERSION:?}${CHANNEL_SUFFIX}-${ARTIFACT:?}.zip" rm -rf "${STAGE:?}" mkdir -p "${STAGE}" "${DIST}" if [[ "${STAGE_ONLY}" != 1 ]]; then rm -f "${DIST:?}/${ZIP_NAME:?}"; fi # --- The executable and what CMake staged beside it -------------------------- cp -a "${EXE}" "${STAGE}/" # Opted-in title diagnostics stage this launcher beside the executable. Keep # it with the original binary so a feedback bundle can be replayed in isolation. if [[ -f "${EXE_DIR}/replay-diagnostics.py" ]]; then cp -a "${EXE_DIR}/replay-diagnostics.py" "${STAGE}/replay-diagnostics.py" fi if [[ -f "${EXE_DIR}/psx_game_version.txt" ]]; then cp -a "${EXE_DIR}/psx_game_version.txt" "${STAGE}/psx_game_version.txt" else printf '%s\n' "${VERSION}" >"${STAGE}/psx_game_version.txt" fi if [[ "${EXE_BASENAME}" == *.exe ]]; then shopt -s nullglob for dll in "${EXE_DIR}"/*.dll "${EXE_DIR}"/*.DLL; do cp -a "${dll}" "${STAGE}/" echo "staged sibling DLL $(basename "${dll}")" done shopt -u nullglob fi if [[ ! -d "${EXE_DIR}/assets/fonts" || ! -d "${EXE_DIR}/assets/img" ]]; then echo "error: ${EXE_DIR}/assets/{fonts,img} missing -- rebuild ${RUNTIME_TARGET}" >&2 exit 1 fi # Everything runtime.cmake staged under /assets ships as a unit: fonts, # img, and the window icon it places at assets/psxrecomp.png (APP_ICON). mkdir -p "${STAGE}/assets" cp -a "${EXE_DIR}/assets/." "${STAGE}/assets/" if [[ ! -f "${STAGE}/assets/img/boxart.tga" && -f "${ROOT}/launcher_assets/img/boxart.tga" ]]; then cp -a "${ROOT}/launcher_assets/img/boxart.tga" "${STAGE}/assets/img/boxart.tga" fi # Bundled OpenBIOS: runtime.cmake stages the exact image the compiled backend # consumed plus its MIT notice at /bios/. The runtime resolves # "bios/openbios.bin" relative to the executable, so ship the directory as a # unit. Nothing else from bios/ may ship. if [[ ! -f "${EXE_DIR}/bios/openbios.bin" || ! -f "${EXE_DIR}/bios/OpenBIOS.LICENSE" ]]; then echo "error: ${EXE_DIR}/bios/{openbios.bin,OpenBIOS.LICENSE} missing -- the build did not link the OpenBIOS backend" >&2 exit 1 fi mkdir -p "${STAGE}/bios" cp -a "${EXE_DIR}/bios/openbios.bin" "${EXE_DIR}/bios/OpenBIOS.LICENSE" "${STAGE}/bios/" # Runtime configs: game.toml is what the runtime reads beside the exe; the # build stages it there, so take the staged copy (it is the packaged config # the cache tag derives from). game_options.toml is optional. for cfg in game.toml game_options.toml; do if [[ -f "${EXE_DIR}/${cfg}" ]]; then cp -a "${EXE_DIR}/${cfg}" "${STAGE}/${cfg}" elif [[ -f "${ROOT}/${cfg}" ]]; then cp -a "${ROOT}/${cfg}" "${STAGE}/${cfg}" fi done [[ -f "${STAGE}/game.toml" ]] || { echo "error: no game.toml to ship (build tree or repo root)" >&2; exit 1; } # Wave-5 F4: without overlay_cache = true the runtime never initialises the # overlay loader and every streamed overlay runs interpreted for every player. if ! grep -qE '^[[:space:]]*overlay_cache[[:space:]]*=[[:space:]]*true' "${STAGE}/game.toml"; then if [[ -z "${SHIP_WITHOUT_OVERLAY_CACHE_KEY_BECAUSE}" ]]; then echo "error: REFUSING TO PACKAGE: ${STAGE}/game.toml has no '[runtime] overlay_cache = true'." >&2 echo " Add the key, or pass --ship-without-overlay-cache-key-because ''." >&2 exit 1 fi echo "warning: packaging without overlay_cache = true (reason: ${SHIP_WITHOUT_OVERLAY_CACHE_KEY_BECAUSE})" >&2 fi for rel in "${RUNTIME_FILES[@]+"${RUNTIME_FILES[@]}"}"; do [[ -n "${rel}" ]] || continue src="" for c in "${EXE_DIR}/${rel}" "${ROOT}/${rel}"; do [[ -f "${c}" ]] && { src="${c}"; break; }; done [[ -n "${src}" ]] || { echo "error: --runtime-file ${rel} not found (build tree or repo)" >&2; exit 1; } mkdir -p "$(dirname "${STAGE}/${rel}")" cp -a "${src}" "${STAGE}/${rel}" done for name in "${RUNTIME_DIRS[@]+"${RUNTIME_DIRS[@]}"}"; do [[ -n "${name}" ]] || continue [[ -d "${EXE_DIR}/${name}" ]] || { echo "error: --runtime-dir ${name} missing under ${EXE_DIR}" >&2; exit 1; } mkdir -p "${STAGE}/${name}" cp -a "${EXE_DIR}/${name}/." "${STAGE}/${name}/" echo "staged runtime dir ${name}/" done # --- Mod catalog, verified against what THIS build declared ------------------- MANIFEST="$(find "${BUILD_DIR}" -maxdepth 3 -name "psx_mod_catalog_${RUNTIME_TARGET}.txt" -type f 2>/dev/null | head -n1 || true)" mods_args=(--build-path "${EXE_DIR}" --stage "${STAGE}" --runtime-target "${RUNTIME_TARGET}") [[ -n "${MANIFEST}" ]] && mods_args+=(--catalog-manifest "${MANIFEST}") psx_add_mod_catalog "${mods_args[@]}" prune_dev_mods() { if ! "${PY}" "${SCRIPT_DIR}/mod_channel_filter.py" "$@"; then echo "error: developer-channel filtering failed" >&2 exit 1 fi } if [[ "${EXCLUDE_DEV_MODS}" -eq 1 ]]; then echo "excluding developer-channel mods from this package" prune_dev_mods "${STAGE}/mods/bundled" left=$( { grep -rlE '^[[:space:]]*channel[[:space:]]*=[[:space:]]*"developer"[[:space:]]*$' \ "${STAGE}/mods" --include=manifest.toml 2>/dev/null || true; } | wc -l) if [[ "${left}" -ne 0 ]]; then echo "error: ${left} developer manifest(s) survived pruning" >&2 exit 1 fi else n=$( { grep -rlE '^[[:space:]]*channel[[:space:]]*=[[:space:]]*"developer"[[:space:]]*$' \ "${STAGE}/mods" --include=manifest.toml 2>/dev/null || true; } | wc -l) [[ "${n}" -eq 0 ]] || echo "note: package includes ${n} developer-channel manifest(s); pass --exclude-dev-mods for a public release" fi if ! "${PY}" "${SCRIPT_DIR}/package_title_release.py" check-diagnostics --channel "${CHANNEL}" "${STAGE}/mods/bundled"; then echo "error: Playtest Diagnostics defaults do not match the ${CHANNEL} channel" >&2 exit 1 fi # --- Overlay shard cache (developer-local only) and toolchain ----------------- GAME_BIN="" for cand in "${RECOMPILER_BUILD}/psxrecomp-game" "${RECOMPILER_BUILD}/psxrecomp-game.exe" "${RECOMPILER_BUILD}/Release/psxrecomp-game.exe"; do if [[ -f "${cand}" ]]; then GAME_BIN="${cand}"; break; fi done if [[ -n "${OVERLAY_CACHE_ROOT}" ]]; then [[ -n "${GAME_BIN}" ]] || { echo "error: --overlay-cache-root needs psxrecomp-game under ${RECOMPILER_BUILD} (cache tag derivation)" >&2; exit 1; } game_id="$(sed -n 's/^[[:space:]]*id[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "${STAGE}/game.toml" | head -1)" [[ -n "${game_id}" ]] || { echo "error: [game] id missing from the staged game.toml (cache namespace)" >&2; exit 1; } cg_tag="$(psx_overlay_cg_tag --runtime-include "${FRAMEWORK}/runtime/include" \ --recompiler "${GAME_BIN}" --game-toml "${STAGE}/game.toml" \ --flavor-from-build "${BUILD_DIR}" --runtime-target "${RUNTIME_TARGET}")" [[ -n "${cg_tag}" ]] || { echo "error: could not derive the overlay cache tag" >&2; exit 1; } psx_add_overlay_cache --game-id "${game_id}" --cache-src-root "${OVERLAY_CACHE_ROOT}" \ --stage "${STAGE}" --cg-tag "${cg_tag}" else if [[ -z "${SHIP_WITHOUT_OVERLAY_CACHE_BECAUSE}" ]]; then echo "error: no --overlay-cache-root given. A shard cache is compiled from the disc, so a" >&2 echo " release without one relies on the compiled static shard plus runtime autocompile." >&2 echo " State that in words: --ship-without-overlay-cache-because ''." >&2 exit 1 fi echo "overlay cache: NONE shipped -- ${SHIP_WITHOUT_OVERLAY_CACHE_BECAUSE}" fi if [[ "${STAGE_OVERLAY_TOOLCHAIN}" -eq 1 ]]; then [[ -n "${GAME_BIN}" ]] || { echo "error: overlay toolchain needs psxrecomp-game under ${RECOMPILER_BUILD} (pass --recompiler-build, or --no-overlay-toolchain)" >&2; exit 1; } case "${ARTIFACT}" in windows-*) platform=win ;; linux-*) platform=linux ;; macos-arm64|macos-aarch64) platform=macos-arm64 ;; macos-x64|macos-x86_64) platform=macos-x64 ;; *) echo "error: cannot map artifact '${ARTIFACT}' to an overlay toolchain platform" >&2; exit 1 ;; esac tc_args=(--stage "${STAGE}" --recomp-dir "$(dirname "${GAME_BIN}")" \ --recomp-tools "${FRAMEWORK}/tools" --recomp-include "${FRAMEWORK}/runtime/include" \ --dl-cache "${DIST}/.dl-cache" --platform "${platform}") [[ "${platform}" == win ]] && tc_args+=(--mingw-bin "${RUNTIME_BIN_DIR}") psx_add_overlay_toolchain "${tc_args[@]}" fi # --- Notices and docs --------------------------------------------------------- mkdir -p "${STAGE}/licenses" if [[ -d "${FRAMEWORK}/runtime/licenses" ]]; then cp -a "${FRAMEWORK}/runtime/licenses/." "${STAGE}/licenses/" fi [[ -f "${FRAMEWORK}/THIRD_PARTY_ATTRIBUTION.md" ]] && cp -a "${FRAMEWORK}/THIRD_PARTY_ATTRIBUTION.md" "${STAGE}/licenses/" [[ -f "${FRAMEWORK}/LICENSE" ]] && cp -a "${FRAMEWORK}/LICENSE" "${STAGE}/licenses/psxrecomp-LICENSE" # recomp-ui: the executable links it (MIT) and assets/ carries its fonts and # images (OFL-1.1, CC BY-SA 4.0), so its license and asset notices ship too. # The build's RECOMP_UI_ROOT says which tree it linked; /recomp-ui is the # New Project Layout default. if ! grep -qs '^PSX_RECOMP_UI:BOOL=OFF' "${BUILD_DIR}/CMakeCache.txt"; then UI_ROOT="$(sed -n 's/^RECOMP_UI_ROOT:[A-Z]*=//p' "${BUILD_DIR}/CMakeCache.txt" 2>/dev/null | head -n1 || true)" [[ -n "${UI_ROOT}" && -f "${UI_ROOT}/recomp_ui.cmake" ]] || UI_ROOT="${ROOT}/recomp-ui" [[ -f "${UI_ROOT}/recomp_ui.cmake" ]] || { echo "error: recomp-ui assets are staged but no recomp-ui tree found (RECOMP_UI_ROOT / ${ROOT}/recomp-ui)" >&2; exit 1; } [[ -f "${UI_ROOT}/LICENSE" ]] || { echo "error: ${UI_ROOT}/LICENSE missing" >&2; exit 1; } cp -a "${UI_ROOT}/LICENSE" "${STAGE}/licenses/recomp-ui-LICENSE" for sub in fonts img; do notice="${UI_ROOT}/assets/common/${sub}/NOTICE.md" [[ -f "${notice}" ]] || { echo "error: ${notice} missing" >&2; exit 1; } cp -a "${notice}" "${STAGE}/assets/${sub}/NOTICE.md" done fi if [[ -z "$(ls -A "${STAGE}/licenses" 2>/dev/null)" ]]; then echo "error: no third-party notices staged (${FRAMEWORK}/runtime/licenses empty?)" >&2 exit 1 fi for d in "${DOCS[@]+"${DOCS[@]}"}"; do [[ -n "${d}" ]] || continue [[ -f "${ROOT}/${d}" ]] || { echo "error: --doc ${d} not found" >&2; exit 1; } cp -a "${ROOT}/${d}" "${STAGE}/$(basename "${d}")" done mkdir -p "${STAGE}/saves" cat >"${STAGE}/README.txt" <&2 printf ' %s\n' "${forbidden[@]}" >&2 exit 1 fi # BIOS *profiles* (SCPH1001.toml inside overlay_toolchain/bios) are text the # toolchain needs; only image files are forbidden. bad_bios="$(find "${STAGE}" -type f \( \( -iname 'SCPH*' -a ! -iname '*.toml' \) -o -iname '*.bin' \) \ ! -path "${STAGE}/bios/openbios.bin" 2>/dev/null || true)" if [[ -n "${bad_bios}" ]]; then echo "error: BIOS image(s) other than the bundled OpenBIOS in the stage:" >&2 printf '%s\n' "${bad_bios}" | sed 's/^/ /' >&2 exit 1 fi bad_disc="$(find "${STAGE}" -type f \( -iname '*.cue' -o -iname '*.iso' -o -iname '*.img' -o -iname '*.chd' -o -iname '*.mcd' -o -iname '*.mcr' \) 2>/dev/null || true)" if [[ -n "${bad_disc}" ]]; then echo "error: disc / memory-card files in the stage:" >&2 printf '%s\n' "${bad_disc}" | sed 's/^/ /' >&2 exit 1 fi if find "${STAGE}" -type f -name '*.c' ! -path "${STAGE}/overlay_toolchain/*" | grep -q .; then echo "error: C sources in the stage outside overlay_toolchain/:" >&2 find "${STAGE}" -type f -name '*.c' ! -path "${STAGE}/overlay_toolchain/*" | sed 's/^/ /' >&2 exit 1 fi # --- Windows: DLLs beside the exe, then Authenticode ------------------------- if [[ "${EXE_BASENAME}" == *.exe ]]; then BUNDLE="${SCRIPT_DIR}/bundle_mingw_dlls.sh" [[ -f "${BUNDLE}" ]] || { echo "error: missing ${BUNDLE}" >&2; exit 1; } bash "${BUNDLE}" \ --runtime-bin "${RUNTIME_BIN_DIR}" --search-dir "${EXE_DIR}" --search-dir "${BUILD_DIR}" \ --exe "${STAGE}/${EXE_BASENAME}" --dest "${STAGE}" --label "${EXE_BASENAME}" \ --require libgcc_s_seh-1.dll --require libstdc++-6.dll --require libwinpthread-1.dll \ --require libssp-0.dll --require zlib1.dll --require z.dll SIGN_SH="${SCRIPT_DIR}/ci/sign_windows.sh" if [[ -f "${SIGN_SH}" ]]; then bash "${SIGN_SH}" "${STAGE}" else echo "note: ${SIGN_SH} missing; Windows binaries ship unsigned" >&2 fi fi # --- Bind the final executable to its original build execution contract ---- # Signing can change the PE bytes, so bind after it and before compression. # Do not accept a sidecar copied into the stage from an unrelated build. "${PY}" "${SCRIPT_DIR}/release_stage.py" stage-execution \ --binary "${STAGE}/${EXE_BASENAME}" \ --manifest "${EXE%.exe}.execution.json" \ --output "${STAGE}/${EXE_BASENAME%.exe}.execution.json" find "${STAGE}" -exec touch -c {} + 2>/dev/null || find "${STAGE}" -exec touch {} + if [[ "${STAGE_ONLY}" == 1 ]]; then echo "Staged ${STAGE} (ZIP compression skipped)" exit 0 fi "${PY}" "${SCRIPT_DIR}/create_release_zip.py" --source "${STAGE}" --output "${DIST}/${ZIP_NAME}" >/dev/null echo "Wrote ${DIST}/${ZIP_NAME}" du -h "${DIST}/${ZIP_NAME}"