mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -04:00
874df9b5bb
Retire the setup-host release model (CI wiped generated/ and shipped
emitters + sources + a wizard so every player regenerated and rebuilt the
title). Titles now commit generated/ and CI ships the compiled game.
- docs/ci/templates/game-release.yml replaces setup-release.yml: gate on
committed game C (tools/ci/check_generated.sh), emit the OpenBIOS backend
in CI (tools/ci/generate_openbios.sh), configure with
PSXRECOMP_REQUIRE_GAME_C=ON and assert the full path was taken, package
with tools/package_game_release.sh, verify the zip is a game not a kit.
- tools/package_game_release.sh: exe, assets, bundled OpenBIOS, configs, mod
catalog + overlay toolchain via release_stage.py, notices; refuses sources,
emitters, generated C, BIOS dumps. clear_generated.sh removed;
package_setup_host.sh marked retired.
- Scaffold: gitignore templates track generated/, package_release.sh.in
wrapper, setup_project.{sh,ps1} commit generated/ after Generate.
- Project Studio: audit flags ignored/untracked generated/, retail BIOS C
and setup-host workflows; merge_gitignore un-ignores generated/; ops emit
the bundled packager + workflow; generate_ci uses the new template.
- Docs: docs/ci/BUNDLED_RELEASES.md replaces HOST_ONLY_RELEASES.md.
- Tests: tools/tests/test_bundled_release_layout.py; fix pre-existing
false positives in the packager-surface and overlay-static tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
321 lines
10 KiB
Python
321 lines
10 KiB
Python
"""Derive project naming tokens (mirrors setup_project.sh)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from fill_tokens import derive_zip_prefix, sanitize_github_name
|
|
|
|
|
|
DEFAULT_LOBBY_HOST = "netplay.retcomm.net"
|
|
DEFAULT_LOBBY_URL = f"ws://{DEFAULT_LOBBY_HOST}:8765"
|
|
|
|
|
|
def project_cmake_name(name: str) -> str:
|
|
return re.sub(r"[^A-Za-z0-9_]", "_", name)
|
|
|
|
|
|
def env_prefix(cmake_name: str) -> str:
|
|
return cmake_name.upper()
|
|
|
|
|
|
def window_title_from_name(name: str) -> str:
|
|
title = re.sub(r"Recomp$", " Recompiled", name)
|
|
title = title.replace("Recompiled Recompiled", "Recompiled")
|
|
return title
|
|
|
|
|
|
def exe_basename(window_title: str) -> str:
|
|
return re.sub(r"[^A-Za-z0-9_]", "_", window_title)
|
|
|
|
|
|
def game_name_from_project(name: str) -> str:
|
|
g = re.sub(r"(Recomp|Recompiled)$", "", name).rstrip()
|
|
return g or name
|
|
|
|
|
|
def normalize_lobby_url(value: str | None) -> str:
|
|
raw = (value or DEFAULT_LOBBY_HOST).strip()
|
|
if raw.startswith("ws://") or raw.startswith("wss://"):
|
|
return raw
|
|
if "://" in raw:
|
|
return raw
|
|
if ":" in raw:
|
|
return f"ws://{raw}"
|
|
return f"ws://{raw}:8765"
|
|
|
|
|
|
def boot_exe_from_game_toml(game_toml: Path) -> str | None:
|
|
"""Best-effort parse of [game].exe basename (e.g. SCUS_944.23)."""
|
|
if not game_toml.is_file():
|
|
return None
|
|
text = game_toml.read_text(encoding="utf-8", errors="replace")
|
|
in_game = False
|
|
for line in text.splitlines():
|
|
s = line.strip()
|
|
if s.startswith("[") and s.endswith("]"):
|
|
in_game = s == "[game]"
|
|
continue
|
|
if not in_game or "=" not in s or s.startswith("#"):
|
|
continue
|
|
key, _, val = s.partition("=")
|
|
if key.strip() != "exe":
|
|
continue
|
|
val = val.strip().strip('"').strip("'")
|
|
# path/to/SCUS_944.23 → SCUS_944.23
|
|
return Path(val).name
|
|
return None
|
|
|
|
|
|
def entry_pc_from_game_toml(game_toml: Path) -> str:
|
|
if not game_toml.is_file():
|
|
return "0x80010000"
|
|
text = game_toml.read_text(encoding="utf-8", errors="replace")
|
|
in_game = False
|
|
for line in text.splitlines():
|
|
s = line.strip()
|
|
if s.startswith("[") and s.endswith("]"):
|
|
in_game = s == "[game]"
|
|
continue
|
|
if not in_game or "=" not in s or s.startswith("#"):
|
|
continue
|
|
key, _, val = s.partition("=")
|
|
if key.strip() == "entry_pc":
|
|
return val.strip().strip('"').strip("'")
|
|
return "0x80010000"
|
|
|
|
|
|
def players_from_cmake(cmake: Path) -> int | None:
|
|
if not cmake.is_file():
|
|
return None
|
|
text = cmake.read_text(encoding="utf-8", errors="replace")
|
|
m = re.search(r"MAX_PLAYERS\s+(\d+)", text)
|
|
if m:
|
|
return int(m.group(1))
|
|
return None
|
|
|
|
|
|
def players_from_game_toml(game_toml: Path) -> int | None:
|
|
"""Best-effort parse of ``[game].players`` from game.toml."""
|
|
if not game_toml.is_file():
|
|
return None
|
|
text = game_toml.read_text(encoding="utf-8", errors="replace")
|
|
in_game = False
|
|
for line in text.splitlines():
|
|
s = line.strip()
|
|
if s.startswith("[") and s.endswith("]"):
|
|
in_game = s == "[game]"
|
|
continue
|
|
if not in_game or "=" not in s or s.startswith("#"):
|
|
continue
|
|
key, _, val = s.partition("=")
|
|
if key.strip() != "players":
|
|
continue
|
|
raw = val.strip().strip('"').strip("'")
|
|
try:
|
|
n = int(raw)
|
|
except ValueError:
|
|
return None
|
|
return n
|
|
return None
|
|
|
|
|
|
def configured_players(root: Path, *, default: int = 2) -> int:
|
|
"""Players from game.toml, else CMakeLists MAX_PLAYERS, else ``default``.
|
|
|
|
Clamped to 1..8 to match the Migrate UI and sio pad slots.
|
|
"""
|
|
root = root.expanduser().resolve()
|
|
n = players_from_game_toml(root / "game.toml")
|
|
if n is None:
|
|
n = players_from_cmake(root / "CMakeLists.txt")
|
|
if n is None:
|
|
n = default
|
|
if n < 1:
|
|
n = 1
|
|
if n > 8:
|
|
n = 8
|
|
return n
|
|
|
|
|
|
def netplay_configured(root: Path) -> bool:
|
|
"""True if the game repo already opts into netplay (CMake and/or game.toml)."""
|
|
root = root.expanduser().resolve()
|
|
cmake = root / "CMakeLists.txt"
|
|
if cmake.is_file():
|
|
try:
|
|
text = cmake.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
text = ""
|
|
for line in text.splitlines():
|
|
s = line.strip()
|
|
if not s or s.startswith("#"):
|
|
continue
|
|
if "ENABLE_NETPLAY_IF_PRESENT" in s:
|
|
return True
|
|
if re.search(r"set\s*\(\s*PSX_NETPLAY\s+ON\b", s, re.IGNORECASE):
|
|
return True
|
|
if "NETPLAY_LOBBY_URL" in s:
|
|
return True
|
|
game_toml = root / "game.toml"
|
|
if game_toml.is_file():
|
|
try:
|
|
gtext = game_toml.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
gtext = ""
|
|
if re.search(r"^\[netplay\]\s*$", gtext, re.MULTILINE):
|
|
return True
|
|
return False
|
|
|
|
|
|
def ci_workflow_present(root: Path) -> bool:
|
|
"""True if ``.github/workflows/release.yml`` exists."""
|
|
root = root.expanduser().resolve()
|
|
return (root / ".github" / "workflows" / "release.yml").is_file()
|
|
|
|
|
|
def disc_probe_configured(root: Path) -> bool:
|
|
"""True if catalog/disc probe artifacts already exist."""
|
|
root = root.expanduser().resolve()
|
|
has_catalog = (root / "catalog_identity.json").is_file()
|
|
game_toml = root / "game.toml"
|
|
has_prepare = False
|
|
if game_toml.is_file():
|
|
try:
|
|
has_prepare = "[prepare_disc]" in game_toml.read_text(
|
|
encoding="utf-8", errors="replace"
|
|
)
|
|
except OSError:
|
|
has_prepare = False
|
|
return has_catalog or has_prepare
|
|
|
|
|
|
def window_title_from_cmake(cmake: Path) -> str | None:
|
|
if not cmake.is_file():
|
|
return None
|
|
text = cmake.read_text(encoding="utf-8", errors="replace")
|
|
m = re.search(r'WINDOW_TITLE\s+"([^"]+)"', text)
|
|
return m.group(1) if m else None
|
|
|
|
|
|
def infer_project_name(root: Path) -> str:
|
|
return root.name
|
|
|
|
|
|
def build_token_map(
|
|
*,
|
|
name: str,
|
|
boot_exe: str,
|
|
players: int,
|
|
zip_prefix: str | None = None,
|
|
window_title: str | None = None,
|
|
entry_pc: str = "0x80010000",
|
|
description: str = "",
|
|
publisher: str = "",
|
|
year: str = "",
|
|
region: str = "USA",
|
|
lobby_url: str = DEFAULT_LOBBY_URL,
|
|
enable_recomp_ui: bool = True,
|
|
enable_wizard: bool = True,
|
|
enable_netplay: bool = False,
|
|
has_boxart: bool = False,
|
|
github_owner: str | None = None,
|
|
github_repo: str | None = None,
|
|
) -> dict[str, str]:
|
|
cmake_name = project_cmake_name(name)
|
|
title = window_title or window_title_from_name(name)
|
|
game = game_name_from_project(name)
|
|
zp = zip_prefix or derive_zip_prefix(name)
|
|
desc = description or "_Add a short pitch in catalog_identity.json / README._"
|
|
publisher_disp = publisher or "—"
|
|
year_disp = year or "—"
|
|
|
|
if enable_recomp_ui:
|
|
recomp_ui_block = "# recomp-ui submodule present (PSX_RECOMP_UI defaults ON)."
|
|
codegen_arg = (
|
|
' CODEGEN_SETUP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/codegen_setup.c"'
|
|
)
|
|
else:
|
|
recomp_ui_block = (
|
|
'set(PSX_RECOMP_UI OFF CACHE BOOL\n'
|
|
' "No recomp-ui submodule in this scaffold" FORCE)'
|
|
)
|
|
codegen_arg = " # CODEGEN_SETUP_SOURCES — add with recomp-ui / wizard later"
|
|
|
|
if enable_netplay:
|
|
netplay_block = (
|
|
'if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")\n'
|
|
" set(PSX_NETPLAY ON CACHE BOOL\n"
|
|
' "Link recomp-net delay-sync (opt-in; needs recomp-net)" FORCE)\n'
|
|
"endif()"
|
|
)
|
|
netplay_runtime = " ENABLE_NETPLAY_IF_PRESENT"
|
|
netplay_lobby = f' NETPLAY_LOBBY_URL "{lobby_url}"'
|
|
else:
|
|
netplay_block = (
|
|
"# Full netplay UI — enable after adding recomp-ui + testing:\n"
|
|
'# if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")\n'
|
|
'# set(PSX_NETPLAY ON CACHE BOOL "…" FORCE)\n'
|
|
"# endif()"
|
|
)
|
|
netplay_runtime = " # ENABLE_NETPLAY_IF_PRESENT"
|
|
netplay_lobby = f' # NETPLAY_LOBBY_URL "ws://{DEFAULT_LOBBY_HOST}:8765"'
|
|
|
|
if enable_wizard:
|
|
wizard_block = (
|
|
"set(PSX_SETUP_WIZARD ON CACHE BOOL\n"
|
|
' "Advertise first-run setup wizard + Generate & rebuild in recomp-ui" FORCE)'
|
|
)
|
|
wizard_runtime = " ENABLE_SETUP_WIZARD"
|
|
else:
|
|
wizard_block = (
|
|
"# First-run setup wizard — enable after testing:\n"
|
|
'# set(PSX_SETUP_WIZARD ON CACHE BOOL "…" FORCE)'
|
|
)
|
|
wizard_runtime = " # ENABLE_SETUP_WIZARD"
|
|
|
|
if has_boxart:
|
|
boxart = (
|
|
' LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"'
|
|
)
|
|
else:
|
|
boxart = (
|
|
' # 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,
|
|
"BOOT_EXE": boot_exe,
|
|
"GAME_NAME": game,
|
|
"ENV_PREFIX": env_prefix(cmake_name),
|
|
"EXE_BASENAME": exe_basename(title),
|
|
"ZIP_PREFIX": zp,
|
|
"GITHUB_OWNER": sanitize_github_name(
|
|
(github_owner or "").strip() or "RetroPortingToolKit"
|
|
),
|
|
"GITHUB_REPO": sanitize_github_name((github_repo or "").strip() or name),
|
|
"DISC_HINT": f"your legally owned {game} disc",
|
|
"GAME_TITLE": title,
|
|
"PLAYERS": str(players),
|
|
"DESCRIPTION": desc,
|
|
"PUBLISHER": publisher_disp,
|
|
"YEAR": year_disp,
|
|
"REGION": region,
|
|
"ENTRY_PC": entry_pc,
|
|
"NETPLAY_RUNTIME_ARG": netplay_runtime,
|
|
"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,
|
|
"CODEGEN_SETUP_ARG": codegen_arg,
|
|
}
|