mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
hotfix ci template
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="PSXRecomp">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1a2332"/>
|
||||
<stop offset="100%" stop-color="#0d121a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#5eead4"/>
|
||||
<stop offset="100%" stop-color="#2dd4bf"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="96" fill="url(#bg)"/>
|
||||
<!-- Classic pad silhouette (outline only) -->
|
||||
<path fill="none" stroke="url(#accent)" stroke-width="26" stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M156 210 C156 178 178 156 210 156 H302 C334 156 356 178 356 210 V248 C356 268 348 286 334 298 L372 360 C380 374 372 392 356 392 H328 C312 392 300 382 294 368 L278 328 H234 L218 368 C212 382 200 392 184 392 H156 C140 392 132 374 140 360 L178 298 C164 286 156 268 156 248 Z"/>
|
||||
<!-- D-pad -->
|
||||
<path fill="none" stroke="url(#accent)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M214 236 V268 M198 252 H230"/>
|
||||
<!-- Face buttons -->
|
||||
<circle cx="298" cy="236" r="10" fill="url(#accent)"/>
|
||||
<circle cx="318" cy="252" r="10" fill="url(#accent)" opacity="0.85"/>
|
||||
<circle cx="298" cy="268" r="10" fill="url(#accent)" opacity="0.7"/>
|
||||
<circle cx="278" cy="252" r="10" fill="url(#accent)" opacity="0.85"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -257,6 +257,10 @@ YourGameRecomp/ # your git repo
|
||||
├── framework_pins.txt # optional scaffold snapshot (gitlinks are authoritative)
|
||||
├── README.md # scaffold stub (legal + quick start)
|
||||
├── VERSION # release pin (e.g. 0.1.0)
|
||||
├── assets/
|
||||
│ ├── psxrecomp.ico # default Windows app icon (RetComM-themed pad)
|
||||
│ ├── psxrecomp.png # staged beside exe / packaging
|
||||
│ └── psxrecomp.svg # source mark (see psxrecomp/assets/)
|
||||
├── seeds/ghidra_funcs.txt # probe: boot-EXE JAL seeds (grow over time)
|
||||
├── codegen_setup.c / .h # title PsxrecompCodegenHostConfig (+ apply hooks)
|
||||
├── launcher_assets/img/
|
||||
@@ -296,6 +300,7 @@ psxrecomp_add_game_runtime(psx-runtime
|
||||
CODEGEN_SETUP_SOURCES codegen_setup.c
|
||||
DEFAULT_GAME_CONFIG_PATH "game.toml"
|
||||
LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"
|
||||
APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ framework pins. It never ships retail discs, BIOS dumps, or `generated/` game C.
|
||||
3. Configure with `-DPSXRECOMP_FORCE_SETUP_HOST=ON` + wizard flags; build the
|
||||
host binary only.
|
||||
4. `package_setup_host.sh` — lean zip (no embedded toolchain by default).
|
||||
Requires `psx_game_version.txt` beside the host exe (stamped at compile
|
||||
time). Refuses to ship if `RELEASE_VERSION` / `VERSION` disagree with that
|
||||
stamp — a mismatched pin makes netplay lobby browsers filter each other out.
|
||||
|
||||
Template: [`templates/setup-release.yml`](templates/setup-release.yml).
|
||||
|
||||
|
||||
@@ -504,6 +504,30 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
cmake --build build-ci --target psx-runtime -j"${JOBS}"
|
||||
# Lobby list pin must match RELEASE_VERSION (package_setup_host enforces).
|
||||
STAMP=""
|
||||
for cand in \
|
||||
build-ci/psx_game_version.txt \
|
||||
build-ci/Release/psx_game_version.txt \
|
||||
build-ci/*/psx_game_version.txt
|
||||
do
|
||||
if [[ -f "${cand}" ]]; then STAMP="${cand}"; break; fi
|
||||
done
|
||||
if [[ -z "${STAMP}" ]]; then
|
||||
# Fallback: stamp next to the host exe
|
||||
STAMP="$(find build-ci -name psx_game_version.txt -type f 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
if [[ -z "${STAMP}" || ! -f "${STAMP}" ]]; then
|
||||
echo "::error::missing psx_game_version.txt after build (update psxrecomp submodule)"
|
||||
exit 1
|
||||
fi
|
||||
BUILT="$(tr -d '[:space:]' <"${STAMP}")"
|
||||
BUILT="${BUILT#v}"
|
||||
if [[ "${BUILT}" != "${RELEASE_VERSION}" ]]; then
|
||||
echo "::error::lobby pin stamp ${BUILT} != RELEASE_VERSION ${RELEASE_VERSION} (${STAMP})"
|
||||
exit 1
|
||||
fi
|
||||
echo "lobby pin ok: ${BUILT} (${STAMP})"
|
||||
|
||||
- name: Package setup zip
|
||||
run: |
|
||||
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rasterize assets/psxrecomp.svg → PNG / ICO.
|
||||
# Master PNG is 512x512 — linuxdeploy / AppImage reject oversized icons.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SVG="${ROOT}/assets/psxrecomp.svg"
|
||||
PNG="${ROOT}/assets/psxrecomp.png"
|
||||
ICO="${ROOT}/assets/psxrecomp.ico"
|
||||
SIZE=512
|
||||
|
||||
if [[ ! -f "${SVG}" ]]; then
|
||||
echo "missing ${SVG}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v rsvg-convert >/dev/null 2>&1; then
|
||||
rsvg-convert -w "${SIZE}" -h "${SIZE}" "${SVG}" -o "${PNG}"
|
||||
elif command -v magick >/dev/null 2>&1; then
|
||||
magick -background none "${SVG}" -resize "${SIZE}x${SIZE}" "${PNG}"
|
||||
elif command -v convert >/dev/null 2>&1; then
|
||||
convert -background none "${SVG}" -resize "${SIZE}x${SIZE}" "${PNG}"
|
||||
elif command -v inkscape >/dev/null 2>&1; then
|
||||
inkscape "${SVG}" -w "${SIZE}" -h "${SIZE}" -o "${PNG}"
|
||||
else
|
||||
python3 - "${PNG}" "${SIZE}" <<'PY'
|
||||
import struct, zlib, sys
|
||||
from pathlib import Path
|
||||
|
||||
def chunk(tag, data):
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff)
|
||||
|
||||
out = Path(sys.argv[1])
|
||||
w = h = int(sys.argv[2])
|
||||
rows = []
|
||||
for y in range(h):
|
||||
row = bytearray([0])
|
||||
for x in range(w):
|
||||
# Approximate dark rounded tile + teal pad blob (fallback only).
|
||||
cx, cy = x - w / 2, y - h / 2
|
||||
rr = (w * 0.42)
|
||||
inside = cx * cx + cy * cy < rr * rr
|
||||
if inside:
|
||||
t = (x + y) / (2 * w)
|
||||
r = int(26 + t * 10)
|
||||
g = int(35 + t * 10)
|
||||
b = int(50 + t * 10)
|
||||
a = 255
|
||||
# teal accent ring
|
||||
if abs((cx * cx + cy * cy) ** 0.5 - rr * 0.55) < w * 0.03:
|
||||
r, g, b, a = 45, 212, 191, 255
|
||||
else:
|
||||
r = g = b = a = 0
|
||||
row.extend([r, g, b, a])
|
||||
rows.append(bytes(row))
|
||||
raw = b"".join(rows)
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
|
||||
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")
|
||||
out.write_bytes(png)
|
||||
print(f"wrote {out} (python fallback)")
|
||||
PY
|
||||
fi
|
||||
|
||||
echo "PNG: ${PNG} (${SIZE}x${SIZE})"
|
||||
|
||||
if command -v magick >/dev/null 2>&1; then
|
||||
magick "${PNG}" -define icon:auto-resize=256,128,64,48,32,16 "${ICO}"
|
||||
echo "ICO: ${ICO}"
|
||||
elif command -v convert >/dev/null 2>&1; then
|
||||
convert "${PNG}" -define icon:auto-resize=256,128,64,48,32,16 "${ICO}"
|
||||
echo "ICO: ${ICO}"
|
||||
else
|
||||
python3 - "${PNG}" "${ICO}" <<'PY'
|
||||
# Minimal single-size ICO from PNG (no Pillow required).
|
||||
import struct, sys, zlib
|
||||
from pathlib import Path
|
||||
|
||||
def png_rgba(path):
|
||||
data = Path(path).read_bytes()
|
||||
assert data[:8] == b"\x89PNG\r\n\x1a\n"
|
||||
i = 8
|
||||
w = h = None
|
||||
idat = b""
|
||||
while i < len(data):
|
||||
ln = struct.unpack(">I", data[i:i+4])[0]
|
||||
tag = data[i+4:i+8]
|
||||
chunk = data[i+8:i+8+ln]
|
||||
i += 12 + ln
|
||||
if tag == b"IHDR":
|
||||
w, h = struct.unpack(">II", chunk[:8])
|
||||
elif tag == b"IDAT":
|
||||
idat += chunk
|
||||
elif tag == b"IEND":
|
||||
break
|
||||
raw = zlib.decompress(idat)
|
||||
stride = 1 + w * 4
|
||||
pixels = bytearray()
|
||||
for y in range(h):
|
||||
row = raw[y*stride+1:(y+1)*stride]
|
||||
pixels.extend(row)
|
||||
return w, h, bytes(pixels)
|
||||
|
||||
w, h, rgba = png_rgba(sys.argv[1])
|
||||
# ICO: BGRA + AND mask, bottom-up
|
||||
stride = ((w * 32 + 31) // 32) * 4
|
||||
and_stride = ((w + 31) // 32) * 4
|
||||
xor = bytearray()
|
||||
and_mask = bytearray()
|
||||
for y in range(h-1, -1, -1):
|
||||
row = bytearray()
|
||||
for x in range(w):
|
||||
i = (y * w + x) * 4
|
||||
r, g, b, a = rgba[i:i+4]
|
||||
row += bytes([b, g, r, a])
|
||||
row += b"\x00" * (stride - w * 4)
|
||||
xor.extend(row)
|
||||
for y in range(h-1, -1, -1):
|
||||
bits = 0
|
||||
byte = 0
|
||||
row = bytearray()
|
||||
for x in range(w):
|
||||
a = rgba[(y * w + x) * 4 + 3]
|
||||
byte = (byte << 1) | (0 if a > 0 else 1)
|
||||
bits += 1
|
||||
if bits == 8:
|
||||
row.append(byte)
|
||||
bits = byte = 0
|
||||
if bits:
|
||||
row.append(byte << (8 - bits))
|
||||
row += b"\x00" * (and_stride - len(row))
|
||||
and_mask.extend(row)
|
||||
|
||||
dib = struct.pack("<IIIHHIIIIII", 40, w, h*2, 1, 32, 0, len(xor), 0, 0, 0, 0) + xor + and_mask
|
||||
# ICONDIR + ICONDIRENTRY
|
||||
entry = struct.pack("<BBBBHHII", w if w < 256 else 0, h if h < 256 else 0, 0, 0, 1, 32, len(dib), 22)
|
||||
ico = struct.pack("<HHH", 0, 1, 1) + entry + dib
|
||||
Path(sys.argv[2]).write_bytes(ico)
|
||||
print(f"wrote {sys.argv[2]} (python ico)")
|
||||
PY
|
||||
fi
|
||||
@@ -733,6 +733,7 @@ function(psxrecomp_add_runtime_target target)
|
||||
EXE_NAME
|
||||
GAME_VERSION
|
||||
MAX_PLAYERS
|
||||
APP_ICON
|
||||
)
|
||||
# GAME_GENERATED_FULL_C is a list (not a single value): the split-TU build
|
||||
# writes the recompiled game as N full_NN.c shards instead of one
|
||||
@@ -874,6 +875,39 @@ function(psxrecomp_add_runtime_target target)
|
||||
endif()
|
||||
set_target_properties(${target} PROPERTIES OUTPUT_NAME "${_psxrt_exe_name}")
|
||||
|
||||
# ---- Windows / desktop app icon ---------------------------------------
|
||||
# Prefer an explicit APP_ICON, then the game-repo copy under assets/, then
|
||||
# the framework default shipped in psxrecomp/assets (RetComM-themed pad).
|
||||
if(NOT PSXRT_APP_ICON)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico")
|
||||
set(PSXRT_APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico")
|
||||
elseif(EXISTS "${PSXRECOMP_ROOT}/assets/psxrecomp.ico")
|
||||
set(PSXRT_APP_ICON "${PSXRECOMP_ROOT}/assets/psxrecomp.ico")
|
||||
endif()
|
||||
endif()
|
||||
if(PSXRT_APP_ICON AND EXISTS "${PSXRT_APP_ICON}")
|
||||
if(WIN32)
|
||||
string(REPLACE "\\" "/" _psxrt_ico_fwd "${PSXRT_APP_ICON}")
|
||||
set(_psxrt_rc "${CMAKE_CURRENT_BINARY_DIR}/${target}_app_icon.rc")
|
||||
file(WRITE "${_psxrt_rc}" "IDI_ICON1 ICON \"${_psxrt_ico_fwd}\"\n")
|
||||
target_sources(${target} PRIVATE "${_psxrt_rc}")
|
||||
message(STATUS "psxrecomp ${target}: APP_ICON=${PSXRT_APP_ICON}")
|
||||
endif()
|
||||
# Stage PNG beside the exe when present (AppImage / desktop / docs).
|
||||
get_filename_component(_psxrt_ico_dir "${PSXRT_APP_ICON}" DIRECTORY)
|
||||
set(_psxrt_png "${_psxrt_ico_dir}/psxrecomp.png")
|
||||
if(EXISTS "${_psxrt_png}")
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
"$<TARGET_FILE_DIR:${target}>/assets"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_psxrt_png}"
|
||||
"$<TARGET_FILE_DIR:${target}>/assets/psxrecomp.png"
|
||||
COMMENT "Staging psxrecomp.png app icon"
|
||||
VERBATIM)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ---- overlay codegen hash (auto cache key) -----------------------------
|
||||
# Hash the recompiler's codegen sources into runtime/include/overlay_codegen_hash.h
|
||||
# (gitignored) so the overlay cache path carries cg<N>_<hash>: any emitter change
|
||||
@@ -1055,6 +1089,19 @@ function(psxrecomp_add_runtime_target target)
|
||||
$<$<CXX_COMPILER_ID:MSVC>:SDL_MAIN_HANDLED>
|
||||
)
|
||||
|
||||
# Stamp the lobby pin next to the exe (and in the build tree). Packagers
|
||||
# must ship VERSION == this stamp — rewriting VERSION after the build
|
||||
# caused Twisted Metal 4 installs where VERSION said 0.3.8 but the binary
|
||||
# still filtered lobbies as 0.3.7.
|
||||
file(GENERATE
|
||||
OUTPUT "$<TARGET_FILE_DIR:${target}>/psx_game_version.txt"
|
||||
CONTENT "${PSXRT_GAME_VERSION}\n"
|
||||
)
|
||||
file(GENERATE
|
||||
OUTPUT "${CMAKE_BINARY_DIR}/psx_game_version.txt"
|
||||
CONTENT "${PSXRT_GAME_VERSION}\n"
|
||||
)
|
||||
|
||||
# OpenBIOS is part of the native runtime product, not a developer-machine
|
||||
# prerequisite. Stage both the exact ROM consumed by the compiled backend
|
||||
# and its required MIT notice beside every native executable. Release
|
||||
@@ -1450,6 +1497,7 @@ endfunction()
|
||||
# CODEGEN_SETUP_SOURCES codegen_setup.c
|
||||
# DEFAULT_GAME_CONFIG_PATH "game.toml"
|
||||
# LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"
|
||||
# APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"
|
||||
# MAX_PLAYERS 2
|
||||
# ENABLE_NETPLAY_IF_PRESENT
|
||||
# ENABLE_SETUP_WIZARD
|
||||
@@ -1520,6 +1568,17 @@ function(psxrecomp_add_game_runtime target)
|
||||
message(STATUS
|
||||
"psxrecomp game_version: ${PSX_GAME_VERSION} "
|
||||
"(from VERSION=${_psxg_release_version})")
|
||||
if(NOT "${PSX_GAME_VERSION}" STREQUAL ""
|
||||
AND NOT "${PSX_GAME_VERSION}" MATCHES "\\$<"
|
||||
AND NOT "${_psxg_release_version}" STREQUAL ""
|
||||
AND NOT "${_psxg_release_version}" STREQUAL "0.0.0"
|
||||
AND NOT "${PSX_GAME_VERSION}" STREQUAL "${_psxg_release_version}")
|
||||
message(WARNING
|
||||
"PSX_GAME_VERSION=${PSX_GAME_VERSION} differs from VERSION file "
|
||||
"(${_psxg_release_version}). Sticky CMakeCache after a VERSION bump "
|
||||
"causes netplay lobby list mismatches. Reconfigure with "
|
||||
"-DPSX_GAME_VERSION=${_psxg_release_version} or delete the build cache.")
|
||||
endif()
|
||||
|
||||
# Prefer game-root recomp-ui (runtime.cmake also auto-discovers this).
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/recomp-ui/recomp_ui.cmake")
|
||||
|
||||
@@ -80,7 +80,8 @@ For **bulk ops across many titles / platforms**, use the sibling tool
|
||||
|
||||
| Op | Purpose |
|
||||
|----|---------|
|
||||
| `rename_psxrecomp_submodule` | `psxrecomp-v4` → `psxrecomp` |
|
||||
| `rename_psxrecomp_submodule` | Promote/keep `psxrecomp/`; delete leftover `psxrecomp-v4` |
|
||||
| `repair_psxrecomp_submodule` | Re-clone when `psxrecomp/.git` gitdir is broken / absorbed |
|
||||
| `ensure_recomp_ui_submodule` | Add `recomp-ui` |
|
||||
| `emit_codegen_setup` | Thin `codegen_setup.c/.h` |
|
||||
| `rewrite_cmake_setup_host` | `psxrecomp_add_game_runtime` + wizard |
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Cross-platform (Windows / macOS / Linux). No force flags; builds stay under
|
||||
the chosen build directory (default ``build-release``).
|
||||
|
||||
Before configure, missing OpenBIOS generated C under ``psxrecomp/generated/``
|
||||
is regenerated (MIT OpenBIOS — no retail dump required) so runtime.cmake can
|
||||
link a BIOS backend.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,6 +25,10 @@ from .gitops import CmdResult
|
||||
DEFAULT_BUILD_DIR = "build-release"
|
||||
DEFAULT_TARGET = "psx-runtime"
|
||||
DEFAULT_BUILD_TYPE = "Release"
|
||||
OPENBIOS_PROFILE = "bios/OpenBIOS.toml"
|
||||
OPENBIOS_STEM = "OpenBIOS"
|
||||
SCPH1001_PROFILE = "bios/SCPH1001.toml"
|
||||
SCPH1001_STEM = "SCPH1001"
|
||||
LogFn = Callable[[str], None]
|
||||
|
||||
|
||||
@@ -155,6 +163,403 @@ def _run_stream(
|
||||
return CmdResult(True, "OK", detail)
|
||||
|
||||
|
||||
def resolve_framework_root(root: Path) -> Path | None:
|
||||
"""Return the psxrecomp framework root (bios/ + recompiler/), or None."""
|
||||
root = root.expanduser().resolve()
|
||||
for cand in (root / "psxrecomp", root):
|
||||
if (cand / "bios" / "OpenBIOS.toml").is_file() and (cand / "recompiler").is_dir():
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
_MAX_PLAYERS_CMAKE_RE = re.compile(
|
||||
r"^\s*MAX_PLAYERS\s+(\d+)\s*$", re.MULTILINE | re.IGNORECASE
|
||||
)
|
||||
_MAX_PLAYERS_RANGE_RE = re.compile(
|
||||
r"MAX_PLAYERS must be in\s+(\d+)\.\.(\d+)", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def project_max_players(root: Path) -> int | None:
|
||||
"""``MAX_PLAYERS N`` from the game ``CMakeLists.txt``, if present."""
|
||||
cmake = root / "CMakeLists.txt"
|
||||
if not cmake.is_file():
|
||||
return None
|
||||
try:
|
||||
text = cmake.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
m = _MAX_PLAYERS_CMAKE_RE.search(text)
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
|
||||
def framework_max_players_range(fw: Path) -> tuple[int, int] | None:
|
||||
"""``(lo, hi)`` from runtime.cmake's FATAL_ERROR range check, if found."""
|
||||
cmake = fw / "runtime" / "runtime.cmake"
|
||||
if not cmake.is_file():
|
||||
return None
|
||||
try:
|
||||
text = cmake.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
m = _MAX_PLAYERS_RANGE_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
return int(m.group(1)), int(m.group(2))
|
||||
|
||||
|
||||
def preflight_max_players(root: Path) -> CmdResult | None:
|
||||
"""Fail fast when the game asks for MAX_PLAYERS the nested framework rejects.
|
||||
|
||||
Single-player titles (Ape Escape, Tomba, …) use ``MAX_PLAYERS 1``. Older
|
||||
psxrecomp pins only allowed 2..5 and abort configure with a cryptic
|
||||
FATAL_ERROR. Detect that before running cmake.
|
||||
"""
|
||||
players = project_max_players(root)
|
||||
if players is None:
|
||||
return None
|
||||
fw = resolve_framework_root(root)
|
||||
if fw is None:
|
||||
return None
|
||||
rng = framework_max_players_range(fw)
|
||||
if rng is None:
|
||||
return None
|
||||
lo, hi = rng
|
||||
if lo <= players <= hi:
|
||||
return None
|
||||
pins = root / "framework_pins.txt"
|
||||
pin_hint = ""
|
||||
if pins.is_file():
|
||||
pin_hint = f" Check {pins.name} vs the checked-out psxrecomp commit."
|
||||
return CmdResult(
|
||||
False,
|
||||
f"MAX_PLAYERS {players} is outside nested psxrecomp range {lo}..{hi}. "
|
||||
f"Update the psxrecomp submodule to a pin that allows 1..8 "
|
||||
f"(runtime.cmake after single-player / rewind support).{pin_hint}",
|
||||
)
|
||||
|
||||
|
||||
def diagnose_configure_failure(detail: str, root: Path) -> str | None:
|
||||
"""Extra hint appended to a failed cmake configure message."""
|
||||
blob = detail or ""
|
||||
if "MAX_PLAYERS must be in" in blob and "got" in blob:
|
||||
pre = preflight_max_players(root)
|
||||
if pre is not None:
|
||||
return pre.message
|
||||
return (
|
||||
"MAX_PLAYERS rejected by nested psxrecomp. Single-player titles need "
|
||||
"a framework pin whose runtime.cmake allows 1..8 — update the "
|
||||
"psxrecomp submodule (and framework_pins.txt)."
|
||||
)
|
||||
if "generated" in blob.lower() and (
|
||||
"GEN_MARKER" in blob or "dispatch.c" in blob or "missing" in blob.lower()
|
||||
):
|
||||
return (
|
||||
"Game generated C may be missing — run Generate (disc→C) before "
|
||||
"Configure, or ensure generated/<boot>_dispatch.c exists."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def bios_backend_present(fw: Path, stem: str) -> bool:
|
||||
"""True when generated/<stem>_{full,dispatch}.c look linkable."""
|
||||
dispatch = fw / "generated" / f"{stem}_dispatch.c"
|
||||
full = fw / "generated" / f"{stem}_full.c"
|
||||
if not dispatch.is_file() or not full.is_file():
|
||||
return False
|
||||
try:
|
||||
text = dispatch.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
return False
|
||||
return f"{stem}_psx_bios_backend" in text
|
||||
|
||||
|
||||
def _allow_no_bios(extra_args: list[str] | None) -> bool:
|
||||
if not extra_args:
|
||||
return False
|
||||
blob = " ".join(extra_args)
|
||||
return (
|
||||
"PSXRECOMP_ALLOW_NO_BIOS=ON" in blob
|
||||
or "PSXRECOMP_ALLOW_NO_BIOS:BOOL=ON" in blob
|
||||
or "PSXRECOMP_ALLOW_NO_BIOS=1" in blob
|
||||
)
|
||||
|
||||
|
||||
def _find_psxrecomp_bios(fw: Path, game_root: Path | None = None) -> Path | None:
|
||||
names = ("psxrecomp-bios.exe", "psxrecomp-bios")
|
||||
dirs: list[Path] = [
|
||||
fw / "recompiler" / "build",
|
||||
fw / "recompiler" / "build-t2",
|
||||
]
|
||||
if game_root is not None:
|
||||
dirs.append(game_root / "build-recompiler")
|
||||
for d in dirs:
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for name in names:
|
||||
p = d / name
|
||||
if p.is_file():
|
||||
return p
|
||||
# Nested generator layouts (e.g. Debug/Release on MSVC)
|
||||
for sub in d.iterdir():
|
||||
if not sub.is_dir():
|
||||
continue
|
||||
for name in names:
|
||||
p = sub / name
|
||||
if p.is_file():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _recompiler_build_usable(build_dir: Path) -> bool:
|
||||
cache = build_dir / "CMakeCache.txt"
|
||||
if not cache.is_file():
|
||||
return False
|
||||
try:
|
||||
text = cache.read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
return False
|
||||
gen = ""
|
||||
for line in text.splitlines():
|
||||
if line.startswith("CMAKE_GENERATOR:INTERNAL="):
|
||||
gen = line.split("=", 1)[1].strip()
|
||||
break
|
||||
if gen.startswith("Ninja"):
|
||||
return (build_dir / "build.ninja").is_file()
|
||||
if "Makefiles" in gen:
|
||||
return (build_dir / "Makefile").is_file()
|
||||
if gen.startswith("Visual Studio"):
|
||||
return any(build_dir.glob("*.sln"))
|
||||
# Unknown generator — trust the cache and let cmake --build diagnose.
|
||||
return True
|
||||
|
||||
|
||||
def _iter_recompiler_build_dirs(fw: Path) -> list[Path]:
|
||||
src = fw / "recompiler"
|
||||
if not src.is_dir():
|
||||
return []
|
||||
dirs: list[Path] = []
|
||||
for name in ("build-t2", "build"):
|
||||
dirs.append(src / name)
|
||||
dirs.extend(sorted(src.glob("cmake-build*")))
|
||||
return dirs
|
||||
|
||||
|
||||
def _any_usable_recompiler_build(fw: Path) -> bool:
|
||||
return any(_recompiler_build_usable(d) for d in _iter_recompiler_build_dirs(fw))
|
||||
|
||||
|
||||
def ensure_bios_emitter(
|
||||
fw: Path,
|
||||
*,
|
||||
game_root: Path | None = None,
|
||||
build_type: str = DEFAULT_BUILD_TYPE,
|
||||
dry_run: bool = False,
|
||||
log: LogFn | None = None,
|
||||
) -> CmdResult:
|
||||
"""Configure ``recompiler/build`` and build ``psxrecomp-bios`` if needed."""
|
||||
host = detect_host()
|
||||
if not host.cmake:
|
||||
return CmdResult(False, "cmake not found on PATH (needed to build psxrecomp-bios)")
|
||||
|
||||
existing = _find_psxrecomp_bios(fw, game_root)
|
||||
if existing is not None and not dry_run:
|
||||
if log:
|
||||
log(f"BIOS emitter ready: {existing}")
|
||||
return CmdResult(True, f"BIOS emitter ready: {existing.name}")
|
||||
|
||||
src = fw / "recompiler"
|
||||
if not (src / "CMakeLists.txt").is_file():
|
||||
return CmdResult(False, f"recompiler sources missing under {src}")
|
||||
|
||||
# Prefer an already-finished tree (matches regen_bios.sh discovery order).
|
||||
build_dir = src / "build"
|
||||
for cand in _iter_recompiler_build_dirs(fw):
|
||||
if _recompiler_build_usable(cand):
|
||||
build_dir = cand
|
||||
break
|
||||
|
||||
gen = default_generator(host)
|
||||
cfg = [host.cmake, "-S", str(src), "-B", str(build_dir)]
|
||||
if gen:
|
||||
cfg.extend(["-G", gen])
|
||||
cfg.append(f"-DCMAKE_BUILD_TYPE={build_type}")
|
||||
|
||||
if dry_run:
|
||||
msg = "dry-run: " + " ".join(cfg)
|
||||
if log:
|
||||
log(msg)
|
||||
log(f"dry-run: {host.cmake} --build {build_dir} --target psxrecomp-bios")
|
||||
return CmdResult(True, msg)
|
||||
|
||||
if not _recompiler_build_usable(build_dir):
|
||||
if log:
|
||||
log("Configuring recompiler for psxrecomp-bios…")
|
||||
build_dir.mkdir(parents=True, exist_ok=True)
|
||||
r = _run_stream(cfg, fw, log=log)
|
||||
if not r.ok:
|
||||
return CmdResult(
|
||||
False,
|
||||
"Failed to configure recompiler (needed for OpenBIOS regen)",
|
||||
r.detail,
|
||||
)
|
||||
|
||||
jobs = str(host.jobs)
|
||||
build_cmd = [
|
||||
host.cmake,
|
||||
"--build",
|
||||
str(build_dir),
|
||||
"--target",
|
||||
"psxrecomp-bios",
|
||||
"-j",
|
||||
jobs,
|
||||
]
|
||||
if log:
|
||||
log("Building psxrecomp-bios…")
|
||||
r = _run_stream(build_cmd, fw, log=log)
|
||||
if not r.ok:
|
||||
return CmdResult(False, "Failed to build psxrecomp-bios", r.detail)
|
||||
|
||||
bios = _find_psxrecomp_bios(fw, game_root)
|
||||
if bios is None:
|
||||
return CmdResult(
|
||||
False,
|
||||
f"psxrecomp-bios not found after build under {build_dir}",
|
||||
)
|
||||
return CmdResult(True, f"Built BIOS emitter: {bios.name}")
|
||||
|
||||
|
||||
def _regen_bios_profile(
|
||||
fw: Path,
|
||||
profile_rel: str,
|
||||
*,
|
||||
game_root: Path | None = None,
|
||||
dry_run: bool = False,
|
||||
log: LogFn | None = None,
|
||||
) -> CmdResult:
|
||||
"""Regen one BIOS profile into ``fw/generated/`` (canonical regen_bios.sh)."""
|
||||
profile = fw / profile_rel
|
||||
if not profile.is_file():
|
||||
return CmdResult(False, f"BIOS profile missing: {profile}")
|
||||
|
||||
script = fw / "tools" / "regen_bios.sh"
|
||||
bash = shutil.which("bash") or shutil.which("bash.exe")
|
||||
|
||||
if script.is_file() and bash:
|
||||
cmd = [bash, str(script), "--config", profile_rel]
|
||||
if dry_run:
|
||||
msg = "dry-run: " + " ".join(cmd) + f" (cwd={fw})"
|
||||
if log:
|
||||
log(msg)
|
||||
return CmdResult(True, msg)
|
||||
return _run_stream(cmd, fw, log=log)
|
||||
|
||||
# Fallback without bash: invoke emitter + optional fingerprint helper.
|
||||
r = ensure_bios_emitter(fw, game_root=game_root, dry_run=dry_run, log=log)
|
||||
if not r.ok:
|
||||
return r
|
||||
if dry_run:
|
||||
return CmdResult(True, f"dry-run: psxrecomp-bios --config {profile_rel}")
|
||||
|
||||
bios = _find_psxrecomp_bios(fw, game_root)
|
||||
if bios is None:
|
||||
return CmdResult(False, "psxrecomp-bios missing after ensure")
|
||||
(fw / "generated").mkdir(parents=True, exist_ok=True)
|
||||
r = _run_stream([str(bios), "--config", profile_rel], fw, log=log)
|
||||
if not r.ok:
|
||||
return CmdResult(False, f"psxrecomp-bios failed for {profile_rel}", r.detail)
|
||||
|
||||
# Best-effort fingerprint (staleness WARN in runtime.cmake).
|
||||
fp = fw / "tools" / "bios_emitter_fingerprint.sh"
|
||||
if fp.is_file() and bash:
|
||||
stem = OPENBIOS_STEM if "OpenBIOS" in profile_rel else SCPH1001_STEM
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bash, str(fp), profile_rel],
|
||||
cwd=str(fw),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode == 0 and proc.stdout.strip():
|
||||
out = fw / "generated" / f"{stem}.emitter.sha"
|
||||
out.write_text(proc.stdout, encoding="utf-8")
|
||||
if log:
|
||||
log(f"Wrote fingerprint {out.name}")
|
||||
except OSError:
|
||||
pass
|
||||
return CmdResult(True, f"Regenerated BIOS profile {profile_rel}")
|
||||
|
||||
|
||||
def ensure_bios_backends(
|
||||
root: Path,
|
||||
*,
|
||||
force: bool = False,
|
||||
include_scph1001: bool = True,
|
||||
dry_run: bool = False,
|
||||
log: LogFn | None = None,
|
||||
) -> CmdResult:
|
||||
"""Ensure linkable OpenBIOS (and optional SCPH1001) under ``psxrecomp/generated``.
|
||||
|
||||
OpenBIOS is bundled and MIT-licensed. SCPH1001 is regenerated only when the
|
||||
retail dump ``bios/SCPH1001.BIN`` is already present beside the profile.
|
||||
"""
|
||||
root = root.expanduser().resolve()
|
||||
fw = resolve_framework_root(root)
|
||||
if fw is None:
|
||||
return CmdResult(True, "No psxrecomp framework — skipping BIOS ensure")
|
||||
|
||||
needed: list[tuple[str, str]] = []
|
||||
if force or not bios_backend_present(fw, OPENBIOS_STEM):
|
||||
needed.append((OPENBIOS_STEM, OPENBIOS_PROFILE))
|
||||
elif log:
|
||||
log(f"OpenBIOS backend already present under {fw / 'generated'}")
|
||||
|
||||
scph_rom = fw / "bios" / "SCPH1001.BIN"
|
||||
if include_scph1001 and scph_rom.is_file():
|
||||
if force or not bios_backend_present(fw, SCPH1001_STEM):
|
||||
needed.append((SCPH1001_STEM, SCPH1001_PROFILE))
|
||||
elif log:
|
||||
log("SCPH1001 backend already present")
|
||||
|
||||
if not needed:
|
||||
return CmdResult(True, "BIOS backends already generated")
|
||||
|
||||
# Prefer regen_bios.sh (builds emitter + fingerprints). It does not configure
|
||||
# the recompiler — ensure a usable tree (or a found binary) first.
|
||||
script = fw / "tools" / "regen_bios.sh"
|
||||
bash = shutil.which("bash") or shutil.which("bash.exe")
|
||||
need_emitter_setup = not (
|
||||
_any_usable_recompiler_build(fw) or _find_psxrecomp_bios(fw, root) is not None
|
||||
)
|
||||
if need_emitter_setup or not (script.is_file() and bash):
|
||||
r = ensure_bios_emitter(fw, game_root=root, dry_run=dry_run, log=log)
|
||||
if not r.ok:
|
||||
return r
|
||||
|
||||
done: list[str] = []
|
||||
for stem, profile in needed:
|
||||
if log:
|
||||
log(f"Regenerating {stem} via {profile}…")
|
||||
r = _regen_bios_profile(
|
||||
fw, profile, game_root=root, dry_run=dry_run, log=log
|
||||
)
|
||||
if not r.ok:
|
||||
return CmdResult(
|
||||
False,
|
||||
f"Failed to regenerate {stem}: {r.message}",
|
||||
r.detail,
|
||||
)
|
||||
if not dry_run and not bios_backend_present(fw, stem):
|
||||
return CmdResult(
|
||||
False,
|
||||
f"Regen finished but {stem} backend still missing under {fw / 'generated'}",
|
||||
)
|
||||
done.append(stem)
|
||||
|
||||
return CmdResult(True, "BIOS ready: " + ", ".join(done))
|
||||
|
||||
|
||||
def configure(
|
||||
root: Path,
|
||||
*,
|
||||
@@ -164,6 +569,7 @@ def configure(
|
||||
extra_args: list[str] | None = None,
|
||||
dry_run: bool = False,
|
||||
log: LogFn | None = None,
|
||||
ensure_bios: bool = True,
|
||||
) -> CmdResult:
|
||||
root = root.expanduser().resolve()
|
||||
host = detect_host()
|
||||
@@ -172,6 +578,19 @@ def configure(
|
||||
if not (root / "CMakeLists.txt").is_file():
|
||||
return CmdResult(False, f"No CMakeLists.txt in {root}")
|
||||
|
||||
if ensure_bios and not _allow_no_bios(extra_args):
|
||||
bios_r = ensure_bios_backends(root, dry_run=dry_run, log=log)
|
||||
if not bios_r.ok:
|
||||
return bios_r
|
||||
if log and bios_r.message:
|
||||
log(bios_r.message)
|
||||
|
||||
pre = preflight_max_players(root)
|
||||
if pre is not None:
|
||||
if log:
|
||||
log(pre.message)
|
||||
return pre
|
||||
|
||||
bdir = Path(build_dir)
|
||||
if not bdir.is_absolute():
|
||||
bdir = root / bdir
|
||||
@@ -191,7 +610,18 @@ def configure(
|
||||
|
||||
r = _run_stream(cmd, root, log=log)
|
||||
if r.ok:
|
||||
r = CmdResult(True, f"Configured {bdir.name} ({build_type}" + (f", {gen}" if gen else "") + ")", r.detail)
|
||||
r = CmdResult(
|
||||
True,
|
||||
f"Configured {bdir.name} ({build_type}" + (f", {gen}" if gen else "") + ")",
|
||||
r.detail,
|
||||
)
|
||||
return r
|
||||
hint = diagnose_configure_failure(r.detail or "", root)
|
||||
if hint:
|
||||
msg = f"{r.message}\n{hint}"
|
||||
if log:
|
||||
log(hint)
|
||||
return CmdResult(False, msg, r.detail)
|
||||
return r
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from .models import (
|
||||
@@ -52,6 +53,57 @@ def _is_real_psxrecomp_tree(path: Path) -> bool:
|
||||
return (path / "runtime" / "runtime.cmake").is_file()
|
||||
|
||||
|
||||
def _git_checkout_ok(path: Path) -> bool:
|
||||
"""True when ``git -C path rev-parse`` sees a live work tree."""
|
||||
if not path.is_dir():
|
||||
return False
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["git", "-C", str(path), "rev-parse", "--is-inside-work-tree"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
return proc.returncode == 0 and proc.stdout.strip() == "true"
|
||||
|
||||
|
||||
def diagnose_psxrecomp_checkout(root: Path) -> str | None:
|
||||
"""Return a human reason when ``psxrecomp/`` is present but not a usable git checkout.
|
||||
|
||||
Common after consolidating away ``psxrecomp-v4``: ``psxrecomp/.git`` still
|
||||
points at ``../.git/modules/psxrecomp-v4`` (deleted), so Studio module ops
|
||||
report ``checkout missing`` even though ``runtime.cmake`` is on disk.
|
||||
"""
|
||||
dest = root / "psxrecomp"
|
||||
if not dest.is_dir():
|
||||
return None
|
||||
if not _is_real_psxrecomp_tree(dest):
|
||||
return None
|
||||
if _git_checkout_ok(dest):
|
||||
return None
|
||||
|
||||
git_file = dest / ".git"
|
||||
if git_file.is_file():
|
||||
text = _read(git_file).strip()
|
||||
if text.lower().startswith("gitdir:"):
|
||||
rel = text.split(":", 1)[1].strip()
|
||||
target = (dest / rel).resolve() if rel else None
|
||||
if target is not None and not target.exists():
|
||||
return (
|
||||
f"psxrecomp/.git points at missing gitdir ({rel}) — "
|
||||
"broken submodule metadata (often leftover psxrecomp-v4)."
|
||||
)
|
||||
return f"psxrecomp/.git gitdir is unusable ({rel or text})."
|
||||
if git_file.is_dir():
|
||||
return "psxrecomp/.git exists but git rev-parse fails."
|
||||
return (
|
||||
"psxrecomp/ has runtime.cmake but is not a git checkout "
|
||||
"(absorbed into the parent tree or missing .git)."
|
||||
)
|
||||
|
||||
|
||||
def _cmake_has(text: str, needle: str) -> bool:
|
||||
return needle in text
|
||||
|
||||
@@ -91,7 +143,10 @@ def audit_project(root: Path) -> AuditReport:
|
||||
title="Root psxrecomp/ submodule",
|
||||
status=CheckStatus.WARN,
|
||||
severity=Severity.REQUIRED,
|
||||
detail="Both psxrecomp/ and psxrecomp-v4 present — consolidate to psxrecomp/.",
|
||||
detail=(
|
||||
"Both psxrecomp/ and psxrecomp-v4 present — keep psxrecomp/ "
|
||||
"and delete the legacy psxrecomp-v4 tree."
|
||||
),
|
||||
fix_op="rename_psxrecomp_submodule",
|
||||
)
|
||||
)
|
||||
@@ -107,15 +162,28 @@ def audit_project(root: Path) -> AuditReport:
|
||||
)
|
||||
)
|
||||
elif has_psx:
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="submodule_psxrecomp",
|
||||
title="Root psxrecomp/ submodule",
|
||||
status=CheckStatus.PASS,
|
||||
severity=Severity.REQUIRED,
|
||||
detail="psxrecomp/ present with runtime.cmake.",
|
||||
broken = diagnose_psxrecomp_checkout(root)
|
||||
if broken:
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="submodule_psxrecomp",
|
||||
title="Root psxrecomp/ submodule",
|
||||
status=CheckStatus.FAIL,
|
||||
severity=Severity.REQUIRED,
|
||||
detail=broken + " Repair re-clones as a real submodule.",
|
||||
fix_op="repair_psxrecomp_submodule",
|
||||
)
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="submodule_psxrecomp",
|
||||
title="Root psxrecomp/ submodule",
|
||||
status=CheckStatus.PASS,
|
||||
severity=Severity.REQUIRED,
|
||||
detail="psxrecomp/ present with runtime.cmake.",
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
CheckResult(
|
||||
@@ -382,14 +450,16 @@ def audit_project(root: Path) -> AuditReport:
|
||||
)
|
||||
|
||||
# VERSION
|
||||
ver_text = ""
|
||||
if (root / "VERSION").is_file():
|
||||
ver_text = _read(root / "VERSION").strip()
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="version_file",
|
||||
title="VERSION",
|
||||
status=CheckStatus.PASS,
|
||||
severity=Severity.RECOMMENDED,
|
||||
detail=_read(root / "VERSION").strip() or "(empty)",
|
||||
detail=ver_text or "(empty)",
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -404,6 +474,48 @@ def audit_project(root: Path) -> AuditReport:
|
||||
)
|
||||
)
|
||||
|
||||
# Lobby pin stamp vs VERSION (build tree) — drift breaks netplay lists.
|
||||
stamp_hits: list[tuple[str, str]] = []
|
||||
for build in sorted(root.glob("build*")):
|
||||
if not build.is_dir():
|
||||
continue
|
||||
for stamp in (
|
||||
build / "psx_game_version.txt",
|
||||
build / "Release" / "psx_game_version.txt",
|
||||
):
|
||||
if stamp.is_file():
|
||||
stamp_hits.append(
|
||||
(str(stamp.relative_to(root)).replace("\\", "/"),
|
||||
_read(stamp).strip())
|
||||
)
|
||||
if ver_text and stamp_hits:
|
||||
bad = [(p, s) for p, s in stamp_hits if s and s.lstrip("vV") != ver_text.lstrip("vV")]
|
||||
if bad:
|
||||
detail = "; ".join(f"{p}={s} (VERSION={ver_text})" for p, s in bad[:3])
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="version_stamp_match",
|
||||
title="Lobby pin stamp",
|
||||
status=CheckStatus.FAIL,
|
||||
severity=Severity.REQUIRED,
|
||||
detail=(
|
||||
"psx_game_version.txt disagrees with VERSION — "
|
||||
"rebuild with -DPSX_GAME_VERSION matching VERSION before "
|
||||
"packaging/releasing. " + detail
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="version_stamp_match",
|
||||
title="Lobby pin stamp",
|
||||
status=CheckStatus.PASS,
|
||||
severity=Severity.RECOMMENDED,
|
||||
detail="Build stamp matches VERSION.",
|
||||
)
|
||||
)
|
||||
|
||||
# symbols
|
||||
if (root / "symbols.toml").is_file():
|
||||
checks.append(
|
||||
@@ -527,6 +639,30 @@ def audit_project(root: Path) -> AuditReport:
|
||||
)
|
||||
)
|
||||
|
||||
# Default RetComM-themed app icon (Windows .ico + PNG)
|
||||
app_ico = root / "assets" / "psxrecomp.ico"
|
||||
if app_ico.is_file():
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="app_icon",
|
||||
title="assets/psxrecomp app icon",
|
||||
status=CheckStatus.PASS,
|
||||
severity=Severity.OPTIONAL,
|
||||
detail="assets/psxrecomp.ico present.",
|
||||
)
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
CheckResult(
|
||||
id="app_icon",
|
||||
title="assets/psxrecomp app icon",
|
||||
status=CheckStatus.WARN,
|
||||
severity=Severity.RECOMMENDED,
|
||||
detail="Missing assets/psxrecomp.ico (RetComM-themed default pad icon).",
|
||||
fix_op="ensure_app_icon",
|
||||
)
|
||||
)
|
||||
|
||||
# gitignore essentials
|
||||
gi = _read(root / ".gitignore")
|
||||
missing_gi = [
|
||||
|
||||
@@ -282,6 +282,10 @@ def build_token_map(
|
||||
' # LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"'
|
||||
)
|
||||
|
||||
app_icon = (
|
||||
' APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"'
|
||||
)
|
||||
|
||||
return {
|
||||
"PROJECT_CMAKE_NAME": cmake_name,
|
||||
"WINDOW_TITLE": title,
|
||||
@@ -302,6 +306,7 @@ def build_token_map(
|
||||
"NETPLAY_LOBBY_URL_ARG": netplay_lobby,
|
||||
"WIZARD_RUNTIME_ARG": wizard_runtime,
|
||||
"BOXART_CMAKE_ARG": boxart,
|
||||
"APP_ICON_CMAKE_ARG": app_icon,
|
||||
"NETPLAY_CMAKE_BLOCK": netplay_block,
|
||||
"WIZARD_CMAKE_BLOCK": wizard_block,
|
||||
"RECOMP_UI_CMAKE_BLOCK": recomp_ui_block,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -20,7 +21,7 @@ from .naming import (
|
||||
window_title_from_cmake,
|
||||
window_title_from_name,
|
||||
)
|
||||
from .paths import ci_setup_release_template, templates_dir, toolkit_dir
|
||||
from .paths import ci_setup_release_template, psxrecomp_root_from_toolkit, templates_dir, toolkit_dir
|
||||
|
||||
|
||||
def _run(cmd: list[str], cwd: Path, dry_run: bool) -> tuple[bool, str]:
|
||||
@@ -70,6 +71,100 @@ def _is_real_psxrecomp(path: Path) -> bool:
|
||||
return (path / "runtime" / "runtime.cmake").is_file()
|
||||
|
||||
|
||||
def _strip_gitmodules_submodule(text: str, name: str) -> str:
|
||||
"""Remove a ``[submodule "name"]`` block (and its indented keys) from .gitmodules."""
|
||||
lines = text.splitlines(keepends=True)
|
||||
out: list[str] = []
|
||||
skipping = False
|
||||
header = f'[submodule "{name}"]'
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("[submodule ") and stripped.endswith("]"):
|
||||
skipping = stripped == header
|
||||
if skipping:
|
||||
continue
|
||||
if skipping:
|
||||
# Indented key = value lines belong to the skipped block.
|
||||
if line[:1] in (" ", "\t") or stripped == "":
|
||||
continue
|
||||
skipping = False
|
||||
if not skipping:
|
||||
out.append(line)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _remove_psxrecomp_v4(root: Path, options: MigrateOptions) -> tuple[bool, str, list[str]]:
|
||||
"""Drop legacy ``psxrecomp-v4`` (submodule or plain tree). Prefer keeping ``psxrecomp/``."""
|
||||
v4 = root / "psxrecomp-v4"
|
||||
changed: list[str] = []
|
||||
if not v4.exists():
|
||||
return True, "psxrecomp-v4 already absent", changed
|
||||
|
||||
notes: list[str] = []
|
||||
if options.dry_run:
|
||||
return True, "dry-run: remove psxrecomp-v4 (deinit/rm or rmtree)", ["psxrecomp-v4"]
|
||||
|
||||
# Registered submodule: deinit + git rm cleans the gitlink and worktree.
|
||||
gm = root / ".gitmodules"
|
||||
gm_text = gm.read_text(encoding="utf-8") if gm.is_file() else ""
|
||||
registered = 'path = psxrecomp-v4' in gm_text or '[submodule "psxrecomp-v4"]' in gm_text
|
||||
|
||||
if registered or (v4 / ".git").exists():
|
||||
ok, out = _run(
|
||||
["git", "submodule", "deinit", "-f", "psxrecomp-v4"], root, False
|
||||
)
|
||||
if out:
|
||||
notes.append(out if ok else f"deinit: {out}")
|
||||
ok_rm, out_rm = _run(["git", "rm", "-f", "psxrecomp-v4"], root, False)
|
||||
if ok_rm:
|
||||
notes.append(out_rm or "git rm psxrecomp-v4")
|
||||
changed.append("psxrecomp-v4")
|
||||
else:
|
||||
# Fallback: force-remove the directory if git rm refused (untracked tree).
|
||||
notes.append(f"git rm: {out_rm}")
|
||||
try:
|
||||
if v4.is_symlink() or v4.is_file():
|
||||
v4.unlink()
|
||||
elif v4.is_dir():
|
||||
shutil.rmtree(v4)
|
||||
changed.append("psxrecomp-v4")
|
||||
notes.append("removed psxrecomp-v4 via rmtree")
|
||||
except OSError as exc:
|
||||
return False, f"could not remove psxrecomp-v4: {exc}", changed
|
||||
else:
|
||||
try:
|
||||
if v4.is_symlink() or v4.is_file():
|
||||
v4.unlink()
|
||||
elif v4.is_dir():
|
||||
shutil.rmtree(v4)
|
||||
changed.append("psxrecomp-v4")
|
||||
notes.append("removed untracked psxrecomp-v4/")
|
||||
except OSError as exc:
|
||||
return False, f"could not remove psxrecomp-v4: {exc}", changed
|
||||
|
||||
# Ensure .gitmodules no longer lists the legacy path (git rm usually does this).
|
||||
if gm.is_file():
|
||||
text = gm.read_text(encoding="utf-8")
|
||||
new = _strip_gitmodules_submodule(text, "psxrecomp-v4")
|
||||
new = new.replace("path = psxrecomp-v4\n", "")
|
||||
if new != text:
|
||||
_write(gm, new, False)
|
||||
if ".gitmodules" not in changed:
|
||||
changed.append(".gitmodules")
|
||||
notes.append("stripped psxrecomp-v4 from .gitmodules")
|
||||
|
||||
# Drop stale git modules metadata if present.
|
||||
mod_meta = root / ".git" / "modules" / "psxrecomp-v4"
|
||||
if mod_meta.is_dir():
|
||||
try:
|
||||
shutil.rmtree(mod_meta)
|
||||
notes.append("removed .git/modules/psxrecomp-v4")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return True, "; ".join(n for n in notes if n) or "removed psxrecomp-v4", changed
|
||||
|
||||
|
||||
def _resolve_tokens(root: Path, options: MigrateOptions) -> dict[str, str]:
|
||||
name = options.project_name or infer_project_name(root)
|
||||
boot = (
|
||||
@@ -82,6 +177,12 @@ def _resolve_tokens(root: Path, options: MigrateOptions) -> dict[str, str]:
|
||||
detected = players_from_cmake(root / "CMakeLists.txt")
|
||||
if detected is not None:
|
||||
players = detected
|
||||
else:
|
||||
from .naming import players_from_game_toml
|
||||
|
||||
detected = players_from_game_toml(root / "game.toml")
|
||||
if detected is not None:
|
||||
players = detected
|
||||
title = options.window_title or window_title_from_cmake(root / "CMakeLists.txt")
|
||||
if not title:
|
||||
# Prefer game.toml window_title
|
||||
@@ -124,11 +225,36 @@ def _resolve_tokens(root: Path, options: MigrateOptions) -> dict[str, str]:
|
||||
|
||||
|
||||
def op_rename_psxrecomp_submodule(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
"""Consolidate on ``psxrecomp/``: promote v4 if needed, always delete leftover v4."""
|
||||
v4 = root / "psxrecomp-v4"
|
||||
dest = root / "psxrecomp"
|
||||
changed: list[str] = []
|
||||
parts: list[str] = []
|
||||
|
||||
if not v4.exists() and _is_real_psxrecomp(dest):
|
||||
# Still scrub orphan .gitmodules / CMake references to the old name.
|
||||
scrubbed: list[str] = []
|
||||
gm = root / ".gitmodules"
|
||||
if gm.is_file():
|
||||
text = gm.read_text(encoding="utf-8")
|
||||
new = _strip_gitmodules_submodule(text, "psxrecomp-v4")
|
||||
if new != text:
|
||||
_write(gm, new, options.dry_run)
|
||||
scrubbed.append(".gitmodules")
|
||||
cmake = root / "CMakeLists.txt"
|
||||
if cmake.is_file():
|
||||
text = cmake.read_text(encoding="utf-8")
|
||||
new = text.replace("psxrecomp-v4", "psxrecomp")
|
||||
if new != text:
|
||||
_write(cmake, new, options.dry_run)
|
||||
scrubbed.append("CMakeLists.txt")
|
||||
if scrubbed:
|
||||
return ApplyResult(
|
||||
"rename_psxrecomp_submodule",
|
||||
True,
|
||||
"Already using psxrecomp/; scrubbed leftover psxrecomp-v4 refs",
|
||||
scrubbed,
|
||||
)
|
||||
return ApplyResult("rename_psxrecomp_submodule", True, "Already using psxrecomp/", [])
|
||||
|
||||
if not v4.exists():
|
||||
@@ -136,43 +262,71 @@ def op_rename_psxrecomp_submodule(root: Path, options: MigrateOptions) -> ApplyR
|
||||
"rename_psxrecomp_submodule", False, "psxrecomp-v4 not found", []
|
||||
)
|
||||
|
||||
if dest.exists() and not _is_real_psxrecomp(dest):
|
||||
stub_bak = root / "psxrecomp.stub.bak"
|
||||
if options.dry_run:
|
||||
msg = f"dry-run: move stub {dest} → {stub_bak}, then {v4} → {dest}"
|
||||
else:
|
||||
if stub_bak.exists():
|
||||
shutil.rmtree(stub_bak)
|
||||
dest.rename(stub_bak)
|
||||
changed.append(str(stub_bak.relative_to(root)))
|
||||
msg = f"Moved stub psxrecomp/ → {stub_bak.name}"
|
||||
elif dest.exists() and _is_real_psxrecomp(dest):
|
||||
# Real tree already — drop v4 submodule entry if possible
|
||||
msg = "psxrecomp/ already real; will update .gitmodules and leave v4 for manual removal"
|
||||
else:
|
||||
msg = ""
|
||||
|
||||
if not dest.exists() or not _is_real_psxrecomp(dest):
|
||||
ok, out = _run(["git", "mv", "psxrecomp-v4", "psxrecomp"], root, options.dry_run)
|
||||
# Case A: real psxrecomp/ already — keep it, remove legacy v4 entirely.
|
||||
if dest.exists() and _is_real_psxrecomp(dest):
|
||||
ok, out, ch = _remove_psxrecomp_v4(root, options)
|
||||
changed.extend(ch)
|
||||
parts.append(out)
|
||||
if not ok:
|
||||
# Fallback without git mv
|
||||
return ApplyResult("rename_psxrecomp_submodule", False, out, changed)
|
||||
# Removing v4 often deletes .git/modules/psxrecomp-v4 while psxrecomp/.git
|
||||
# still points there — repair into a real submodule checkout.
|
||||
if not options.dry_run and not _psxrecomp_git_ok(dest):
|
||||
fix = op_repair_psxrecomp_submodule(root, options)
|
||||
changed.extend(fix.changed_paths)
|
||||
parts.append(fix.message)
|
||||
if not fix.ok:
|
||||
return ApplyResult(
|
||||
"rename_psxrecomp_submodule", False, "; ".join(parts), changed
|
||||
)
|
||||
else:
|
||||
# Case B: stub or missing psxrecomp/ — promote v4 into place.
|
||||
if dest.exists() and not _is_real_psxrecomp(dest):
|
||||
stub_bak = root / "psxrecomp.stub.bak"
|
||||
if options.dry_run:
|
||||
out = "dry-run: shutil.move psxrecomp-v4 → psxrecomp"
|
||||
parts.append(f"dry-run: move stub {dest} → {stub_bak}")
|
||||
else:
|
||||
shutil.move(str(v4), str(dest))
|
||||
out = "Moved psxrecomp-v4 → psxrecomp (without git mv)"
|
||||
changed.append("psxrecomp")
|
||||
msg = (msg + "; " if msg else "") + out
|
||||
if stub_bak.exists():
|
||||
shutil.rmtree(stub_bak)
|
||||
dest.rename(stub_bak)
|
||||
changed.append(str(stub_bak.relative_to(root)))
|
||||
parts.append(f"Moved stub psxrecomp/ → {stub_bak.name}")
|
||||
|
||||
# Rewrite .gitmodules
|
||||
if not dest.exists() or not _is_real_psxrecomp(dest):
|
||||
ok, out = _run(
|
||||
["git", "mv", "psxrecomp-v4", "psxrecomp"], root, options.dry_run
|
||||
)
|
||||
if not ok:
|
||||
if options.dry_run:
|
||||
out = "dry-run: shutil.move psxrecomp-v4 → psxrecomp"
|
||||
else:
|
||||
shutil.move(str(v4), str(dest))
|
||||
out = "Moved psxrecomp-v4 → psxrecomp (without git mv)"
|
||||
changed.append("psxrecomp")
|
||||
parts.append(out)
|
||||
|
||||
# After promote, any leftover v4 path must still go (rare dual gitlink).
|
||||
if v4.exists():
|
||||
ok, out, ch = _remove_psxrecomp_v4(root, options)
|
||||
changed.extend(ch)
|
||||
parts.append(out)
|
||||
if not ok:
|
||||
return ApplyResult("rename_psxrecomp_submodule", False, out, changed)
|
||||
|
||||
# Rewrite .gitmodules: rename v4 → psxrecomp if still named v4, then strip orphans.
|
||||
gm = root / ".gitmodules"
|
||||
if gm.is_file():
|
||||
text = gm.read_text(encoding="utf-8")
|
||||
new = text.replace('path = psxrecomp-v4', "path = psxrecomp")
|
||||
new = new.replace('[submodule "psxrecomp-v4"]', '[submodule "psxrecomp"]')
|
||||
# If both names somehow remain, drop the legacy block.
|
||||
if '[submodule "psxrecomp-v4"]' in new or "path = psxrecomp-v4" in new:
|
||||
new = _strip_gitmodules_submodule(new, "psxrecomp-v4")
|
||||
if new != text:
|
||||
_write(gm, new, options.dry_run)
|
||||
changed.append(".gitmodules")
|
||||
if ".gitmodules" not in changed:
|
||||
changed.append(".gitmodules")
|
||||
parts.append("updated .gitmodules")
|
||||
|
||||
# Patch CMakeLists PSXRECOMP_ROOT path if present
|
||||
cmake = root / "CMakeLists.txt"
|
||||
@@ -182,13 +336,51 @@ def op_rename_psxrecomp_submodule(root: Path, options: MigrateOptions) -> ApplyR
|
||||
if new != text:
|
||||
_write(cmake, new, options.dry_run)
|
||||
changed.append("CMakeLists.txt")
|
||||
parts.append("rewrote CMakeLists psxrecomp-v4 → psxrecomp")
|
||||
|
||||
return ApplyResult("rename_psxrecomp_submodule", True, msg or "Renamed submodule", changed)
|
||||
msg = "; ".join(p for p in parts if p) or "Consolidated on psxrecomp/"
|
||||
return ApplyResult("rename_psxrecomp_submodule", True, msg, changed)
|
||||
|
||||
|
||||
def _gitmodules_psxrecomp_url_branch(root: Path) -> tuple[str, str]:
|
||||
"""Return (url, branch) for the game's psxrecomp submodule entry."""
|
||||
url = "https://github.com/mstan/psxrecomp.git"
|
||||
branch = "master"
|
||||
gm = root / ".gitmodules"
|
||||
if not gm.is_file():
|
||||
return url, branch
|
||||
text = gm.read_text(encoding="utf-8")
|
||||
for block in re.split(r"(?=\[submodule)", text):
|
||||
if not re.search(r"^\s*path\s*=\s*psxrecomp\s*$", block, re.M):
|
||||
continue
|
||||
m = re.search(r"^\s*url\s*=\s*(\S+)", block, re.M)
|
||||
if m:
|
||||
url = m.group(1)
|
||||
m = re.search(r"^\s*branch\s*=\s*(\S+)", block, re.M)
|
||||
if m:
|
||||
branch = m.group(1)
|
||||
break
|
||||
return url, branch
|
||||
|
||||
|
||||
def _psxrecomp_git_ok(path: Path) -> bool:
|
||||
if not path.is_dir():
|
||||
return False
|
||||
ok, out = _run(
|
||||
["git", "-C", str(path), "rev-parse", "--is-inside-work-tree"],
|
||||
path.parent,
|
||||
False,
|
||||
)
|
||||
return ok and out.strip() == "true"
|
||||
|
||||
|
||||
def op_ensure_psxrecomp_submodule(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
if _is_real_psxrecomp(root / "psxrecomp"):
|
||||
dest = root / "psxrecomp"
|
||||
if _is_real_psxrecomp(dest) and _psxrecomp_git_ok(dest):
|
||||
return ApplyResult("ensure_psxrecomp_submodule", True, "Already present", [])
|
||||
if _is_real_psxrecomp(dest) and not _psxrecomp_git_ok(dest):
|
||||
# Tree on disk but broken gitdir — repair, don't no-op.
|
||||
return op_repair_psxrecomp_submodule(root, options)
|
||||
url = "https://github.com/mstan/psxrecomp.git"
|
||||
ok, out = _run(
|
||||
["git", "submodule", "add", "-b", "master", url, "psxrecomp"],
|
||||
@@ -210,6 +402,162 @@ def op_ensure_psxrecomp_submodule(root: Path, options: MigrateOptions) -> ApplyR
|
||||
)
|
||||
|
||||
|
||||
def op_repair_psxrecomp_submodule(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
"""Re-establish ``psxrecomp/`` as a real git submodule after a broken gitdir.
|
||||
|
||||
Moves the on-disk tree aside (``psxrecomp.broken.bak``), clears absorbed
|
||||
index entries, then ``submodule update --init`` / ``submodule add``.
|
||||
"""
|
||||
dest = root / "psxrecomp"
|
||||
changed: list[str] = []
|
||||
parts: list[str] = []
|
||||
|
||||
if not dest.is_dir():
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
False,
|
||||
"psxrecomp/ missing — use ensure_psxrecomp_submodule instead",
|
||||
[],
|
||||
)
|
||||
|
||||
if _is_real_psxrecomp(dest) and _psxrecomp_git_ok(dest):
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
True,
|
||||
"psxrecomp/ checkout already healthy",
|
||||
[],
|
||||
)
|
||||
|
||||
url, branch = _gitmodules_psxrecomp_url_branch(root)
|
||||
if options.dry_run:
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
True,
|
||||
f"dry-run: move psxrecomp/ aside, re-init submodule from {url}@{branch}",
|
||||
["psxrecomp", ".gitmodules"],
|
||||
)
|
||||
|
||||
# Ensure .gitmodules lists the canonical submodule before re-init.
|
||||
gm = root / ".gitmodules"
|
||||
gm_text = gm.read_text(encoding="utf-8") if gm.is_file() else ""
|
||||
if 'path = psxrecomp' not in gm_text and 'path=psxrecomp' not in gm_text:
|
||||
block = (
|
||||
f'[submodule "psxrecomp"]\n'
|
||||
f"\tpath = psxrecomp\n"
|
||||
f"\turl = {url}\n"
|
||||
f"\tbranch = {branch}\n"
|
||||
)
|
||||
_write(gm, (gm_text.rstrip() + "\n\n" if gm_text.strip() else "") + block, False)
|
||||
changed.append(".gitmodules")
|
||||
parts.append("wrote .gitmodules psxrecomp entry")
|
||||
else:
|
||||
# Make sure url/branch are present on the existing block (best-effort).
|
||||
if url and url not in gm_text:
|
||||
parts.append(f"using url {url}")
|
||||
|
||||
bak = root / "psxrecomp.broken.bak"
|
||||
if bak.exists():
|
||||
n = 1
|
||||
while (root / f"psxrecomp.broken.bak.{n}").exists():
|
||||
n += 1
|
||||
bak = root / f"psxrecomp.broken.bak.{n}"
|
||||
|
||||
# Drop absorbed tree from the index (mode 100644 flood) so a gitlink can land.
|
||||
ok_rm, out_rm = _run(["git", "rm", "-rf", "--cached", "psxrecomp"], root, False)
|
||||
if ok_rm:
|
||||
parts.append(out_rm or "git rm --cached psxrecomp")
|
||||
elif out_rm:
|
||||
parts.append(f"git rm --cached: {out_rm}")
|
||||
|
||||
try:
|
||||
dest.rename(bak)
|
||||
changed.append(bak.name)
|
||||
parts.append(f"moved broken tree → {bak.name}")
|
||||
except OSError as exc:
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
False,
|
||||
f"could not move broken psxrecomp/: {exc}",
|
||||
changed,
|
||||
)
|
||||
|
||||
# Prefer update --init when .gitmodules already declares the path.
|
||||
_run(["git", "submodule", "sync", "--", "psxrecomp"], root, False)
|
||||
ok_up, out_up = _run(
|
||||
[
|
||||
"git",
|
||||
"submodule",
|
||||
"update",
|
||||
"--init",
|
||||
"--force",
|
||||
"--recursive",
|
||||
"psxrecomp",
|
||||
],
|
||||
root,
|
||||
False,
|
||||
)
|
||||
if ok_up and _psxrecomp_git_ok(dest) and _is_real_psxrecomp(dest):
|
||||
changed.append("psxrecomp")
|
||||
parts.append(out_up or "submodule update --init psxrecomp")
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
True,
|
||||
"; ".join(parts),
|
||||
changed,
|
||||
)
|
||||
|
||||
# Fresh add (works when no gitlink was ever recorded).
|
||||
if dest.exists():
|
||||
try:
|
||||
shutil.rmtree(dest)
|
||||
except OSError:
|
||||
pass
|
||||
ok_add, out_add = _run(
|
||||
["git", "submodule", "add", "-b", branch, "--force", url, "psxrecomp"],
|
||||
root,
|
||||
False,
|
||||
)
|
||||
if not ok_add:
|
||||
# Restore bak so the user keeps a buildable tree.
|
||||
if bak.exists() and not dest.exists():
|
||||
try:
|
||||
bak.rename(dest)
|
||||
parts.append("restored broken tree after failed add")
|
||||
except OSError:
|
||||
pass
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
False,
|
||||
f"submodule add failed: {out_add}; update: {out_up}",
|
||||
changed,
|
||||
)
|
||||
|
||||
_run(
|
||||
["git", "submodule", "update", "--init", "--recursive", "psxrecomp"],
|
||||
root,
|
||||
False,
|
||||
)
|
||||
if not (_psxrecomp_git_ok(dest) and _is_real_psxrecomp(dest)):
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
False,
|
||||
"re-init finished but psxrecomp/ still not a healthy checkout",
|
||||
changed,
|
||||
)
|
||||
|
||||
changed.append("psxrecomp")
|
||||
if ".gitmodules" not in changed:
|
||||
changed.append(".gitmodules")
|
||||
parts.append(out_add or f"submodule add {url}")
|
||||
parts.append(f"old tree kept at {bak.name} (delete when satisfied)")
|
||||
return ApplyResult(
|
||||
"repair_psxrecomp_submodule",
|
||||
True,
|
||||
"; ".join(parts),
|
||||
changed,
|
||||
)
|
||||
|
||||
|
||||
def op_ensure_recomp_ui_submodule(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
if (root / "recomp-ui").is_dir() and (
|
||||
(root / "recomp-ui" / "CMakeLists.txt").is_file()
|
||||
@@ -258,6 +606,13 @@ def op_emit_codegen_setup(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
|
||||
|
||||
def op_emit_version(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
"""Write starter VERSION only when missing.
|
||||
|
||||
Never bump VERSION here — a newer VERSION without a rebuild leaves
|
||||
PSX_GAME_VERSION sticky in CMakeCache / an old binary, which breaks
|
||||
netplay lobby list filters. Releases must pin VERSION then rebuild with
|
||||
-DPSX_GAME_VERSION=<pin> (CI / package_setup_host enforce the stamp).
|
||||
"""
|
||||
dst = root / "VERSION"
|
||||
if dst.is_file() and not options.force:
|
||||
return ApplyResult("emit_version", True, "VERSION already exists", [])
|
||||
@@ -434,6 +789,58 @@ def op_emit_boxart_stub(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
)
|
||||
|
||||
|
||||
def _app_icon_source_dir(root: Path) -> Path | None:
|
||||
"""Prefer game submodule assets, then toolkit-adjacent psxrecomp checkout."""
|
||||
for cand in (
|
||||
root / "psxrecomp" / "assets",
|
||||
root / "psxrecomp-v4" / "assets",
|
||||
):
|
||||
if (cand / "psxrecomp.ico").is_file() or (cand / "psxrecomp.svg").is_file():
|
||||
return cand
|
||||
pr = psxrecomp_root_from_toolkit()
|
||||
if pr is not None:
|
||||
cand = pr / "assets"
|
||||
if (cand / "psxrecomp.ico").is_file() or (cand / "psxrecomp.svg").is_file():
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
def op_ensure_app_icon(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
dest_dir = root / "assets"
|
||||
dest_ico = dest_dir / "psxrecomp.ico"
|
||||
if dest_ico.is_file() and not options.force:
|
||||
return ApplyResult("ensure_app_icon", True, "assets/psxrecomp.ico already present", [])
|
||||
src_dir = _app_icon_source_dir(root)
|
||||
if src_dir is None:
|
||||
return ApplyResult(
|
||||
"ensure_app_icon",
|
||||
False,
|
||||
"No psxrecomp/assets icon source (add psxrecomp submodule or update toolkit)",
|
||||
[],
|
||||
)
|
||||
names = [n for n in ("psxrecomp.svg", "psxrecomp.png", "psxrecomp.ico") if (src_dir / n).is_file()]
|
||||
if not names:
|
||||
return ApplyResult("ensure_app_icon", False, f"Empty icon source: {src_dir}", [])
|
||||
if options.dry_run:
|
||||
return ApplyResult(
|
||||
"ensure_app_icon",
|
||||
True,
|
||||
f"dry-run: copy {', '.join(names)} → assets/",
|
||||
[f"assets/{n}" for n in names],
|
||||
)
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
changed: list[str] = []
|
||||
for name in names:
|
||||
shutil.copy2(src_dir / name, dest_dir / name)
|
||||
changed.append(f"assets/{name}")
|
||||
return ApplyResult(
|
||||
"ensure_app_icon",
|
||||
True,
|
||||
f"Installed app icon from {src_dir.name}/ ({', '.join(names)})",
|
||||
changed,
|
||||
)
|
||||
|
||||
|
||||
def op_rewrite_cmake_setup_host(root: Path, options: MigrateOptions) -> ApplyResult:
|
||||
tokens = _resolve_tokens(root, options)
|
||||
# Force wizard for setup-host policy
|
||||
@@ -676,6 +1083,7 @@ def op_record_framework_pins(root: Path, options: MigrateOptions) -> ApplyResult
|
||||
_OPS = {
|
||||
"rename_psxrecomp_submodule": op_rename_psxrecomp_submodule,
|
||||
"ensure_psxrecomp_submodule": op_ensure_psxrecomp_submodule,
|
||||
"repair_psxrecomp_submodule": op_repair_psxrecomp_submodule,
|
||||
"ensure_recomp_ui_submodule": op_ensure_recomp_ui_submodule,
|
||||
"emit_codegen_setup": op_emit_codegen_setup,
|
||||
"emit_version": op_emit_version,
|
||||
@@ -685,6 +1093,7 @@ _OPS = {
|
||||
"emit_mods_preloaded": op_emit_mods_preloaded,
|
||||
"relocate_boxart": op_relocate_boxart,
|
||||
"emit_boxart_stub": op_emit_boxart_stub,
|
||||
"ensure_app_icon": op_ensure_app_icon,
|
||||
"rewrite_cmake_setup_host": op_rewrite_cmake_setup_host,
|
||||
"emit_packager": op_emit_packager,
|
||||
"emit_ci_workflow": op_emit_ci_workflow,
|
||||
|
||||
@@ -11,6 +11,7 @@ from .models import AuditReport, LayoutClass, MigrateOptions, Plan, PlanStep
|
||||
# Topological order for apply. Ops not listed are appended at the end.
|
||||
_OP_ORDER = [
|
||||
"rename_psxrecomp_submodule",
|
||||
"repair_psxrecomp_submodule",
|
||||
"ensure_psxrecomp_submodule",
|
||||
"ensure_recomp_ui_submodule",
|
||||
"emit_codegen_setup",
|
||||
@@ -21,6 +22,7 @@ _OP_ORDER = [
|
||||
"emit_mods_preloaded",
|
||||
"relocate_boxart",
|
||||
"emit_boxart_stub",
|
||||
"ensure_app_icon",
|
||||
"rewrite_cmake_setup_host",
|
||||
"emit_packager",
|
||||
"emit_ci_workflow",
|
||||
@@ -30,7 +32,8 @@ _OP_ORDER = [
|
||||
]
|
||||
|
||||
_OP_TITLES = {
|
||||
"rename_psxrecomp_submodule": "Rename psxrecomp-v4 → psxrecomp",
|
||||
"rename_psxrecomp_submodule": "Consolidate on psxrecomp/ (remove psxrecomp-v4)",
|
||||
"repair_psxrecomp_submodule": "Repair broken psxrecomp/ git checkout",
|
||||
"ensure_psxrecomp_submodule": "Add psxrecomp submodule",
|
||||
"ensure_recomp_ui_submodule": "Add recomp-ui submodule",
|
||||
"emit_codegen_setup": "Emit codegen_setup.c / .h",
|
||||
@@ -41,6 +44,7 @@ _OP_TITLES = {
|
||||
"emit_mods_preloaded": "Stub mods/preloaded catalog",
|
||||
"relocate_boxart": "Relocate boxart → launcher_assets/",
|
||||
"emit_boxart_stub": "Create launcher_assets stub dir",
|
||||
"ensure_app_icon": "Install assets/psxrecomp app icon",
|
||||
"rewrite_cmake_setup_host": "Rewrite CMakeLists.txt (setup-host)",
|
||||
"emit_packager": "Emit scripts/package_setup_release.sh",
|
||||
"emit_ci_workflow": "Emit setup-host release.yml",
|
||||
|
||||
@@ -304,6 +304,8 @@ set(PSX_SETUP_WIZARD ON CACHE BOOL
|
||||
|
||||
Set-Content -Encoding UTF8 -Path $BoxartBlockFile -Value ' # LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"'
|
||||
$HasBoxart = $false
|
||||
$AppIconBlockFile = Join-Path $env:TEMP "psxrecomp_app_icon_block.cmake"
|
||||
Set-Content -Encoding UTF8 -Path $AppIconBlockFile -Value ' APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"'
|
||||
|
||||
function Fill-Template([string]$src, [string]$dst) {
|
||||
python $FillTokens $src $dst `
|
||||
@@ -326,6 +328,7 @@ function Fill-Template([string]$src, [string]$dst) {
|
||||
--set "NETPLAY_LOBBY_URL_ARG=$NetplayLobbyUrlArg" `
|
||||
--set "WIZARD_RUNTIME_ARG=$WizardRuntimeArg" `
|
||||
--set-file "BOXART_CMAKE_ARG=$BoxartBlockFile" `
|
||||
--set-file "APP_ICON_CMAKE_ARG=$AppIconBlockFile" `
|
||||
--set-file "NETPLAY_CMAKE_BLOCK=$NetplayBlockFile" `
|
||||
--set-file "WIZARD_CMAKE_BLOCK=$WizardBlockFile" `
|
||||
--set-file "RECOMP_UI_CMAKE_BLOCK=$RecompUiBlockFile" `
|
||||
@@ -366,6 +369,7 @@ New-Item -ItemType Directory -Force -Path (Join-Path $Root "launcher_assets\img"
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root "scripts") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root "tools") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root "mods\preloaded\packages") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root "assets") | Out-Null
|
||||
# Empty mod catalog tree (runtime copies mods/preloaded → beside the exe as mods/).
|
||||
@'
|
||||
# Preloaded mods
|
||||
@@ -392,8 +396,24 @@ if ($useRecompUi) {
|
||||
}
|
||||
git submodule update --init --recursive
|
||||
|
||||
# RetComM-themed default app icon (Windows .ico + PNG for packaging).
|
||||
$iconSrc = Join-Path $Root "psxrecomp\assets"
|
||||
$iconDst = Join-Path $Root "assets"
|
||||
if (Test-Path $iconSrc) {
|
||||
New-Item -ItemType Directory -Force -Path $iconDst | Out-Null
|
||||
foreach ($name in @("psxrecomp.svg", "psxrecomp.png", "psxrecomp.ico")) {
|
||||
$src = Join-Path $iconSrc $name
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Force $src (Join-Path $iconDst $name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
git -C psxrecomp checkout --detach -q HEAD
|
||||
git add psxrecomp
|
||||
if (Test-Path $iconDst) {
|
||||
git add assets 2>$null
|
||||
}
|
||||
if ($useRecompUi) {
|
||||
git -C recomp-ui checkout --detach -q HEAD
|
||||
git add recomp-ui
|
||||
|
||||
@@ -458,9 +458,10 @@ fi
|
||||
NETPLAY_BLOCK_FILE=$(mktemp)
|
||||
WIZARD_BLOCK_FILE=$(mktemp)
|
||||
BOXART_BLOCK_FILE=$(mktemp)
|
||||
APP_ICON_BLOCK_FILE=$(mktemp)
|
||||
RECOMP_UI_BLOCK_FILE=$(mktemp)
|
||||
CODEGEN_ARG_FILE=$(mktemp)
|
||||
trap 'rm -f "$NETPLAY_BLOCK_FILE" "$WIZARD_BLOCK_FILE" "$BOXART_BLOCK_FILE" "$RECOMP_UI_BLOCK_FILE" "$CODEGEN_ARG_FILE"' EXIT
|
||||
trap 'rm -f "$NETPLAY_BLOCK_FILE" "$WIZARD_BLOCK_FILE" "$BOXART_BLOCK_FILE" "$APP_ICON_BLOCK_FILE" "$RECOMP_UI_BLOCK_FILE" "$CODEGEN_ARG_FILE"' EXIT
|
||||
|
||||
if [ "$ENABLE_RECOMP_UI" -eq 1 ]; then
|
||||
printf '%s\n' '# recomp-ui submodule present (PSX_RECOMP_UI defaults ON).' \
|
||||
@@ -512,6 +513,8 @@ fi
|
||||
printf '%s\n' ' # LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"' \
|
||||
>"$BOXART_BLOCK_FILE"
|
||||
HAS_BOXART=0
|
||||
printf '%s\n' ' APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"' \
|
||||
>"$APP_ICON_BLOCK_FILE"
|
||||
|
||||
fill_template() {
|
||||
src=$1
|
||||
@@ -536,6 +539,7 @@ fill_template() {
|
||||
--set "NETPLAY_LOBBY_URL_ARG=$NETPLAY_LOBBY_URL_ARG" \
|
||||
--set "WIZARD_RUNTIME_ARG=$WIZARD_RUNTIME_ARG" \
|
||||
--set-file "BOXART_CMAKE_ARG=$BOXART_BLOCK_FILE" \
|
||||
--set-file "APP_ICON_CMAKE_ARG=$APP_ICON_BLOCK_FILE" \
|
||||
--set-file "NETPLAY_CMAKE_BLOCK=$NETPLAY_BLOCK_FILE" \
|
||||
--set-file "WIZARD_CMAKE_BLOCK=$WIZARD_BLOCK_FILE" \
|
||||
--set-file "RECOMP_UI_CMAKE_BLOCK=$RECOMP_UI_BLOCK_FILE" \
|
||||
@@ -574,7 +578,7 @@ fill_template "$TEMPLATE_DIR/VERSION.in" "$ROOT/VERSION"
|
||||
fill_template "$TEMPLATE_DIR/README.md.in" "$ROOT/README.md"
|
||||
fill_template "$TEMPLATE_DIR/symbols.toml.in" "$ROOT/symbols.toml"
|
||||
mkdir -p "$ROOT/seeds" "$ROOT/launcher_assets/img" "$ROOT/scripts" "$ROOT/tools" \
|
||||
"$ROOT/mods/preloaded/packages"
|
||||
"$ROOT/mods/preloaded/packages" "$ROOT/assets"
|
||||
cp "$SCRIPT_DIR/sync_symbols.py" "$ROOT/tools/sync_symbols.py"
|
||||
chmod +x "$ROOT/tools/sync_symbols.py"
|
||||
# Empty mod catalog tree (runtime copies mods/preloaded → beside the exe as mods/).
|
||||
@@ -602,8 +606,21 @@ if [ "$ENABLE_RECOMP_UI" -eq 1 ]; then
|
||||
fi
|
||||
git submodule update --init --recursive
|
||||
|
||||
# RetComM-themed default app icon (Windows .ico + PNG for packaging).
|
||||
if [ -d psxrecomp/assets ]; then
|
||||
mkdir -p "$ROOT/assets"
|
||||
for _icon in psxrecomp.svg psxrecomp.png psxrecomp.ico; do
|
||||
if [ -f "psxrecomp/assets/$_icon" ]; then
|
||||
cp -f "psxrecomp/assets/$_icon" "$ROOT/assets/$_icon"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
git -C psxrecomp checkout --detach -q HEAD
|
||||
git add psxrecomp
|
||||
if [ -d "$ROOT/assets" ]; then
|
||||
git add assets 2>/dev/null || true
|
||||
fi
|
||||
if [ "$ENABLE_RECOMP_UI" -eq 1 ]; then
|
||||
git -C recomp-ui checkout --detach -q HEAD
|
||||
git add recomp-ui
|
||||
|
||||
@@ -21,6 +21,7 @@ psxrecomp_add_game_runtime(psx-runtime
|
||||
DEFAULT_GAME_CONFIG_PATH "game.toml"
|
||||
MAX_PLAYERS @PLAYERS@
|
||||
@BOXART_CMAKE_ARG@
|
||||
@APP_ICON_CMAKE_ARG@
|
||||
GEN_MARKER "generated/@BOOT_EXE@_dispatch.c"
|
||||
GEN_FULL_GLOB "generated/@BOOT_EXE@_full_*.c"
|
||||
@CODEGEN_SETUP_ARG@
|
||||
|
||||
@@ -22,6 +22,8 @@ You must own the original game. Disc images under `disc/` are gitignored and
|
||||
must never be committed. Retail BIOS dumps are not redistributed; OpenBIOS is
|
||||
used for Generate unless you supply your own SCPH locally.
|
||||
|
||||
Default app icon: `assets/psxrecomp.ico` (and `.png` / `.svg`) — RetComM-themed controller mark from `psxrecomp/assets/`. Windows builds embed it via `APP_ICON`.
|
||||
|
||||
Optional box art under `launcher_assets/img/` may come from
|
||||
[libretro-thumbnails](https://github.com/libretro-thumbnails/libretro-thumbnails)
|
||||
(`Named_Boxarts`); see `BOXART_SOURCE.txt` when present.
|
||||
|
||||
+60
-19
@@ -20,10 +20,14 @@
|
||||
# [--embed-toolchain] # optional: copy PSXRECOMP_TOOLCHAIN_DIR into zip
|
||||
#
|
||||
# Env:
|
||||
# RELEASE_VERSION / <version-env> / VERSION file
|
||||
# RELEASE_VERSION / <version-env> / VERSION file (must match binary stamp)
|
||||
# PSXRECOMP_TOOLCHAIN_DIR | TOOLCHAIN_DIR | BPE_TOOLCHAIN_DIR (only with --embed-toolchain)
|
||||
# PSXRECOMP_EMBED_TOOLCHAIN=1 same as --embed-toolchain
|
||||
# PSXRECOMP_RUNTIME_BIN_DIR | BPE_RUNTIME_BIN_DIR (Windows MinGW DLL search)
|
||||
#
|
||||
# Lobby pin: the host exe must have been built with current runtime.cmake so
|
||||
# $<TARGET_FILE_DIR>/psx_game_version.txt exists. This script refuses to ship a
|
||||
# VERSION file that disagrees with that stamp (netplay list filter bug).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -108,35 +112,28 @@ if [[ ${#PROJECT_FILES[@]} -eq 0 && ${#PROJECT_DIRS[@]} -eq 0 ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Resolve version.
|
||||
VERSION=""
|
||||
# Resolve *requested* version from env / VERSION file (may be empty until stamp).
|
||||
REQUESTED=""
|
||||
if [[ -n "${VERSION_ENV}" ]]; then
|
||||
VERSION="${!VERSION_ENV:-}"
|
||||
REQUESTED="${!VERSION_ENV:-}"
|
||||
fi
|
||||
if [[ -z "${VERSION}" && -n "${RELEASE_VERSION:-}" ]]; then
|
||||
VERSION="${RELEASE_VERSION}"
|
||||
if [[ -z "${REQUESTED}" && -n "${RELEASE_VERSION:-}" ]]; then
|
||||
REQUESTED="${RELEASE_VERSION}"
|
||||
fi
|
||||
if [[ -z "${VERSION}" && -n "${BPE_RELEASE_VERSION:-}" ]]; then
|
||||
VERSION="${BPE_RELEASE_VERSION}"
|
||||
if [[ -z "${REQUESTED}" && -n "${BPE_RELEASE_VERSION:-}" ]]; then
|
||||
REQUESTED="${BPE_RELEASE_VERSION}"
|
||||
fi
|
||||
if [[ -z "${VERSION}" && -f "${ROOT}/VERSION" ]]; then
|
||||
VERSION="$(tr -d '[:space:]' <"${ROOT}/VERSION")"
|
||||
if [[ -z "${REQUESTED}" && -f "${ROOT}/VERSION" ]]; then
|
||||
REQUESTED="$(tr -d '[:space:]' <"${ROOT}/VERSION")"
|
||||
fi
|
||||
VERSION="$(printf '%s' "${VERSION}" | tr -d '[:space:]')"
|
||||
VERSION="${VERSION#v}"
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "error: VERSION empty (set ${VERSION_ENV} / RELEASE_VERSION or write VERSION)" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "${VERSION}" >"${ROOT}/VERSION"
|
||||
REQUESTED="$(printf '%s' "${REQUESTED}" | tr -d '[:space:]')"
|
||||
REQUESTED="${REQUESTED#v}"
|
||||
|
||||
DIST="${ROOT}/dist"
|
||||
STAGE="${DIST}/stage-setup-${ARTIFACT}"
|
||||
ZIP_NAME="${ZIP_PREFIX}-${VERSION}-${ARTIFACT}.zip"
|
||||
|
||||
rm -rf "${STAGE}"
|
||||
mkdir -p "${STAGE}" "${DIST}"
|
||||
rm -f "${DIST}/${ZIP_NAME}"
|
||||
|
||||
EXE=""
|
||||
for cand in \
|
||||
@@ -155,7 +152,51 @@ if [[ -z "${EXE}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Authoritative lobby pin = stamp written at compile time next to the exe
|
||||
# (see runtime.cmake file(GENERATE) psx_game_version.txt). Never invent a
|
||||
# newer VERSION for the zip than what was baked into PSX_GAME_VERSION.
|
||||
normalize_ver() {
|
||||
local v
|
||||
v="$(printf '%s' "${1:-}" | tr -d '[:space:]')"
|
||||
v="${v#v}"
|
||||
printf '%s' "${v}"
|
||||
}
|
||||
BUILT=""
|
||||
for stamp in \
|
||||
"$(dirname "${EXE}")/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
|
||||
echo " Refusing to package (VERSION file alone can drift from PSX_GAME_VERSION)." >&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
|
||||
echo " then re-run this packager. Shipping mismatched pins breaks netplay lobby lists." >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION="${BUILT}"
|
||||
printf '%s\n' "${VERSION}" >"${ROOT}/VERSION"
|
||||
ZIP_NAME="${ZIP_PREFIX}-${VERSION}-${ARTIFACT}.zip"
|
||||
rm -f "${DIST}/${ZIP_NAME}"
|
||||
|
||||
cp -a "${EXE}" "${STAGE}/"
|
||||
# Ship the stamp beside the exe so installers / RetComM can prefer it over VERSION.
|
||||
if [[ -f "$(dirname "${EXE}")/psx_game_version.txt" ]]; then
|
||||
cp -a "$(dirname "${EXE}")/psx_game_version.txt" "${STAGE}/psx_game_version.txt"
|
||||
else
|
||||
printf '%s\n' "${VERSION}" >"${STAGE}/psx_game_version.txt"
|
||||
fi
|
||||
EXE_BASENAME="$(basename "${EXE}")"
|
||||
EXE_DIR="$(dirname "${EXE}")"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user