mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
self-build: loop breaker, install-folder preflight, boot-EXE name lint
Follow-ups to the marker-skew hardening, closing the remaining legs of
the generate+build loop class:
- Loop breaker: host_prepare_generate stamps a successful generate into
.psxrecomp_last_generate at the project root. When apply() is about to
reopen the wizard because sources are missing and the stamp is recent,
the wizard's prepare note becomes a diagnostic naming exactly which
gate is unmet (the expected *_dispatch.c and/or the BIOS backends) and
what generated/ actually contains, instead of silently asking for
another round. The stamp is dropped once sources are visible so a much
later manual wipe of generated/ reads as a fresh first run.
- Preflight: generate now refuses up front when the project root sits
under the Windows temp directory (Explorer's run-from-inside-the-ZIP
extraction, which evaporates) or is not writable, with instructions,
instead of failing downstream in ways that present as the same loop.
- CI lint: tools/ci/check_boot_exe.sh cross-checks the three baked
copies of the boot-EXE name (game.toml boot_exe/exe, CMakeLists
GEN_MARKER, codegen_setup.c gen_marker_relpath) and the setup-release
template gains a fail-fast "Lint boot-EXE name consistency" step, so a
skew fails the maintainer's release instead of the player's install.
op_emit_ci_workflow's step-name drift detection flags older titles
automatically. Script is bash-3.2-clean for macOS runners; verified
against fixture repos (consistent / skewed / boot_exe override /
single-source) and real ports - where it immediately caught
Tomba2Recomp's placeholder gen_marker_relpath ("SLUS_01234" vs
SCUS_944.54), a live instance of the reported loop.
Both gitignore templates learn the new sidecar names.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfPHUBEYiSCwaGiwRc8ea7
This commit is contained in:
@@ -69,6 +69,8 @@ psx_crash.txt
|
||||
psx_last_run_report.json
|
||||
psx_screenshot.bmp
|
||||
psx_freeze_heartbeat.json
|
||||
.psxrecomp_last_generate
|
||||
.psxrecomp_write_probe
|
||||
starvation_dump.jsonl
|
||||
|
||||
# Diagnostic dumps (root-level) — keep catalog / probe trackable
|
||||
|
||||
@@ -195,6 +195,14 @@ jobs:
|
||||
shell: bash
|
||||
run: bash psxrecomp/tools/ci/record_pins.sh
|
||||
|
||||
# A skew between game.toml, CMakeLists GEN_MARKER and codegen_setup.c
|
||||
# makes every player's self-build loop generate+build forever. The CLI
|
||||
# also refuses the skew at generate time, but failing the release is
|
||||
# cheaper than failing the player.
|
||||
- name: Lint boot-EXE name consistency
|
||||
shell: bash
|
||||
run: bash psxrecomp/tools/ci/check_boot_exe.sh .
|
||||
|
||||
- name: Setup host mode (no game C / BIOS backends in CI)
|
||||
shell: bash
|
||||
run: bash psxrecomp/tools/ci/clear_generated.sh
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
# include <windows.h>
|
||||
@@ -3582,6 +3583,165 @@ static void host_update_disc_set(RecompLauncherCPrepareProgressFn on_progress,
|
||||
"Could not record every disc — continuing with the boot disc.");
|
||||
}
|
||||
|
||||
/* ---- self-build loop breaker + preflight ------------------------------- */
|
||||
|
||||
#define HOST_LAST_GENERATE_SIDECAR ".psxrecomp_last_generate"
|
||||
/* A wizard loop iterates in minutes; a recent stamp with sources still
|
||||
* missing means "generate worked but nothing gates on its output" — a
|
||||
* config skew — not a user who wiped generated/ last week. */
|
||||
#define HOST_LOOP_BREAKER_WINDOW_SECS (6 * 60 * 60)
|
||||
|
||||
static char g_loop_breaker_note[1024];
|
||||
|
||||
/* Comma-join the *_dispatch.c basenames under <root>/generated. */
|
||||
static void list_generated_dispatch(char* out, size_t cap) {
|
||||
char dir[1100];
|
||||
size_t used = 0;
|
||||
out[0] = '\0';
|
||||
if (!join_path(dir, sizeof(dir), g_project_root, "generated"))
|
||||
return;
|
||||
#if defined(_WIN32)
|
||||
{
|
||||
char pat[1200];
|
||||
WIN32_FIND_DATAA fd;
|
||||
HANDLE h;
|
||||
if (!join_path(pat, sizeof(pat), dir, "*_dispatch.c"))
|
||||
return;
|
||||
h = FindFirstFileA(pat, &fd);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
do {
|
||||
int n = snprintf(out + used, cap - used, "%s%s",
|
||||
used ? ", " : "", fd.cFileName);
|
||||
if (n <= 0 || (size_t)n >= cap - used)
|
||||
break;
|
||||
used += (size_t)n;
|
||||
} while (FindNextFileA(h, &fd));
|
||||
FindClose(h);
|
||||
}
|
||||
#else
|
||||
{
|
||||
static const char suffix[] = "_dispatch.c";
|
||||
DIR* d = opendir(dir);
|
||||
struct dirent* e;
|
||||
if (!d)
|
||||
return;
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
size_t len = strlen(e->d_name);
|
||||
int n;
|
||||
if (len <= sizeof(suffix) - 1 ||
|
||||
strcmp(e->d_name + len - (sizeof(suffix) - 1), suffix) != 0)
|
||||
continue;
|
||||
n = snprintf(out + used, cap - used, "%s%s",
|
||||
used ? ", " : "", e->d_name);
|
||||
if (n <= 0 || (size_t)n >= cap - used)
|
||||
break;
|
||||
used += (size_t)n;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void host_record_generate_ok(void) {
|
||||
char sidecar[1200], line[32];
|
||||
if (!join_path(sidecar, sizeof(sidecar), g_project_root,
|
||||
HOST_LAST_GENERATE_SIDECAR))
|
||||
return;
|
||||
snprintf(line, sizeof(line), "%lld", (long long)time(NULL));
|
||||
write_line_file(sidecar, line);
|
||||
}
|
||||
|
||||
static void host_clear_generate_stamp(void) {
|
||||
char sidecar[1200];
|
||||
if (join_path(sidecar, sizeof(sidecar), g_project_root,
|
||||
HOST_LAST_GENERATE_SIDECAR))
|
||||
remove(sidecar);
|
||||
}
|
||||
|
||||
/* apply() consults this when the wizard is about to reopen because sources
|
||||
* are missing. If a generate succeeded here recently, re-running it will
|
||||
* loop — say exactly what is missing and what exists instead of silently
|
||||
* re-prompting. Returns NULL when there is nothing to warn about. */
|
||||
static const char* host_loop_breaker_note(void) {
|
||||
char sidecar[1200], line[64], found[512], marker_abs[1200];
|
||||
long long then, now;
|
||||
const char* marker_rel;
|
||||
int game_missing, bios_missing;
|
||||
if (!join_path(sidecar, sizeof(sidecar), g_project_root,
|
||||
HOST_LAST_GENERATE_SIDECAR))
|
||||
return NULL;
|
||||
if (!read_line_file(sidecar, line, sizeof(line)))
|
||||
return NULL;
|
||||
then = strtoll(line, NULL, 10);
|
||||
now = (long long)time(NULL);
|
||||
if (then <= 0 || now < then || now - then > HOST_LOOP_BREAKER_WINDOW_SECS)
|
||||
return NULL;
|
||||
marker_rel = cfg_or(g_cfg->gen_marker_relpath,
|
||||
"generated/SLUS_011.89_dispatch.c");
|
||||
game_missing =
|
||||
!join_path(marker_abs, sizeof(marker_abs), g_project_root,
|
||||
marker_rel) ||
|
||||
!path_is_file(marker_abs);
|
||||
bios_missing = bios_backends_missing();
|
||||
if (!game_missing && !bios_missing)
|
||||
return NULL;
|
||||
list_generated_dispatch(found, sizeof(found));
|
||||
snprintf(g_loop_breaker_note, sizeof(g_loop_breaker_note),
|
||||
"A Generate completed here recently, yet the launcher still "
|
||||
"cannot find %s%s%s. generated/ contains: %s. Running Generate "
|
||||
"again will very likely loop — please report this to the port "
|
||||
"maintainer: the project's boot-EXE names disagree.",
|
||||
game_missing ? marker_rel : "",
|
||||
(game_missing && bios_missing) ? " and " : "",
|
||||
bios_missing ? "the BIOS backends under psxrecomp/generated/"
|
||||
: "",
|
||||
found[0] ? found : "no *_dispatch.c at all");
|
||||
fprintf(stderr, "psxrecomp-codegen: %s\n", g_loop_breaker_note);
|
||||
return g_loop_breaker_note;
|
||||
}
|
||||
|
||||
/* Generating into Explorer's temporary zip extraction (Windows) or an
|
||||
* unwritable folder always ends badly — the output evaporates or never
|
||||
* lands, and the wizard reopens forever. Refuse with instructions. */
|
||||
static int host_preflight_project_root(char* err_msg, size_t err_cap) {
|
||||
#if defined(_WIN32)
|
||||
{
|
||||
char tmp[MAX_PATH];
|
||||
DWORD n = GetTempPathA((DWORD)sizeof(tmp), tmp);
|
||||
if (n > 0 && n < (DWORD)sizeof(tmp) &&
|
||||
_strnicmp(g_project_root, tmp, strlen(tmp)) == 0) {
|
||||
snprintf(err_msg, err_cap,
|
||||
"This game is running from a temporary folder (%s), "
|
||||
"usually because the downloaded ZIP was opened without "
|
||||
"extracting it. Extract the whole ZIP to a normal "
|
||||
"folder, then run the game from there.",
|
||||
g_project_root);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
char probe[1200];
|
||||
FILE* f;
|
||||
if (!join_path(probe, sizeof(probe), g_project_root,
|
||||
".psxrecomp_write_probe"))
|
||||
return 1; /* path too long — let the CLI surface the real error */
|
||||
f = fopen(probe, "wb");
|
||||
if (!f) {
|
||||
snprintf(err_msg, err_cap,
|
||||
"The install folder is not writable: %s. Move the game "
|
||||
"folder somewhere writable (e.g. your user folder) or "
|
||||
"fix its permissions, then retry.",
|
||||
g_project_root);
|
||||
return 0;
|
||||
}
|
||||
fclose(f);
|
||||
remove(probe);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int host_prepare_generate(const char* source_path, char* out_path,
|
||||
size_t out_cap, char* err_msg, size_t err_cap,
|
||||
RecompLauncherCPrepareProgressFn on_progress,
|
||||
@@ -3594,6 +3754,8 @@ static int host_prepare_generate(const char* source_path, char* out_path,
|
||||
snprintf(err_msg, err_cap, "No disc selected.");
|
||||
return 0;
|
||||
}
|
||||
if (!host_preflight_project_root(err_msg, err_cap))
|
||||
return 0;
|
||||
activate_toolchain_path();
|
||||
if (!find_python(g_python, sizeof(g_python))) {
|
||||
/* First-run / pruned cache: page 0 normally installed the pack; heal. */
|
||||
@@ -3699,6 +3861,10 @@ static int host_prepare_generate(const char* source_path, char* out_path,
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
/* Stamp the success so apply() can tell "generate worked but the
|
||||
* launcher still cannot see its output" apart from a plain first run. */
|
||||
host_record_generate_ok();
|
||||
|
||||
snprintf(out_path, out_cap, "%s", source_path);
|
||||
if (on_progress)
|
||||
on_progress(progress_ctx, 1.0f, "Generate complete");
|
||||
@@ -4365,6 +4531,19 @@ void psxrecomp_codegen_host_apply(RecompLauncherCGameInfo* gi,
|
||||
if (psxrecomp_codegen_host_sources_missing(cfg) || force_setup) {
|
||||
gi->needs_setup = 1;
|
||||
gi->prepare_required_before_continue = 1;
|
||||
if (!force_setup) {
|
||||
/* Loop breaker: a recent successful generate with sources still
|
||||
* missing is a config skew, not a first run — say so in the
|
||||
* wizard instead of silently asking for another round. */
|
||||
const char* note = host_loop_breaker_note();
|
||||
if (note)
|
||||
gi->prepare_disc_note = note;
|
||||
}
|
||||
} else {
|
||||
/* Sources are visible again — the last generate resolved; drop the
|
||||
* stamp so a much later manual wipe of generated/ reads as a fresh
|
||||
* first run, not a loop. */
|
||||
host_clear_generate_stamp();
|
||||
}
|
||||
#endif /* PSX_HAS_SETUP_WIZARD */
|
||||
}
|
||||
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assert the three baked copies of the boot-EXE name agree.
|
||||
#
|
||||
# A game project carries the boot-EXE name in three places, all filled from
|
||||
# the same @BOOT_EXE@ token at instantiation: game.toml ([prepare_disc]
|
||||
# boot_exe, or the basename of [game] exe), CMakeLists.txt (GEN_MARKER),
|
||||
# and codegen_setup.c (.gen_marker_relpath). generate names its output
|
||||
# after game.toml while the build and the launcher gate on the other two,
|
||||
# so a skew makes every player's self-build loop generate+build forever.
|
||||
# The CLI also refuses the skew at generate time; failing the release here
|
||||
# is cheaper than failing the player.
|
||||
#
|
||||
# Usage: check_boot_exe.sh [game-repo-root] (default: .)
|
||||
# Runs on bash 3.2+ (macOS runners) — no associative arrays.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${1:-.}"
|
||||
|
||||
toml_get() { # <section> <key> — first quoted value of key inside [section]
|
||||
awk -v sec="$1" -v key="$2" '
|
||||
/^[[:space:]]*\[/ { in_sec = ($0 ~ ("^[[:space:]]*\\[" sec "\\]")) }
|
||||
in_sec && $0 ~ ("^[[:space:]]*" key "[[:space:]]*=") {
|
||||
if (match($0, /"[^"]*"/)) { print substr($0, RSTART + 1, RLENGTH - 2) }
|
||||
exit
|
||||
}' "${ROOT}/game.toml"
|
||||
}
|
||||
|
||||
n_toml="" n_cmake="" n_setup=""
|
||||
|
||||
if [[ -f "${ROOT}/game.toml" ]]; then
|
||||
n_toml="$(toml_get prepare_disc boot_exe || true)"
|
||||
if [[ -z "${n_toml}" ]]; then
|
||||
exe="$(toml_get game exe || true)"
|
||||
n_toml="${exe##*/}"
|
||||
fi
|
||||
fi
|
||||
if [[ -f "${ROOT}/CMakeLists.txt" ]]; then
|
||||
n_cmake="$(sed -n \
|
||||
's/.*GEN_MARKER[[:space:]]*"generated\/\(.*\)_dispatch\.c".*/\1/p' \
|
||||
"${ROOT}/CMakeLists.txt" | head -n1)"
|
||||
fi
|
||||
if [[ -f "${ROOT}/codegen_setup.c" ]]; then
|
||||
n_setup="$(sed -n \
|
||||
's/.*\.gen_marker_relpath[[:space:]]*=[[:space:]]*"generated\/\(.*\)_dispatch\.c".*/\1/p' \
|
||||
"${ROOT}/codegen_setup.c" | head -n1)"
|
||||
fi
|
||||
|
||||
found=0
|
||||
ref=""
|
||||
mismatch=0
|
||||
report() { # <label> <value>
|
||||
[[ -z "$2" ]] && return 0
|
||||
echo " $1 -> $2"
|
||||
found=$((found + 1))
|
||||
if [[ -z "${ref}" ]]; then
|
||||
ref="$2"
|
||||
elif [[ "$2" != "${ref}" ]]; then
|
||||
mismatch=1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "boot-EXE name sources:"
|
||||
report "game.toml (boot_exe / game.exe)" "${n_toml}"
|
||||
report "CMakeLists.txt GEN_MARKER" "${n_cmake}"
|
||||
report "codegen_setup.c gen_marker_relpath" "${n_setup}"
|
||||
|
||||
if [[ "${found}" -lt 2 ]]; then
|
||||
echo "warning: fewer than two boot-EXE sources found under ${ROOT} —" \
|
||||
"nothing to cross-check (non-standard layout?)"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${mismatch}" -ne 0 ]]; then
|
||||
echo "error: the boot-EXE names above disagree. generate names its output" >&2
|
||||
echo "after game.toml, while the build (GEN_MARKER) and the launcher" >&2
|
||||
echo "(gen_marker_relpath) gate on the exact other names — players'" >&2
|
||||
echo "self-builds would loop generate+build forever. Make all three name" >&2
|
||||
echo "the same boot EXE." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "boot-EXE names agree: ${ref}"
|
||||
@@ -71,6 +71,8 @@ psx_crash.txt
|
||||
psx_last_run_report.json
|
||||
psx_screenshot.bmp
|
||||
psx_freeze_heartbeat.json
|
||||
.psxrecomp_last_generate
|
||||
.psxrecomp_write_probe
|
||||
starvation_dump.jsonl
|
||||
|
||||
# Diagnostic dumps (root-level) — keep catalog / probe trackable
|
||||
|
||||
Reference in New Issue
Block a user