Files
psxrecomp/tools/new_project_layout/setup_project.sh
T
Alex Vanderveen 874df9b5bb release: ship bundled, compiled games from committed generated/ C
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>
2026-09-30 17:11:14 -04:00

1132 lines
41 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# New Project Layout scaffolding (Linux / macOS).
#
# Path flags (pass on the CLI — use your shell's path completion):
# --disc <cue> REQUIRED legal Redump .cue (kept on your dump drive)
# --dir <parent> Parent directory for the new repo (default: .)
# --bios <SCPH1001.BIN> Optional retail BIOS for --generate / generate prompt
# --boot-exe <name> Optional override until disc probe runs
# --stage-disc Copy full cue+bins into repo disc/ (large; optional)
# --no-stage-disc Default: probe in place, extract boot EXE only
# --psxrecomp-ref / --recomp-ui-ref / --recomp-net-ref / URLs
# --github-owner <org> README download-badge owner (default RetroPortingToolKit)
# --github-repo <name> README download-badge repo (default project name)
#
# Everything else is prompted on a TTY (or passed via flags / --yes defaults):
# name, players (1–8, default 2), zip-prefix, github owner/repo (README badges),
# marketing blurb, recomp-ui, wizard, netplay, lobby URL (if netplay), CI
# workflow, fetch-boxart, generate, build (if generate), GitHub repo (gh)
#
# Non-interactive: --yes (or PSXRECOMP_SETUP_YES=1) requires --name and --disc;
# boolean opts stay off unless explicitly flagged; lobby URL defaults to
# netplay.retcomm.net when netplay is enabled.
#
# Disc layout (default): game.toml disc= absolute path to your Redump cue;
# only the small boot EXE is written under disc/. Use --stage-disc to copy
# the full multi-track dump into the repo (gitignored) instead.
#
# GitHub publish order (avoids competing "initial" commits / missing CI):
# scaffold + CI workflow → commit → gh repo create (no push) → generate/build
# → single git push -u origin HEAD → verify Actions workflows if CI enabled.
#
# Usage:
# sh tools/new_project_layout/setup_project.sh --disc /path/to/game.cue
# sh tools/new_project_layout/setup_project.sh --disc game.cue --name Foo --yes
# sh tools/new_project_layout/setup_project.sh --disc game.cue --stage-disc
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
TEMPLATE_DIR="$SCRIPT_DIR/templates"
PROBE_DISC="$SCRIPT_DIR/probe_disc.py"
FILL_TOKENS="$SCRIPT_DIR/fill_tokens.py"
FETCH_BOXART="$SCRIPT_DIR/fetch_boxart.py"
WRITE_RECOMP_JSON="$SCRIPT_DIR/write_recomp_json.py"
NAME=""
DISC=""
PARENT="."
BOOT_EXE="SLUS_01234"
PLAYERS=""
ZIP_PREFIX=""
ENABLE_RECOMP_UI=""
ENABLE_NETPLAY=""
ENABLE_WIZARD=""
ENABLE_CI=""
FETCH_BOXART_FLAG=""
DO_GENERATE=""
DO_BUILD=""
CREATE_GITHUB=""
GITHUB_VISIBILITY="private"
GITHUB_OWNER=""
GITHUB_REPO=""
BIOS_PATH=""
LOBBY_URL=""
DESCRIPTION=""
PUBLISHER=""
YEAR=""
REGION=""
YES_MODE=0
DEFAULT_LOBBY_HOST="netplay.retcomm.net"
# mstan/psxrecomp and recomp-ui both use master (not main).
# Nested recomp-net defaults to whatever SHA psxrecomp pins; override with
# --recomp-net-ref (branch/tag/SHA) or RECOMP_NET_REF=… so netplay can track
# main (or feat/rollback) instead of a stale nested pin.
PSXRECOMP_REF="master"
RECOMP_UI_REF="master"
RECOMP_NET_REF="${RECOMP_NET_REF:-}"
PSXRECOMP_URL="${PSXRECOMP_URL:-https://github.com/mstan/psxrecomp.git}"
RECOMP_UI_URL="${RECOMP_UI_URL:-https://github.com/RetroPortingToolKit/recomp-ui.git}"
# Track whether bools were set on the CLI (so prompts can skip).
SET_RECOMP_UI=0
SET_NETPLAY=0
SET_WIZARD=0
SET_CI=0
SET_BOXART=0
SET_GENERATE=0
SET_BUILD=0
SET_GITHUB=0
SET_LOBBY_URL=0
SET_DESCRIPTION=0
SET_PUBLISHER=0
SET_YEAR=0
SET_REGION=0
STAGE_DISC=0
usage() {
sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'
exit "${1:-0}"
}
is_tty() {
[ -t 0 ] && [ -t 1 ]
}
prompt_line() {
# prompt_line "Question" default_var_name [default_value]
_q=$1
_var=$2
_def=${3:-}
if [ -n "$_def" ]; then
printf '%s [%s]: ' "$_q" "$_def" >/dev/tty
else
printf '%s: ' "$_q" >/dev/tty
fi
# shellcheck disable=SC2162
read _ans </dev/tty || _ans=
if [ -z "$_ans" ]; then
_ans=$_def
fi
eval "$_var=\$_ans"
}
prompt_yn() {
# prompt_yn "Question" var_name default_y_or_n
_q=$1
_var=$2
_def=$3
_hint=y/N
[ "$_def" = "1" ] || [ "$_def" = "y" ] || [ "$_def" = "Y" ] && _hint=Y/n
while :; do
printf '%s [%s]: ' "$_q" "$_hint" >/dev/tty
# shellcheck disable=SC2162
read _ans </dev/tty || _ans=
if [ -z "$_ans" ]; then
if [ "$_def" = "1" ] || [ "$_def" = "y" ] || [ "$_def" = "Y" ]; then
eval "$_var=1"
else
eval "$_var=0"
fi
return 0
fi
case "$_ans" in
y|Y|yes|YES) eval "$_var=1"; return 0 ;;
n|N|no|NO) eval "$_var=0"; return 0 ;;
*) printf ' please answer y or n\n' >/dev/tty ;;
esac
done
}
# Bare host → ws://host:8765; host:port → ws://host:port; ws(s):// kept as-is.
normalize_lobby_url() {
_in=$1
if [ -z "$_in" ]; then
_in="$DEFAULT_LOBBY_HOST"
fi
case "$_in" in
ws://*|wss://*) printf '%s' "$_in" ;;
*://*) printf '%s' "$_in" ;;
*:*) printf 'ws://%s' "$_in" ;;
*) printf 'ws://%s:8765' "$_in" ;;
esac
}
while [ $# -gt 0 ]; do
case "$1" in
--name) NAME=$2; shift 2 ;;
--disc) DISC=$2; shift 2 ;;
--dir) PARENT=$2; shift 2 ;;
--boot-exe) BOOT_EXE=$2; shift 2 ;;
--players) PLAYERS=$2; shift 2 ;;
--zip-prefix) ZIP_PREFIX=$2; shift 2 ;;
--enable-recomp-ui) ENABLE_RECOMP_UI=1; SET_RECOMP_UI=1; shift ;;
--no-recomp-ui) ENABLE_RECOMP_UI=0; SET_RECOMP_UI=1; shift ;;
--enable-netplay) ENABLE_NETPLAY=1; SET_NETPLAY=1; shift ;;
--no-netplay) ENABLE_NETPLAY=0; SET_NETPLAY=1; shift ;;
--lobby-url) LOBBY_URL=$2; SET_LOBBY_URL=1; shift 2 ;;
--enable-wizard) ENABLE_WIZARD=1; SET_WIZARD=1; shift ;;
--no-wizard) ENABLE_WIZARD=0; SET_WIZARD=1; shift ;;
--enable-ci) ENABLE_CI=1; SET_CI=1; shift ;;
--no-ci) ENABLE_CI=0; SET_CI=1; shift ;;
--fetch-boxart) FETCH_BOXART_FLAG=1; SET_BOXART=1; shift ;;
--no-fetch-boxart) FETCH_BOXART_FLAG=0; SET_BOXART=1; shift ;;
--generate) DO_GENERATE=1; SET_GENERATE=1; shift ;;
--no-generate) DO_GENERATE=0; SET_GENERATE=1; shift ;;
--enable-build) DO_BUILD=1; SET_BUILD=1; shift ;;
--no-build) DO_BUILD=0; SET_BUILD=1; shift ;;
--create-github) CREATE_GITHUB=1; SET_GITHUB=1; shift ;;
--no-github) CREATE_GITHUB=0; SET_GITHUB=1; shift ;;
--github-visibility) GITHUB_VISIBILITY=$2; shift 2 ;;
--github-owner) GITHUB_OWNER=$2; shift 2 ;;
--github-repo) GITHUB_REPO=$2; shift 2 ;;
--description) DESCRIPTION=$2; SET_DESCRIPTION=1; shift 2 ;;
--publisher) PUBLISHER=$2; SET_PUBLISHER=1; shift 2 ;;
--year) YEAR=$2; SET_YEAR=1; shift 2 ;;
--region) REGION=$2; SET_REGION=1; shift 2 ;;
--bios) BIOS_PATH=$2; shift 2 ;;
--yes|-y) YES_MODE=1; shift ;;
--stage-disc) STAGE_DISC=1; shift ;;
--no-stage-disc) STAGE_DISC=0; shift ;;
--psxrecomp-ref) PSXRECOMP_REF=$2; shift 2 ;;
--recomp-ui-ref) RECOMP_UI_REF=$2; shift 2 ;;
--recomp-net-ref) RECOMP_NET_REF=$2; shift 2 ;;
-h|--help) usage 0 ;;
*) echo "Unknown arg: $1" >&2; usage 1 ;;
esac
done
if [ "${PSXRECOMP_SETUP_YES:-0}" = "1" ]; then
YES_MODE=1
fi
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: missing required command: $1" >&2
exit 1
fi
}
need_cmd git
need_cmd python3
# --- Required path: disc ---------------------------------------------------
if [ -z "$DISC" ]; then
echo "error: --disc <path-to-game.cue> is required (use your shell to tab-complete the path)." >&2
echo " Example: sh $0 --disc ~/dumps/MyGame\\ \\(USA\\).cue" >&2
exit 1
fi
if [ ! -f "$DISC" ]; then
echo "error: --disc not found: $DISC" >&2
exit 1
fi
# --- Interactive / flag config ---------------------------------------------
if [ -z "$NAME" ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
echo "error: --name is required in non-interactive mode (--yes / non-TTY)" >&2
exit 1
fi
prompt_line "Project name (e.g. MyGameRecomp)" NAME ""
if [ -z "$NAME" ]; then
echo "error: project name is required" >&2
exit 1
fi
fi
if [ -z "$PLAYERS" ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
PLAYERS=2
else
prompt_line "Max players (1-8)" PLAYERS "2"
fi
fi
case "$PLAYERS" in
''|*[!0-9]*)
echo "error: players must be an integer 1–8 (got: $PLAYERS)" >&2
exit 1
;;
esac
if [ "$PLAYERS" -lt 1 ] || [ "$PLAYERS" -gt 8 ]; then
echo "error: players must be 1–8 (got: $PLAYERS)" >&2
exit 1
fi
if [ -z "$ZIP_PREFIX" ]; then
_derived=$(PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
python3 -c 'from fill_tokens import derive_zip_prefix; import sys; print(derive_zip_prefix(sys.argv[1]))' "$NAME")
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
ZIP_PREFIX=$_derived
else
prompt_line "Zip / CI artifact prefix" ZIP_PREFIX "$_derived"
fi
fi
if [ -z "$GITHUB_OWNER" ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
GITHUB_OWNER=RetroPortingToolKit
else
prompt_line "GitHub owner / org (README download badges)" GITHUB_OWNER \
"RetroPortingToolKit"
fi
fi
if [ -z "$GITHUB_REPO" ]; then
_derived_repo=$(PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
python3 -c 'from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))' "$NAME")
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
GITHUB_REPO=$_derived_repo
else
prompt_line "GitHub repo name (README download badges)" GITHUB_REPO "$_derived_repo"
fi
fi
GITHUB_OWNER=$(PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
python3 -c 'from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))' "$GITHUB_OWNER")
GITHUB_REPO=$(PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
python3 -c 'from fill_tokens import sanitize_github_name; import sys; print(sanitize_github_name(sys.argv[1]))' "$GITHUB_REPO")
# Optional marketing (catalog_identity.json + README)
if [ "$SET_DESCRIPTION" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
DESCRIPTION=""
else
prompt_line "Short game description (optional)" DESCRIPTION ""
fi
fi
if [ "$SET_PUBLISHER" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
PUBLISHER=""
else
prompt_line "Publisher (optional)" PUBLISHER ""
fi
fi
if [ "$SET_YEAR" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
YEAR=""
else
prompt_line "Release year (optional)" YEAR ""
fi
fi
if [ "$SET_REGION" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
REGION="USA"
else
prompt_line "Region" REGION "USA"
fi
fi
if [ "$SET_RECOMP_UI" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
ENABLE_RECOMP_UI=0
else
prompt_yn "Include recomp-ui launcher submodule?" ENABLE_RECOMP_UI 1
fi
fi
if [ "$ENABLE_RECOMP_UI" -eq 0 ]; then
if [ "${ENABLE_WIZARD:-0}" = "1" ] || [ "${ENABLE_NETPLAY:-0}" = "1" ]; then
echo "warning: wizard/netplay require recomp-ui — forcing both off." >&2
fi
ENABLE_WIZARD=0
ENABLE_NETPLAY=0
SET_WIZARD=1
SET_NETPLAY=1
echo " (recomp-ui declined — skipping wizard/netplay; PSX_RECOMP_UI=OFF)"
else
if [ "$SET_WIZARD" -eq 0 ]; then
# Default ON: the wizard is the first-run disc picker of the shipped
# (compiled) game.
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
ENABLE_WIZARD=1
else
prompt_yn "Enable first-run setup wizard + Generate & rebuild?" ENABLE_WIZARD 1
fi
fi
# 1-player titles cannot use multiplayer netplay — skip prompts entirely.
if [ "$PLAYERS" -eq 1 ]; then
if [ "${ENABLE_NETPLAY:-0}" = "1" ]; then
echo "warning: --enable-netplay ignored for 1-player title." >&2
fi
ENABLE_NETPLAY=0
SET_NETPLAY=1
echo " (1-player title — netplay skipped)"
elif [ "$SET_NETPLAY" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
ENABLE_NETPLAY=0
else
prompt_yn "Enable netplay UI (needs nested recomp-net)?" ENABLE_NETPLAY 0
fi
fi
fi
NETPLAY_LOBBY_WS=""
if [ "${ENABLE_NETPLAY:-0}" -eq 1 ]; then
if [ "$SET_LOBBY_URL" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
LOBBY_URL="$DEFAULT_LOBBY_HOST"
else
prompt_line "Netplay lobby server URL (host or ws://…)" LOBBY_URL \
"$DEFAULT_LOBBY_HOST"
fi
fi
NETPLAY_LOBBY_WS=$(normalize_lobby_url "$LOBBY_URL")
fi
if [ "$SET_CI" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
ENABLE_CI=0
else
prompt_yn "Add GitHub Actions release workflow (Linux/Windows/macOS)?" ENABLE_CI 1
fi
fi
if [ "$SET_BOXART" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
FETCH_BOXART_FLAG=0
else
prompt_yn "Fetch libretro boxart now? (needs network)" FETCH_BOXART_FLAG 1
fi
fi
if [ "$SET_GENERATE" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
DO_GENERATE=0
else
prompt_yn "Run Generate now (emitters + OpenBIOS + game C; generated/ is committed for release CI)?" DO_GENERATE 0
fi
fi
if [ "$SET_BUILD" -eq 0 ]; then
if [ "${DO_GENERATE:-0}" -eq 1 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
DO_BUILD=0
else
prompt_yn "Configure & build psx-runtime after Generate?" DO_BUILD 1
fi
else
DO_BUILD=0
SET_BUILD=1
fi
elif [ "${DO_BUILD:-0}" -eq 1 ] && [ "${DO_GENERATE:-0}" -eq 0 ]; then
echo "warning: --enable-build requires Generate — enabling Generate" >&2
DO_GENERATE=1
SET_GENERATE=1
fi
if [ "$SET_GITHUB" -eq 0 ]; then
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
CREATE_GITHUB=0
else
prompt_yn "Create GitHub repo with gh (needs auth)?" CREATE_GITHUB 0
fi
fi
if [ "${CREATE_GITHUB:-0}" -eq 1 ]; then
case "$GITHUB_VISIBILITY" in
public|private|internal) ;;
*)
if [ "$YES_MODE" -eq 1 ] || ! is_tty; then
GITHUB_VISIBILITY=private
else
prompt_line "GitHub visibility (public/private)" GITHUB_VISIBILITY "private"
fi
;;
esac
fi
if [ -n "$BIOS_PATH" ] && [ ! -f "$BIOS_PATH" ]; then
echo "error: --bios not found: $BIOS_PATH" >&2
exit 1
fi
if [ -n "$BIOS_PATH" ] && [ "$DO_GENERATE" -eq 0 ]; then
echo "warning: --bios ignored without generate" >&2
fi
PROJECT_CMAKE_NAME=$(printf '%s' "$NAME" | tr -c 'A-Za-z0-9_' '_')
ENV_PREFIX=$(printf '%s' "$PROJECT_CMAKE_NAME" | tr 'a-z' 'A-Z')
WINDOW_TITLE=$(printf '%s' "$NAME" | sed 's/Recomp$/ Recompiled/;s/Recompiled Recompiled/Recompiled/')
# Must match runtime.cmake OUTPUT_NAME: MAKE_C_IDENTIFIER(WINDOW_TITLE).
# Using $NAME alone (TwistedMetal4Recomp) breaks CI packaging — binary is
# TwistedMetal4_Recompiled.
EXE_BASENAME=$(printf '%s' "$WINDOW_TITLE" | tr -c 'A-Za-z0-9_' '_')
GAME_NAME=$(printf '%s' "$NAME" | sed 's/Recomp$//;s/Recompiled$//' | sed 's/[[:space:]]*$//')
[ -n "$GAME_NAME" ] || GAME_NAME="$NAME"
DISC_HINT="your legally owned ${GAME_NAME} disc"
ENTRY_PC="0x80010000"
[ -n "$REGION" ] || REGION="USA"
# Catalog keeps blanks; README table uses em-dash placeholders.
PUBLISHER_DISP=${PUBLISHER:-—}
YEAR_DISP=${YEAR:-—}
DESCRIPTION_MD="$DESCRIPTION"
if [ -z "$DESCRIPTION_MD" ]; then
DESCRIPTION_MD="_Add a short pitch in catalog_identity.json / README._"
fi
# Folder = GitHub/catalog install_dir slug (hyphenated), not display $NAME with spaces.
INSTALL_DIR_NAME="$GITHUB_REPO"
ROOT=$(CDPATH= cd -- "$PARENT" && pwd)/"$INSTALL_DIR_NAME"
if [ -e "$ROOT" ]; then
echo "error: target already exists: $ROOT" >&2
exit 1
fi
if [ "$INSTALL_DIR_NAME" != "$NAME" ]; then
echo "note: project folder is '$INSTALL_DIR_NAME' (catalog/GitHub slug; display name stays '$NAME')" >&2
fi
# --- CMake block files -----------------------------------------------------
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" "$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).' \
>"$RECOMP_UI_BLOCK_FILE"
printf '%s\n' ' CODEGEN_SETUP_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/codegen_setup.c"' \
>"$CODEGEN_ARG_FILE"
else
printf '%s\n' \
'set(PSX_RECOMP_UI OFF CACHE BOOL
"No recomp-ui submodule in this scaffold" FORCE)' \
>"$RECOMP_UI_BLOCK_FILE"
printf '%s\n' ' # CODEGEN_SETUP_SOURCES — add with recomp-ui / wizard later' \
>"$CODEGEN_ARG_FILE"
fi
if [ "$ENABLE_NETPLAY" -eq 1 ]; then
# PSX_NETPLAY defaults RNET_ENABLE_ICE=ON; recomp-net FetchContents
# libjuice via pinned URL (not git) so Retro AppImage builds configure.
printf '%s\n' \
'if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")
set(PSX_NETPLAY ON CACHE BOOL
"Link recomp-net delay-sync (opt-in; needs recomp-net)" FORCE)
endif()' >"$NETPLAY_BLOCK_FILE"
NETPLAY_RUNTIME_ARG=" ENABLE_NETPLAY_IF_PRESENT"
NETPLAY_LOBBY_URL_ARG=" NETPLAY_LOBBY_URL \"$NETPLAY_LOBBY_WS\""
else
printf '%s\n' \
'# Full netplay UI — enable after adding recomp-ui + testing:
# if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")
# set(PSX_NETPLAY ON CACHE BOOL "…" FORCE)
# endif()' >"$NETPLAY_BLOCK_FILE"
NETPLAY_RUNTIME_ARG=" # ENABLE_NETPLAY_IF_PRESENT"
NETPLAY_LOBBY_URL_ARG=" # NETPLAY_LOBBY_URL \"ws://${DEFAULT_LOBBY_HOST}:8765\""
fi
if [ "$ENABLE_WIZARD" -eq 1 ]; then
printf '%s\n' \
'set(PSX_SETUP_WIZARD ON CACHE BOOL
"Advertise first-run setup wizard + Generate & rebuild in recomp-ui" FORCE)' \
>"$WIZARD_BLOCK_FILE"
WIZARD_RUNTIME_ARG=" ENABLE_SETUP_WIZARD"
else
printf '%s\n' \
'# First-run setup wizard — enable after testing:
# set(PSX_SETUP_WIZARD ON CACHE BOOL "…" FORCE)' >"$WIZARD_BLOCK_FILE"
WIZARD_RUNTIME_ARG=" # ENABLE_SETUP_WIZARD"
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
dst=$2
python3 "$FILL_TOKENS" "$src" "$dst" \
--set "PROJECT_CMAKE_NAME=$PROJECT_CMAKE_NAME" \
--set "WINDOW_TITLE=$WINDOW_TITLE" \
--set "BOOT_EXE=$BOOT_EXE" \
--set "GAME_NAME=$GAME_NAME" \
--set "ENV_PREFIX=$ENV_PREFIX" \
--set "EXE_BASENAME=$EXE_BASENAME" \
--set "ZIP_PREFIX=$ZIP_PREFIX" \
--set "GITHUB_OWNER=$GITHUB_OWNER" \
--set "GITHUB_REPO=$GITHUB_REPO" \
--set "DISC_HINT=$DISC_HINT" \
--set "GAME_TITLE=$WINDOW_TITLE" \
--set "PLAYERS=$PLAYERS" \
--set "DESCRIPTION=$DESCRIPTION_MD" \
--set "PUBLISHER=$PUBLISHER_DISP" \
--set "YEAR=$YEAR_DISP" \
--set "REGION=$REGION" \
--set "ENTRY_PC=$ENTRY_PC" \
--set "NETPLAY_RUNTIME_ARG=$NETPLAY_RUNTIME_ARG" \
--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" \
--set-file "CODEGEN_SETUP_ARG=$CODEGEN_ARG_FILE"
}
echo "== New Project Layout =="
echo " repo: $ROOT"
echo " disc: $DISC"
echo " zip prefix: $ZIP_PREFIX"
echo " gh slug: $GITHUB_OWNER/$GITHUB_REPO"
echo " players: $PLAYERS"
echo " recomp-ui: $ENABLE_RECOMP_UI"
echo " wizard: $ENABLE_WIZARD"
echo " netplay: $ENABLE_NETPLAY"
if [ "$ENABLE_NETPLAY" -eq 1 ]; then
echo " lobby URL: $NETPLAY_LOBBY_WS"
fi
echo " CI: $ENABLE_CI"
echo " boxart: $FETCH_BOXART_FLAG"
echo " generate: $DO_GENERATE"
echo " build: $DO_BUILD"
echo " github: $CREATE_GITHUB"
echo " region: $REGION"
mkdir -p "$ROOT"
cd "$ROOT"
git init -q
fill_template "$TEMPLATE_DIR/CMakeLists.txt.in" "$ROOT/CMakeLists.txt"
fill_template "$TEMPLATE_DIR/game.toml.in" "$ROOT/game.toml"
fill_template "$TEMPLATE_DIR/game_options.toml.in" "$ROOT/game_options.toml"
fill_template "$TEMPLATE_DIR/codegen_setup.c.in" "$ROOT/codegen_setup.c"
fill_template "$TEMPLATE_DIR/codegen_setup.h.in" "$ROOT/codegen_setup.h"
fill_template "$TEMPLATE_DIR/gitignore.in" "$ROOT/.gitignore"
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/assets" "$ROOT/.github"
cp "$SCRIPT_DIR/sync_symbols.py" "$ROOT/tools/sync_symbols.py"
chmod +x "$ROOT/tools/sync_symbols.py"
if [ -f "$TEMPLATE_DIR/raid-discord.png" ]; then
cp "$TEMPLATE_DIR/raid-discord.png" "$ROOT/.github/raid-discord.png"
fi
# Empty mod catalog tree (build stages mods/preloaded/packages → <exe>/mods/bundled).
cat > "$ROOT/mods/preloaded/README.md" <<'EOF'
# Preloaded mods
Ship reviewed, default-disabled packages here:
```text
packages/<package-id>/<version>/
manifest.toml
…
```
Build wiring copies `mods/preloaded/packages` next to the game executable as
`mods/bundled/`. That tree is build output: every build wipes and re-stages it,
so nothing you place there by hand survives.
Player-installed `.psxmod` archives live in `mods/installed/`, which the
launcher owns and no build ever touches. Install them through the launcher Mods
manager rather than committing them here.
See `psxrecomp/docs/MOD_PACKAGES.md`.
EOF
: > "$ROOT/mods/preloaded/packages/.gitkeep"
echo "== Adding submodules =="
git submodule add -b "$PSXRECOMP_REF" "$PSXRECOMP_URL" psxrecomp
if [ "$ENABLE_RECOMP_UI" -eq 1 ]; then
git submodule add -b "$RECOMP_UI_REF" "$RECOMP_UI_URL" recomp-ui
fi
git submodule update --init --recursive
# Retro-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
fi
if [ -d psxrecomp/lib/recomp-net ]; then
# Nested submodule pins a SHA inside psxrecomp — not live remote main.
# Optional override so netplay can follow a branch that has rollback.h etc.
if [ -n "$RECOMP_NET_REF" ]; then
echo "== Override recomp-net → $RECOMP_NET_REF =="
git -C psxrecomp/lib/recomp-net fetch -q origin "$RECOMP_NET_REF"
git -C psxrecomp/lib/recomp-net checkout --detach -q FETCH_HEAD
git -C psxrecomp add lib/recomp-net
else
git -C psxrecomp/lib/recomp-net checkout --detach -q HEAD 2>/dev/null || true
fi
fi
echo "== Framework pins =="
if [ -x psxrecomp/tools/ci/record_pins.sh ]; then
bash psxrecomp/tools/ci/record_pins.sh --root "$ROOT" | tee "$ROOT/framework_pins.txt"
else
{
echo "psxrecomp=$(git -C psxrecomp rev-parse HEAD)"
if [ "$ENABLE_RECOMP_UI" -eq 1 ]; then
echo "recomp-ui=$(git -C recomp-ui rev-parse HEAD)"
else
echo "recomp-ui=<not added>"
fi
if [ -d psxrecomp/lib/recomp-net ]; then
echo "recomp-net=$(git -C psxrecomp/lib/recomp-net rev-parse HEAD 2>/dev/null || echo missing)"
fi
} | tee "$ROOT/framework_pins.txt"
fi
if [ "$ENABLE_NETPLAY" -eq 1 ] && [ ! -f psxrecomp/lib/recomp-net/CMakeLists.txt ]; then
echo "warning: netplay enabled but psxrecomp/lib/recomp-net missing after recursive update." >&2
fi
echo "== Packager stub =="
fill_template "$TEMPLATE_DIR/package_release.sh.in" \
"$ROOT/scripts/package_release.sh"
chmod +x "$ROOT/scripts/package_release.sh"
CI_WORKFLOW_OK=0
if [ "$ENABLE_CI" -eq 1 ]; then
echo "== CI release workflow =="
WF_SRC="$ROOT/psxrecomp/docs/ci/templates/game-release.yml"
if [ -f "$WF_SRC" ]; then
mkdir -p "$ROOT/.github/workflows"
python3 "$FILL_TOKENS" "$WF_SRC" "$ROOT/.github/workflows/release.yml" \
--ci-placeholders \
--set "ZIP_PREFIX=$ZIP_PREFIX" \
--set "GAME_TITLE=$WINDOW_TITLE" \
--set "WINDOW_TITLE=$WINDOW_TITLE"
echo " wrote .github/workflows/release.yml"
CI_WORKFLOW_OK=1
else
echo "error: CI enabled but missing $WF_SRC" >&2
echo " Bump --psxrecomp-ref to a pin that has docs/ci/templates/." >&2
exit 1
fi
else
echo "== CI workflow skipped =="
fi
DISC_BASENAME=""
PROBED=0
SEED_COUNT=0
mkdir -p "$ROOT/disc"
DISC_ABS=$(CDPATH= cd -- "$(dirname -- "$DISC")" && pwd)/$(basename -- "$DISC")
DISC_BASENAME=$(basename -- "$DISC_ABS")
case "$DISC" in
*.cue|*.CUE) ;;
*)
echo "warning: prefer a Redump-style .cue (with sibling .bin tracks)." >&2
;;
esac
PROBE_CUE="$DISC_ABS"
DISC_TOML_PATH="$DISC_ABS"
GEN_DISC_HINT="$DISC_ABS"
if [ "$STAGE_DISC" -eq 1 ]; then
echo "== Staging full disc dump into disc/ (optional; large) =="
python3 - "$DISC_ABS" "$ROOT/disc" <<'PY'
import re, shutil, sys
from pathlib import Path
cue = Path(sys.argv[1])
out = Path(sys.argv[2])
out.mkdir(parents=True, exist_ok=True)
shutil.copy2(cue, out / cue.name)
print(f" copied {cue.name}")
text = cue.read_text(encoding="utf-8", errors="ignore")
for m in re.finditer(r'FILE\s+"([^"]+)"', text, re.I):
src = cue.parent / m.group(1)
if src.is_file():
dst = out / src.name
if not dst.exists():
shutil.copy2(src, dst)
print(f" copied track: {src.name}")
else:
print(f" warning: cue FILE missing: {src}", file=sys.stderr)
print(" disc staged under disc/ (gitignored — never commit dumps)")
PY
PROBE_CUE="$ROOT/disc/$DISC_BASENAME"
DISC_TOML_PATH="disc/$DISC_BASENAME"
GEN_DISC_HINT="disc/$DISC_BASENAME"
else
echo "== External disc (no full copy) =="
echo " cue remains at: $DISC_ABS"
echo " will extract boot EXE only into disc/"
fi
case "$DISC_BASENAME" in
*.cue|*.CUE)
echo "== Probing disc (identity + seeds + TOC fp) =="
if python3 "$PROBE_DISC" "$PROBE_CUE" \
--json-out "$ROOT/disc_probe.json" \
--write-game-toml "$ROOT/game.toml" \
--write-catalog "$ROOT/catalog_identity.json" \
--write-seeds "$ROOT/seeds/ghidra_funcs.txt" \
--write-boot-exe "$ROOT/disc" \
--disc-rel "$DISC_TOML_PATH" \
--out-dir disc \
--players "$PLAYERS" \
--display-name "$GAME_NAME" \
--description "$DESCRIPTION" \
--publisher "$PUBLISHER" \
--year "$YEAR" \
--region "$REGION"; then
PROBED=1
SEED_COUNT=$(grep -c '^0x' "$ROOT/seeds/ghidra_funcs.txt" 2>/dev/null || echo 0)
BOOT_FROM_TOML=$(python3 -c '
import re,sys
t=open(sys.argv[1],encoding="utf-8").read()
m=re.search(r"boot_exe\s*=\s*\"([^\"]+)\"", t)
print(m.group(1) if m else "")
' "$ROOT/game.toml")
ENTRY_FROM_TOML=$(python3 -c '
import re,sys
t=open(sys.argv[1],encoding="utf-8").read()
m=re.search(r"entry_pc\s*=\s*\"([^\"]+)\"", t)
print(m.group(1) if m else "")
' "$ROOT/game.toml")
if [ -n "$BOOT_FROM_TOML" ] && [ "$BOOT_FROM_TOML" != "$BOOT_EXE" ]; then
BOOT_EXE="$BOOT_FROM_TOML"
fill_template "$TEMPLATE_DIR/CMakeLists.txt.in" "$ROOT/CMakeLists.txt"
fill_template "$TEMPLATE_DIR/codegen_setup.c.in" "$ROOT/codegen_setup.c"
echo " synced boot EXE → $BOOT_EXE (CMake / codegen)"
fi
if [ -n "$ENTRY_FROM_TOML" ]; then
ENTRY_PC="$ENTRY_FROM_TOML"
fill_template "$TEMPLATE_DIR/symbols.toml.in" "$ROOT/symbols.toml"
echo " symbols.toml BootEntry → $ENTRY_PC"
fi
if [ "$STAGE_DISC" -eq 0 ]; then
echo " game.toml disc → $DISC_TOML_PATH"
echo " (local absolute path — do not commit machine-specific dumps)"
fi
else
echo "warning: disc probe failed — left template game.toml; fill by hand." >&2
fi
;;
*)
echo "warning: --disc is not a .cue; skipped probe autofill." >&2
;;
esac
# Community metadata (https://recomp.fyi/spec) — serial from the probe if it ran.
echo "== Writing .recomp.json =="
python3 "$WRITE_RECOMP_JSON" "$ROOT/.recomp.json" \
--game "$GAME_NAME" \
--project "$NAME" \
--region "$REGION" \
--probe-json "$ROOT/disc_probe.json" \
|| echo "warning: .recomp.json not written" >&2
if [ "$FETCH_BOXART_FLAG" -eq 1 ]; then
echo "== Fetching libretro boxart =="
CUE_HINT="${DISC_BASENAME:-$GAME_NAME}"
if python3 "$FETCH_BOXART" \
--out "$ROOT/launcher_assets/img/boxart.tga" \
--cue-stem "$CUE_HINT" \
--display-name "$GAME_NAME"; then
HAS_BOXART=1
printf '%s\n' ' LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"' \
>"$BOXART_BLOCK_FILE"
fill_template "$TEMPLATE_DIR/CMakeLists.txt.in" "$ROOT/CMakeLists.txt"
echo " wired LAUNCHER_BOXART in CMakeLists.txt"
PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" python3 -c \
'from pathlib import Path; import sys; from project_studio.readme_metrics import inject_readme_boxart; inject_readme_boxart(Path(sys.argv[1]), sys.argv[2])' \
"$ROOT/README.md" "$GAME_NAME" \
&& echo " injected boxart into README.md" \
|| echo "warning: README boxart inject failed" >&2
else
echo "warning: boxart fetch failed — leave LAUNCHER_BOXART commented." >&2
fi
fi
echo "== Sync symbols header =="
(
cd "$ROOT"
python3 tools/sync_symbols.py --game "$GAME_NAME" || true
)
git add CMakeLists.txt game.toml codegen_setup.c codegen_setup.h .gitignore VERSION README.md seeds scripts tools mods framework_pins.txt symbols.toml psx_symbols.h || true
if [ -d "$ROOT/.github" ]; then
git add .github || true
fi
if [ -f "$ROOT/catalog_identity.json" ]; then
git add catalog_identity.json || true
fi
if [ -f "$ROOT/disc_probe.json" ]; then
git add disc_probe.json || true
fi
if [ -f "$ROOT/.recomp.json" ]; then
git add .recomp.json || true
fi
if [ "$HAS_BOXART" -eq 1 ]; then
git add launcher_assets/img/boxart.tga launcher_assets/img/boxart.png launcher_assets/img/BOXART_SOURCE.txt || true
fi
COMMITTED=0
if git -c user.email=setup@localhost -c user.name=setup \
commit -q -m "Initial New Project Layout scaffold" 2>/dev/null; then
COMMITTED=1
else
echo " (skipped initial commit — commit manually when ready)"
fi
# Create the GitHub repo + origin remote now, but do NOT push yet.
# Push once after generate/build so CI workflow and final pins land together
# (early --push caused a second local "initial" commit to conflict on re-run).
GITHUB_CREATED=0
if [ "${CREATE_GITHUB:-0}" -eq 1 ]; then
echo "== GitHub repo (gh, create only — push deferred) =="
if ! command -v gh >/dev/null 2>&1; then
echo "warning: gh not installed — skip create; push manually later." >&2
elif [ "$COMMITTED" -eq 0 ]; then
echo "warning: no initial commit — skip gh repo create." >&2
else
VIS_FLAG="--private"
[ "$GITHUB_VISIBILITY" = "public" ] && VIS_FLAG="--public"
[ "$GITHUB_VISIBILITY" = "internal" ] && VIS_FLAG="--internal"
GH_SLUG="$GITHUB_OWNER/$GITHUB_REPO"
GH_DESC=$(PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
python3 -c 'from fill_tokens import GITHUB_ABOUT_DESCRIPTION; print(GITHUB_ABOUT_DESCRIPTION)')
GH_HOME=$(PYTHONPATH="$SCRIPT_DIR${PYTHONPATH:+:$PYTHONPATH}" \
python3 -c 'from fill_tokens import GITHUB_ABOUT_HOMEPAGE; print(GITHUB_ABOUT_HOMEPAGE)')
if gh repo create "$GH_SLUG" $VIS_FLAG --source="$ROOT" --remote=origin \
--description "$GH_DESC" --homepage "$GH_HOME"; then
echo " created origin ($GITHUB_VISIBILITY); push deferred until end"
GITHUB_CREATED=1
else
echo "warning: gh repo create failed — if the repo already exists," >&2
echo " add origin and push manually at the end." >&2
if ! git remote get-url origin >/dev/null 2>&1; then
_gh_url=$(gh repo view "$GH_SLUG" --json url -q .url 2>/dev/null || true)
if [ -n "$_gh_url" ]; then
git remote add origin "$_gh_url"
echo " attached existing origin → $_gh_url"
GITHUB_CREATED=1
fi
else
GITHUB_CREATED=1
fi
fi
if [ "$GITHUB_CREATED" -eq 1 ]; then
if gh repo edit "$GH_SLUG" --description "$GH_DESC" --homepage "$GH_HOME"; then
echo " GitHub About: R.A.I.D. description + Discord homepage"
else
echo "warning: gh repo edit (About) failed — set description/homepage by hand." >&2
fi
fi
fi
fi
GEN_DISC_HINT="${GEN_DISC_HINT:-disc/$DISC_BASENAME}"
GENERATED_OK=0
BUILD_OK=0
if [ "$DO_GENERATE" -eq 1 ]; then
echo "== Generate (emitters + OpenBIOS + game C) =="
if [ ! -x "$ROOT/psxrecomp/tools/ci/build_emitters.sh" ]; then
echo "error: missing psxrecomp/tools/ci/build_emitters.sh" >&2
exit 1
fi
(
cd "$ROOT"
bash psxrecomp/tools/ci/build_emitters.sh
# Quote disc/bios paths (Redump cues often contain spaces).
set -- --config game.toml --project-root . --disc "$GEN_DISC_HINT"
if [ -n "$BIOS_PATH" ]; then
BIOS_ABS=$(CDPATH= cd -- "$(dirname -- "$BIOS_PATH")" && pwd)/$(basename -- "$BIOS_PATH")
set -- "$@" --bios "$BIOS_ABS"
fi
python3 psxrecomp/psxrecomp_cli.py generate "$@"
) && GENERATED_OK=1
if [ "$GENERATED_OK" -eq 1 ]; then
# Release CI builds the compiled game from this tree, so it is part
# of the repo, not a local by-product.
if git add generated 2>/dev/null && \
git -c user.email=setup@localhost -c user.name=setup \
commit -q -m "Add generated game C" 2>/dev/null; then
echo " generate OK (generated/ committed — release CI builds from it)"
else
echo " generate OK — commit generated/ before tagging a release: git add generated"
fi
else
echo "warning: generate failed — fix seeds/disc and re-run generate by hand." >&2
DO_BUILD=0
fi
fi
if [ "${DO_BUILD:-0}" -eq 1 ] && [ "$GENERATED_OK" -eq 1 ]; then
echo "== Configure & build psx-runtime =="
if (
cd "$ROOT"
cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build-release --target psx-runtime
); then
BUILD_OK=1
echo " build OK → build-release/ (gitignored)"
else
echo "warning: build failed — fix and re-run cmake by hand." >&2
fi
fi
# Return registered release.yml workflow name, or empty.
github_release_workflow_name() {
_owner=$1
gh api "repos/${_owner}/actions/workflows" \
--jq '.workflows[] | select(.path|endswith("release.yml")) | .name' \
2>/dev/null | head -n1 || true
}
# After the first push, Actions sometimes does not index release.yml (especially
# if the repo was private at first push). Poll, then nudge with a tiny commit.
ensure_actions_registers_release_yml() {
_owner=$1
_wf_path=".github/workflows/release.yml"
[ -f "$_wf_path" ] || return 0
command -v gh >/dev/null 2>&1 || return 0
# If the user asked for public, force visibility before re-checking (idempotent).
if [ "${GITHUB_VISIBILITY}" = "public" ]; then
gh repo edit "$_owner" --visibility public --accept-visibility-change-consequences \
>/dev/null 2>&1 || true
fi
_i=0
while [ "$_i" -lt 5 ]; do
_wf=$(github_release_workflow_name "$_owner")
if [ -n "$_wf" ]; then
echo " Actions workflow registered: $_wf"
echo " (runs on workflow_dispatch or push of v* tags)"
return 0
fi
_i=$((_i + 1))
[ "$_i" -lt 5 ] && sleep 2
done
echo " Actions has not listed release.yml yet — nudging with a follow-up commit…"
{
printf '\n'
printf '# Actions registration nudge (%s UTC)\n' "$(date -u +%Y-%m-%dT%H:%MZ)"
} >>"$_wf_path"
git add "$_wf_path"
if git -c user.email=setup@localhost -c user.name=setup \
commit -q -m "ci: nudge Actions to register release.yml"; then
if git push origin HEAD; then
_i=0
while [ "$_i" -lt 6 ]; do
_wf=$(github_release_workflow_name "$_owner")
if [ -n "$_wf" ]; then
echo " Actions workflow registered after nudge: $_wf"
echo " (runs on workflow_dispatch or push of v* tags)"
return 0
fi
_i=$((_i + 1))
[ "$_i" -lt 6 ] && sleep 2
done
echo "warning: release.yml nudged but Actions still has not listed it;" >&2
echo " open the Actions tab once, or re-push after making the repo public." >&2
else
echo "warning: nudge commit created but push failed." >&2
fi
else
echo "warning: could not create Actions nudge commit." >&2
fi
}
# Single publish after scaffold (+ optional generate/build).
GITHUB_PUSHED=0
if [ "${CREATE_GITHUB:-0}" -eq 1 ] && git remote get-url origin >/dev/null 2>&1; then
echo "== GitHub push (final) =="
if [ "$COMMITTED" -eq 0 ]; then
echo "warning: no local commit to push." >&2
elif git push -u origin HEAD; then
GITHUB_PUSHED=1
echo " pushed $(git rev-parse --abbrev-ref HEAD) → origin"
if [ "$CI_WORKFLOW_OK" -eq 1 ] && command -v gh >/dev/null 2>&1; then
_owner_repo=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)
if [ -n "$_owner_repo" ]; then
ensure_actions_registers_release_yml "$_owner_repo"
fi
fi
else
echo "warning: git push failed (non-fast-forward if origin already has" >&2
echo " a different initial commit). Resolve with:" >&2
echo " git fetch origin && git push -u origin HEAD" >&2
echo " or, if this local tree should win: git push -u origin HEAD --force" >&2
fi
fi
if [ "$PROBED" -eq 1 ]; then
STEP1="Review game.toml + catalog_identity.json + seeds/ghidra_funcs.txt ($SEED_COUNT JAL seeds)."
else
STEP1="Edit game.toml / re-run probe_disc.py --write-seeds."
fi
BUILD_HINT="cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build build-release --target psx-runtime"
if [ "$BUILD_OK" -eq 1 ]; then
STEP2="Runtime already built under build-release/ — soak offline boot."
elif [ "$GENERATED_OK" -eq 1 ]; then
STEP2="Build playable runtime:
$BUILD_HINT"
else
STEP2="Build emitters, Generate (and commit it — release CI builds from generated/), then playable runtime:
./psxrecomp/tools/ci/build_emitters.sh
python3 psxrecomp/psxrecomp_cli.py generate \\
--config game.toml --project-root . --disc \"$GEN_DISC_HINT\"
git add generated && git commit -m 'Add generated game C'
$BUILD_HINT"
fi
CI_NOTE="CI workflow not installed (declined)."
if [ "$ENABLE_CI" -eq 1 ] && [ "$CI_WORKFLOW_OK" -eq 1 ]; then
CI_NOTE="CI: .github/workflows/release.yml ready (zip prefix=$ZIP_PREFIX; builds the committed generated/ C into the shipped game; submodule gitlinks pin the build)."
if [ "$GITHUB_PUSHED" -eq 1 ]; then
CI_NOTE="$CI_NOTE Pushed — open Actions → Release builds (workflow_dispatch)."
fi
fi
GH_NOTE="GitHub remote not created (declined)."
if [ "${CREATE_GITHUB:-0}" -eq 1 ]; then
if [ "$GITHUB_PUSHED" -eq 1 ]; then
GH_NOTE="GitHub: created/attached origin and pushed ($GITHUB_VISIBILITY)."
elif [ "$GITHUB_CREATED" -eq 1 ]; then
GH_NOTE="GitHub: origin ready but push failed — see warnings above."
else
GH_NOTE="GitHub: create/push did not complete — check gh auth / remote."
fi
fi
cat <<EOF
== Done ==
Next steps:
1. $STEP1
2. $STEP2
3. Label symbols in symbols.toml → python3 tools/sync_symbols.py
4. Soak offline boot → netplay QA (if enabled) → tag vX.Y.Z.
5. $CI_NOTE
Pins: submodule gitlinks (framework_pins.txt is an optional snapshot)
6. $GH_NOTE
Docs: psxrecomp/docs/GAME_PROJECT_SETUP.md · psxrecomp/docs/SYMBOLS.md
EOF