mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge BIOS setup-host PR #343 for combined validation
This commit is contained in:
+185
-20
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "psxrecomp_codegen_host.h"
|
||||
|
||||
#include "psx_bios_known_images.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -1135,15 +1137,74 @@ static int resolve_build_paths(void) {
|
||||
return join_path(g_exe_path, sizeof(g_exe_path), g_build_dir, exe_name);
|
||||
}
|
||||
|
||||
/* Pair a <stem>_dispatch.c with its <stem>_full.c, the same pairing
|
||||
* runtime.cmake requires before it will link a BIOS backend. */
|
||||
static int dispatch_has_full(const char* dir, const char* name) {
|
||||
static const char suffix[] = "_dispatch.c";
|
||||
char stem[512], full[600], path[1200];
|
||||
size_t len = strlen(name);
|
||||
size_t slen = sizeof(suffix) - 1;
|
||||
if (len <= slen || strcmp(name + len - slen, suffix) != 0)
|
||||
return 0;
|
||||
if (len - slen >= sizeof(stem))
|
||||
return 0;
|
||||
memcpy(stem, name, len - slen);
|
||||
stem[len - slen] = '\0';
|
||||
if ((size_t)snprintf(full, sizeof(full), "%s_full.c", stem) >= sizeof(full))
|
||||
return 0;
|
||||
if (!join_path(path, sizeof(path), dir, full))
|
||||
return 0;
|
||||
return path_is_file(path);
|
||||
}
|
||||
|
||||
/* Does psxrecomp/generated/ hold ANY recompiled BIOS backend?
|
||||
*
|
||||
* This used to probe two hardcoded names, OpenBIOS_dispatch.c and
|
||||
* SCPH1001_dispatch.c. A port that pins a different image — via
|
||||
* PSXRECOMP_BIOS_STEMS / game.toml recompiler.bios_config, as every wave-3
|
||||
* kit does with SCPH5552 — emits its backend under that other stem, so the
|
||||
* probe was permanently unsatisfied: Generate kept succeeding, the wizard
|
||||
* kept reopening, and first-run setup could never complete. Accept any stem
|
||||
* that has both halves instead of naming images here. */
|
||||
static int generated_has_bios_backend(const char* dir) {
|
||||
#if defined(_WIN32)
|
||||
char pat[1200];
|
||||
WIN32_FIND_DATAA fd;
|
||||
HANDLE h;
|
||||
int found = 0;
|
||||
if (!join_path(pat, sizeof(pat), dir, "*_dispatch.c"))
|
||||
return 0;
|
||||
h = FindFirstFileA(pat, &fd);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
return 0;
|
||||
do {
|
||||
if (dispatch_has_full(dir, fd.cFileName)) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
} while (FindNextFileA(h, &fd));
|
||||
FindClose(h);
|
||||
return found;
|
||||
#else
|
||||
DIR* d = opendir(dir);
|
||||
struct dirent* e;
|
||||
int found = 0;
|
||||
if (!d)
|
||||
return 0;
|
||||
while (!found && (e = readdir(d)) != NULL) {
|
||||
if (dispatch_has_full(dir, e->d_name))
|
||||
found = 1;
|
||||
}
|
||||
closedir(d);
|
||||
return found;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int bios_backends_missing(void) {
|
||||
char openbios[1100], scph[1100];
|
||||
if (!join_path(openbios, sizeof(openbios), g_project_root,
|
||||
"psxrecomp/generated/OpenBIOS_dispatch.c"))
|
||||
char gen[1100];
|
||||
if (!join_path(gen, sizeof(gen), g_project_root, "psxrecomp/generated"))
|
||||
return 1;
|
||||
if (!join_path(scph, sizeof(scph), g_project_root,
|
||||
"psxrecomp/generated/SCPH1001_dispatch.c"))
|
||||
return 1;
|
||||
return !(path_is_file(openbios) || path_is_file(scph));
|
||||
return !generated_has_bios_backend(gen);
|
||||
}
|
||||
|
||||
int psxrecomp_codegen_host_sources_missing(
|
||||
@@ -1209,7 +1270,7 @@ static void write_sidecar_near_exe(const char* near_exe, const char* name,
|
||||
write_line_file(path, value ? value : "");
|
||||
}
|
||||
|
||||
/* IEEE CRC-32 (zlib / Ethernet) — SCPH-1001 identity for setup discovery. */
|
||||
/* IEEE CRC-32 (zlib / Ethernet) — retail BIOS identity for setup discovery. */
|
||||
static uint32_t host_crc32(const unsigned char* data, size_t len) {
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
size_t i, j;
|
||||
@@ -1221,17 +1282,23 @@ static uint32_t host_crc32(const unsigned char* data, size_t len) {
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
/* Does this file match the retail image THIS build pins? A setup host has no
|
||||
* linked backend to ask, so it consults psx_bios_known_images.h rather than
|
||||
* assuming SCPH-1001 — which made every non-SCPH1001 kit reject a perfectly
|
||||
* good dump. An unknown pinned stem adopts nothing and the player is asked. */
|
||||
static int retail_bios_file_ok_c(const char* path) {
|
||||
const PsxKnownBiosImage* want = psx_expected_bios();
|
||||
FILE* f;
|
||||
long size;
|
||||
unsigned char* buf;
|
||||
uint32_t crc;
|
||||
if (!want) return 0;
|
||||
if (!path || !path[0] || !path_is_file(path)) return 0;
|
||||
f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
|
||||
size = ftell(f);
|
||||
if (size != 512 * 1024) { fclose(f); return 0; }
|
||||
if (size != (long)want->size) { fclose(f); return 0; }
|
||||
if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return 0; }
|
||||
buf = (unsigned char*)malloc((size_t)size);
|
||||
if (!buf) { fclose(f); return 0; }
|
||||
@@ -1243,22 +1310,22 @@ static int retail_bios_file_ok_c(const char* path) {
|
||||
fclose(f);
|
||||
crc = host_crc32(buf, (size_t)size);
|
||||
free(buf);
|
||||
return crc == 0x37157331u; /* SCPH-1001 */
|
||||
return crc == want->crc32;
|
||||
}
|
||||
|
||||
/* Prefer a player-supplied SCPH1001 next to the project/exe for Generate.
|
||||
* Missing → leave empty (OpenBIOS). Does not override an explicit OpenBIOS. */
|
||||
/* Prefer a player-supplied dump of the pinned retail image next to the
|
||||
* project/exe for Generate. Missing → leave empty (OpenBIOS). Does not
|
||||
* override an explicit OpenBIOS. */
|
||||
static int discover_retail_bios_c(char* out, size_t cap) {
|
||||
static const char* names[] = {
|
||||
"SCPH1001.BIN", "scph1001.bin", "SCPH-1001.BIN", "scph-1001.bin",
|
||||
"SCPH1001.bin", "scph1001.BIN",
|
||||
};
|
||||
char names[8][32];
|
||||
int nnames = psx_known_bios_filenames(psx_expected_bios(), names, 8);
|
||||
static const char* subs[] = {
|
||||
"bios", "", "system", "firmware", "psxrecomp/bios", "psxrecomp-v4/bios",
|
||||
};
|
||||
char roots[3][1100];
|
||||
int nroots = 0;
|
||||
int r, s, n;
|
||||
if (nnames <= 0) { out[0] = 0; return 0; }
|
||||
if (g_project_root[0]) {
|
||||
snprintf(roots[nroots], sizeof(roots[0]), "%s", g_project_root);
|
||||
++nroots;
|
||||
@@ -1282,7 +1349,7 @@ static int discover_retail_bios_c(char* out, size_t cap) {
|
||||
} else {
|
||||
snprintf(dir, sizeof(dir), "%s", walk);
|
||||
}
|
||||
for (n = 0; n < (int)(sizeof(names) / sizeof(names[0])); ++n) {
|
||||
for (n = 0; n < nnames; ++n) {
|
||||
char cand[1300];
|
||||
if (!join_path(cand, sizeof(cand), dir, names[n])) continue;
|
||||
if (!retail_bios_file_ok_c(cand)) continue;
|
||||
@@ -3667,6 +3734,7 @@ 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;
|
||||
const char* cause;
|
||||
int game_missing, bios_missing;
|
||||
if (!join_path(sidecar, sizeof(sidecar), g_project_root,
|
||||
HOST_LAST_GENERATE_SIDECAR))
|
||||
@@ -3687,16 +3755,30 @@ static const char* host_loop_breaker_note(void) {
|
||||
if (!game_missing && !bios_missing)
|
||||
return NULL;
|
||||
list_generated_dispatch(found, sizeof(found));
|
||||
/* Each branch has its own cause, so do not assert a single one. A missing
|
||||
* game dispatch really does point at disagreeing boot-EXE names. Missing
|
||||
* BIOS backends do not: they mean Generate never emitted them, normally
|
||||
* because no retail BIOS was available to emit them from. Blaming
|
||||
* boot-EXE names for that sent players after the wrong thing. */
|
||||
if (game_missing)
|
||||
cause = "please report this to the port maintainer: the project's "
|
||||
"boot-EXE names disagree";
|
||||
else
|
||||
cause = "Generate produced the game code but no BIOS backend, which "
|
||||
"normally means it had no retail BIOS to work from. Select "
|
||||
"the PlayStation BIOS dump this port requires (named in the "
|
||||
"README; it must be exactly 512 KB) in the launcher, then "
|
||||
"run Generate again";
|
||||
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.",
|
||||
"again will very likely loop — %s.",
|
||||
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");
|
||||
found[0] ? found : "no *_dispatch.c at all",
|
||||
cause);
|
||||
fprintf(stderr, "psxrecomp-codegen: %s\n", g_loop_breaker_note);
|
||||
return g_loop_breaker_note;
|
||||
}
|
||||
@@ -4200,9 +4282,92 @@ static int host_paths_same_file(const char* a, const char* b) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* --setup-selfcheck: report what the setup host believes about this tree, as
|
||||
* JSON on stdout, then exit. 0 = generated sources complete, 2 = the wizard
|
||||
* would reopen, 1 = could not tell (no project root).
|
||||
*
|
||||
* This exists because the decision layer -- "are the generated sources
|
||||
* present?" -- had no headless entry point. Disc selection, BIOS selection and
|
||||
* generation were already scriptable via psxrecomp_cli.py; the verdict on
|
||||
* whether setup is DONE was reachable only by clicking through the wizard, so
|
||||
* nothing in CI could assert it. A stem mismatch there shipped a first-run
|
||||
* loop on 26 titles before anyone noticed.
|
||||
*
|
||||
* Deliberately ahead of the PSX_HAS_GAME_DISPATCH early return below, so a
|
||||
* product build answers too. Every title already calls this function with
|
||||
* argc/argv, so no per-title change is needed to gain the flag. */
|
||||
static void host_json_str(const char* s) {
|
||||
putchar('"');
|
||||
for (; s && *s; ++s) {
|
||||
if (*s == '\\' || *s == '"')
|
||||
putchar('\\');
|
||||
putchar(*s);
|
||||
}
|
||||
putchar('"');
|
||||
}
|
||||
|
||||
static void host_selfcheck_or_return(const PsxrecompCodegenHostConfig* cfg,
|
||||
int argc, char** argv) {
|
||||
const PsxKnownBiosImage* want;
|
||||
const char* marker_rel;
|
||||
char marker_abs[1200];
|
||||
int i, missing, game_ok, bios_ok;
|
||||
|
||||
for (i = 1; i < argc; ++i)
|
||||
if (argv[i] && strcmp(argv[i], "--setup-selfcheck") == 0)
|
||||
break;
|
||||
if (i >= argc)
|
||||
return;
|
||||
|
||||
if (!cfg || !cfg->cmake_target || !cfg->exe_basename) {
|
||||
printf("{\"error\": \"no codegen host config linked\"}\n");
|
||||
exit(1);
|
||||
}
|
||||
/* Sets g_cfg and g_project_root as a side effect. */
|
||||
missing = psxrecomp_codegen_host_sources_missing(cfg);
|
||||
if (!g_project_root[0]) {
|
||||
printf("{\"error\": \"project root not found\"}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
marker_rel = cfg_or(cfg->gen_marker_relpath,
|
||||
"generated/SLUS_011.89_dispatch.c");
|
||||
game_ok = join_path(marker_abs, sizeof(marker_abs), g_project_root,
|
||||
marker_rel) && path_is_file(marker_abs);
|
||||
bios_ok = !bios_backends_missing();
|
||||
want = psx_expected_bios();
|
||||
|
||||
printf("{\n");
|
||||
printf(" \"display_name\": ");
|
||||
host_json_str(cfg_or(cfg->display_name, "Game"));
|
||||
printf(",\n \"project_root\": ");
|
||||
host_json_str(g_project_root);
|
||||
printf(",\n \"expected_bios_stem\": ");
|
||||
host_json_str(PSX_EXPECTED_BIOS_STEM);
|
||||
printf(",\n \"expected_bios_id\": ");
|
||||
host_json_str(want ? want->id : "");
|
||||
printf(",\n \"expected_bios_crc32\": ");
|
||||
if (want) {
|
||||
char crcbuf[16];
|
||||
snprintf(crcbuf, sizeof(crcbuf), "0x%08X", want->crc32);
|
||||
host_json_str(crcbuf);
|
||||
} else {
|
||||
printf("null");
|
||||
}
|
||||
printf(",\n \"game_dispatch\": ");
|
||||
host_json_str(marker_rel);
|
||||
printf(",\n \"game_dispatch_present\": %s", game_ok ? "true" : "false");
|
||||
printf(",\n \"bios_backends_present\": %s", bios_ok ? "true" : "false");
|
||||
printf(",\n \"sources_missing\": %s", missing ? "true" : "false");
|
||||
printf("\n}\n");
|
||||
fflush(stdout);
|
||||
exit(missing ? 2 : 0);
|
||||
}
|
||||
|
||||
/* Setup-host zip-root exe → build-release product (bios/mods/assets/settings). */
|
||||
void psxrecomp_codegen_host_forward_if_built(
|
||||
const PsxrecompCodegenHostConfig* cfg, int argc, char** argv) {
|
||||
host_selfcheck_or_return(cfg, argc, argv); /* exits when requested */
|
||||
#if defined(PSX_HAS_GAME_DISPATCH)
|
||||
/* Full game binary — already the product tree. */
|
||||
(void)cfg;
|
||||
|
||||
+9
-9
@@ -688,7 +688,7 @@ def _build_recompiler_targets(
|
||||
|
||||
progress.log(" ".join(cmake_args))
|
||||
proc = subprocess.run(
|
||||
cmake_args, cwd=str(project_root), capture_output=True, text=True
|
||||
cmake_args, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||
)
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
if stream:
|
||||
@@ -705,7 +705,7 @@ def _build_recompiler_targets(
|
||||
for target in targets:
|
||||
build_cmd += ["--target", target]
|
||||
progress.log(" ".join(build_cmd))
|
||||
proc = subprocess.run(build_cmd, capture_output=True, text=True)
|
||||
proc = subprocess.run(build_cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
if stream:
|
||||
for line in stream.splitlines():
|
||||
@@ -818,7 +818,7 @@ def regen_bios_profile(
|
||||
[str(bios_tool), "--config", profile_rel],
|
||||
cwd=str(fw),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
text=True, encoding="utf-8", errors="replace",
|
||||
)
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
if not stream:
|
||||
@@ -938,7 +938,7 @@ def run_prepare_disc(
|
||||
str(project_root),
|
||||
str(source),
|
||||
]
|
||||
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True)
|
||||
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
out = (proc.stdout or "") + (proc.stderr or "")
|
||||
for line in out.splitlines():
|
||||
if line.strip():
|
||||
@@ -1141,7 +1141,7 @@ def cmd_generate(args: argparse.Namespace, progress: ProgressReporter) -> int:
|
||||
cmd,
|
||||
cwd=str(project_root),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
text=True, encoding="utf-8", errors="replace",
|
||||
)
|
||||
ri_warn = 0
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
@@ -1314,7 +1314,7 @@ def _cmake_configure(
|
||||
*extra,
|
||||
]
|
||||
progress.log(" ".join(cmd))
|
||||
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True)
|
||||
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
if stream:
|
||||
for line in stream.splitlines():
|
||||
@@ -1397,7 +1397,7 @@ def _cmake_build(
|
||||
target,
|
||||
]
|
||||
progress.log(" ".join(cmd))
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
if stream:
|
||||
for line in stream.splitlines():
|
||||
@@ -1583,7 +1583,7 @@ def run_pgo_train(
|
||||
r = subprocess.run(
|
||||
["xcrun", "--find", "llvm-profdata"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
text=True, encoding="utf-8", errors="replace",
|
||||
check=False,
|
||||
)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
@@ -1964,7 +1964,7 @@ def cmd_analyze(args: argparse.Namespace, progress: ProgressReporter) -> int:
|
||||
|
||||
progress.phase("analyze", pct=0.3, message=f"Analyzing {exe_path.name}…")
|
||||
progress.log(" ".join(cmd))
|
||||
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True)
|
||||
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
for stream in (proc.stdout, proc.stderr):
|
||||
if stream:
|
||||
for line in stream.splitlines():
|
||||
|
||||
@@ -649,6 +649,8 @@ if(BUILD_TESTING)
|
||||
foreach(_t IN ITEMS
|
||||
bios_selection_guards
|
||||
capture_history
|
||||
cli_retail_bios_profile
|
||||
codegen_host_bios_stems
|
||||
default_renderer_guards
|
||||
mod_owned_display_controls
|
||||
fmv_quiet_guards
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/* psx_bios_known_images.h — identities of the retail BIOS images this
|
||||
* framework ships a build profile for.
|
||||
*
|
||||
* A *built* host asks its linked backends (psx_bios_registry) which image it
|
||||
* accepts. A *setup* host has no linked backend — that is what makes it a
|
||||
* setup host — so first-run discovery, bios.cfg seeding and picker copy had
|
||||
* nowhere to look and hardcoded SCPH-1001. Any kit pinning another image via
|
||||
* PSXRECOMP_BIOS_STEM / recompiler.bios_config (every wave-3 kit pins
|
||||
* SCPH5552) then refused to auto-discover a perfectly good dump and told the
|
||||
* player their BIOS was the wrong one.
|
||||
*
|
||||
* Keep in sync with bios/<stem>.toml. A stem absent from this table still
|
||||
* builds and runs; it only loses first-run auto-discovery and falls back to
|
||||
* asking the player, which is safe.
|
||||
*/
|
||||
#ifndef PSX_BIOS_KNOWN_IMAGES_H
|
||||
#define PSX_BIOS_KNOWN_IMAGES_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Set by runtime.cmake from PSXRECOMP_BIOS_STEM. */
|
||||
#ifndef PSX_EXPECTED_BIOS_STEM
|
||||
#define PSX_EXPECTED_BIOS_STEM "SCPH1001"
|
||||
#endif
|
||||
|
||||
typedef struct PsxKnownBiosImage {
|
||||
const char* stem; /* generated/<stem>_dispatch.c, bios/<stem>.toml */
|
||||
const char* id; /* profile id, e.g. "SCPH-5552" */
|
||||
uint32_t crc32; /* IEEE CRC-32 (zlib/Ethernet) over the whole image */
|
||||
uint32_t size; /* bytes */
|
||||
} PsxKnownBiosImage;
|
||||
|
||||
/* Each entry verified two ways: against the dump itself, and against the
|
||||
* identity the recompiler emits into generated/<stem>_dispatch.c. */
|
||||
static const PsxKnownBiosImage psx_known_bios_images[] = {
|
||||
{ "SCPH1001", "SCPH-1001", 0x37157331u, 524288u },
|
||||
{ "SCPH5552", "SCPH-5552", 0xD786F0B9u, 524288u },
|
||||
};
|
||||
|
||||
static inline const PsxKnownBiosImage* psx_known_bios_by_stem(const char* stem) {
|
||||
size_t i;
|
||||
if (!stem || !stem[0])
|
||||
return 0;
|
||||
for (i = 0; i < sizeof(psx_known_bios_images) / sizeof(psx_known_bios_images[0]); ++i) {
|
||||
if (strcmp(psx_known_bios_images[i].stem, stem) == 0)
|
||||
return &psx_known_bios_images[i];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The retail image THIS build was configured for, or NULL when the pinned
|
||||
* stem is not in the table. Callers must handle NULL by asking the player
|
||||
* rather than assuming SCPH-1001. */
|
||||
static inline const PsxKnownBiosImage* psx_expected_bios(void) {
|
||||
return psx_known_bios_by_stem(PSX_EXPECTED_BIOS_STEM);
|
||||
}
|
||||
|
||||
/* A short label for the pinned image, for player-facing copy: "SCPH5552.BIN".
|
||||
* Falls back to the raw stem when the identity is not in the table. */
|
||||
static inline const char* psx_expected_bios_label(void) {
|
||||
static char label[40];
|
||||
if (!label[0]) {
|
||||
const PsxKnownBiosImage* e = psx_expected_bios();
|
||||
const char* stem = e ? e->stem : PSX_EXPECTED_BIOS_STEM;
|
||||
size_t n = strlen(stem);
|
||||
if (n > sizeof(label) - 5)
|
||||
n = sizeof(label) - 5;
|
||||
memcpy(label, stem, n);
|
||||
memcpy(label + n, ".BIN", 5);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
/* Filename spellings worth probing for <img> during first-run discovery,
|
||||
* written into out[0..n). Covers the stem and the dashed profile id, each in
|
||||
* upper and lower case: SCPH5552.BIN, scph5552.bin, SCPH-5552.BIN, ... */
|
||||
static inline int psx_known_bios_filenames(const PsxKnownBiosImage* img,
|
||||
char out[][32], int cap) {
|
||||
const char* bases[2];
|
||||
int n = 0, b, lower;
|
||||
if (!img)
|
||||
return 0;
|
||||
bases[0] = img->stem;
|
||||
bases[1] = img->id;
|
||||
for (b = 0; b < 2; ++b) {
|
||||
for (lower = 0; lower < 2; ++lower) {
|
||||
const char* ext = lower ? "bin" : "BIN";
|
||||
size_t i, len = strlen(bases[b]);
|
||||
char* dst;
|
||||
if (n >= cap || len + 5 > 32)
|
||||
continue;
|
||||
dst = out[n++];
|
||||
for (i = 0; i < len; ++i) {
|
||||
char c = bases[b][i];
|
||||
dst[i] = (char)(lower && c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c);
|
||||
}
|
||||
dst[len] = '.';
|
||||
memcpy(dst + len + 1, ext, 3);
|
||||
dst[len + 4] = 0;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
#endif /* PSX_BIOS_KNOWN_IMAGES_H */
|
||||
+32
-10
@@ -515,7 +515,7 @@ else()
|
||||
if(PSXRECOMP_HAS_RECOMP_NET)
|
||||
message(FATAL_ERROR
|
||||
"psxrecomp: PSX_NETPLAY needs retcomm-rbengine.\n"
|
||||
" git submodule update --init lib/retcomm-rbengine\n"
|
||||
" git submodule update --init --recursive lib/retcomm-rbengine\n"
|
||||
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine")
|
||||
endif()
|
||||
endif()
|
||||
@@ -544,7 +544,13 @@ if(PSX_REWIND)
|
||||
message(FATAL_ERROR
|
||||
"psxrecomp: PSX_REWIND=ON exposes the Rewind launcher controls "
|
||||
"but no retcomm-rbengine snap-ring backend was found.\n"
|
||||
" git submodule update --init lib/retcomm-rbengine\n"
|
||||
"A source ZIP downloaded from GitHub never contains submodule "
|
||||
"contents and cannot build. Clone instead:\n"
|
||||
" git clone --recurse-submodules <repo-url>\n"
|
||||
"In an existing clone, run this from the GAME repo root. Note "
|
||||
"--recursive: rbengine is a submodule of psxrecomp, not of the "
|
||||
"game, so a non-recursive init leaves it empty.\n"
|
||||
" git submodule update --init --recursive\n"
|
||||
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine\n"
|
||||
" or configure with -DPSX_REWIND=OFF to hide Rewind.")
|
||||
endif()
|
||||
@@ -1248,15 +1254,18 @@ function(psxrecomp_add_runtime_target target)
|
||||
# where releases are validated. Dev checkouts still resolve the relative
|
||||
# default without prompting via the exe-dir upward search, which also tries
|
||||
# <ancestor>/psxrecomp-v4/<relative> for game-project layouts.
|
||||
# Follow the stem this build actually pins; assuming SCPH1001 here handed
|
||||
# every non-SCPH1001 kit a default path that could never resolve.
|
||||
set(_psxrt_stem_bios "bios/${PSXRECOMP_BIOS_STEM}.BIN")
|
||||
if(NOT PSXRT_DEFAULT_BIOS_PATH)
|
||||
set(PSXRT_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN")
|
||||
set(PSXRT_DEFAULT_BIOS_PATH "${_psxrt_stem_bios}")
|
||||
elseif(IS_ABSOLUTE "${PSXRT_DEFAULT_BIOS_PATH}")
|
||||
message(WARNING
|
||||
"DEFAULT_BIOS_PATH '${PSXRT_DEFAULT_BIOS_PATH}' is absolute; refusing to "
|
||||
"bake a build-machine path into the binary (release exes must prompt on "
|
||||
"user machines). Using relative 'bios/SCPH1001.BIN' instead — drop the "
|
||||
"user machines). Using relative '${_psxrt_stem_bios}' instead — drop the "
|
||||
"DEFAULT_BIOS_PATH argument from this game's CMakeLists.")
|
||||
set(PSXRT_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN")
|
||||
set(PSXRT_DEFAULT_BIOS_PATH "${_psxrt_stem_bios}")
|
||||
endif()
|
||||
if(NOT DEFINED PSXRT_DEFAULT_GAME_CONFIG_PATH)
|
||||
set(PSXRT_DEFAULT_GAME_CONFIG_PATH "")
|
||||
@@ -1715,6 +1724,10 @@ function(psxrecomp_add_runtime_target target)
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
DEFAULT_DEBUG_PORT=${PSXRT_DEBUG_PORT}
|
||||
PSX_DEFAULT_BIOS_PATH="${PSXRT_DEFAULT_BIOS_PATH}"
|
||||
# The retail stem this build pins. A setup host has no linked
|
||||
# backend to ask, so this is how it knows which image to look for
|
||||
# and name (psx_bios_known_images.h) instead of assuming SCPH-1001.
|
||||
PSX_EXPECTED_BIOS_STEM="${PSXRECOMP_BIOS_STEM}"
|
||||
# Where the shipped redistributable image lives, relative to the exe.
|
||||
# This is what a player gets when they choose no BIOS.
|
||||
PSX_BUNDLED_BIOS_PATH="${PSXRECOMP_BUNDLED_BIOS_PATH}"
|
||||
@@ -1909,11 +1922,20 @@ function(psxrecomp_add_runtime_target target)
|
||||
if(PSX_RECOMP_UI AND NOT PSXRT_ORACLE)
|
||||
if(NOT RECOMP_UI_ROOT OR NOT EXISTS "${RECOMP_UI_ROOT}/recomp_ui.cmake")
|
||||
message(FATAL_ERROR
|
||||
"PSX_RECOMP_UI=ON but recomp-ui is missing.\n"
|
||||
"Add at the game repo root:\n"
|
||||
" git submodule add -b master "
|
||||
"https://github.com/RetroPortingToolKit/recomp-ui.git recomp-ui\n"
|
||||
"Or set -DRECOMP_UI_ROOT=/path/to/recomp-ui")
|
||||
"PSX_RECOMP_UI=ON but recomp-ui is missing from the game "
|
||||
"repo root.\n"
|
||||
"A source ZIP downloaded from GitHub never contains "
|
||||
"submodule contents and cannot build. Clone instead:\n"
|
||||
" git clone --recurse-submodules <repo-url>\n"
|
||||
"In a clone that already declares recomp-ui, fetch the "
|
||||
"pinned commit:\n"
|
||||
" git submodule update --init --recursive\n"
|
||||
"Only if this repo does not declare recomp-ui yet, add it "
|
||||
"(use the fork this project pins, not necessarily "
|
||||
"upstream):\n"
|
||||
" git submodule add <recomp-ui-url> recomp-ui\n"
|
||||
"Or point at an existing checkout: "
|
||||
"-DRECOMP_UI_ROOT=/path/to/recomp-ui")
|
||||
endif()
|
||||
# recomp-ui gates its Mods view behind RECOMP_UI_ENABLE_MODS, which
|
||||
# defaults OFF there -- correct for a cross-console launcher, since a
|
||||
|
||||
+63
-39
@@ -16,6 +16,7 @@
|
||||
#include "boot_state.h"
|
||||
#include "bios_hle.h"
|
||||
#include "bios_hle_plan.h"
|
||||
#include "psx_bios_known_images.h"
|
||||
#include "psx_bios_backend.h"
|
||||
#include "psx_cycles.h"
|
||||
#include "starvation_ring.h"
|
||||
@@ -2514,11 +2515,32 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested,
|
||||
const bool bundled_only =
|
||||
openbios_allowed && bundled && !player_bios_selectable;
|
||||
|
||||
/* 0. Setup host (CI zip root): no BIOS backends are linked yet, so there
|
||||
* is nothing an image could be validated against — bios_backend_for_file()
|
||||
* rejects every file, including the correct one, and a title with
|
||||
* openbios = false has no fallback to land on. This MUST precede the
|
||||
* explicit-choice branch below: a remembered bios.cfg pick reached
|
||||
* validate_bios_for_launch() first and deadlocked first-run setup — the
|
||||
* player was told their good SCPH-1001 was "not an image this build was
|
||||
* compiled from", and could never supply the BIOS that Generate needs in
|
||||
* order to emit the backend. Play belongs to the product binary under
|
||||
* build-release/ after Generate & rebuild. */
|
||||
if (psx_bios_registry_count == 0) {
|
||||
launcher_warning("Setup host — finish Generate & rebuild",
|
||||
"This executable is the first-run setup host (no game/BIOS code "
|
||||
"linked).\n\n"
|
||||
"Use Generate & rebuild in the launcher. After that succeeds, open "
|
||||
"this same shortcut again — it starts the game from build-release/ "
|
||||
"(where bios/, mods/, and settings live).\n\n"
|
||||
"Or run build-release/<game>.exe directly.");
|
||||
return {};
|
||||
}
|
||||
|
||||
/* 1. An explicit choice: --bios, else a remembered pick. A product build
|
||||
* with only its bundled backend has no meaningful player choice: ignore
|
||||
* stale settings/bios.cfg paths instead of validating an image the hidden
|
||||
* launcher row cannot clear. Setup hosts (registry_count == 0) retain their
|
||||
* picker/generation flow. */
|
||||
* launcher row cannot clear. Setup hosts (registry_count == 0) already
|
||||
* returned above. */
|
||||
std::filesystem::path chosen;
|
||||
if (!bundled_only) {
|
||||
if (requested_is_explicit && requested && requested[0]) {
|
||||
@@ -2551,19 +2573,6 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested,
|
||||
return {};
|
||||
}
|
||||
|
||||
/* Setup host (CI zip root): no BIOS backends linked yet. Play belongs to
|
||||
* the product binary under build-release/ after Generate & rebuild. */
|
||||
if (psx_bios_registry_count == 0) {
|
||||
launcher_warning("Setup host — finish Generate & rebuild",
|
||||
"This executable is the first-run setup host (no game/BIOS code "
|
||||
"linked).\n\n"
|
||||
"Use Generate & rebuild in the launcher. After that succeeds, open "
|
||||
"this same shortcut again — it starts the game from build-release/ "
|
||||
"(where bios/, mods/, and settings live).\n\n"
|
||||
"Or run build-release/<game>.exe directly.");
|
||||
return {};
|
||||
}
|
||||
|
||||
/* 3. This title requires a retail BIOS: ask for one. */
|
||||
const std::string accepted = bios_accepted_images();
|
||||
launcher_info((s_picker_game_name + " — PlayStation BIOS needed").c_str(),
|
||||
@@ -2571,7 +2580,8 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested,
|
||||
"Step 1 of 2 — PlayStation BIOS\n\n"
|
||||
"In the next window, select your PlayStation BIOS dump. This build "
|
||||
"requires the exact image it was compiled from: " + accepted + ". "
|
||||
"Usually named SCPH1001.BIN and exactly 512 KB. Dump from your own "
|
||||
"Usually named " + std::string(psx_expected_bios_label()) +
|
||||
" and exactly 512 KB. Dump from your own "
|
||||
"console or otherwise legally obtain it.\n\n"
|
||||
"(This is NOT the game disc — that is asked for next.)");
|
||||
std::string bios_title =
|
||||
@@ -2704,24 +2714,27 @@ static bool retail_bios_file_ok(const std::filesystem::path& path) {
|
||||
const PsxBiosBackend* b = bios_backend_for_file(path, nullptr, nullptr);
|
||||
return b && b->image && !b->image->image_bundled;
|
||||
}
|
||||
/* Setup host (no backends linked yet): accept validated SCPH-1001 only. */
|
||||
constexpr uint64_t kSize = 512u * 1024u;
|
||||
constexpr uint32_t kScph1001Crc = 0x37157331u;
|
||||
/* Setup host (no backends linked yet): accept the retail image THIS build
|
||||
* pins, from psx_bios_known_images.h. This used to hardcode SCPH-1001, so
|
||||
* a kit pinning anything else refused to seed from a correct dump. An
|
||||
* unknown pinned stem seeds nothing and the player is asked instead. */
|
||||
const PsxKnownBiosImage* want = psx_expected_bios();
|
||||
if (!want) return false;
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f.is_open()) return false;
|
||||
const auto size = static_cast<uint64_t>(f.tellg());
|
||||
if (size != kSize) return false;
|
||||
if (size != static_cast<uint64_t>(want->size)) return false;
|
||||
std::vector<uint8_t> data(static_cast<size_t>(size));
|
||||
if (!read_at(f, 0, data.data(), data.size())) return false;
|
||||
return crc32_compute(data.data(), data.size()) == kScph1001Crc;
|
||||
return crc32_compute(data.data(), data.size()) == want->crc32;
|
||||
}
|
||||
|
||||
static std::filesystem::path discover_retail_bios_near(const char* argv0) {
|
||||
namespace fs = std::filesystem;
|
||||
static const char* kNames[] = {
|
||||
"SCPH1001.BIN", "scph1001.bin", "SCPH-1001.BIN", "scph-1001.bin",
|
||||
"SCPH1001.bin", "scph1001.BIN",
|
||||
};
|
||||
char name_buf[8][32];
|
||||
const int name_count =
|
||||
psx_known_bios_filenames(psx_expected_bios(), name_buf, 8);
|
||||
if (name_count <= 0) return {};
|
||||
static const char* kSubdirs[] = {
|
||||
"bios", "", "system", "firmware", "psxrecomp/bios", "psxrecomp-v4/bios",
|
||||
};
|
||||
@@ -2730,8 +2743,8 @@ static std::filesystem::path discover_retail_bios_near(const char* argv0) {
|
||||
for (fs::path root = exe_dir; !root.empty(); root = root.parent_path()) {
|
||||
for (const char* sub : kSubdirs) {
|
||||
const fs::path dir = (sub && sub[0]) ? (root / sub) : root;
|
||||
for (const char* name : kNames) {
|
||||
const fs::path cand = dir / name;
|
||||
for (int ni = 0; ni < name_count; ++ni) {
|
||||
const fs::path cand = dir / name_buf[ni];
|
||||
if (retail_bios_file_ok(cand)) {
|
||||
auto abs = fs::weakly_canonical(cand, ec);
|
||||
if (ec) abs = fs::absolute(cand, ec);
|
||||
@@ -7666,12 +7679,14 @@ namespace {
|
||||
out->ok = 1;
|
||||
std::snprintf(out->detail, sizeof(out->detail),
|
||||
"OpenBIOS will be emitted on Generate & rebuild "
|
||||
"(optional: pick SCPH1001). Play uses "
|
||||
"build-release/ after rebuild.");
|
||||
"(optional: pick %s). Play uses "
|
||||
"build-release/ after rebuild.",
|
||||
psx_expected_bios_label());
|
||||
return 1;
|
||||
}
|
||||
std::snprintf(out->detail, sizeof(out->detail),
|
||||
"PlayStation BIOS required (SCPH1001.BIN).");
|
||||
"PlayStation BIOS required (%s).",
|
||||
psx_expected_bios_label());
|
||||
return 1;
|
||||
}
|
||||
/* Match runtime resolve: relative picks like bios/SCPH1001.BIN must not
|
||||
@@ -7689,12 +7704,16 @@ namespace {
|
||||
"BIOS file not found.");
|
||||
return 1;
|
||||
}
|
||||
const PsxKnownBiosImage* want = psx_expected_bios();
|
||||
const std::streamoff want_size =
|
||||
want ? (std::streamoff)want->size : (std::streamoff)(512 * 1024);
|
||||
const std::streamoff size = f.tellg();
|
||||
if (size != 512 * 1024) {
|
||||
if (size != want_size) {
|
||||
std::snprintf(out->detail, sizeof(out->detail),
|
||||
"BIOS must be exactly 512 KiB (got %lld). Use "
|
||||
"SCPH1001.BIN.",
|
||||
(long long)size);
|
||||
"BIOS must be exactly %lld bytes (got %lld). Use "
|
||||
"%s.",
|
||||
(long long)want_size, (long long)size,
|
||||
psx_expected_bios_label());
|
||||
return 1;
|
||||
}
|
||||
std::vector<uint8_t> data((size_t)size);
|
||||
@@ -7704,15 +7723,20 @@ namespace {
|
||||
return 1;
|
||||
}
|
||||
const uint32_t crc = crc32_compute(data.data(), data.size());
|
||||
if (crc != 0x37157331u) {
|
||||
if (want && crc != want->crc32) {
|
||||
out->warn = 1;
|
||||
std::snprintf(out->detail, sizeof(out->detail),
|
||||
"CRC32 %08X (validated dump is SCPH1001 CRC32 "
|
||||
"37157331).",
|
||||
crc);
|
||||
"CRC32 %08X (this build expects %s, CRC32 %08X).",
|
||||
crc, want->id, want->crc32);
|
||||
} else if (!want) {
|
||||
out->warn = 1;
|
||||
std::snprintf(out->detail, sizeof(out->detail),
|
||||
"CRC32 %08X (this build pins %s, whose identity "
|
||||
"is not recorded here).",
|
||||
crc, PSX_EXPECTED_BIOS_STEM);
|
||||
} else {
|
||||
std::snprintf(out->detail, sizeof(out->detail),
|
||||
"SCPH1001.BIN (CRC OK).");
|
||||
"%s (CRC OK).", psx_expected_bios_label());
|
||||
}
|
||||
/* Setup host (no backends yet): file is fine for first Generate. */
|
||||
if (psx_bios_registry_count == 0) {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression: the codegen host must accept ANY recompiled BIOS stem.
|
||||
|
||||
bios_backends_missing() in host/psxrecomp_codegen_host.c used to probe two
|
||||
hardcoded filenames, psxrecomp/generated/OpenBIOS_dispatch.c and
|
||||
SCPH1001_dispatch.c. Ports that pin another image via recompiler.bios_config /
|
||||
PSXRECOMP_BIOS_STEMS emit <stem>_dispatch.c + <stem>_full.c instead, so the
|
||||
probe could never be satisfied: Generate succeeded, the setup wizard reopened,
|
||||
and first-run setup looped forever. Every wave-3 kit pins SCPH5552 and every
|
||||
one of them shipped with that loop.
|
||||
|
||||
test_cli_retail_bios_profile.py already covers the psxrecomp_cli.py half of
|
||||
this feature. This is the C host half, which had no coverage.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HOST_C = ROOT / "host" / "psxrecomp_codegen_host.c"
|
||||
|
||||
PROBE = """
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "psxrecomp_codegen_host.h"
|
||||
/* recomp-ui symbol the host references but sources_missing() never reaches. */
|
||||
int recomp_launcher_relaunch_exe(char* o, size_t c) { (void)o; (void)c; return 0; }
|
||||
int main(void) {
|
||||
PsxrecompCodegenHostConfig cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.cmake_target = "psx-runtime";
|
||||
cfg.exe_basename = "Probe";
|
||||
cfg.gen_marker_relpath = "generated/SCUS_943.51_dispatch.c";
|
||||
printf("%d", psxrecomp_codegen_host_sources_missing(&cfg));
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def find_recomp_ui() -> Path | None:
|
||||
env = os.environ.get("RECOMP_UI_ROOT")
|
||||
candidates = ([Path(env)] if env else []) + [
|
||||
ROOT.parent / "recomp-ui",
|
||||
ROOT / "recomp-ui",
|
||||
]
|
||||
for c in candidates:
|
||||
if (c / "src" / "recomp_launcher.h").is_file():
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def make_project(root: Path, bios_files: list[str]) -> None:
|
||||
"""A project tree the host recognises, with the given BIOS artefacts."""
|
||||
(root / "generated").mkdir(parents=True, exist_ok=True)
|
||||
(root / "generated" / "SCUS_943.51_dispatch.c").write_text("", encoding="utf-8")
|
||||
fw_gen = root / "psxrecomp" / "generated"
|
||||
fw_gen.mkdir(parents=True, exist_ok=True)
|
||||
(root / "game.toml").write_text("[game]\n", encoding="utf-8")
|
||||
(root / "psxrecomp" / "psxrecomp_cli.py").write_text("", encoding="utf-8")
|
||||
for name in bios_files:
|
||||
(fw_gen / name).write_text("", encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ui = find_recomp_ui()
|
||||
if ui is None:
|
||||
print("SKIP: recomp-ui not found (set RECOMP_UI_ROOT)")
|
||||
return 0
|
||||
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
|
||||
if cc is None:
|
||||
print("SKIP: no C compiler on PATH")
|
||||
return 0
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
probe_c = tmp / "probe.c"
|
||||
probe_c.write_text(PROBE, encoding="utf-8")
|
||||
exe = tmp / ("probe.exe" if os.name == "nt" else "probe")
|
||||
build = subprocess.run(
|
||||
[cc, "-std=c11", "-o", str(exe), str(probe_c), str(HOST_C),
|
||||
"-I", str(ROOT / "host"),
|
||||
"-I", str(ROOT / "runtime" / "include"),
|
||||
"-I", str(ui / "src"), "-I", str(ui / "src" / "common")],
|
||||
capture_output=True, text=True)
|
||||
if build.returncode != 0:
|
||||
print("FAIL: could not build probe\n" + build.stderr[-2000:])
|
||||
return 1
|
||||
|
||||
cases = [
|
||||
# (BIOS artefacts present, expected sources_missing, why)
|
||||
(["SCPH5552_dispatch.c", "SCPH5552_full.c"], 0,
|
||||
"a pinned non-SCPH1001 stem must satisfy the host"),
|
||||
(["OpenBIOS_dispatch.c", "OpenBIOS_full.c"], 0,
|
||||
"the bundled stem must still satisfy the host"),
|
||||
(["SCPH5552_dispatch.c"], 1,
|
||||
"a dispatch with no _full.c is not a linkable backend"),
|
||||
([], 1,
|
||||
"no BIOS backend at all means setup is genuinely incomplete"),
|
||||
]
|
||||
failures = 0
|
||||
for artefacts, expected, why in cases:
|
||||
project = tmp / ("case%d" % len(artefacts + [why]))
|
||||
shutil.rmtree(project, ignore_errors=True)
|
||||
make_project(project, artefacts)
|
||||
env = dict(os.environ, PSXRECOMP_PROJECT_ROOT=str(project))
|
||||
run = subprocess.run([str(exe)], capture_output=True, text=True, env=env)
|
||||
got = run.stdout.strip()
|
||||
if got != str(expected):
|
||||
print("FAIL: %s\n artefacts=%s expected=%s got=%r"
|
||||
% (why, artefacts or "(none)", expected, got))
|
||||
failures += 1
|
||||
else:
|
||||
print("ok: %s" % why)
|
||||
if failures:
|
||||
return 1
|
||||
|
||||
print("PASS: codegen host accepts any paired BIOS stem")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user