mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
4897 lines
172 KiB
C
4897 lines
172 KiB
C
/* Portable generate → rebuild (--no-pgo from setup) → relaunch host. */
|
|
|
|
#include "psxrecomp_codegen_host.h"
|
|
|
|
#include "psx_bios_known_images.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#if defined(_WIN32)
|
|
# include <windows.h>
|
|
#else
|
|
# include <dirent.h>
|
|
# include <errno.h>
|
|
# include <fcntl.h>
|
|
# include <spawn.h>
|
|
# include <strings.h>
|
|
# include <sys/stat.h>
|
|
# include <sys/wait.h>
|
|
# include <unistd.h>
|
|
# if defined(__APPLE__)
|
|
# include <mach-o/dyld.h>
|
|
# endif
|
|
extern char** environ;
|
|
#endif
|
|
|
|
/* Forward decls — used by toolchain cache helpers before their definitions. */
|
|
static int rmtree_path(const char* path);
|
|
static int mkdir_p(const char* path);
|
|
static int cmake_path_runs(const char* cmake_path);
|
|
static int toolchain_bin_is_healthy(const char* bin);
|
|
static void heal_broken_toolchain_pointers(void);
|
|
static void clear_project_toolchain_stamp(void);
|
|
static int host_paths_same_file(const char* a, const char* b);
|
|
static void prune_old_toolchain_tags(const char* keep_pack);
|
|
#if defined(_WIN32)
|
|
static int junction_dir(const char* link_path, const char* target_path);
|
|
static int run_cmdline_wait(const char* cmdline, DWORD* out_code);
|
|
#endif
|
|
|
|
static const PsxrecompCodegenHostConfig* g_cfg;
|
|
static char g_project_root[1024];
|
|
static char g_cli_path[1100];
|
|
static char g_game_toml[1100];
|
|
static char g_python[512];
|
|
static char g_cmake[512];
|
|
static char g_build_dir[1100];
|
|
static char g_exe_path[1100];
|
|
static char g_helper_path[1100];
|
|
static char g_cmake_target[256];
|
|
static char g_exe_basename[256];
|
|
static char g_display[128];
|
|
static char g_toolchain_bin[1400];
|
|
/* Last ensure-toolchain JSONL result path (bin/); shared-cache fallback. */
|
|
static char g_cli_toolchain_bin[1400];
|
|
static int g_ready;
|
|
static int g_relaunch_is_helper;
|
|
/* Wizard BIOS pick (survives cwd-relative bios.cfg misses on Windows). */
|
|
static char g_wizard_bios[1100];
|
|
/* Set when heal removes a broken latest/ / stamp at wizard open. */
|
|
static char g_tc_repair_note[320];
|
|
/* Set when host_persist_setup receives an explicit bios_path (including ""
|
|
* for OpenBIOS). Distinguishes intentional OpenBIOS clear from "unset". */
|
|
static int g_wizard_bios_explicit;
|
|
|
|
static const char* cfg_or(const char* v, const char* d) {
|
|
return (v && v[0]) ? v : d;
|
|
}
|
|
|
|
static int ascii_strcasecmp(const char* a, const char* b) {
|
|
#if defined(_WIN32)
|
|
return _stricmp(a, b);
|
|
#else
|
|
return strcasecmp(a, b);
|
|
#endif
|
|
}
|
|
|
|
static int path_is_file(const char* path) {
|
|
#if defined(_WIN32)
|
|
DWORD attr = GetFileAttributesA(path);
|
|
return attr != INVALID_FILE_ATTRIBUTES &&
|
|
(attr & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
|
#else
|
|
struct stat st;
|
|
return stat(path, &st) == 0 && S_ISREG(st.st_mode);
|
|
#endif
|
|
}
|
|
|
|
/* Read the first line of a small text file into out, trimming EOL and any
|
|
* trailing spaces. Returns 1 on success. Used for the exe-name marker CMake
|
|
* writes; anything unreadable just leaves the caller on its fallback. */
|
|
static int read_first_line(const char* path, char* out, size_t cap) {
|
|
FILE* f;
|
|
size_t n;
|
|
if (!path || !out || cap == 0)
|
|
return 0;
|
|
f = fopen(path, "rb");
|
|
if (!f)
|
|
return 0;
|
|
if (!fgets(out, (int)cap, f)) {
|
|
fclose(f);
|
|
out[0] = '\0';
|
|
return 0;
|
|
}
|
|
fclose(f);
|
|
n = strlen(out);
|
|
while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' ||
|
|
out[n - 1] == ' ' || out[n - 1] == '\t'))
|
|
out[--n] = '\0';
|
|
return out[0] != '\0';
|
|
}
|
|
|
|
static int path_is_absolute(const char* path) {
|
|
if (!path || !path[0]) return 0;
|
|
#if defined(_WIN32)
|
|
if (path[0] == '/' || path[0] == '\\') return 1;
|
|
if (path[0] && path[1] == ':') return 1;
|
|
return 0;
|
|
#else
|
|
return path[0] == '/';
|
|
#endif
|
|
}
|
|
|
|
static int join_path(char* out, size_t cap, const char* a, const char* b);
|
|
static int dirname_copy(char* out, size_t cap, const char* path);
|
|
static int absolutize_existing_file(char* out, size_t cap, const char* path);
|
|
|
|
static int path_is_dir(const char* path) {
|
|
#if defined(_WIN32)
|
|
DWORD attr = GetFileAttributesA(path);
|
|
return attr != INVALID_FILE_ATTRIBUTES &&
|
|
(attr & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
#else
|
|
struct stat st;
|
|
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
|
|
#endif
|
|
}
|
|
|
|
static int join_path(char* out, size_t cap, const char* a, const char* b) {
|
|
size_t na = strlen(a);
|
|
int need_slash = na > 0 && a[na - 1] != '/' && a[na - 1] != '\\';
|
|
int n = snprintf(out, cap, "%s%s%s", a, need_slash ? "/" : "", b);
|
|
return n > 0 && (size_t)n < cap;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
/* Microsoft Store Python redirects %LOCALAPPDATA% writes into
|
|
* Packages\PythonSoftwareFoundation.Python.*\LocalCache\Local\...
|
|
* Map a virtual LocalAppData path to those on-disk mirrors. */
|
|
static int store_python_localcache_mirror(const char* virtual_path, char* out,
|
|
size_t cap) {
|
|
const char* local = getenv("LOCALAPPDATA");
|
|
if (!local || !local[0] || !virtual_path || !virtual_path[0])
|
|
return 0;
|
|
size_t llen = strlen(local);
|
|
if (_strnicmp(virtual_path, local, (int)llen) != 0)
|
|
return 0;
|
|
const char* suffix = virtual_path + llen;
|
|
if (*suffix != '\\' && *suffix != '/')
|
|
return 0;
|
|
while (*suffix == '\\' || *suffix == '/')
|
|
++suffix;
|
|
if (!suffix[0])
|
|
return 0;
|
|
|
|
char packages[1100];
|
|
if (!join_path(packages, sizeof(packages), local, "Packages"))
|
|
return 0;
|
|
char pattern[1200];
|
|
snprintf(pattern, sizeof(pattern),
|
|
"%s\\PythonSoftwareFoundation.Python.*", packages);
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h = FindFirstFileA(pattern, &fd);
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return 0;
|
|
int found = 0;
|
|
do {
|
|
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
continue;
|
|
char mirror[1400];
|
|
int n = snprintf(mirror, sizeof(mirror),
|
|
"%s\\%s\\LocalCache\\Local\\%s", packages,
|
|
fd.cFileName, suffix);
|
|
if (n <= 0 || (size_t)n >= sizeof(mirror))
|
|
continue;
|
|
DWORD attr = GetFileAttributesA(mirror);
|
|
if (attr != INVALID_FILE_ATTRIBUTES) {
|
|
snprintf(out, cap, "%s", mirror);
|
|
found = 1;
|
|
break;
|
|
}
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
return found;
|
|
}
|
|
|
|
/* Prefer the real on-disk directory (handles Store Python redirection). */
|
|
static int resolve_existing_dir(const char* path, char* out, size_t cap) {
|
|
if (path && path[0] && path_is_dir(path)) {
|
|
snprintf(out, cap, "%s", path);
|
|
return 1;
|
|
}
|
|
char alt[1400];
|
|
if (path && store_python_localcache_mirror(path, alt, sizeof(alt)) &&
|
|
path_is_dir(alt)) {
|
|
snprintf(out, cap, "%s", alt);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int python_path_is_store(const char* path) {
|
|
if (!path || !path[0])
|
|
return 0;
|
|
char lower[1100];
|
|
size_t n = strlen(path);
|
|
if (n >= sizeof(lower))
|
|
n = sizeof(lower) - 1;
|
|
for (size_t i = 0; i < n; ++i) {
|
|
char c = path[i];
|
|
lower[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
|
}
|
|
lower[n] = '\0';
|
|
return strstr(lower, "windowsapps") != NULL ||
|
|
strstr(lower, "pythonsoftwarefoundation") != NULL;
|
|
}
|
|
|
|
/* Run a short cmdline and capture the first stdout line (trimmed). */
|
|
static int capture_cmd_first_line(const char* cmdline, char* out, size_t cap) {
|
|
SECURITY_ATTRIBUTES sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.nLength = sizeof(sa);
|
|
sa.bInheritHandle = TRUE;
|
|
HANDLE rd = NULL, wr = NULL;
|
|
if (!CreatePipe(&rd, &wr, &sa, 0))
|
|
return 0;
|
|
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
STARTUPINFOA si;
|
|
PROCESS_INFORMATION pi;
|
|
memset(&si, 0, sizeof(si));
|
|
memset(&pi, 0, sizeof(pi));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = wr;
|
|
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
|
|
char mutable_cmd[1024];
|
|
snprintf(mutable_cmd, sizeof(mutable_cmd), "%s", cmdline);
|
|
if (!CreateProcessA(NULL, mutable_cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW,
|
|
NULL, NULL, &si, &pi)) {
|
|
CloseHandle(rd);
|
|
CloseHandle(wr);
|
|
return 0;
|
|
}
|
|
CloseHandle(wr);
|
|
|
|
char buf[512];
|
|
size_t got = 0;
|
|
DWORD n = 0;
|
|
out[0] = '\0';
|
|
while (ReadFile(rd, buf, sizeof(buf), &n, NULL) && n > 0) {
|
|
for (DWORD i = 0; i < n; ++i) {
|
|
char c = buf[i];
|
|
if (c == '\r')
|
|
continue;
|
|
if (c == '\n') {
|
|
out[got] = '\0';
|
|
got = cap; /* mark complete */
|
|
break;
|
|
}
|
|
if (got + 1 < cap)
|
|
out[got++] = c;
|
|
}
|
|
if (got >= cap)
|
|
break;
|
|
}
|
|
if (got > 0 && got < cap)
|
|
out[got] = '\0';
|
|
CloseHandle(rd);
|
|
WaitForSingleObject(pi.hProcess, 8000);
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
return out[0] != '\0';
|
|
}
|
|
#else
|
|
static int resolve_existing_dir(const char* path, char* out, size_t cap) {
|
|
if (path && path[0] && path_is_dir(path)) {
|
|
snprintf(out, cap, "%s", path);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
static int dirname_copy(char* out, size_t cap, const char* path) {
|
|
size_t n = strlen(path);
|
|
while (n > 0 && (path[n - 1] == '/' || path[n - 1] == '\\'))
|
|
--n;
|
|
while (n > 0 && path[n - 1] != '/' && path[n - 1] != '\\')
|
|
--n;
|
|
while (n > 0 && (path[n - 1] == '/' || path[n - 1] == '\\'))
|
|
--n;
|
|
if (n == 0) {
|
|
if (cap < 2) return 0;
|
|
out[0] = '.';
|
|
out[1] = '\0';
|
|
return 1;
|
|
}
|
|
if (n >= cap) return 0;
|
|
memcpy(out, path, n);
|
|
out[n] = '\0';
|
|
return 1;
|
|
}
|
|
|
|
/* Resolve an existing file to an absolute path. Tries the string as-is, then
|
|
* under g_project_root, then next to g_exe_path. Relative bios.cfg lines like
|
|
* "bios/SCPH1001.BIN" otherwise break after relaunch when cwd is build/. */
|
|
static int absolutize_existing_file(char* out, size_t cap, const char* path) {
|
|
char cand[1100];
|
|
if (!out || cap < 2 || !path || !path[0]) return 0;
|
|
out[0] = '\0';
|
|
|
|
for (int pass = 0; pass < 3; ++pass) {
|
|
const char* candidate = NULL;
|
|
if (pass == 0) {
|
|
candidate = path;
|
|
} else if (pass == 1) {
|
|
if (path_is_absolute(path) || !g_project_root[0]) continue;
|
|
if (!join_path(cand, sizeof(cand), g_project_root, path)) continue;
|
|
candidate = cand;
|
|
} else {
|
|
char dir[1100];
|
|
if (path_is_absolute(path) || !g_exe_path[0]) continue;
|
|
if (!dirname_copy(dir, sizeof(dir), g_exe_path)) continue;
|
|
if (!join_path(cand, sizeof(cand), dir, path)) continue;
|
|
candidate = cand;
|
|
}
|
|
if (!candidate || !path_is_file(candidate)) continue;
|
|
#if defined(_WIN32)
|
|
{
|
|
DWORD n = GetFullPathNameA(candidate, (DWORD)cap, out, NULL);
|
|
if (n > 0 && n < (DWORD)cap) return 1;
|
|
}
|
|
#else
|
|
/* glibc _FORTIFY_SOURCE aborts if the realpath destination is smaller
|
|
* than PATH_MAX (4096), even when the resolved path fits. Allocate. */
|
|
{
|
|
char* rp = realpath(candidate, NULL);
|
|
if (rp) {
|
|
snprintf(out, cap, "%s", rp);
|
|
free(rp);
|
|
return 1;
|
|
}
|
|
}
|
|
#endif
|
|
if ((size_t)snprintf(out, cap, "%s", candidate) < cap) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int resolve_cli_path(const char* root, char* out, size_t cap) {
|
|
const char* candidates[] = {
|
|
cfg_or(g_cfg->psxrecomp_cli_relpath, "psxrecomp/psxrecomp_cli.py"),
|
|
"psxrecomp/psxrecomp_cli.py",
|
|
};
|
|
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); ++i) {
|
|
if (!candidates[i] || !candidates[i][0])
|
|
continue;
|
|
if (!join_path(out, cap, root, candidates[i]))
|
|
continue;
|
|
if (path_is_file(out))
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int looks_like_project_root(const char* root) {
|
|
char cli[1100], toml[1100];
|
|
if (!join_path(toml, sizeof(toml), root,
|
|
cfg_or(g_cfg->seed_cfg_relpath, "game.toml")))
|
|
return 0;
|
|
if (!path_is_file(toml))
|
|
return 0;
|
|
return resolve_cli_path(root, cli, sizeof(cli));
|
|
}
|
|
|
|
static int find_on_path(const char* name, char* out, size_t cap) {
|
|
#if defined(_WIN32)
|
|
char cmd[640];
|
|
snprintf(cmd, sizeof(cmd), "where %s >nul 2>nul", name);
|
|
if (system(cmd) == 0) {
|
|
snprintf(out, cap, "%s", name);
|
|
return 1;
|
|
}
|
|
#else
|
|
char cmd[640];
|
|
snprintf(cmd, sizeof(cmd), "command -v %s >/dev/null 2>&1", name);
|
|
if (system(cmd) == 0) {
|
|
snprintf(out, cap, "%s", name);
|
|
return 1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
/* resolve_toolchain_bin is defined later; pack python lives beside bin/. */
|
|
static int resolve_toolchain_bin(char* out, size_t cap);
|
|
|
|
/* cmake-clang-v1 1.0.6+: python-build-standalone under <pack>/python/. */
|
|
static int python_exe_under_py_root(const char* py_root, char* out, size_t cap) {
|
|
char cand[1200];
|
|
if (!py_root || !py_root[0] || !path_is_dir(py_root))
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
if (join_path(cand, sizeof(cand), py_root, "python.exe") &&
|
|
path_is_file(cand) && !python_path_is_store(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
if (join_path(cand, sizeof(cand), py_root, "python3.exe") &&
|
|
path_is_file(cand) && !python_path_is_store(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
#endif
|
|
if (join_path(cand, sizeof(cand), py_root, "bin/python3") &&
|
|
path_is_file(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
if (join_path(cand, sizeof(cand), py_root, "bin/python") &&
|
|
path_is_file(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
#if defined(__APPLE__)
|
|
#if defined(__aarch64__)
|
|
if (join_path(cand, sizeof(cand), py_root,
|
|
"aarch64-apple-darwin/bin/python3") &&
|
|
path_is_file(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
#else
|
|
if (join_path(cand, sizeof(cand), py_root,
|
|
"x86_64-apple-darwin/bin/python3") &&
|
|
path_is_file(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
#endif
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static int find_toolchain_python(char* out, size_t cap) {
|
|
char bin[1400], pack[1400], py_root[1400];
|
|
if (!resolve_toolchain_bin(bin, sizeof(bin)))
|
|
return 0;
|
|
if (!dirname_copy(pack, sizeof(pack), bin) || !pack[0])
|
|
return 0;
|
|
if (!join_path(py_root, sizeof(py_root), pack, "python"))
|
|
return 0;
|
|
return python_exe_under_py_root(py_root, out, cap);
|
|
}
|
|
|
|
static int python_env_usable(const char* env) {
|
|
if (!env || !env[0] || !path_is_file(env))
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
if (python_path_is_store(env))
|
|
return 0;
|
|
#endif
|
|
return 1;
|
|
}
|
|
|
|
/* Prefer portable pack CPython (Retro / cmake-clang-v1), then system. */
|
|
static int find_python(char* out, size_t cap) {
|
|
const char* env = getenv("RETCOMM_PYTHON");
|
|
if (python_env_usable(env)) {
|
|
snprintf(out, cap, "%s", env);
|
|
return 1;
|
|
}
|
|
env = getenv("PYTHON");
|
|
if (python_env_usable(env)) {
|
|
snprintf(out, cap, "%s", env);
|
|
return 1;
|
|
}
|
|
#if defined(_WIN32)
|
|
if (find_toolchain_python(out, cap))
|
|
return 1;
|
|
/* Prefer python.org / py-launcher installs over the Microsoft Store
|
|
* stub: Store Python redirects LocalAppData writes into LocalCache. */
|
|
char resolved[1100];
|
|
if (capture_cmd_first_line(
|
|
"py -3 -c \"import sys; print(sys.executable)\"", resolved,
|
|
sizeof(resolved)) &&
|
|
path_is_file(resolved) && !python_path_is_store(resolved)) {
|
|
snprintf(out, cap, "%s", resolved);
|
|
return 1;
|
|
}
|
|
if (capture_cmd_first_line(
|
|
"python -c \"import sys; print(sys.executable)\"", resolved,
|
|
sizeof(resolved)) &&
|
|
path_is_file(resolved) && !python_path_is_store(resolved)) {
|
|
snprintf(out, cap, "%s", resolved);
|
|
return 1;
|
|
}
|
|
const char* candidates[] = {"python.exe", "python3.exe", "py.exe"};
|
|
#else
|
|
const char* candidates[] = {"python3", "python"};
|
|
#endif
|
|
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); ++i) {
|
|
if (!find_on_path(candidates[i], out, cap))
|
|
continue;
|
|
#if defined(_WIN32)
|
|
if (python_path_is_store(out))
|
|
continue;
|
|
#endif
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int toolchain_bin_has_cmake(const char* bin, char* out, size_t cap) {
|
|
char cmake[1200];
|
|
#if defined(_WIN32)
|
|
if (join_path(cmake, sizeof(cmake), bin, "cmake.exe") && path_is_file(cmake)) {
|
|
snprintf(out, cap, "%s", bin);
|
|
return 1;
|
|
}
|
|
#else
|
|
if (join_path(cmake, sizeof(cmake), bin, "cmake") && path_is_file(cmake)) {
|
|
snprintf(out, cap, "%s", bin);
|
|
return 1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static int resolve_toolchain_bin_under(const char* wrap, char* out, size_t cap) {
|
|
char cand[1100], cmake[1200], root[1400];
|
|
if (!wrap || !wrap[0])
|
|
return 0;
|
|
if (!resolve_existing_dir(wrap, root, sizeof(root)))
|
|
return 0;
|
|
wrap = root;
|
|
if (join_path(cand, sizeof(cand), wrap, "bin") &&
|
|
toolchain_bin_has_cmake(cand, out, cap))
|
|
return 1;
|
|
#if defined(_WIN32)
|
|
WIN32_FIND_DATAA fd;
|
|
char pattern[1200];
|
|
snprintf(pattern, sizeof(pattern), "%s\\*", wrap);
|
|
HANDLE h = FindFirstFileA(pattern, &fd);
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return 0;
|
|
int found = 0;
|
|
do {
|
|
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
continue;
|
|
if (fd.cFileName[0] == '.')
|
|
continue;
|
|
char nested[1100], nbin[1100];
|
|
if (!join_path(nested, sizeof(nested), wrap, fd.cFileName))
|
|
continue;
|
|
if (!join_path(nbin, sizeof(nbin), nested, "bin"))
|
|
continue;
|
|
if (join_path(cmake, sizeof(cmake), nbin, "cmake.exe") && path_is_file(cmake)) {
|
|
snprintf(out, cap, "%s", nbin);
|
|
found = 1;
|
|
break;
|
|
}
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
return found;
|
|
#else
|
|
DIR* dir = opendir(wrap);
|
|
if (!dir)
|
|
return 0;
|
|
int found = 0;
|
|
struct dirent* ent;
|
|
while ((ent = readdir(dir)) != NULL) {
|
|
if (ent->d_name[0] == '.')
|
|
continue;
|
|
char nested[1100], nbin[1100];
|
|
if (!join_path(nested, sizeof(nested), wrap, ent->d_name))
|
|
continue;
|
|
if (!path_is_dir(nested))
|
|
continue;
|
|
if (!join_path(nbin, sizeof(nbin), nested, "bin"))
|
|
continue;
|
|
if (join_path(cmake, sizeof(cmake), nbin, "cmake") && path_is_file(cmake)) {
|
|
snprintf(out, cap, "%s", nbin);
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
closedir(dir);
|
|
return found;
|
|
#endif
|
|
}
|
|
|
|
/* Prefer tagged installs (CLI writes latest/ / offline/) then any child. */
|
|
static int resolve_toolchain_cache_base(const char* base, char* out, size_t cap) {
|
|
static const char* prefer[] = {"latest", "offline", NULL};
|
|
char cand[1100];
|
|
if (!base || !base[0])
|
|
return 0;
|
|
for (int i = 0; prefer[i]; ++i) {
|
|
if (join_path(cand, sizeof(cand), base, prefer[i]) &&
|
|
resolve_toolchain_bin_under(cand, out, cap))
|
|
return 1;
|
|
}
|
|
return resolve_toolchain_bin_under(base, out, cap);
|
|
}
|
|
|
|
/* Collect …/cmake-clang-v1 cache roots (same order as toolchain_pack.py).
|
|
* Honors RETCOMM_TOOLCHAIN_CACHE / RETCOMM_DATA_HOME; searches both XDG and
|
|
* ~/.local/share so a pack installed by install.sh is always found. */
|
|
static int collect_toolchain_cache_bases(char bases[][1400], int max_n) {
|
|
int n = 0;
|
|
const char* cache = getenv("RETCOMM_TOOLCHAIN_CACHE");
|
|
if (cache && cache[0] && n < max_n) {
|
|
snprintf(bases[n++], 1400, "%s", cache);
|
|
}
|
|
const char* data = getenv("RETCOMM_DATA_HOME");
|
|
if (data && data[0] && n < max_n) {
|
|
if (join_path(bases[n], 1400, data, "toolchains/cmake-clang-v1"))
|
|
++n;
|
|
}
|
|
#if defined(_WIN32)
|
|
const char* local = getenv("LOCALAPPDATA");
|
|
if (local && local[0]) {
|
|
if (n < max_n &&
|
|
join_path(bases[n], 1400, local, "retcomm/toolchains/cmake-clang-v1"))
|
|
++n;
|
|
if (n < max_n &&
|
|
join_path(bases[n], 1400, local,
|
|
"psxrecomp/toolchains/cmake-clang-v1"))
|
|
++n;
|
|
/* Store Python may have written only into LocalCache mirrors. */
|
|
for (int i = 0, lim = n; i < lim && n < max_n; ++i) {
|
|
char mirror[1400];
|
|
if (store_python_localcache_mirror(bases[i], mirror,
|
|
sizeof(mirror)))
|
|
snprintf(bases[n++], 1400, "%s", mirror);
|
|
}
|
|
}
|
|
#endif
|
|
const char* xdg = getenv("XDG_DATA_HOME");
|
|
const char* home = getenv("HOME");
|
|
if (xdg && xdg[0]) {
|
|
if (n < max_n &&
|
|
join_path(bases[n], 1400, xdg, "retcomm/toolchains/cmake-clang-v1"))
|
|
++n;
|
|
if (n < max_n &&
|
|
join_path(bases[n], 1400, xdg,
|
|
"psxrecomp/toolchains/cmake-clang-v1"))
|
|
++n;
|
|
}
|
|
if (home && home[0]) {
|
|
if (n < max_n &&
|
|
join_path(bases[n], 1400, home,
|
|
".local/share/retcomm/toolchains/cmake-clang-v1"))
|
|
++n;
|
|
if (n < max_n &&
|
|
join_path(bases[n], 1400, home,
|
|
".local/share/psxrecomp/toolchains/cmake-clang-v1"))
|
|
++n;
|
|
}
|
|
/* Dedup in-place. */
|
|
int w = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
int dup = 0;
|
|
for (int j = 0; j < w; ++j) {
|
|
#if defined(_WIN32)
|
|
if (_stricmp(bases[i], bases[j]) == 0) {
|
|
#else
|
|
if (strcmp(bases[i], bases[j]) == 0) {
|
|
#endif
|
|
dup = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!dup) {
|
|
if (w != i)
|
|
snprintf(bases[w], 1400, "%s", bases[i]);
|
|
++w;
|
|
}
|
|
}
|
|
return w;
|
|
}
|
|
|
|
/* Preferred install root for new downloads (first retcomm cache base). */
|
|
static int preferred_toolchain_cache_root(char* out, size_t cap) {
|
|
char bases[12][1400];
|
|
int n = collect_toolchain_cache_bases(bases, 12);
|
|
for (int i = 0; i < n; ++i) {
|
|
if (strstr(bases[i], "retcomm") != NULL) {
|
|
snprintf(out, cap, "%s", bases[i]);
|
|
return 1;
|
|
}
|
|
}
|
|
if (n < 1)
|
|
return 0;
|
|
snprintf(out, cap, "%s", bases[0]);
|
|
return 1;
|
|
}
|
|
|
|
static int resolve_shared_toolchain_cache(char* out, size_t cap) {
|
|
char bases[12][1400];
|
|
int n = collect_toolchain_cache_bases(bases, 12);
|
|
for (int i = 0; i < n; ++i) {
|
|
if (resolve_toolchain_cache_base(bases[i], out, cap))
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* CLI writes project_root/toolchain/.psxrecomp-bin with the bin path. */
|
|
static int resolve_toolchain_stamp(char* out, size_t cap) {
|
|
char stamp[1200], line[1400], resolved[1400];
|
|
FILE* f;
|
|
if (!g_project_root[0])
|
|
return 0;
|
|
if (!join_path(stamp, sizeof(stamp), g_project_root,
|
|
"toolchain/.psxrecomp-bin"))
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
{
|
|
char alt[1400];
|
|
if (!path_is_file(stamp) &&
|
|
store_python_localcache_mirror(stamp, alt, sizeof(alt)) &&
|
|
path_is_file(alt))
|
|
snprintf(stamp, sizeof(stamp), "%s", alt);
|
|
}
|
|
#endif
|
|
if (!path_is_file(stamp))
|
|
return 0;
|
|
f = fopen(stamp, "r");
|
|
if (!f)
|
|
return 0;
|
|
if (!fgets(line, sizeof(line), f)) {
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
fclose(f);
|
|
size_t n = strlen(line);
|
|
while (n && (line[n - 1] == '\n' || line[n - 1] == '\r' ||
|
|
line[n - 1] == ' '))
|
|
line[--n] = '\0';
|
|
if (!line[0])
|
|
return 0;
|
|
if (resolve_existing_dir(line, resolved, sizeof(resolved)) &&
|
|
toolchain_bin_has_cmake(resolved, out, cap))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int resolve_toolchain_bin(char* out, size_t cap) {
|
|
/* Fresh ensure-toolchain result (bin directory). */
|
|
if (g_cli_toolchain_bin[0]) {
|
|
char resolved[1400];
|
|
if (resolve_existing_dir(g_cli_toolchain_bin, resolved,
|
|
sizeof(resolved)) &&
|
|
toolchain_bin_has_cmake(resolved, out, cap))
|
|
return 1;
|
|
}
|
|
|
|
const char* env_keys[] = {
|
|
"RETCOMM_TOOLCHAIN_DIR", "PSXRECOMP_TOOLCHAIN_DIR", "TOOLCHAIN_DIR",
|
|
"BPE_TOOLCHAIN_DIR", NULL};
|
|
for (int i = 0; env_keys[i]; ++i) {
|
|
const char* e = getenv(env_keys[i]);
|
|
if (e && e[0] && resolve_toolchain_bin_under(e, out, cap))
|
|
return 1;
|
|
}
|
|
if (resolve_toolchain_stamp(out, cap))
|
|
return 1;
|
|
if (g_project_root[0]) {
|
|
char wrap[1100];
|
|
if (join_path(wrap, sizeof(wrap), g_project_root, "toolchain") &&
|
|
resolve_toolchain_bin_under(wrap, out, cap))
|
|
return 1;
|
|
}
|
|
/* CLI downloads land here — must match toolchain_pack.py. */
|
|
if (resolve_shared_toolchain_cache(out, cap))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static void activate_toolchain_path(void) {
|
|
char pack_root[1400];
|
|
g_toolchain_bin[0] = '\0';
|
|
#if !defined(_WIN32)
|
|
/* A cached portable pack must not shadow Linux/macOS native tools. */
|
|
return;
|
|
#endif
|
|
if (!resolve_toolchain_bin(g_toolchain_bin, sizeof(g_toolchain_bin)))
|
|
return;
|
|
/* Pack root (parent of bin/) — Windows cmake-clang-v1 ships zlib here. */
|
|
pack_root[0] = '\0';
|
|
(void)dirname_copy(pack_root, sizeof(pack_root), g_toolchain_bin);
|
|
|
|
/* Prepend pack python/ (Windows) or python/bin (Unix), then bin/. */
|
|
char py_path_dir[1400];
|
|
char py_exe[1400];
|
|
py_path_dir[0] = '\0';
|
|
py_exe[0] = '\0';
|
|
if (pack_root[0]) {
|
|
char py_root[1400];
|
|
if (join_path(py_root, sizeof(py_root), pack_root, "python") &&
|
|
python_exe_under_py_root(py_root, py_exe, sizeof(py_exe))) {
|
|
#if defined(_WIN32)
|
|
/* PBS Windows: <pack>/python/python.exe */
|
|
snprintf(py_path_dir, sizeof(py_path_dir), "%s", py_root);
|
|
#else
|
|
/* PBS Unix: <pack>/python/bin/python3 */
|
|
if (!dirname_copy(py_path_dir, sizeof(py_path_dir), py_exe))
|
|
py_path_dir[0] = '\0';
|
|
#endif
|
|
}
|
|
}
|
|
|
|
const char* old = getenv("PATH");
|
|
#if defined(_WIN32)
|
|
char neu[8192];
|
|
if (py_path_dir[0])
|
|
snprintf(neu, sizeof(neu), "%s;%s%s%s", py_path_dir, g_toolchain_bin,
|
|
old ? ";" : "", old ? old : "");
|
|
else
|
|
snprintf(neu, sizeof(neu), "%s%s%s", g_toolchain_bin, old ? ";" : "",
|
|
old ? old : "");
|
|
_putenv_s("PATH", neu);
|
|
if (py_exe[0])
|
|
_putenv_s("RETCOMM_PYTHON", py_exe);
|
|
#else
|
|
char neu[8192];
|
|
if (py_path_dir[0])
|
|
snprintf(neu, sizeof(neu), "%s:%s%s%s", py_path_dir, g_toolchain_bin,
|
|
old ? ":" : "", old ? old : "");
|
|
else
|
|
snprintf(neu, sizeof(neu), "%s%s%s", g_toolchain_bin, old ? ":" : "",
|
|
old ? old : "");
|
|
setenv("PATH", neu, 1);
|
|
if (py_exe[0])
|
|
setenv("RETCOMM_PYTHON", py_exe, 1);
|
|
#endif
|
|
if (pack_root[0]) {
|
|
#if defined(_WIN32)
|
|
_putenv_s("RETCOMM_TOOLCHAIN_DIR", pack_root);
|
|
_putenv_s("ZLIB_ROOT", pack_root);
|
|
{
|
|
const char* prev = getenv("CMAKE_PREFIX_PATH");
|
|
char pref[8192];
|
|
if (prev && prev[0] && !strstr(prev, pack_root))
|
|
snprintf(pref, sizeof(pref), "%s;%s", pack_root, prev);
|
|
else if (!prev || !prev[0])
|
|
snprintf(pref, sizeof(pref), "%s", pack_root);
|
|
else
|
|
pref[0] = '\0';
|
|
if (pref[0])
|
|
_putenv_s("CMAKE_PREFIX_PATH", pref);
|
|
}
|
|
#else
|
|
setenv("RETCOMM_TOOLCHAIN_DIR", pack_root, 1);
|
|
setenv("ZLIB_ROOT", pack_root, 1);
|
|
{
|
|
const char* prev = getenv("CMAKE_PREFIX_PATH");
|
|
char pref[8192];
|
|
if (prev && prev[0] && !strstr(prev, pack_root))
|
|
snprintf(pref, sizeof(pref), "%s:%s", pack_root, prev);
|
|
else if (!prev || !prev[0])
|
|
snprintf(pref, sizeof(pref), "%s", pack_root);
|
|
else
|
|
pref[0] = '\0';
|
|
if (pref[0])
|
|
setenv("CMAKE_PREFIX_PATH", pref, 1);
|
|
}
|
|
/* Linux packs 1.0.4+: bundled libxml2.so.2 for lld / thin LTO. */
|
|
{
|
|
char libdir[1500];
|
|
if (join_path(libdir, sizeof(libdir), pack_root, "lib") &&
|
|
path_is_dir(libdir)) {
|
|
const char* prev = getenv("LD_LIBRARY_PATH");
|
|
char llp[8192];
|
|
if (prev && prev[0] && !strstr(prev, libdir))
|
|
snprintf(llp, sizeof(llp), "%s:%s", libdir, prev);
|
|
else if (!prev || !prev[0])
|
|
snprintf(llp, sizeof(llp), "%s", libdir);
|
|
else
|
|
llp[0] = '\0';
|
|
if (llp[0])
|
|
setenv("LD_LIBRARY_PATH", llp, 1);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static int find_cmake(char* out, size_t cap) {
|
|
const char* env = getenv("CMAKE");
|
|
if (env && env[0] && path_is_file(env)) {
|
|
snprintf(out, cap, "%s", env);
|
|
return 1;
|
|
}
|
|
#if !defined(_WIN32)
|
|
return find_on_path("cmake", out, cap);
|
|
#endif
|
|
char tc[1100], cand[1200];
|
|
if (resolve_toolchain_bin(tc, sizeof(tc))) {
|
|
#if defined(_WIN32)
|
|
if (join_path(cand, sizeof(cand), tc, "cmake.exe") && path_is_file(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
#else
|
|
if (join_path(cand, sizeof(cand), tc, "cmake") && path_is_file(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
#endif
|
|
}
|
|
#if defined(_WIN32)
|
|
return find_on_path("cmake.exe", out, cap);
|
|
#else
|
|
return find_on_path("cmake", out, cap);
|
|
#endif
|
|
}
|
|
|
|
/* Walk start and up to 10 parents for game.toml + psxrecomp_cli.py. */
|
|
static int walk_up_for_project_root(const char* start, char* out, size_t cap) {
|
|
char cur[1024];
|
|
if (!start || !start[0])
|
|
return 0;
|
|
snprintf(cur, sizeof(cur), "%s", start);
|
|
for (int i = 0; i < 10; ++i) {
|
|
if (looks_like_project_root(cur)) {
|
|
snprintf(out, cap, "%s", cur);
|
|
return 1;
|
|
}
|
|
char parent[1024];
|
|
if (!dirname_copy(parent, sizeof(parent), cur))
|
|
break;
|
|
if (strcmp(parent, cur) == 0)
|
|
break;
|
|
snprintf(cur, sizeof(cur), "%s", parent);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
/* Canonical path of the running executable on POSIX hosts. /proc/self/exe is
|
|
* Linux-only; macOS has no procfs, so there the path comes from dyld. */
|
|
static int host_posix_exe_path(char* out, size_t cap) {
|
|
char* rp = NULL;
|
|
# if defined(__APPLE__)
|
|
char raw[1100];
|
|
uint32_t n = (uint32_t)sizeof(raw);
|
|
if (_NSGetExecutablePath(raw, &n) != 0)
|
|
return 0;
|
|
rp = realpath(raw, NULL);
|
|
# else
|
|
rp = realpath("/proc/self/exe", NULL);
|
|
# endif
|
|
if (!rp)
|
|
return 0;
|
|
snprintf(out, cap, "%s", rp);
|
|
free(rp);
|
|
return out[0] != '\0';
|
|
}
|
|
#endif
|
|
|
|
/* Directory containing this process's executable (not cwd). Dolphin / .desktop
|
|
* and Finder launches leave cwd as $HOME — VIDEO/PGO/setup must still find the
|
|
* zip tree. Linux: $APPIMAGE parent, else /proc/self/exe. macOS: dyld's
|
|
* executable path. Windows: GetModuleFileName. */
|
|
static int resolve_host_exe_dir(char* out, size_t cap) {
|
|
char exe[1100];
|
|
if (!out || cap < 2)
|
|
return 0;
|
|
out[0] = '\0';
|
|
exe[0] = '\0';
|
|
|
|
#if defined(_WIN32)
|
|
{
|
|
DWORD n = GetModuleFileNameA(NULL, exe, (DWORD)sizeof(exe));
|
|
if (n == 0 || n >= (DWORD)sizeof(exe))
|
|
return 0;
|
|
}
|
|
#else
|
|
{
|
|
const char* appimg = getenv("APPIMAGE");
|
|
if (appimg && appimg[0] && path_is_file(appimg)) {
|
|
snprintf(exe, sizeof(exe), "%s", appimg);
|
|
} else if (!host_posix_exe_path(exe, sizeof(exe))) {
|
|
return 0;
|
|
}
|
|
}
|
|
#endif
|
|
if (!exe[0])
|
|
return 0;
|
|
return dirname_copy(out, cap, exe);
|
|
}
|
|
|
|
/* Last path segment of `path` (points into `path`; not nul-trimmed copy). */
|
|
static const char* path_base_name(const char* path) {
|
|
const char* base = path ? path : "";
|
|
for (const char* p = base; *p; ++p) {
|
|
if ((*p == '/' || *p == '\\') && p[1])
|
|
base = p + 1;
|
|
}
|
|
return base;
|
|
}
|
|
|
|
static int name_is_releases(const char* name) {
|
|
if (!name || !name[0]) return 0;
|
|
#if defined(_WIN32)
|
|
return _stricmp(name, "releases") == 0;
|
|
#else
|
|
return strcmp(name, "releases") == 0;
|
|
#endif
|
|
}
|
|
|
|
/* Retro stages Play under apps/<title>/releases/<tag>/ while the generate
|
|
* tree lives at apps/<title>/src/current/. Walking parents of the release dir
|
|
* never visits that sibling — probe it explicitly. */
|
|
static int try_retcomm_src_current(const char* start, char* out, size_t cap) {
|
|
char cur[1024];
|
|
if (!start || !start[0] || !out || cap < 2)
|
|
return 0;
|
|
snprintf(cur, sizeof(cur), "%s", start);
|
|
for (int i = 0; i < 8; ++i) {
|
|
char parent[1024];
|
|
if (!dirname_copy(parent, sizeof(parent), cur))
|
|
break;
|
|
|
|
char cand[1100];
|
|
/* …/releases → …/src/current */
|
|
if (name_is_releases(path_base_name(cur))) {
|
|
if (join_path(cand, sizeof(cand), parent, "src/current") &&
|
|
looks_like_project_root(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
}
|
|
/* …/releases/<tag> → …/src/current */
|
|
if (name_is_releases(path_base_name(parent))) {
|
|
char install[1024];
|
|
if (dirname_copy(install, sizeof(install), parent) &&
|
|
join_path(cand, sizeof(cand), install, "src/current") &&
|
|
looks_like_project_root(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (strcmp(parent, cur) == 0)
|
|
break;
|
|
snprintf(cur, sizeof(cur), "%s", parent);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int discover_project_root(char* out, size_t cap) {
|
|
const char* env_name =
|
|
cfg_or(g_cfg->project_root_env, "PSXRECOMP_PROJECT_ROOT");
|
|
const char* env = getenv(env_name);
|
|
if (env && env[0] && looks_like_project_root(env)) {
|
|
snprintf(out, cap, "%s", env);
|
|
return 1;
|
|
}
|
|
|
|
/* 1) cwd (terminal launches from the unzip / repo root). */
|
|
char start[1024];
|
|
#if defined(_WIN32)
|
|
if (!GetCurrentDirectoryA((DWORD)sizeof(start), start))
|
|
start[0] = '\0';
|
|
#else
|
|
if (!getcwd(start, sizeof(start)))
|
|
start[0] = '\0';
|
|
#endif
|
|
if (start[0] && walk_up_for_project_root(start, out, cap))
|
|
return 1;
|
|
if (start[0] && try_retcomm_src_current(start, out, cap))
|
|
return 1;
|
|
|
|
/* 2) exe dir (GUI double-click: cwd is often $HOME / Desktop). */
|
|
char exe_dir[1024];
|
|
if (resolve_host_exe_dir(exe_dir, sizeof(exe_dir)) &&
|
|
walk_up_for_project_root(exe_dir, out, cap))
|
|
return 1;
|
|
if (resolve_host_exe_dir(exe_dir, sizeof(exe_dir)) &&
|
|
try_retcomm_src_current(exe_dir, out, cap))
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int resolve_build_paths(void) {
|
|
const char* env_name =
|
|
cfg_or(g_cfg->build_dir_env, "PSXRECOMP_BUILD_DIR");
|
|
const char* env = getenv(env_name);
|
|
if (env && env[0]) {
|
|
snprintf(g_build_dir, sizeof(g_build_dir), "%s", env);
|
|
} else {
|
|
const char* names[] = {
|
|
cfg_or(g_cfg->build_dir_name, "build"),
|
|
"build-release",
|
|
"build",
|
|
"build-ci",
|
|
};
|
|
int found = 0;
|
|
for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); ++i) {
|
|
if (!names[i] || !names[i][0])
|
|
continue;
|
|
char cand[1100];
|
|
if (!join_path(cand, sizeof(cand), g_project_root, names[i]))
|
|
continue;
|
|
if (path_is_dir(cand)) {
|
|
snprintf(g_build_dir, sizeof(g_build_dir), "%s", cand);
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
/* Setup zips ship without a pre-made build tree. Prefer the configured
|
|
* name so rebuild can cmake -B it on first Generate & rebuild. */
|
|
if (!found) {
|
|
const char* prefer = cfg_or(g_cfg->build_dir_name, "build-release");
|
|
if (!join_path(g_build_dir, sizeof(g_build_dir), g_project_root,
|
|
prefer))
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Prefer the name CMake published over the one baked into codegen_setup.c.
|
|
* Both are MAKE_C_IDENTIFIER(WINDOW_TITLE), but from two separate copies of
|
|
* the title, so renaming a game leaves this one pointing at an executable
|
|
* that is never produced. runtime.cmake writes the name it really used to
|
|
* psxrecomp_exe_name-<target>.txt beside the build. */
|
|
char basename[256];
|
|
snprintf(basename, sizeof(basename), "%s", g_exe_basename);
|
|
if (g_cfg && g_cfg->cmake_target && g_cfg->cmake_target[0]) {
|
|
char marker[1200], published[256];
|
|
snprintf(published, sizeof(published), "psxrecomp_exe_name-%s.txt",
|
|
g_cfg->cmake_target);
|
|
if (join_path(marker, sizeof(marker), g_build_dir, published) &&
|
|
read_first_line(marker, published, sizeof(published)) &&
|
|
published[0])
|
|
snprintf(basename, sizeof(basename), "%s", published);
|
|
}
|
|
|
|
char exe_name[300];
|
|
#if defined(_WIN32)
|
|
snprintf(exe_name, sizeof(exe_name), "%s.exe", basename);
|
|
#else
|
|
snprintf(exe_name, sizeof(exe_name), "%s", basename);
|
|
#endif
|
|
return join_path(g_exe_path, sizeof(g_exe_path), g_build_dir, exe_name);
|
|
}
|
|
|
|
#ifndef PSX_SETUP_BIOS_STEMS
|
|
#define PSX_SETUP_BIOS_STEMS "OpenBIOS|SCPH1001"
|
|
#endif
|
|
#ifndef PSX_SETUP_FRAMEWORK_REL
|
|
#define PSX_SETUP_FRAMEWORK_REL "psxrecomp"
|
|
#endif
|
|
|
|
/* Match runtime.cmake: a requested pair with its backend descriptor. A stale
|
|
* pre-descriptor pair or unrelated game dispatch cannot complete BIOS setup. */
|
|
static int generated_bios_backend_linkable(const char* dir, const char* stem) {
|
|
char full[600], path[1200], descriptor[600], line[4096];
|
|
FILE* f;
|
|
int found = 0;
|
|
if ((size_t)snprintf(full, sizeof(full), "%s_full.c", stem) >= sizeof(full))
|
|
return 0;
|
|
if (!join_path(path, sizeof(path), dir, full) || !path_is_file(path))
|
|
return 0;
|
|
snprintf(full, sizeof(full), "%s_dispatch.c", stem);
|
|
if (!join_path(path, sizeof(path), dir, full))
|
|
return 0;
|
|
f = fopen(path, "r");
|
|
if (!f) return 0;
|
|
snprintf(descriptor, sizeof(descriptor), "%s_psx_bios_backend", stem);
|
|
while (fgets(line, sizeof(line), f)) {
|
|
if (strstr(line, descriptor)) { found = 1; break; }
|
|
}
|
|
fclose(f);
|
|
return found;
|
|
}
|
|
|
|
/* Does the configured framework hold a linkable requested 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. CMake supplies
|
|
* the requested stems; accept any linkable member, not an unrelated pair. */
|
|
static int generated_has_bios_backend(const char* dir) {
|
|
const char* next = PSX_SETUP_BIOS_STEMS;
|
|
while (*next) {
|
|
char stem[512];
|
|
const char* end = strchr(next, '|');
|
|
size_t len = end ? (size_t)(end - next) : strlen(next);
|
|
if (len && len < sizeof(stem)) {
|
|
memcpy(stem, next, len);
|
|
stem[len] = 0;
|
|
if (generated_bios_backend_linkable(dir, stem)) return 1;
|
|
}
|
|
if (!end) break;
|
|
next = end + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int bios_backends_missing(void) {
|
|
char framework[1100], gen[1200];
|
|
if (!join_path(framework, sizeof(framework), g_project_root, PSX_SETUP_FRAMEWORK_REL) ||
|
|
!join_path(gen, sizeof(gen), framework, "generated"))
|
|
return 1;
|
|
return !generated_has_bios_backend(gen);
|
|
}
|
|
|
|
int psxrecomp_codegen_host_sources_missing(
|
|
const PsxrecompCodegenHostConfig* cfg) {
|
|
if (!cfg || !cfg->cmake_target || !cfg->exe_basename)
|
|
return 0;
|
|
g_cfg = cfg;
|
|
if (!g_project_root[0] &&
|
|
!discover_project_root(g_project_root, sizeof(g_project_root)))
|
|
return 0;
|
|
char marker[1100];
|
|
if (!join_path(marker, sizeof(marker), g_project_root,
|
|
cfg_or(cfg->gen_marker_relpath,
|
|
"generated/SLUS_011.89_dispatch.c")))
|
|
return 1;
|
|
if (!path_is_file(marker))
|
|
return 1;
|
|
return bios_backends_missing();
|
|
}
|
|
|
|
static int read_line_file(const char* path, char* out, size_t cap) {
|
|
FILE* f = fopen(path, "r");
|
|
if (!f) return 0;
|
|
if (!fgets(out, (int)cap, f)) {
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
fclose(f);
|
|
size_t n = strlen(out);
|
|
while (n && (out[n - 1] == '\n' || out[n - 1] == '\r' ||
|
|
out[n - 1] == ' ' || out[n - 1] == '\t'))
|
|
out[--n] = '\0';
|
|
return n > 0;
|
|
}
|
|
|
|
static int write_line_file(const char* path, const char* line) {
|
|
FILE* f;
|
|
if (!path || !path[0])
|
|
return 0;
|
|
if (!line || !line[0]) {
|
|
remove(path);
|
|
return 1;
|
|
}
|
|
f = fopen(path, "w");
|
|
if (!f)
|
|
return 0;
|
|
fprintf(f, "%s\n", line);
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
/* Sidecars are loaded next to argv[0]. Setup host is often zip-root while the
|
|
* rebuilt binary lives under build/ — write beside the game binary too. */
|
|
static void write_sidecar_near_exe(const char* near_exe, const char* name,
|
|
const char* value) {
|
|
char dir[1100], path[1200];
|
|
if (!near_exe || !near_exe[0] || !name || !name[0])
|
|
return;
|
|
if (!dirname_copy(dir, sizeof(dir), near_exe))
|
|
return;
|
|
if (!join_path(path, sizeof(path), dir, name))
|
|
return;
|
|
write_line_file(path, value ? value : "");
|
|
}
|
|
|
|
/* 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;
|
|
for (i = 0; i < len; ++i) {
|
|
crc ^= data[i];
|
|
for (j = 0; j < 8; ++j)
|
|
crc = (crc >> 1) ^ ((crc & 1u) ? 0xEDB88320u : 0u);
|
|
}
|
|
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 != (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; }
|
|
if (fread(buf, 1, (size_t)size, f) != (size_t)size) {
|
|
free(buf);
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
fclose(f);
|
|
crc = host_crc32(buf, (size_t)size);
|
|
free(buf);
|
|
return crc == want->crc32;
|
|
}
|
|
|
|
/* 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) {
|
|
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;
|
|
}
|
|
if (g_exe_path[0]) {
|
|
char dir[1100];
|
|
if (dirname_copy(dir, sizeof(dir), g_exe_path)) {
|
|
snprintf(roots[nroots], sizeof(roots[0]), "%s", dir);
|
|
++nroots;
|
|
}
|
|
}
|
|
for (r = 0; r < nroots; ++r) {
|
|
char walk[1100];
|
|
int depth;
|
|
snprintf(walk, sizeof(walk), "%s", roots[r]);
|
|
for (depth = 0; depth < 6 && walk[0]; ++depth) {
|
|
for (s = 0; s < (int)(sizeof(subs) / sizeof(subs[0])); ++s) {
|
|
char dir[1200];
|
|
if (subs[s][0]) {
|
|
if (!join_path(dir, sizeof(dir), walk, subs[s])) continue;
|
|
} else {
|
|
snprintf(dir, sizeof(dir), "%s", walk);
|
|
}
|
|
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;
|
|
if (!absolutize_existing_file(out, cap, cand))
|
|
snprintf(out, cap, "%s", cand);
|
|
return 1;
|
|
}
|
|
}
|
|
{
|
|
char parent[1100];
|
|
if (!dirname_copy(parent, sizeof(parent), walk)) break;
|
|
if (strcmp(parent, walk) == 0) break;
|
|
snprintf(walk, sizeof(walk), "%s", parent);
|
|
}
|
|
}
|
|
}
|
|
out[0] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
static int bios_cfg_sidecar_exists(void) {
|
|
char cand[1100];
|
|
char dir[1100];
|
|
if (g_project_root[0] &&
|
|
join_path(cand, sizeof(cand), g_project_root, "bios.cfg") &&
|
|
path_is_file(cand))
|
|
return 1;
|
|
if (g_exe_path[0] && dirname_copy(dir, sizeof(dir), g_exe_path) &&
|
|
join_path(cand, sizeof(cand), dir, "bios.cfg") && path_is_file(cand))
|
|
return 1;
|
|
if (path_is_file("bios.cfg")) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int resolve_bios_arg(char* out, size_t cap) {
|
|
char cand[1100];
|
|
char line[1100];
|
|
char abs[1100];
|
|
if (g_wizard_bios[0] && absolutize_existing_file(abs, sizeof(abs),
|
|
g_wizard_bios)) {
|
|
snprintf(out, cap, "%s", abs);
|
|
snprintf(g_wizard_bios, sizeof(g_wizard_bios), "%s", abs);
|
|
return 1;
|
|
}
|
|
if (join_path(cand, sizeof(cand), g_project_root, "bios.cfg") &&
|
|
read_line_file(cand, line, sizeof(line)) &&
|
|
absolutize_existing_file(out, cap, line))
|
|
return 1;
|
|
if (g_exe_path[0]) {
|
|
char dir[1100];
|
|
if (dirname_copy(dir, sizeof(dir), g_exe_path) &&
|
|
join_path(cand, sizeof(cand), dir, "bios.cfg") &&
|
|
read_line_file(cand, line, sizeof(line)) &&
|
|
absolutize_existing_file(out, cap, line))
|
|
return 1;
|
|
}
|
|
if (read_line_file("bios.cfg", line, sizeof(line)) &&
|
|
absolutize_existing_file(out, cap, line))
|
|
return 1;
|
|
/* bios.cfg present but empty = intentional OpenBIOS — do not rediscover. */
|
|
if (g_wizard_bios_explicit || bios_cfg_sidecar_exists()) {
|
|
out[0] = '\0';
|
|
return 0;
|
|
}
|
|
/* No remembered pick — adopt SCPH1001 if present; else OpenBIOS (empty). */
|
|
if (discover_retail_bios_c(out, cap)) {
|
|
snprintf(g_wizard_bios, sizeof(g_wizard_bios), "%s", out);
|
|
if (g_project_root[0] &&
|
|
join_path(cand, sizeof(cand), g_project_root, "bios.cfg"))
|
|
write_line_file(cand, out);
|
|
write_line_file("bios.cfg", out);
|
|
if (g_exe_path[0])
|
|
write_sidecar_near_exe(g_exe_path, "bios.cfg", out);
|
|
fprintf(stderr, "psxrecomp-codegen: setup adopted retail BIOS %s\n",
|
|
out);
|
|
return 1;
|
|
}
|
|
out[0] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
/* bios_path NULL = leave bios.cfg untouched; "" = clear (OpenBIOS); else write. */
|
|
/* ---- multi-disc roster (game.toml [game] discs) --------------------------
|
|
*
|
|
* The RUNTIME launcher has published a disc roster for a while, built from
|
|
* game.toml [game] discs. The SETUP host never did -- so on a first run the
|
|
* wizard could not know a title was a 3-disc set and asked for a single image.
|
|
* The other discs were then discovered missing much later, mid-game, at the
|
|
* first swap.
|
|
*
|
|
* Mirrors the runtime's rule: a roster is published only when there is more
|
|
* than one image, so single-disc titles see no behavioural change at all.
|
|
* Matches LNG_MAX_DISCS in recomp-ui; a longer list is clamped, exactly as the
|
|
* launcher clamps it. */
|
|
#define PSX_HOST_MAX_DISCS 8
|
|
|
|
static char g_disc_paths[PSX_HOST_MAX_DISCS][1024];
|
|
/* What the PLAYER located in the wizard, as opposed to what game.toml already
|
|
* claimed. Generate writes these into game.toml so the runtime's hot-swap
|
|
* roster ([game] discs) describes the images this machine actually has. */
|
|
static char g_wizard_discs[PSX_HOST_MAX_DISCS][1024];
|
|
static int g_wizard_disc_count;
|
|
static RecompLauncherCDisc g_disc_roster[PSX_HOST_MAX_DISCS];
|
|
static int g_num_discs;
|
|
|
|
static int host_path_is_absolute(const char* p) {
|
|
if (!p || !p[0]) return 0;
|
|
if (p[0] == '/' || p[0] == '\\') return 1;
|
|
return (p[1] == ':' && ((p[0] >= 'A' && p[0] <= 'Z') ||
|
|
(p[0] >= 'a' && p[0] <= 'z')));
|
|
}
|
|
|
|
/* Minimal reader for one known key rather than a TOML library: this host shells
|
|
* out to Python for every real parse, and pulling in a parser to read a single
|
|
* array of strings would be a dependency for one line of config.
|
|
*
|
|
* Only [game] discs is recognised, and only string entries -- anything else in
|
|
* the file is skipped rather than guessed at. */
|
|
static void load_game_toml_disc_roster(void) {
|
|
FILE* f;
|
|
char line[1600];
|
|
int in_game = 0, in_array = 0, i;
|
|
|
|
g_num_discs = 0;
|
|
if (!g_game_toml[0]) return;
|
|
f = fopen(g_game_toml, "rb");
|
|
if (!f) return;
|
|
|
|
while (fgets(line, sizeof(line), f)) {
|
|
const char* p = line;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (!in_array) {
|
|
if (*p == '[') { /* section header */
|
|
in_game = (strncmp(p, "[game]", 6) == 0);
|
|
continue;
|
|
}
|
|
if (*p == '#' || !*p) continue;
|
|
if (!in_game) continue;
|
|
if (strncmp(p, "discs", 5) != 0) continue;
|
|
p += 5;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
/* Guard against a longer key that merely starts with "discs". */
|
|
if (*p != '=') continue;
|
|
++p;
|
|
in_array = 1;
|
|
}
|
|
for (; *p; ++p) {
|
|
if (*p == ']') { in_array = 0; break; }
|
|
if (*p == '#') break; /* trailing comment */
|
|
if (*p != '"') continue;
|
|
{
|
|
const char* q = ++p;
|
|
size_t n;
|
|
while (*p && *p != '"') ++p;
|
|
if (!*p) break; /* unterminated -- give up */
|
|
n = (size_t)(p - q);
|
|
if (g_num_discs < PSX_HOST_MAX_DISCS &&
|
|
n < sizeof(g_disc_paths[0])) {
|
|
memcpy(g_disc_paths[g_num_discs], q, n);
|
|
g_disc_paths[g_num_discs][n] = '\0';
|
|
++g_num_discs;
|
|
}
|
|
}
|
|
}
|
|
if (!in_array) break;
|
|
}
|
|
fclose(f);
|
|
|
|
/* game.toml may hold repo-relative paths. The launcher existence-checks
|
|
* what we publish, and its cwd is not necessarily the project root, so a
|
|
* relative entry would render as "missing" on a perfectly good install. */
|
|
for (i = 0; i < g_num_discs; ++i) {
|
|
char abs[1024];
|
|
if (host_path_is_absolute(g_disc_paths[i])) continue;
|
|
if (!g_project_root[0]) continue;
|
|
if (join_path(abs, sizeof(abs), g_project_root, g_disc_paths[i]))
|
|
snprintf(g_disc_paths[i], sizeof(g_disc_paths[i]), "%s", abs);
|
|
}
|
|
|
|
/* One image is not a set: leave GameInfo dark so nothing changes for
|
|
* single-disc titles. */
|
|
if (g_num_discs < 2) { g_num_discs = 0; return; }
|
|
for (i = 0; i < g_num_discs; ++i) {
|
|
g_disc_roster[i].number = i + 1;
|
|
g_disc_roster[i].label = NULL; /* launcher formats "Disc N" */
|
|
g_disc_roster[i].path = g_disc_paths[i];
|
|
}
|
|
}
|
|
|
|
static int host_persist_setup(void* ctx, const char* rom_path,
|
|
const char* bios_path) {
|
|
char path[1200];
|
|
char abs_bios[1100];
|
|
(void)ctx;
|
|
if (bios_path) {
|
|
g_wizard_bios_explicit = 1;
|
|
if (bios_path[0] &&
|
|
absolutize_existing_file(abs_bios, sizeof(abs_bios), bios_path)) {
|
|
snprintf(g_wizard_bios, sizeof(g_wizard_bios), "%s", abs_bios);
|
|
} else {
|
|
g_wizard_bios[0] = '\0';
|
|
}
|
|
if (g_project_root[0] &&
|
|
join_path(path, sizeof(path), g_project_root, "bios.cfg"))
|
|
write_line_file(path, g_wizard_bios[0] ? g_wizard_bios : "");
|
|
write_line_file("bios.cfg", g_wizard_bios[0] ? g_wizard_bios : "");
|
|
if (g_exe_path[0])
|
|
write_sidecar_near_exe(g_exe_path, "bios.cfg",
|
|
g_wizard_bios[0] ? g_wizard_bios : "");
|
|
if (g_build_dir[0]) {
|
|
char build_exe[1200];
|
|
if (join_path(build_exe, sizeof(build_exe), g_build_dir,
|
|
g_exe_basename))
|
|
write_sidecar_near_exe(build_exe, "bios.cfg",
|
|
g_wizard_bios[0] ? g_wizard_bios : "");
|
|
}
|
|
}
|
|
if (rom_path && rom_path[0]) {
|
|
if (g_project_root[0] &&
|
|
join_path(path, sizeof(path), g_project_root, "disc.cfg"))
|
|
write_line_file(path, rom_path);
|
|
write_line_file("disc.cfg", rom_path);
|
|
if (g_exe_path[0])
|
|
write_sidecar_near_exe(g_exe_path, "disc.cfg", rom_path);
|
|
if (g_build_dir[0]) {
|
|
char build_exe[1200];
|
|
if (join_path(build_exe, sizeof(build_exe), g_build_dir,
|
|
g_exe_basename))
|
|
write_sidecar_near_exe(build_exe, "disc.cfg", rom_path);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Multi-disc flush (RecompLauncherCGameInfo.persist_setup_discs).
|
|
*
|
|
* disc.cfg becomes one path per line, in disc order. Readers that take only
|
|
* the first line keep working and get disc 1 -- which is what they got before
|
|
* this existed -- so the format change is additive in the direction that
|
|
* matters. A slot the player has not located is written as an EMPTY line
|
|
* rather than skipped, so line N is always disc N and filling one in later
|
|
* cannot renumber the others.
|
|
*
|
|
* The BIOS half is delegated to host_persist_setup: passing rom_path NULL runs
|
|
* its bios.cfg logic and skips its single-path disc write, so the two entry
|
|
* points cannot drift on BIOS handling. */
|
|
static int host_persist_setup_discs(void* ctx, const char* const* disc_paths,
|
|
int disc_count, const char* bios_path) {
|
|
char body[PSX_HOST_MAX_DISCS * 1030];
|
|
char path[1200];
|
|
size_t o = 0;
|
|
int i, any = 0;
|
|
|
|
host_persist_setup(ctx, NULL, bios_path);
|
|
|
|
if (!disc_paths || disc_count <= 0)
|
|
return 0;
|
|
if (disc_count > PSX_HOST_MAX_DISCS)
|
|
disc_count = PSX_HOST_MAX_DISCS;
|
|
|
|
g_wizard_disc_count = 0;
|
|
for (i = 0; i < disc_count; ++i) {
|
|
const char* d = disc_paths[i] ? disc_paths[i] : "";
|
|
snprintf(g_wizard_discs[i], sizeof(g_wizard_discs[i]), "%s", d);
|
|
if (d[0]) g_wizard_disc_count = i + 1; /* trailing blanks are not a set */
|
|
}
|
|
|
|
body[0] = '\0';
|
|
for (i = 0; i < disc_count; ++i) {
|
|
const char* d = disc_paths[i] ? disc_paths[i] : "";
|
|
size_t n = strlen(d);
|
|
if (o + n + 2 >= sizeof(body))
|
|
return 0;
|
|
if (i) body[o++] = '\n';
|
|
if (n) { memcpy(body + o, d, n); o += n; any = 1; }
|
|
body[o] = '\0';
|
|
}
|
|
/* Nothing located: leave any existing disc.cfg alone rather than replacing
|
|
* it with a file of blank lines. */
|
|
if (!any)
|
|
return 0;
|
|
|
|
if (g_project_root[0] &&
|
|
join_path(path, sizeof(path), g_project_root, "disc.cfg"))
|
|
write_line_file(path, body);
|
|
write_line_file("disc.cfg", body);
|
|
if (g_exe_path[0])
|
|
write_sidecar_near_exe(g_exe_path, "disc.cfg", body);
|
|
if (g_build_dir[0]) {
|
|
char build_exe[1200];
|
|
if (join_path(build_exe, sizeof(build_exe), g_build_dir, g_exe_basename))
|
|
write_sidecar_near_exe(build_exe, "disc.cfg", body);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void persist_relaunch_sidecars(const char* near_exe,
|
|
const char* disc_path) {
|
|
char bios_line[1100];
|
|
char project_sidecar[1200];
|
|
char abs_bios[1100];
|
|
|
|
if (disc_path && disc_path[0]) {
|
|
write_sidecar_near_exe(near_exe, "disc.cfg", disc_path);
|
|
write_line_file("disc.cfg", disc_path);
|
|
if (g_project_root[0] &&
|
|
join_path(project_sidecar, sizeof(project_sidecar), g_project_root,
|
|
"disc.cfg"))
|
|
write_line_file(project_sidecar, disc_path);
|
|
}
|
|
|
|
bios_line[0] = '\0';
|
|
if (g_wizard_bios[0] &&
|
|
absolutize_existing_file(abs_bios, sizeof(abs_bios), g_wizard_bios)) {
|
|
snprintf(bios_line, sizeof(bios_line), "%s", abs_bios);
|
|
} else if (g_wizard_bios_explicit) {
|
|
/* Intentional OpenBIOS: clear sidecars — do not revive a stale
|
|
* bios.cfg (e.g. root-owned file that remove() could not delete). */
|
|
write_sidecar_near_exe(near_exe, "bios.cfg", "");
|
|
write_line_file("bios.cfg", "");
|
|
if (g_project_root[0] &&
|
|
join_path(project_sidecar, sizeof(project_sidecar), g_project_root,
|
|
"bios.cfg"))
|
|
write_line_file(project_sidecar, "");
|
|
if (g_build_dir[0]) {
|
|
char build_exe[1200];
|
|
if (join_path(build_exe, sizeof(build_exe), g_build_dir,
|
|
g_exe_basename))
|
|
write_sidecar_near_exe(build_exe, "bios.cfg", "");
|
|
}
|
|
return;
|
|
} else {
|
|
char line[1100];
|
|
line[0] = '\0';
|
|
if (!read_line_file("bios.cfg", line, sizeof(line)) &&
|
|
g_project_root[0] &&
|
|
join_path(project_sidecar, sizeof(project_sidecar), g_project_root,
|
|
"bios.cfg"))
|
|
read_line_file(project_sidecar, line, sizeof(line));
|
|
if (line[0])
|
|
absolutize_existing_file(bios_line, sizeof(bios_line), line);
|
|
}
|
|
if (bios_line[0]) {
|
|
snprintf(g_wizard_bios, sizeof(g_wizard_bios), "%s", bios_line);
|
|
write_sidecar_near_exe(near_exe, "bios.cfg", bios_line);
|
|
write_line_file("bios.cfg", bios_line);
|
|
if (g_project_root[0] &&
|
|
join_path(project_sidecar, sizeof(project_sidecar), g_project_root,
|
|
"bios.cfg"))
|
|
write_line_file(project_sidecar, bios_line);
|
|
}
|
|
}
|
|
|
|
static int json_get_string(const char* line, const char* key, char* out,
|
|
size_t out_cap) {
|
|
char pattern[96];
|
|
snprintf(pattern, sizeof(pattern), "\"%s\":\"", key);
|
|
const char* p = strstr(line, pattern);
|
|
if (!p) return 0;
|
|
p += strlen(pattern);
|
|
size_t i = 0;
|
|
while (*p && *p != '"' && i + 1 < out_cap) {
|
|
if (*p == '\\' && p[1]) {
|
|
++p;
|
|
out[i++] = *p++;
|
|
continue;
|
|
}
|
|
out[i++] = *p++;
|
|
}
|
|
out[i] = '\0';
|
|
return i > 0;
|
|
}
|
|
|
|
static int json_get_number(const char* line, const char* key, double* out) {
|
|
char pattern[96];
|
|
snprintf(pattern, sizeof(pattern), "\"%s\":", key);
|
|
const char* p = strstr(line, pattern);
|
|
if (!p) return 0;
|
|
p += strlen(pattern);
|
|
while (*p == ' ') ++p;
|
|
char* end = NULL;
|
|
double v = strtod(p, &end);
|
|
if (end == p) return 0;
|
|
*out = v;
|
|
return 1;
|
|
}
|
|
|
|
static void handle_progress_line(const char* line,
|
|
RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx) {
|
|
if (!line || line[0] != '{')
|
|
return;
|
|
char event[64] = "";
|
|
json_get_string(line, "event", event, sizeof(event));
|
|
/* Capture ensure-toolchain result even when UI progress is absent. */
|
|
if (strcmp(event, "result") == 0) {
|
|
char tb[1400], resolved[1400];
|
|
if (json_get_string(line, "toolchain_bin", tb, sizeof(tb)) && tb[0] &&
|
|
resolve_existing_dir(tb, resolved, sizeof(resolved))) {
|
|
snprintf(g_cli_toolchain_bin, sizeof(g_cli_toolchain_bin), "%s",
|
|
resolved);
|
|
/* Env expects pack root (parent of bin/), not bin/ itself. */
|
|
char pack_root[1400];
|
|
if (dirname_copy(pack_root, sizeof(pack_root), resolved) &&
|
|
pack_root[0]) {
|
|
#if defined(_WIN32)
|
|
_putenv_s("PSXRECOMP_TOOLCHAIN_DIR", pack_root);
|
|
#else
|
|
setenv("PSXRECOMP_TOOLCHAIN_DIR", pack_root, 1);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
if (!on_progress)
|
|
return;
|
|
if (strcmp(event, "phase") == 0) {
|
|
char message[240] = "";
|
|
char phase[64] = "";
|
|
double pct = -1.0;
|
|
json_get_string(line, "message", message, sizeof(message));
|
|
json_get_string(line, "phase", phase, sizeof(phase));
|
|
if (!json_get_number(line, "pct", &pct))
|
|
pct = -1.0;
|
|
if (!message[0] && phase[0])
|
|
snprintf(message, sizeof(message), "%s", phase);
|
|
on_progress(progress_ctx, (float)pct, message[0] ? message : NULL);
|
|
} else if (strcmp(event, "log") == 0 || strcmp(event, "error") == 0) {
|
|
char message[240] = "";
|
|
if (json_get_string(line, "message", message, sizeof(message)))
|
|
on_progress(progress_ctx, -1.0f, message);
|
|
}
|
|
}
|
|
|
|
/* Last-lines capture so a failed CLI run can say WHY in the wizard, which has
|
|
* no console: the child's stderr shares the progress pipe, JSON progress rows
|
|
* contribute their "message", raw rows (compiler/traceback text) contribute
|
|
* as-is, and the most recent error-looking line is appended to err_msg. This
|
|
* is what turns "psxrecomp rebuild failed (exit 1)" into "… failed (exit 1):
|
|
* Could NOT find OpenGL (missing: OPENGL_INCLUDE_DIR)". */
|
|
typedef struct {
|
|
char last[480];
|
|
char last_err[480];
|
|
} CliTail;
|
|
|
|
static int cli_tail_line_is_error(const char* s) {
|
|
return strstr(s, "rror") != NULL || strstr(s, "ailed") != NULL ||
|
|
strstr(s, "FAILED") != NULL || strstr(s, "Traceback") != NULL ||
|
|
strstr(s, "fatal") != NULL || strstr(s, "Fatal") != NULL;
|
|
}
|
|
|
|
static void cli_tail_note(CliTail* t, const char* line) {
|
|
char msg[480];
|
|
const char* rec = line;
|
|
int is_error_event = 0;
|
|
if (line[0] == '{') {
|
|
/* sdk_progress emits compact JSON: {"event":"error","message":…}. */
|
|
is_error_event = strstr(line, "\"event\":\"error\"") != NULL;
|
|
if (!json_get_string(line, "message", msg, sizeof(msg)))
|
|
return; /* structured row without text (e.g. result) */
|
|
rec = msg;
|
|
}
|
|
if (!rec[0])
|
|
return;
|
|
snprintf(t->last, sizeof(t->last), "%s", rec);
|
|
if (is_error_event || cli_tail_line_is_error(rec))
|
|
snprintf(t->last_err, sizeof(t->last_err), "%s", rec);
|
|
}
|
|
|
|
static void cli_fail_msg(char* err_msg, size_t err_cap, const char* fail_label,
|
|
long code, const CliTail* t) {
|
|
const char* why = t->last_err[0] ? t->last_err : t->last;
|
|
if (code == 3) {
|
|
/* The CLI names the failing check (a digest mismatch, or a .chd it
|
|
* cannot read) and cli_tail_note has captured that line. A flat
|
|
* "wrong dump" contradicts it and sends players hunting a bad rip
|
|
* when the dump is fine. Keep the reason when there is one. */
|
|
if (why[0])
|
|
snprintf(err_msg, err_cap, "Disc verification failed: %s", why);
|
|
else
|
|
snprintf(err_msg, err_cap, "Disc verification failed (wrong dump).");
|
|
return;
|
|
}
|
|
if (why[0])
|
|
snprintf(err_msg, err_cap, "%s failed (exit %ld): %s", fail_label,
|
|
code, why);
|
|
else
|
|
snprintf(err_msg, err_cap, "%s failed (exit %ld).", fail_label, code);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
static int run_cli_win(const char* cmdline,
|
|
RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx, char* err_msg, size_t err_cap,
|
|
const char* fail_label) {
|
|
SECURITY_ATTRIBUTES sa;
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.nLength = sizeof(sa);
|
|
sa.bInheritHandle = TRUE;
|
|
HANDLE rd = NULL, wr = NULL;
|
|
if (!CreatePipe(&rd, &wr, &sa, 0)) {
|
|
snprintf(err_msg, err_cap, "CreatePipe failed.");
|
|
return 0;
|
|
}
|
|
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
STARTUPINFOA si;
|
|
PROCESS_INFORMATION pi;
|
|
memset(&si, 0, sizeof(si));
|
|
memset(&pi, 0, sizeof(pi));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = wr;
|
|
/* stderr shares the pipe: Python tracebacks and raw tool output feed the
|
|
* CliTail capture instead of vanishing (the wizard has no console). */
|
|
si.hStdError = wr;
|
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
|
|
|
char mutable_cmd[4096];
|
|
snprintf(mutable_cmd, sizeof(mutable_cmd), "%s", cmdline);
|
|
if (!CreateProcessA(NULL, mutable_cmd, NULL, NULL, TRUE, 0, NULL,
|
|
g_project_root, &si, &pi)) {
|
|
CloseHandle(rd);
|
|
CloseHandle(wr);
|
|
snprintf(err_msg, err_cap, "Failed to spawn %s.", fail_label);
|
|
return 0;
|
|
}
|
|
CloseHandle(wr);
|
|
|
|
char buf[512];
|
|
/* Long JSONL rows (toolchain paths under LocalAppData) need headroom. */
|
|
char line[16384];
|
|
size_t line_len = 0;
|
|
DWORD n = 0;
|
|
CliTail tail;
|
|
memset(&tail, 0, sizeof(tail));
|
|
while (ReadFile(rd, buf, sizeof(buf), &n, NULL) && n > 0) {
|
|
for (DWORD i = 0; i < n; ++i) {
|
|
char c = buf[i];
|
|
if (c == '\r') continue;
|
|
if (c == '\n') {
|
|
line[line_len] = '\0';
|
|
handle_progress_line(line, on_progress, progress_ctx);
|
|
cli_tail_note(&tail, line);
|
|
line_len = 0;
|
|
continue;
|
|
}
|
|
if (line_len + 1 < sizeof(line))
|
|
line[line_len++] = c;
|
|
}
|
|
}
|
|
if (line_len) {
|
|
line[line_len] = '\0';
|
|
handle_progress_line(line, on_progress, progress_ctx);
|
|
cli_tail_note(&tail, line);
|
|
}
|
|
CloseHandle(rd);
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
DWORD code = 1;
|
|
GetExitCodeProcess(pi.hProcess, &code);
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
if (code == 0) return 1;
|
|
cli_fail_msg(err_msg, err_cap, fail_label, (long)code, &tail);
|
|
return 0;
|
|
}
|
|
#else
|
|
static int run_cli_posix(char* const argv[],
|
|
RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx, char* err_msg, size_t err_cap,
|
|
const char* fail_label) {
|
|
int pipefd[2];
|
|
if (pipe(pipefd) != 0) {
|
|
snprintf(err_msg, err_cap, "pipe() failed: %s", strerror(errno));
|
|
return 0;
|
|
}
|
|
|
|
posix_spawn_file_actions_t actions;
|
|
posix_spawn_file_actions_init(&actions);
|
|
posix_spawn_file_actions_addclose(&actions, pipefd[0]);
|
|
posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDOUT_FILENO);
|
|
/* stderr shares the pipe: Python tracebacks and raw tool output feed the
|
|
* CliTail capture instead of vanishing (the wizard has no console). */
|
|
posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDERR_FILENO);
|
|
posix_spawn_file_actions_addclose(&actions, pipefd[1]);
|
|
|
|
pid_t pid = 0;
|
|
int rc = posix_spawnp(&pid, argv[0], &actions, NULL, argv, environ);
|
|
posix_spawn_file_actions_destroy(&actions);
|
|
close(pipefd[1]);
|
|
if (rc != 0) {
|
|
close(pipefd[0]);
|
|
snprintf(err_msg, err_cap, "Failed to spawn %s: %s", fail_label,
|
|
strerror(rc));
|
|
return 0;
|
|
}
|
|
|
|
FILE* out = fdopen(pipefd[0], "r");
|
|
if (!out) {
|
|
close(pipefd[0]);
|
|
waitpid(pid, NULL, 0);
|
|
snprintf(err_msg, err_cap, "fdopen failed.");
|
|
return 0;
|
|
}
|
|
char line[16384];
|
|
CliTail tail;
|
|
memset(&tail, 0, sizeof(tail));
|
|
while (fgets(line, sizeof(line), out)) {
|
|
size_t n = strlen(line);
|
|
while (n && (line[n - 1] == '\n' || line[n - 1] == '\r'))
|
|
line[--n] = '\0';
|
|
handle_progress_line(line, on_progress, progress_ctx);
|
|
cli_tail_note(&tail, line);
|
|
}
|
|
fclose(out);
|
|
|
|
int status = 0;
|
|
if (waitpid(pid, &status, 0) < 0) {
|
|
snprintf(err_msg, err_cap, "waitpid failed: %s", strerror(errno));
|
|
return 0;
|
|
}
|
|
int code = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
|
|
if (code == 0) return 1;
|
|
cli_fail_msg(err_msg, err_cap, fail_label, (long)code, &tail);
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
/* ---- Host-native toolchain install (no Store Python AppData redirect) ---- */
|
|
|
|
static const char* k_tc_repo = "RetroPortingToolKit/RetroPorting-Toolchains";
|
|
|
|
static const char* toolchain_zip_asset_name(void) {
|
|
#if defined(_WIN32)
|
|
return "cmake-clang-v1-windows-x64.zip";
|
|
#elif defined(__APPLE__)
|
|
return "cmake-clang-v1-macos-universal.zip";
|
|
#else
|
|
return "cmake-clang-v1-linux-x64.zip";
|
|
#endif
|
|
}
|
|
|
|
/* Framework floor for accepted packs, not a per-title pin. v1.0.14 is the
|
|
* first cmake-clang-v1 with the Linux build sysroot; older cached packs
|
|
* compile against host headers (absent on stock SteamOS) and mis-link after a
|
|
* pack upgrade (__isoc23_strtoul). The cache is otherwise accepted forever
|
|
* once any cmake runs, so the floor is what makes a stale pre-sysroot pack
|
|
* get replaced. RETCOMM_TOOLCHAIN_MIN_VERSION overrides in either direction;
|
|
* "0" / "off" / "none" disables the floor (pre-floor behavior). Mirrors
|
|
* tools/toolchain_pack.py default_min_version(). */
|
|
#define TOOLCHAIN_DEFAULT_MIN_VERSION "1.0.14"
|
|
|
|
static const char* toolchain_min_version(void) {
|
|
const char* env = getenv("RETCOMM_TOOLCHAIN_MIN_VERSION");
|
|
if (env && env[0]) {
|
|
if (strcmp(env, "0") == 0 || ascii_strcasecmp(env, "off") == 0 ||
|
|
ascii_strcasecmp(env, "none") == 0)
|
|
return "";
|
|
return env;
|
|
}
|
|
return TOOLCHAIN_DEFAULT_MIN_VERSION;
|
|
}
|
|
|
|
/* Parse leading dotted integers from a version / tag (optional leading 'v'). */
|
|
static int version_cmp(const char* a, const char* b) {
|
|
const char* pa = a ? a : "";
|
|
const char* pb = b ? b : "";
|
|
if ((pa[0] == 'v' || pa[0] == 'V') && pa[1] >= '0' && pa[1] <= '9')
|
|
++pa;
|
|
if ((pb[0] == 'v' || pb[0] == 'V') && pb[1] >= '0' && pb[1] <= '9')
|
|
++pb;
|
|
for (;;) {
|
|
long va = 0, vb = 0;
|
|
int ha = 0, hb = 0;
|
|
while (*pa >= '0' && *pa <= '9') {
|
|
va = va * 10 + (*pa - '0');
|
|
++pa;
|
|
ha = 1;
|
|
}
|
|
while (*pb >= '0' && *pb <= '9') {
|
|
vb = vb * 10 + (*pb - '0');
|
|
++pb;
|
|
hb = 1;
|
|
}
|
|
if (!ha && !hb)
|
|
return 0;
|
|
if (va != vb)
|
|
return va < vb ? -1 : 1;
|
|
if (*pa == '.')
|
|
++pa;
|
|
if (*pb == '.')
|
|
++pb;
|
|
if (!ha || !hb)
|
|
return ha ? 1 : (hb ? -1 : 0);
|
|
}
|
|
}
|
|
|
|
static int read_pack_version(const char* pack_root, char* out, size_t cap) {
|
|
char meta[1400], buf[4096];
|
|
FILE* f;
|
|
const char* p;
|
|
size_t n;
|
|
if (!pack_root || !pack_root[0] || !out || cap < 2)
|
|
return 0;
|
|
out[0] = '\0';
|
|
if (!join_path(meta, sizeof(meta), pack_root, "retcomm-toolchain.json"))
|
|
return 0;
|
|
f = fopen(meta, "rb");
|
|
if (!f)
|
|
return 0;
|
|
n = fread(buf, 1, sizeof(buf) - 1, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
p = strstr(buf, "\"version\"");
|
|
if (!p)
|
|
return 0;
|
|
p = strchr(p + 9, '"');
|
|
if (!p)
|
|
return 0;
|
|
++p;
|
|
{
|
|
size_t i = 0;
|
|
while (*p && *p != '"' && i + 1 < cap) {
|
|
out[i++] = *p++;
|
|
}
|
|
out[i] = '\0';
|
|
return i > 0;
|
|
}
|
|
}
|
|
|
|
static int pack_meets_min_version(const char* pack_root) {
|
|
char ver[64];
|
|
const char* need = toolchain_min_version();
|
|
if (!need || !need[0])
|
|
return 1;
|
|
if (!read_pack_version(pack_root, ver, sizeof(ver)))
|
|
return 0;
|
|
return version_cmp(ver, need) >= 0;
|
|
}
|
|
|
|
static int mkdir_p(const char* path) {
|
|
char tmp[1400];
|
|
size_t n;
|
|
if (!path || !path[0])
|
|
return 0;
|
|
n = strlen(path);
|
|
if (n >= sizeof(tmp))
|
|
return 0;
|
|
memcpy(tmp, path, n + 1);
|
|
#if defined(_WIN32)
|
|
{
|
|
char* p = tmp;
|
|
if (n >= 2 && tmp[1] == ':')
|
|
p = tmp + 2;
|
|
while (*p == '\\' || *p == '/')
|
|
++p;
|
|
for (; *p; ++p) {
|
|
if (*p == '\\' || *p == '/') {
|
|
char save = *p;
|
|
*p = '\0';
|
|
if (tmp[0])
|
|
CreateDirectoryA(tmp, NULL);
|
|
*p = save;
|
|
}
|
|
}
|
|
return CreateDirectoryA(tmp, NULL) ||
|
|
GetLastError() == ERROR_ALREADY_EXISTS || path_is_dir(tmp);
|
|
}
|
|
#else
|
|
{
|
|
char* p = tmp;
|
|
if (*p == '/')
|
|
++p;
|
|
for (; *p; ++p) {
|
|
if (*p == '/') {
|
|
*p = '\0';
|
|
if (mkdir(tmp, 0755) != 0 && errno != EEXIST && !path_is_dir(tmp))
|
|
return 0;
|
|
*p = '/';
|
|
}
|
|
}
|
|
return mkdir(tmp, 0755) == 0 || errno == EEXIST || path_is_dir(tmp);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static int rmtree_path(const char* path) {
|
|
char cmd[1600];
|
|
if (!path || !path[0] || !path_is_dir(path))
|
|
return 1;
|
|
#if defined(_WIN32)
|
|
snprintf(cmd, sizeof(cmd), "cmd.exe /c rmdir /s /q \"%s\"", path);
|
|
#else
|
|
snprintf(cmd, sizeof(cmd), "rm -rf \"%s\"", path);
|
|
#endif
|
|
return system(cmd) == 0 || !path_is_dir(path);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
static int run_cmdline_wait(const char* cmdline, DWORD* out_code) {
|
|
STARTUPINFOA si;
|
|
PROCESS_INFORMATION pi;
|
|
char mutable_cmd[8192];
|
|
DWORD code = 1;
|
|
HANDLE hin, hout, herr;
|
|
memset(&si, 0, sizeof(si));
|
|
memset(&pi, 0, sizeof(pi));
|
|
si.cb = sizeof(si);
|
|
/* GUI launchers often have INVALID std handles. Passing those with
|
|
* STARTF_USESTDHANDLES makes console children (cmake/curl) fail even when
|
|
* the exe is fine — do not inherit broken handles. */
|
|
hin = GetStdHandle(STD_INPUT_HANDLE);
|
|
hout = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
herr = GetStdHandle(STD_ERROR_HANDLE);
|
|
if (hin != INVALID_HANDLE_VALUE && hin != NULL &&
|
|
hout != INVALID_HANDLE_VALUE && hout != NULL &&
|
|
herr != INVALID_HANDLE_VALUE && herr != NULL) {
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = hin;
|
|
si.hStdOutput = hout;
|
|
si.hStdError = herr;
|
|
}
|
|
snprintf(mutable_cmd, sizeof(mutable_cmd), "%s", cmdline);
|
|
if (!CreateProcessA(NULL, mutable_cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW,
|
|
NULL, NULL, &si, &pi))
|
|
return 0;
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
GetExitCodeProcess(pi.hProcess, &code);
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
if (out_code)
|
|
*out_code = code;
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
static int shared_toolchain_latest_dir(char* out, size_t cap) {
|
|
char cache[1400];
|
|
if (!preferred_toolchain_cache_root(cache, sizeof(cache)))
|
|
return 0;
|
|
return join_path(out, cap, cache, "latest");
|
|
}
|
|
|
|
/* True when root/bin/cmake(.exe) exists (no nested unwrap). */
|
|
static int pack_root_has_cmake_direct(const char* root) {
|
|
char bin[1400], cmake[1400];
|
|
if (!root || !root[0])
|
|
return 0;
|
|
if (!join_path(bin, sizeof(bin), root, "bin"))
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
return join_path(cmake, sizeof(cmake), bin, "cmake.exe") && path_is_file(cmake);
|
|
#else
|
|
return join_path(cmake, sizeof(cmake), bin, "cmake") && path_is_file(cmake);
|
|
#endif
|
|
}
|
|
|
|
/* Resolve pack root with bin/cmake — flat or single nested child (zip layout). */
|
|
static int unwrap_toolchain_pack_root(const char* in, char* out, size_t cap) {
|
|
char bin[1400];
|
|
if (!in || !in[0] || !out || cap < 2)
|
|
return 0;
|
|
if (pack_root_has_cmake_direct(in)) {
|
|
snprintf(out, cap, "%s", in);
|
|
return 1;
|
|
}
|
|
if (resolve_toolchain_bin_under(in, bin, sizeof(bin)) &&
|
|
dirname_copy(out, cap, bin))
|
|
return pack_root_has_cmake_direct(out);
|
|
return 0;
|
|
}
|
|
|
|
static int pack_root_has_cmake(const char* root) {
|
|
char unwrapped[1400];
|
|
return unwrap_toolchain_pack_root(root, unwrapped, sizeof(unwrapped));
|
|
}
|
|
|
|
/* Read retcomm-toolchain.json "version" (best-effort; empty if missing). */
|
|
static void read_toolchain_pack_version(const char* pack_root, char* out,
|
|
size_t cap) {
|
|
char path[1400], buf[2048];
|
|
FILE* f;
|
|
out[0] = '\0';
|
|
if (!pack_root || !pack_root[0] || !out || cap < 2)
|
|
return;
|
|
if (!join_path(path, sizeof(path), pack_root, "retcomm-toolchain.json"))
|
|
return;
|
|
f = fopen(path, "r");
|
|
if (!f)
|
|
return;
|
|
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
|
|
fclose(f);
|
|
buf[n] = '\0';
|
|
const char* p = strstr(buf, "\"version\"");
|
|
if (!p)
|
|
return;
|
|
p = strchr(p + 9, '"');
|
|
if (!p)
|
|
return;
|
|
++p;
|
|
const char* end = strchr(p, '"');
|
|
if (!end || end <= p)
|
|
return;
|
|
size_t len = (size_t)(end - p);
|
|
if (len >= cap)
|
|
len = cap - 1;
|
|
memcpy(out, p, len);
|
|
out[len] = '\0';
|
|
}
|
|
|
|
static void sanitize_toolchain_tag(const char* ver, char* out, size_t cap) {
|
|
size_t j = 0;
|
|
if (!out || cap < 2) return;
|
|
if (!ver || !ver[0]) {
|
|
snprintf(out, cap, "offline");
|
|
return;
|
|
}
|
|
for (size_t i = 0; ver[i] && j + 1 < cap; ++i) {
|
|
unsigned char c = (unsigned char)ver[i];
|
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-')
|
|
out[j++] = (char)c;
|
|
else
|
|
out[j++] = '_';
|
|
}
|
|
out[j] = '\0';
|
|
if (!out[0])
|
|
snprintf(out, cap, "offline");
|
|
}
|
|
|
|
/* Point cache_root/latest at pack_root (symlink preferred; copy fallback).
|
|
* Never install pack contents *into* latest/ — latest is pointer-only. */
|
|
static int set_toolchain_latest_pointer(const char* cache_root,
|
|
const char* pack_root) {
|
|
char latest[1400], resolved_pack[1400];
|
|
if (!cache_root || !pack_root || !pack_root[0])
|
|
return 0;
|
|
if (!unwrap_toolchain_pack_root(pack_root, resolved_pack,
|
|
sizeof(resolved_pack)))
|
|
return 0;
|
|
if (!join_path(latest, sizeof(latest), cache_root, "latest"))
|
|
return 0;
|
|
/* Already points at a usable pack (symlink or real dir with bin/). */
|
|
if (pack_root_has_cmake_direct(latest)) {
|
|
#if !defined(_WIN32)
|
|
char latest_res[1400], pack_res[1400];
|
|
if (realpath(latest, latest_res) && realpath(resolved_pack, pack_res) &&
|
|
strcmp(latest_res, pack_res) == 0)
|
|
return 1;
|
|
#endif
|
|
if (strcmp(latest, resolved_pack) == 0)
|
|
return 1;
|
|
}
|
|
rmtree_path(latest);
|
|
#if defined(_WIN32)
|
|
if (junction_dir(latest, resolved_pack))
|
|
return 1;
|
|
{
|
|
char cmd[3200];
|
|
DWORD code = 1;
|
|
mkdir_p(latest);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cmd.exe /c robocopy \"%s\" \"%s\" /E /NFL /NDL /NJH /NJS /nc "
|
|
"/ns /np",
|
|
resolved_pack, latest);
|
|
if (run_cmdline_wait(cmd, &code) && code <= 7 &&
|
|
pack_root_has_cmake_direct(latest))
|
|
return 1;
|
|
}
|
|
#else
|
|
if (symlink(resolved_pack, latest) == 0)
|
|
return 1;
|
|
{
|
|
char cmd[3200];
|
|
snprintf(cmd, sizeof(cmd), "cp -a \"%s\" \"%s\"", resolved_pack, latest);
|
|
if (system(cmd) == 0 && pack_root_has_cmake_direct(latest))
|
|
return 1;
|
|
}
|
|
#endif
|
|
return pack_root_has_cmake_direct(latest);
|
|
}
|
|
|
|
static int write_project_toolchain_stamp(const char* bin_dir) {
|
|
char tc_dir[1200], stamp[1200];
|
|
FILE* f;
|
|
if (!g_project_root[0] || !bin_dir || !bin_dir[0])
|
|
return 0;
|
|
if (!join_path(tc_dir, sizeof(tc_dir), g_project_root, "toolchain"))
|
|
return 0;
|
|
mkdir_p(tc_dir);
|
|
if (!join_path(stamp, sizeof(stamp), tc_dir, ".psxrecomp-bin"))
|
|
return 0;
|
|
f = fopen(stamp, "w");
|
|
if (!f)
|
|
return 0;
|
|
fprintf(f, "%s\n", bin_dir);
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
static int activate_installed_pack_root(const char* pack_root) {
|
|
char root[1400], bin[1400];
|
|
if (!unwrap_toolchain_pack_root(pack_root, root, sizeof(root)))
|
|
return 0;
|
|
if (!join_path(bin, sizeof(bin), root, "bin"))
|
|
return 0;
|
|
snprintf(g_cli_toolchain_bin, sizeof(g_cli_toolchain_bin), "%s", bin);
|
|
#if defined(_WIN32)
|
|
_putenv_s("RETCOMM_TOOLCHAIN_DIR", root);
|
|
_putenv_s("PSXRECOMP_TOOLCHAIN_DIR", root);
|
|
#else
|
|
setenv("RETCOMM_TOOLCHAIN_DIR", root, 1);
|
|
setenv("PSXRECOMP_TOOLCHAIN_DIR", root, 1);
|
|
#endif
|
|
write_project_toolchain_stamp(bin);
|
|
activate_toolchain_path();
|
|
/* File presence is not enough — cmake and clang/lld must actually run
|
|
* (missing DLLs / libicu / MOTW used to pass find_cmake then fail later). */
|
|
if (!find_cmake(g_cmake, sizeof(g_cmake)))
|
|
return 0;
|
|
if (!cmake_path_runs(g_cmake))
|
|
return 0;
|
|
return toolchain_bin_is_healthy(bin);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
/* Point a real LocalAppData path at a Store-Python LocalCache pack (no copy). */
|
|
static int junction_dir(const char* link_path, const char* target_path) {
|
|
char cmd[3200];
|
|
DWORD code = 1;
|
|
char parent[1400];
|
|
if (!link_path || !target_path || !path_is_dir(target_path))
|
|
return 0;
|
|
if (path_is_dir(link_path) || path_is_file(link_path)) {
|
|
/* Replace broken/empty dir; keep a usable pack. */
|
|
if (pack_root_has_cmake(link_path))
|
|
return 1;
|
|
rmtree_path(link_path);
|
|
}
|
|
if (!dirname_copy(parent, sizeof(parent), link_path))
|
|
return 0;
|
|
mkdir_p(parent);
|
|
snprintf(cmd, sizeof(cmd), "cmd.exe /c mklink /J \"%s\" \"%s\"", link_path,
|
|
target_path);
|
|
if (!run_cmdline_wait(cmd, &code))
|
|
return 0;
|
|
return code == 0 && path_is_dir(link_path);
|
|
}
|
|
|
|
static int find_store_localcache_pack_root(char* out, size_t cap) {
|
|
const char* local = getenv("LOCALAPPDATA");
|
|
char packages[1100], pattern[1200];
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h;
|
|
if (!local || !local[0])
|
|
return 0;
|
|
if (!join_path(packages, sizeof(packages), local, "Packages"))
|
|
return 0;
|
|
snprintf(pattern, sizeof(pattern),
|
|
"%s\\PythonSoftwareFoundation.Python.*", packages);
|
|
h = FindFirstFileA(pattern, &fd);
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return 0;
|
|
do {
|
|
const char* suffixes[] = {
|
|
"LocalCache\\Local\\retcomm\\toolchains\\cmake-clang-v1\\latest",
|
|
"LocalCache\\Local\\retcomm\\toolchains\\cmake-clang-v1\\offline",
|
|
"LocalCache\\Local\\psxrecomp\\toolchains\\cmake-clang-v1\\latest",
|
|
"LocalCache\\Local\\psxrecomp\\toolchains\\cmake-clang-v1\\offline",
|
|
NULL};
|
|
int i;
|
|
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
continue;
|
|
for (i = 0; suffixes[i]; ++i) {
|
|
char cand[1400], nested[1400];
|
|
snprintf(cand, sizeof(cand), "%s\\%s\\%s", packages, fd.cFileName,
|
|
suffixes[i]);
|
|
if (pack_root_has_cmake(cand)) {
|
|
snprintf(out, cap, "%s", cand);
|
|
FindClose(h);
|
|
return 1;
|
|
}
|
|
/* Nested single child with bin/cmake.exe */
|
|
if (path_is_dir(cand)) {
|
|
WIN32_FIND_DATAA kd;
|
|
char kp[1400];
|
|
HANDLE hk;
|
|
snprintf(kp, sizeof(kp), "%s\\*", cand);
|
|
hk = FindFirstFileA(kp, &kd);
|
|
if (hk == INVALID_HANDLE_VALUE)
|
|
continue;
|
|
do {
|
|
if (!(kd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
continue;
|
|
if (kd.cFileName[0] == '.')
|
|
continue;
|
|
if (!join_path(nested, sizeof(nested), cand, kd.cFileName))
|
|
continue;
|
|
if (pack_root_has_cmake(nested)) {
|
|
snprintf(out, cap, "%s", nested);
|
|
FindClose(hk);
|
|
FindClose(h);
|
|
return 1;
|
|
}
|
|
} while (FindNextFileA(hk, &kd));
|
|
FindClose(hk);
|
|
}
|
|
}
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
return 0;
|
|
}
|
|
|
|
/* allow_copy=0: point this process at LocalCache (cheap).
|
|
* allow_copy=1: also junction/robocopy into real %LOCALAPPDATA% (ensure). */
|
|
static int harvest_store_python_toolchain(int allow_copy) {
|
|
char cache_root[1400], real_latest[1400], proj_tc[1200];
|
|
if (!find_store_localcache_pack_root(cache_root, sizeof(cache_root)))
|
|
return 0;
|
|
if (!allow_copy)
|
|
return activate_installed_pack_root(cache_root);
|
|
if (!shared_toolchain_latest_dir(real_latest, sizeof(real_latest)))
|
|
return activate_installed_pack_root(cache_root);
|
|
if (!junction_dir(real_latest, cache_root) &&
|
|
!pack_root_has_cmake(real_latest)) {
|
|
/* Junction failed — robocopy into the real tree. */
|
|
char parent[1400], cmd[3200];
|
|
DWORD code = 1;
|
|
if (!dirname_copy(parent, sizeof(parent), real_latest))
|
|
return activate_installed_pack_root(cache_root);
|
|
mkdir_p(parent);
|
|
rmtree_path(real_latest);
|
|
mkdir_p(real_latest);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cmd.exe /c robocopy \"%s\" \"%s\" /E /NFL /NDL /NJH /NJS /nc "
|
|
"/ns /np",
|
|
cache_root, real_latest);
|
|
if (!run_cmdline_wait(cmd, &code) || code > 7 ||
|
|
!pack_root_has_cmake(real_latest))
|
|
return activate_installed_pack_root(cache_root);
|
|
}
|
|
if (g_project_root[0] &&
|
|
join_path(proj_tc, sizeof(proj_tc), g_project_root, "toolchain")) {
|
|
if (!pack_root_has_cmake(proj_tc))
|
|
junction_dir(proj_tc, pack_root_has_cmake(real_latest)
|
|
? real_latest
|
|
: cache_root);
|
|
}
|
|
return activate_installed_pack_root(
|
|
pack_root_has_cmake(real_latest) ? real_latest : cache_root);
|
|
}
|
|
|
|
static int host_download_url_to_file(const char* url, const char* dest,
|
|
char* err_msg, size_t err_cap) {
|
|
char cmd[4096];
|
|
DWORD code = 1;
|
|
char parent[1400];
|
|
if (!dirname_copy(parent, sizeof(parent), dest)) {
|
|
snprintf(err_msg, err_cap, "Bad download destination.");
|
|
return 0;
|
|
}
|
|
mkdir_p(parent);
|
|
DeleteFileA(dest);
|
|
/* Windows 10+ ships curl.exe. -L follows GitHub release redirects. */
|
|
snprintf(cmd, sizeof(cmd),
|
|
"curl.exe -fsSL --retry 3 --retry-delay 2 -o \"%s\" \"%s\"", dest,
|
|
url);
|
|
if (!run_cmdline_wait(cmd, &code) || code != 0) {
|
|
snprintf(err_msg, err_cap,
|
|
"Toolchain download failed (curl exit %lu). Check network / "
|
|
"curl.exe.",
|
|
(unsigned long)code);
|
|
return 0;
|
|
}
|
|
return path_is_file(dest);
|
|
}
|
|
|
|
static int host_extract_zip(const char* zip_path, const char* dest_dir,
|
|
char* err_msg, size_t err_cap) {
|
|
char cmd[3200];
|
|
DWORD code = 1;
|
|
char parent[1400];
|
|
if (!path_is_file(zip_path)) {
|
|
snprintf(err_msg, err_cap, "Toolchain zip not found: %s", zip_path);
|
|
return 0;
|
|
}
|
|
if (!dirname_copy(parent, sizeof(parent), dest_dir)) {
|
|
snprintf(err_msg, err_cap, "Bad extract destination.");
|
|
return 0;
|
|
}
|
|
mkdir_p(parent);
|
|
rmtree_path(dest_dir);
|
|
mkdir_p(dest_dir);
|
|
/* tar.exe on Windows 10+ extracts .zip */
|
|
snprintf(cmd, sizeof(cmd), "tar.exe -xf \"%s\" -C \"%s\"", zip_path,
|
|
dest_dir);
|
|
if (!run_cmdline_wait(cmd, &code) || code != 0) {
|
|
snprintf(err_msg, err_cap, "Failed to extract toolchain zip (tar exit %lu).",
|
|
(unsigned long)code);
|
|
return 0;
|
|
}
|
|
if (pack_root_has_cmake(dest_dir))
|
|
return 1;
|
|
/* Single nested directory layout — resolve_toolchain_bin_under handles it. */
|
|
{
|
|
WIN32_FIND_DATAA fd;
|
|
char pattern[1400], child[1400];
|
|
HANDLE h;
|
|
snprintf(pattern, sizeof(pattern), "%s\\*", dest_dir);
|
|
h = FindFirstFileA(pattern, &fd);
|
|
if (h != INVALID_HANDLE_VALUE) {
|
|
do {
|
|
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
continue;
|
|
if (fd.cFileName[0] == '.')
|
|
continue;
|
|
if (!join_path(child, sizeof(child), dest_dir, fd.cFileName))
|
|
continue;
|
|
if (pack_root_has_cmake(child)) {
|
|
FindClose(h);
|
|
return 1;
|
|
}
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
}
|
|
}
|
|
snprintf(err_msg, err_cap, "Toolchain zip missing bin/cmake.exe.");
|
|
return 0;
|
|
}
|
|
#else
|
|
static int host_download_url_to_file(const char* url, const char* dest,
|
|
char* err_msg, size_t err_cap) {
|
|
char cmd[4096];
|
|
char parent[1400];
|
|
if (!dirname_copy(parent, sizeof(parent), dest)) {
|
|
snprintf(err_msg, err_cap, "Bad download destination.");
|
|
return 0;
|
|
}
|
|
mkdir_p(parent);
|
|
unlink(dest);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"curl -fsSL --retry 3 --retry-delay 2 -o \"%s\" \"%s\"", dest, url);
|
|
if (system(cmd) != 0) {
|
|
snprintf(err_msg, err_cap, "Toolchain download failed (curl).");
|
|
return 0;
|
|
}
|
|
return path_is_file(dest);
|
|
}
|
|
|
|
static int host_extract_zip(const char* zip_path, const char* dest_dir,
|
|
char* err_msg, size_t err_cap) {
|
|
char cmd[3200];
|
|
char parent[1400];
|
|
if (!path_is_file(zip_path)) {
|
|
snprintf(err_msg, err_cap, "Toolchain zip not found: %s", zip_path);
|
|
return 0;
|
|
}
|
|
if (!dirname_copy(parent, sizeof(parent), dest_dir)) {
|
|
snprintf(err_msg, err_cap, "Bad extract destination.");
|
|
return 0;
|
|
}
|
|
mkdir_p(parent);
|
|
rmtree_path(dest_dir);
|
|
mkdir_p(dest_dir);
|
|
snprintf(cmd, sizeof(cmd), "unzip -q \"%s\" -d \"%s\"", zip_path, dest_dir);
|
|
if (system(cmd) != 0) {
|
|
snprintf(cmd, sizeof(cmd), "tar -xf \"%s\" -C \"%s\"", zip_path,
|
|
dest_dir);
|
|
if (system(cmd) != 0) {
|
|
snprintf(err_msg, err_cap, "Failed to extract toolchain zip.");
|
|
return 0;
|
|
}
|
|
}
|
|
if (pack_root_has_cmake(dest_dir))
|
|
return 1;
|
|
/* Nested child with bin/ is fine — resolve_toolchain_bin_under handles it. */
|
|
{
|
|
DIR* dir = opendir(dest_dir);
|
|
struct dirent* ent;
|
|
if (!dir) {
|
|
snprintf(err_msg, err_cap, "Toolchain zip missing bin/cmake.");
|
|
return 0;
|
|
}
|
|
while ((ent = readdir(dir)) != NULL) {
|
|
char child[1400];
|
|
if (ent->d_name[0] == '.')
|
|
continue;
|
|
if (!join_path(child, sizeof(child), dest_dir, ent->d_name))
|
|
continue;
|
|
if (path_is_dir(child) && pack_root_has_cmake(child)) {
|
|
closedir(dir);
|
|
return 1;
|
|
}
|
|
}
|
|
closedir(dir);
|
|
}
|
|
snprintf(err_msg, err_cap, "Toolchain zip missing bin/cmake.");
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
static int link_or_stamp_project_toolchain(const char* pack_root) {
|
|
char root[1400], proj_tc[1200], bin[1400];
|
|
if (!unwrap_toolchain_pack_root(pack_root, root, sizeof(root)))
|
|
return 0;
|
|
if (!join_path(bin, sizeof(bin), root, "bin"))
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
if (g_project_root[0] &&
|
|
join_path(proj_tc, sizeof(proj_tc), g_project_root, "toolchain")) {
|
|
if (!pack_root_has_cmake(proj_tc))
|
|
junction_dir(proj_tc, root);
|
|
}
|
|
#else
|
|
if (g_project_root[0] &&
|
|
join_path(proj_tc, sizeof(proj_tc), g_project_root, "toolchain")) {
|
|
if (!pack_root_has_cmake(proj_tc) && !path_is_dir(proj_tc)) {
|
|
char cmd[2800];
|
|
snprintf(cmd, sizeof(cmd), "ln -s \"%s\" \"%s\"", root, proj_tc);
|
|
(void)system(cmd);
|
|
}
|
|
}
|
|
#endif
|
|
write_project_toolchain_stamp(bin);
|
|
return 1;
|
|
}
|
|
|
|
/* True when *child* is *parent* or a path under *parent* (dir prefix). */
|
|
static int path_is_under_dir(const char* child, const char* parent) {
|
|
char ca[1400], pa[1400];
|
|
size_t n;
|
|
if (!child || !child[0] || !parent || !parent[0])
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
{
|
|
DWORD nc = GetFullPathNameA(child, (DWORD)sizeof(ca), ca, NULL);
|
|
DWORD np = GetFullPathNameA(parent, (DWORD)sizeof(pa), pa, NULL);
|
|
if (nc == 0 || nc >= (DWORD)sizeof(ca) || np == 0 ||
|
|
np >= (DWORD)sizeof(pa))
|
|
return 0;
|
|
}
|
|
#else
|
|
{
|
|
char* rc = realpath(child, NULL);
|
|
char* rp = realpath(parent, NULL);
|
|
if (!rc || !rp) {
|
|
free(rc);
|
|
free(rp);
|
|
return 0;
|
|
}
|
|
snprintf(ca, sizeof(ca), "%s", rc);
|
|
snprintf(pa, sizeof(pa), "%s", rp);
|
|
free(rc);
|
|
free(rp);
|
|
}
|
|
#endif
|
|
n = strlen(pa);
|
|
#if defined(_WIN32)
|
|
if (_strnicmp(ca, pa, (int)n) != 0)
|
|
return 0;
|
|
#else
|
|
if (strncmp(ca, pa, n) != 0)
|
|
return 0;
|
|
#endif
|
|
if (ca[n] == '\0')
|
|
return 1;
|
|
return ca[n] == '/' || ca[n] == '\\';
|
|
}
|
|
|
|
/* After a successful install into the managed cache, drop other versioned
|
|
* <tag>/ siblings (and *.broken quarantines). Keeps *keep_pack*, latest/, and
|
|
* dot-directories (staging). Does not touch RETCOMM_TOOLCHAIN_DIR overrides
|
|
* outside the preferred install root. */
|
|
static void prune_old_toolchain_tags(const char* keep_pack) {
|
|
char cache_root[1400];
|
|
int removed = 0;
|
|
if (!keep_pack || !keep_pack[0])
|
|
return;
|
|
if (!preferred_toolchain_cache_root(cache_root, sizeof(cache_root)))
|
|
return;
|
|
if (!path_is_dir(cache_root))
|
|
return;
|
|
if (!path_is_under_dir(keep_pack, cache_root) &&
|
|
!host_paths_same_file(keep_pack, cache_root))
|
|
return;
|
|
|
|
#if defined(_WIN32)
|
|
{
|
|
WIN32_FIND_DATAA fd;
|
|
HANDLE h;
|
|
char pattern[1400];
|
|
snprintf(pattern, sizeof(pattern), "%s\\*", cache_root);
|
|
h = FindFirstFileA(pattern, &fd);
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return;
|
|
do {
|
|
char child[1400];
|
|
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
|
|
continue;
|
|
if (fd.cFileName[0] == '.')
|
|
continue;
|
|
if (_stricmp(fd.cFileName, "latest") == 0)
|
|
continue;
|
|
if (!join_path(child, sizeof(child), cache_root, fd.cFileName))
|
|
continue;
|
|
if (path_is_under_dir(keep_pack, child) ||
|
|
host_paths_same_file(keep_pack, child) ||
|
|
path_is_under_dir(child, keep_pack))
|
|
continue;
|
|
if (rmtree_path(child))
|
|
removed = 1;
|
|
} while (FindNextFileA(h, &fd));
|
|
FindClose(h);
|
|
}
|
|
#else
|
|
{
|
|
DIR* d = opendir(cache_root);
|
|
struct dirent* ent;
|
|
if (!d)
|
|
return;
|
|
while ((ent = readdir(d)) != NULL) {
|
|
char child[1400];
|
|
struct stat st;
|
|
if (ent->d_name[0] == '.')
|
|
continue;
|
|
if (strcmp(ent->d_name, "latest") == 0)
|
|
continue;
|
|
if (!join_path(child, sizeof(child), cache_root, ent->d_name))
|
|
continue;
|
|
if (lstat(child, &st) != 0)
|
|
continue;
|
|
if (!S_ISDIR(st.st_mode) && !S_ISLNK(st.st_mode))
|
|
continue;
|
|
if (path_is_under_dir(keep_pack, child) ||
|
|
host_paths_same_file(keep_pack, child) ||
|
|
path_is_under_dir(child, keep_pack))
|
|
continue;
|
|
if (rmtree_path(child))
|
|
removed = 1;
|
|
}
|
|
closedir(d);
|
|
}
|
|
#endif
|
|
(void)removed;
|
|
}
|
|
|
|
static int host_install_toolchain_from_zip(
|
|
const char* zip_path, RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx, char* err_msg, size_t err_cap) {
|
|
/* Match retcomm-toolchains install.sh:
|
|
* …/cmake-clang-v1/<tag>/ (real pack with bin/)
|
|
* …/cmake-clang-v1/latest → <tag> (pointer only)
|
|
* Never extract the zip *into* latest/ — resolvers expect latest/bin. */
|
|
char cache_root[1400], staging[1400], pack[1400], ver[64], tag[80];
|
|
char dest[1400], latest[1400];
|
|
if (!preferred_toolchain_cache_root(cache_root, sizeof(cache_root))) {
|
|
snprintf(err_msg, err_cap, "Cannot resolve shared toolchain directory.");
|
|
return 0;
|
|
}
|
|
mkdir_p(cache_root);
|
|
if (!join_path(staging, sizeof(staging), cache_root, ".staging-host")) {
|
|
snprintf(err_msg, err_cap, "Staging path too long.");
|
|
return 0;
|
|
}
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.45f, "Extracting portable cmake/clang…");
|
|
if (!host_extract_zip(zip_path, staging, err_msg, err_cap))
|
|
return 0;
|
|
if (!unwrap_toolchain_pack_root(staging, pack, sizeof(pack))) {
|
|
snprintf(err_msg, err_cap, "Extracted toolchain but cmake was not found.");
|
|
rmtree_path(staging);
|
|
return 0;
|
|
}
|
|
read_toolchain_pack_version(pack, ver, sizeof(ver));
|
|
sanitize_toolchain_tag(ver[0] ? ver : "offline", tag, sizeof(tag));
|
|
/* Never use "latest" as a tag directory name — reserved for the pointer. */
|
|
if (strcmp(tag, "latest") == 0)
|
|
snprintf(tag, sizeof(tag), "offline");
|
|
if (!join_path(dest, sizeof(dest), cache_root, tag)) {
|
|
snprintf(err_msg, err_cap, "Install path too long.");
|
|
rmtree_path(staging);
|
|
return 0;
|
|
}
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.7f, "Installing into shared toolchain cache…");
|
|
rmtree_path(dest);
|
|
mkdir_p(cache_root);
|
|
/* Move usable pack to <tag>/ (flat bin/ at dest). */
|
|
if (strcmp(pack, staging) == 0) {
|
|
#if defined(_WIN32)
|
|
if (!MoveFileExA(staging, dest, MOVEFILE_COPY_ALLOWED |
|
|
MOVEFILE_REPLACE_EXISTING)) {
|
|
char cmd[3200];
|
|
DWORD code = 1;
|
|
mkdir_p(dest);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cmd.exe /c robocopy \"%s\" \"%s\" /E /NFL /NDL /NJH /NJS "
|
|
"/nc /ns /np /MOVE",
|
|
staging, dest);
|
|
(void)run_cmdline_wait(cmd, &code);
|
|
}
|
|
#else
|
|
if (rename(staging, dest) != 0) {
|
|
char cmd[3200];
|
|
snprintf(cmd, sizeof(cmd), "mv \"%s\" \"%s\"", staging, dest);
|
|
if (system(cmd) != 0) {
|
|
snprintf(cmd, sizeof(cmd), "cp -a \"%s\" \"%s\"", staging, dest);
|
|
(void)system(cmd);
|
|
rmtree_path(staging);
|
|
}
|
|
}
|
|
#endif
|
|
} else {
|
|
/* Nested zip: move the child with bin/ up to dest. */
|
|
#if defined(_WIN32)
|
|
if (!MoveFileExA(pack, dest, MOVEFILE_COPY_ALLOWED |
|
|
MOVEFILE_REPLACE_EXISTING)) {
|
|
char cmd[3200];
|
|
DWORD code = 1;
|
|
mkdir_p(dest);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cmd.exe /c robocopy \"%s\" \"%s\" /E /NFL /NDL /NJH /NJS "
|
|
"/nc /ns /np /MOVE",
|
|
pack, dest);
|
|
(void)run_cmdline_wait(cmd, &code);
|
|
}
|
|
#else
|
|
if (rename(pack, dest) != 0) {
|
|
char cmd[3200];
|
|
snprintf(cmd, sizeof(cmd), "mv \"%s\" \"%s\"", pack, dest);
|
|
if (system(cmd) != 0) {
|
|
snprintf(cmd, sizeof(cmd), "cp -a \"%s\" \"%s\"", pack, dest);
|
|
(void)system(cmd);
|
|
}
|
|
}
|
|
#endif
|
|
rmtree_path(staging);
|
|
}
|
|
if (!unwrap_toolchain_pack_root(dest, pack, sizeof(pack))) {
|
|
snprintf(err_msg, err_cap, "Installed toolchain missing bin/cmake.");
|
|
return 0;
|
|
}
|
|
if (!set_toolchain_latest_pointer(cache_root, pack)) {
|
|
/* Pointer is best-effort — pack at <tag>/ is still usable. */
|
|
}
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.85f, "Activating toolchain…");
|
|
link_or_stamp_project_toolchain(pack);
|
|
if (activate_installed_pack_root(pack) ||
|
|
(join_path(latest, sizeof(latest), cache_root, "latest") &&
|
|
activate_installed_pack_root(latest))) {
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.92f,
|
|
"Removing older toolchain installs…");
|
|
prune_old_toolchain_tags(pack);
|
|
return 1;
|
|
}
|
|
snprintf(err_msg, err_cap,
|
|
"Extracted toolchain but cmake.exe is missing or will not run "
|
|
"(bin\\cmake.exe --version failed).");
|
|
return 0;
|
|
}
|
|
|
|
static int host_download_and_install_toolchain(
|
|
RecompLauncherCPrepareProgressFn on_progress, void* progress_ctx,
|
|
char* err_msg, size_t err_cap) {
|
|
char url[512], zip_path[1400], tmp_dir[1100];
|
|
const char* asset = toolchain_zip_asset_name();
|
|
snprintf(url, sizeof(url),
|
|
"https://github.com/%s/releases/latest/download/%s", k_tc_repo,
|
|
asset);
|
|
#if defined(_WIN32)
|
|
{
|
|
char tmp[512];
|
|
DWORD n = GetTempPathA(sizeof(tmp), tmp);
|
|
if (n == 0 || n >= sizeof(tmp)) {
|
|
snprintf(err_msg, err_cap, "GetTempPath failed.");
|
|
return 0;
|
|
}
|
|
snprintf(tmp_dir, sizeof(tmp_dir), "%spsxrecomp-tc-%lu", tmp,
|
|
(unsigned long)GetCurrentProcessId());
|
|
}
|
|
#else
|
|
snprintf(tmp_dir, sizeof(tmp_dir), "/tmp/psxrecomp-tc-%d", (int)getpid());
|
|
#endif
|
|
mkdir_p(tmp_dir);
|
|
if (!join_path(zip_path, sizeof(zip_path), tmp_dir, asset)) {
|
|
snprintf(err_msg, err_cap, "Temp path too long.");
|
|
return 0;
|
|
}
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.1f, "Downloading portable cmake/clang…");
|
|
if (!host_download_url_to_file(url, zip_path, err_msg, err_cap)) {
|
|
rmtree_path(tmp_dir);
|
|
return 0;
|
|
}
|
|
if (!host_install_toolchain_from_zip(zip_path, on_progress, progress_ctx,
|
|
err_msg, err_cap)) {
|
|
rmtree_path(tmp_dir);
|
|
return 0;
|
|
}
|
|
rmtree_path(tmp_dir);
|
|
return 1;
|
|
}
|
|
|
|
/* Promote a usable legacy psxrecomp cache into the shared retcomm tree. */
|
|
static int migrate_legacy_psxrecomp_toolchain(void) {
|
|
char legacy[1400], dest[1400], parent[1400];
|
|
#if defined(_WIN32)
|
|
const char* local = getenv("LOCALAPPDATA");
|
|
if (!local || !local[0])
|
|
return 0;
|
|
if (!join_path(legacy, sizeof(legacy), local,
|
|
"psxrecomp/toolchains/cmake-clang-v1/latest"))
|
|
return 0;
|
|
#else
|
|
const char* xdg = getenv("XDG_DATA_HOME");
|
|
const char* home = getenv("HOME");
|
|
if (xdg && xdg[0]) {
|
|
if (!join_path(legacy, sizeof(legacy), xdg,
|
|
"psxrecomp/toolchains/cmake-clang-v1/latest"))
|
|
return 0;
|
|
} else if (home && home[0]) {
|
|
if (!join_path(legacy, sizeof(legacy), home,
|
|
".local/share/psxrecomp/toolchains/cmake-clang-v1/latest"))
|
|
return 0;
|
|
} else {
|
|
return 0;
|
|
}
|
|
#endif
|
|
if (!pack_root_has_cmake(legacy))
|
|
return 0;
|
|
if (!shared_toolchain_latest_dir(dest, sizeof(dest)))
|
|
return 0;
|
|
if (pack_root_has_cmake(dest))
|
|
return 1;
|
|
if (!dirname_copy(parent, sizeof(parent), dest))
|
|
return 0;
|
|
mkdir_p(parent);
|
|
#if defined(_WIN32)
|
|
if (junction_dir(dest, legacy))
|
|
return 1;
|
|
{
|
|
char cmd[3200];
|
|
DWORD code = 1;
|
|
mkdir_p(dest);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cmd.exe /c robocopy \"%s\" \"%s\" /E /NFL /NDL /NJH /NJS /nc "
|
|
"/ns /np",
|
|
legacy, dest);
|
|
if (run_cmdline_wait(cmd, &code) && code <= 7 && pack_root_has_cmake(dest))
|
|
return 1;
|
|
}
|
|
#else
|
|
{
|
|
char cmd[2800];
|
|
snprintf(cmd, sizeof(cmd), "ln -sfn \"%s\" \"%s\"", legacy, dest);
|
|
if (system(cmd) == 0 && pack_root_has_cmake(dest))
|
|
return 1;
|
|
snprintf(cmd, sizeof(cmd), "cp -a \"%s\" \"%s\"", legacy, dest);
|
|
if (system(cmd) == 0 && pack_root_has_cmake(dest))
|
|
return 1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static int pack_root_from_bin(const char* bin, char* out, size_t cap) {
|
|
return bin && bin[0] && dirname_copy(out, cap, bin);
|
|
}
|
|
|
|
static int run_cmd_exit_zero(const char* cmdline) {
|
|
#if defined(_WIN32)
|
|
DWORD code = 1;
|
|
if (!run_cmdline_wait(cmdline, &code))
|
|
return 0;
|
|
return code == 0;
|
|
#else
|
|
int st = system(cmdline);
|
|
return st == 0;
|
|
#endif
|
|
}
|
|
|
|
/* True when cmake_path exists and ``cmake --version`` succeeds. */
|
|
static int cmake_path_runs(const char* cmake_path) {
|
|
char cmd[1600];
|
|
if (!cmake_path || !cmake_path[0] || !path_is_file(cmake_path))
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
/* CreateProcess does not honor >NUL — use cmd /C. Nested quotes are the
|
|
* documented form for a quoted exe path under cmd /C. */
|
|
snprintf(cmd, sizeof(cmd), "cmd.exe /C \"\"%s\" --version >NUL 2>&1\"",
|
|
cmake_path);
|
|
#else
|
|
snprintf(cmd, sizeof(cmd), "\"%s\" --version >/dev/null 2>&1", cmake_path);
|
|
#endif
|
|
return run_cmd_exit_zero(cmd);
|
|
}
|
|
|
|
#if !defined(_WIN32)
|
|
/* Native release kits probe host tools; setup never installs system packages. */
|
|
static int posix_command_runs(const char* command) {
|
|
char cmd[1600];
|
|
if (!command || !command[0])
|
|
return 0;
|
|
snprintf(cmd, sizeof(cmd), "\"%s\" --version >/dev/null 2>&1", command);
|
|
return run_cmd_exit_zero(cmd);
|
|
}
|
|
|
|
static int host_system_toolchain_ready(void) {
|
|
char python[1200], tool[1200];
|
|
#if defined(__APPLE__)
|
|
/* Avoid xcrun prompting to install tools during a readiness check. */
|
|
if (!run_cmd_exit_zero("xcode-select -p >/dev/null 2>&1"))
|
|
return 0;
|
|
#endif
|
|
if (!find_on_path("cmake", g_cmake, sizeof(g_cmake)) ||
|
|
!posix_command_runs(g_cmake))
|
|
return 0;
|
|
if (!(find_on_path("python3", python, sizeof(python)) ||
|
|
find_on_path("python", python, sizeof(python))) ||
|
|
!posix_command_runs(python))
|
|
return 0;
|
|
if (!find_on_path("ninja", tool, sizeof(tool)) || !posix_command_runs(tool))
|
|
return 0;
|
|
if (!(find_on_path("cc", tool, sizeof(tool)) ||
|
|
find_on_path("gcc", tool, sizeof(tool)) ||
|
|
find_on_path("clang", tool, sizeof(tool))) || !posix_command_runs(tool))
|
|
return 0;
|
|
if (!(find_on_path("c++", tool, sizeof(tool)) ||
|
|
find_on_path("g++", tool, sizeof(tool)) ||
|
|
find_on_path("clang++", tool, sizeof(tool))) || !posix_command_runs(tool))
|
|
return 0;
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
/* True when pack bin/ can compile+link a tiny C program (catches broken
|
|
* ld.lld / missing libicuuc.so.* etc. that cmake --version still passes). */
|
|
static int toolchain_bin_compiler_works(const char* bin) {
|
|
char clang[1200], lld[1200], src[1400], exe[1400], cmd[4096];
|
|
FILE* f;
|
|
int ok;
|
|
if (!bin || !bin[0])
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
if (!join_path(clang, sizeof(clang), bin, "clang.exe") || !path_is_file(clang))
|
|
return 0;
|
|
if (!join_path(lld, sizeof(lld), bin, "ld.lld.exe") || !path_is_file(lld)) {
|
|
/* Some Windows packs only ship lld.exe — accept either. */
|
|
if (!join_path(lld, sizeof(lld), bin, "lld.exe") || !path_is_file(lld))
|
|
return 0;
|
|
}
|
|
#else
|
|
if (!join_path(clang, sizeof(clang), bin, "clang") || !path_is_file(clang)) {
|
|
# if defined(__APPLE__)
|
|
/* The macOS pack (cmake-clang-v1-macos-universal) ships CMake, Ninja,
|
|
* ccache and Python only; compiling uses the system toolchain from
|
|
* Apple's Command Line Tools, checked by macos_system_compiler_works.
|
|
* A pack without clang is the normal macOS layout, not a broken one. */
|
|
return 1;
|
|
# else
|
|
return 0;
|
|
# endif
|
|
}
|
|
if (!join_path(lld, sizeof(lld), bin, "ld.lld") || !path_is_file(lld))
|
|
return 0;
|
|
#endif
|
|
if (!cmake_path_runs(clang))
|
|
return 0;
|
|
|
|
#if defined(_WIN32)
|
|
{
|
|
char tdir[512];
|
|
DWORD tn = GetTempPathA(sizeof(tdir), tdir);
|
|
if (tn == 0 || tn >= sizeof(tdir))
|
|
return 0;
|
|
snprintf(src, sizeof(src), "%spsxrecomp-tc-probe-%lu.c", tdir,
|
|
(unsigned long)GetCurrentProcessId());
|
|
snprintf(exe, sizeof(exe), "%spsxrecomp-tc-probe-%lu.exe", tdir,
|
|
(unsigned long)GetCurrentProcessId());
|
|
}
|
|
#else
|
|
snprintf(src, sizeof(src), "/tmp/psxrecomp-tc-probe-%d.c", (int)getpid());
|
|
snprintf(exe, sizeof(exe), "/tmp/psxrecomp-tc-probe-%d", (int)getpid());
|
|
#endif
|
|
|
|
f = fopen(src, "wb");
|
|
if (!f)
|
|
return 0;
|
|
fputs("int main(void){return 0;}\n", f);
|
|
fclose(f);
|
|
|
|
#if defined(_WIN32)
|
|
/* Prepend pack bin so clang picks this tree's lld, not a system linker. */
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cmd.exe /C \"set \"PATH=%s;%%PATH%%\" && \"%s\" \"%s\" -o \"%s\" "
|
|
">NUL 2>&1\"",
|
|
bin, clang, src, exe);
|
|
#else
|
|
/* Prefer the pack linker explicitly so PATH cannot hide a broken lld. */
|
|
(void)lld; /* used via -fuse-ld when present; path already validated */
|
|
snprintf(cmd, sizeof(cmd),
|
|
"env PATH=\"%s:$PATH\" \"%s\" -fuse-ld=lld \"%s\" -o \"%s\" "
|
|
">/dev/null 2>&1",
|
|
bin, clang, src, exe);
|
|
#endif
|
|
ok = run_cmd_exit_zero(cmd);
|
|
#if defined(_WIN32)
|
|
DeleteFileA(src);
|
|
DeleteFileA(exe);
|
|
#else
|
|
unlink(src);
|
|
unlink(exe);
|
|
#endif
|
|
return ok;
|
|
}
|
|
|
|
#if defined(__APPLE__)
|
|
/* True when Apple's Command Line Tools are installed and /usr/bin/clang can
|
|
* compile and link a tiny program. xcode-select runs first so a machine
|
|
* without the CLT gets a repair note instead of the xcrun install prompt on
|
|
* every wizard refresh. */
|
|
static int macos_system_compiler_works(void) {
|
|
char src[256], exe[256], cmd[1024];
|
|
FILE* f;
|
|
int ok;
|
|
if (!run_cmd_exit_zero("xcode-select -p >/dev/null 2>&1"))
|
|
return 0;
|
|
snprintf(src, sizeof(src), "/tmp/psxrecomp-cc-probe-%d.c", (int)getpid());
|
|
snprintf(exe, sizeof(exe), "/tmp/psxrecomp-cc-probe-%d", (int)getpid());
|
|
f = fopen(src, "wb");
|
|
if (!f)
|
|
return 0;
|
|
fputs("int main(void){return 0;}\n", f);
|
|
fclose(f);
|
|
snprintf(cmd, sizeof(cmd), "/usr/bin/clang \"%s\" -o \"%s\" >/dev/null 2>&1",
|
|
src, exe);
|
|
ok = run_cmd_exit_zero(cmd);
|
|
unlink(src);
|
|
unlink(exe);
|
|
return ok;
|
|
}
|
|
#endif
|
|
|
|
/* Pack health says nothing about the macOS system compiler (see above), so
|
|
* readiness checks it separately and explains how to fix it. The pack itself
|
|
* is left alone: a missing compiler must never delete a good download. */
|
|
static int host_system_compiler_ready(void) {
|
|
#if defined(__APPLE__)
|
|
if (macos_system_compiler_works())
|
|
return 1;
|
|
snprintf(g_tc_repair_note, sizeof(g_tc_repair_note),
|
|
"Apple's Command Line Tools are required to build the game. "
|
|
"Open Terminal, run: xcode-select --install then reopen this "
|
|
"app.");
|
|
return 0;
|
|
#else
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
static int toolchain_bin_is_healthy(const char* bin) {
|
|
char cmake[1200];
|
|
if (!bin || !bin[0])
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
if (!join_path(cmake, sizeof(cmake), bin, "cmake.exe") ||
|
|
!cmake_path_runs(cmake))
|
|
return 0;
|
|
#else
|
|
if (!join_path(cmake, sizeof(cmake), bin, "cmake") || !cmake_path_runs(cmake))
|
|
return 0;
|
|
#endif
|
|
return toolchain_bin_compiler_works(bin);
|
|
}
|
|
|
|
static void clear_project_toolchain_stamp(void) {
|
|
char stamp[1200];
|
|
if (!g_project_root[0])
|
|
return;
|
|
if (!join_path(stamp, sizeof(stamp), g_project_root,
|
|
"toolchain/.psxrecomp-bin"))
|
|
return;
|
|
#if defined(_WIN32)
|
|
DeleteFileA(stamp);
|
|
#else
|
|
unlink(stamp);
|
|
#endif
|
|
}
|
|
|
|
/* True when path is …/latest or …/latest/bin (host unpack / pointer). */
|
|
static int path_is_toolchain_latest_leaf(const char* path) {
|
|
const char* base;
|
|
size_t n;
|
|
if (!path || !path[0])
|
|
return 0;
|
|
n = strlen(path);
|
|
while (n > 1 && (path[n - 1] == '/' || path[n - 1] == '\\'))
|
|
--n;
|
|
base = path;
|
|
for (size_t i = 0; i < n; ++i) {
|
|
if (path[i] == '/' || path[i] == '\\')
|
|
base = path + i + 1;
|
|
}
|
|
if (strncmp(base, "latest", 6) == 0 &&
|
|
(base[6] == '\0' || base[6] == '/' || base[6] == '\\'))
|
|
return 1;
|
|
if (strcmp(base, "bin") != 0)
|
|
return 0;
|
|
/* parent directory name == latest */
|
|
{
|
|
const char* p = base;
|
|
while (p > path && p[-1] != '/' && p[-1] != '\\')
|
|
--p;
|
|
if (p <= path)
|
|
return 0;
|
|
--p;
|
|
while (p > path && p[-1] != '/' && p[-1] != '\\')
|
|
--p;
|
|
return strncmp(p, "latest", 6) == 0 &&
|
|
(p[6] == '/' || p[6] == '\\' || p[6] == '\0');
|
|
}
|
|
}
|
|
|
|
/* Remove unusable latest/ under shared cache roots (heal before re-download).
|
|
* Also rejects packs where cmake runs but clang/ld.lld cannot link (Linux ICU
|
|
* / missing libxml2). Versioned <tag>/ siblings are pruned after a successful
|
|
* install/update (see prune_old_toolchain_tags). */
|
|
static void heal_broken_toolchain_pointers(void) {
|
|
char bases[12][1400];
|
|
int n = collect_toolchain_cache_bases(bases, 12);
|
|
int removed = 0;
|
|
for (int i = 0; i < n; ++i) {
|
|
char latest[1400], root[1400], bin[1400];
|
|
int present = 0;
|
|
if (!join_path(latest, sizeof(latest), bases[i], "latest"))
|
|
continue;
|
|
#if defined(_WIN32)
|
|
present = path_is_dir(latest) || path_is_file(latest);
|
|
#else
|
|
{
|
|
struct stat st;
|
|
present = (lstat(latest, &st) == 0);
|
|
}
|
|
#endif
|
|
if (!present)
|
|
continue;
|
|
if (unwrap_toolchain_pack_root(latest, root, sizeof(root)) &&
|
|
join_path(bin, sizeof(bin), root, "bin") &&
|
|
toolchain_bin_is_healthy(bin))
|
|
continue;
|
|
rmtree_path(latest);
|
|
#if !defined(_WIN32)
|
|
unlink(latest); /* dangling symlink after rmtree no-op */
|
|
#endif
|
|
removed = 1;
|
|
}
|
|
if (removed) {
|
|
clear_project_toolchain_stamp();
|
|
g_toolchain_bin[0] = '\0';
|
|
g_cli_toolchain_bin[0] = '\0';
|
|
g_cmake[0] = '\0';
|
|
snprintf(g_tc_repair_note, sizeof(g_tc_repair_note),
|
|
"Removed a broken portable toolchain cache (compiler/linker "
|
|
"failed a smoke test). Download the latest pack to continue.");
|
|
}
|
|
}
|
|
|
|
/* If the active resolution points at an unhealthy pack, drop stamp / latest
|
|
* (or quarantine a versioned <tag>/ so resolve cannot keep re-selecting it). */
|
|
static void discard_unhealthy_active_toolchain(void) {
|
|
char pack[1400];
|
|
if (!g_toolchain_bin[0])
|
|
return;
|
|
if (toolchain_bin_is_healthy(g_toolchain_bin))
|
|
return;
|
|
if (pack_root_from_bin(g_toolchain_bin, pack, sizeof(pack))) {
|
|
if (path_is_toolchain_latest_leaf(pack) ||
|
|
path_is_toolchain_latest_leaf(g_toolchain_bin)) {
|
|
rmtree_path(pack);
|
|
#if !defined(_WIN32)
|
|
unlink(pack);
|
|
#endif
|
|
} else if (path_is_dir(pack)) {
|
|
char quarantine[1500];
|
|
snprintf(quarantine, sizeof(quarantine), "%s.broken", pack);
|
|
rmtree_path(quarantine);
|
|
#if defined(_WIN32)
|
|
if (!MoveFileExA(pack, quarantine, MOVEFILE_REPLACE_EXISTING))
|
|
rmtree_path(pack);
|
|
#else
|
|
if (rename(pack, quarantine) != 0)
|
|
rmtree_path(pack);
|
|
#endif
|
|
}
|
|
}
|
|
clear_project_toolchain_stamp();
|
|
g_toolchain_bin[0] = '\0';
|
|
g_cli_toolchain_bin[0] = '\0';
|
|
g_cmake[0] = '\0';
|
|
if (!g_tc_repair_note[0]) {
|
|
snprintf(g_tc_repair_note, sizeof(g_tc_repair_note),
|
|
"Portable toolchain is installed but cannot compile/link "
|
|
"(path or library error). Redownload the latest pack.");
|
|
}
|
|
}
|
|
|
|
static int active_toolchain_meets_min(void) {
|
|
char pack[1400];
|
|
if (!g_toolchain_bin[0] && !resolve_toolchain_bin(g_toolchain_bin,
|
|
sizeof(g_toolchain_bin)))
|
|
return 0;
|
|
if (!pack_root_from_bin(g_toolchain_bin, pack, sizeof(pack)))
|
|
return 0;
|
|
return pack_meets_min_version(pack);
|
|
}
|
|
|
|
static int host_portable_cmake_ready(void) {
|
|
if (!find_cmake(g_cmake, sizeof(g_cmake)))
|
|
return 0;
|
|
if (!cmake_path_runs(g_cmake))
|
|
return 0;
|
|
if (!active_toolchain_meets_min())
|
|
return 0;
|
|
if (!g_toolchain_bin[0] &&
|
|
!resolve_toolchain_bin(g_toolchain_bin, sizeof(g_toolchain_bin)))
|
|
return 0;
|
|
return toolchain_bin_is_healthy(g_toolchain_bin);
|
|
}
|
|
|
|
static const char* host_toolchain_repair_note(void) {
|
|
return g_tc_repair_note[0] ? g_tc_repair_note : NULL;
|
|
}
|
|
|
|
static int host_toolchain_is_ready(void) {
|
|
int attempt;
|
|
if (!g_project_root[0])
|
|
return 0;
|
|
g_tc_repair_note[0] = '\0';
|
|
#if !defined(_WIN32)
|
|
return host_system_toolchain_ready();
|
|
#endif
|
|
migrate_legacy_psxrecomp_toolchain();
|
|
/* Wizard open: drop broken latest/ before treating the pack as ready. */
|
|
heal_broken_toolchain_pointers();
|
|
for (attempt = 0; attempt < 3; ++attempt) {
|
|
activate_toolchain_path();
|
|
if (host_portable_cmake_ready())
|
|
return host_system_compiler_ready();
|
|
if (!g_toolchain_bin[0])
|
|
break;
|
|
discard_unhealthy_active_toolchain();
|
|
}
|
|
#if defined(_WIN32)
|
|
/* Reuse a Store-Python LocalCache install without copying. */
|
|
if (harvest_store_python_toolchain(0)) {
|
|
char pack[1400];
|
|
if (pack_root_from_bin(g_cli_toolchain_bin[0] ? g_cli_toolchain_bin
|
|
: g_toolchain_bin,
|
|
pack, sizeof(pack)) &&
|
|
pack_root_has_cmake(pack) && pack_meets_min_version(pack) &&
|
|
host_portable_cmake_ready())
|
|
return 1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
/* Fill *out with the installed pack's version string (retcomm-toolchain.json). */
|
|
static int host_local_toolchain_version(char* out, size_t cap) {
|
|
char pack[1400];
|
|
if (!out || cap == 0)
|
|
return 0;
|
|
out[0] = '\0';
|
|
activate_toolchain_path();
|
|
if (!g_toolchain_bin[0] &&
|
|
!resolve_toolchain_bin(g_toolchain_bin, sizeof(g_toolchain_bin)))
|
|
return 0;
|
|
if (!pack_root_from_bin(g_toolchain_bin, pack, sizeof(pack)))
|
|
return 0;
|
|
return read_pack_version(pack, out, cap);
|
|
}
|
|
|
|
/* Parse "tag_name":"…" or a …/releases/tag/<ver> URL into *out. */
|
|
static int parse_github_release_tag(const char* text, char* out, size_t cap) {
|
|
const char* p;
|
|
size_t i;
|
|
if (!text || !out || cap < 2)
|
|
return 0;
|
|
out[0] = '\0';
|
|
p = strstr(text, "\"tag_name\"");
|
|
if (p) {
|
|
p = strchr(p + 10, '"');
|
|
if (!p)
|
|
return 0;
|
|
++p;
|
|
i = 0;
|
|
while (*p && *p != '"' && i + 1 < cap)
|
|
out[i++] = *p++;
|
|
out[i] = '\0';
|
|
return i > 0;
|
|
}
|
|
p = strstr(text, "/releases/tag/");
|
|
if (!p)
|
|
return 0;
|
|
p += strlen("/releases/tag/");
|
|
i = 0;
|
|
while (*p && *p != '"' && *p != '\'' && *p != ' ' && *p != '\n' &&
|
|
*p != '\r' && *p != '?' && *p != '#' && i + 1 < cap)
|
|
out[i++] = *p++;
|
|
out[i] = '\0';
|
|
return i > 0;
|
|
}
|
|
|
|
/* Query GitHub for the latest cmake-clang-v1 release tag (short timeout). */
|
|
static int host_remote_toolchain_version(char* out, size_t cap) {
|
|
char url[320], tmp[1400], buf[8192];
|
|
FILE* f;
|
|
size_t n;
|
|
char err[256];
|
|
if (!out || cap < 2)
|
|
return 0;
|
|
out[0] = '\0';
|
|
snprintf(url, sizeof(url),
|
|
"https://api.github.com/repos/%s/releases/latest", k_tc_repo);
|
|
#if defined(_WIN32)
|
|
{
|
|
char tdir[512];
|
|
DWORD tn = GetTempPathA(sizeof(tdir), tdir);
|
|
if (tn == 0 || tn >= sizeof(tdir))
|
|
return 0;
|
|
snprintf(tmp, sizeof(tmp), "%spsxrecomp-tc-latest-%lu.json", tdir,
|
|
(unsigned long)GetCurrentProcessId());
|
|
}
|
|
#else
|
|
snprintf(tmp, sizeof(tmp), "/tmp/psxrecomp-tc-latest-%d.json", (int)getpid());
|
|
#endif
|
|
/* GitHub API wants a UA; keep the request short so wizard open stays snappy. */
|
|
#if defined(_WIN32)
|
|
{
|
|
char cmd[4096];
|
|
DWORD code = 1;
|
|
DeleteFileA(tmp);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"curl.exe -fsSL --connect-timeout 5 --max-time 15 "
|
|
"-A psxrecomp-codegen -H \"Accept: application/vnd.github+json\" "
|
|
"-o \"%s\" \"%s\"",
|
|
tmp, url);
|
|
if (!run_cmdline_wait(cmd, &code) || code != 0 || !path_is_file(tmp)) {
|
|
/* Fallback via cmd so stdout redirect works under CreateProcess. */
|
|
DeleteFileA(tmp);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"cmd.exe /C \"curl.exe -fsSIL --connect-timeout 5 "
|
|
"--max-time 15 -A psxrecomp-codegen -o NUL "
|
|
"-w %%{url_effective} "
|
|
"https://github.com/%s/releases/latest > \"%s\"\"",
|
|
k_tc_repo, tmp);
|
|
if (!run_cmdline_wait(cmd, &code) || code != 0 || !path_is_file(tmp))
|
|
return 0;
|
|
}
|
|
}
|
|
#else
|
|
{
|
|
char cmd[4096];
|
|
unlink(tmp);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"curl -fsSL --connect-timeout 5 --max-time 15 "
|
|
"-A psxrecomp-codegen -H 'Accept: application/vnd.github+json' "
|
|
"-o '%s' '%s'",
|
|
tmp, url);
|
|
if (system(cmd) != 0 || !path_is_file(tmp)) {
|
|
unlink(tmp);
|
|
snprintf(cmd, sizeof(cmd),
|
|
"curl -fsSIL --connect-timeout 5 --max-time 15 "
|
|
"-A psxrecomp-codegen -o /dev/null -w '%%{url_effective}' "
|
|
"'https://github.com/%s/releases/latest' > '%s'",
|
|
k_tc_repo, tmp);
|
|
if (system(cmd) != 0 || !path_is_file(tmp))
|
|
return 0;
|
|
}
|
|
}
|
|
#endif
|
|
(void)err;
|
|
f = fopen(tmp, "rb");
|
|
if (!f) {
|
|
#if defined(_WIN32)
|
|
DeleteFileA(tmp);
|
|
#else
|
|
unlink(tmp);
|
|
#endif
|
|
return 0;
|
|
}
|
|
n = fread(buf, 1, sizeof(buf) - 1, f);
|
|
fclose(f);
|
|
#if defined(_WIN32)
|
|
DeleteFileA(tmp);
|
|
#else
|
|
unlink(tmp);
|
|
#endif
|
|
buf[n] = '\0';
|
|
return parse_github_release_tag(buf, out, cap);
|
|
}
|
|
|
|
/* 1 = installed pack is older than GitHub /releases/latest (prompt to update).
|
|
* 0 = up to date, not installed, offline, or RETCOMM_TOOLCHAIN_SKIP_UPDATE.
|
|
* Always fills local/remote when discoverable. */
|
|
static int host_toolchain_update_available(char* local_ver, size_t local_cap,
|
|
char* remote_ver, size_t remote_cap) {
|
|
const char* skip = getenv("RETCOMM_TOOLCHAIN_SKIP_UPDATE");
|
|
if (local_ver && local_cap)
|
|
local_ver[0] = '\0';
|
|
if (remote_ver && remote_cap)
|
|
remote_ver[0] = '\0';
|
|
#if !defined(_WIN32)
|
|
return 0;
|
|
#endif
|
|
if (skip && skip[0] && skip[0] != '0')
|
|
return 0;
|
|
if (!g_project_root[0])
|
|
return 0;
|
|
migrate_legacy_psxrecomp_toolchain();
|
|
(void)host_local_toolchain_version(local_ver, local_cap);
|
|
if (!host_remote_toolchain_version(remote_ver, remote_cap))
|
|
return 0;
|
|
if (!local_ver || !local_ver[0])
|
|
return 0; /* missing install → page 0 is "install", not "update" */
|
|
if (!remote_ver || !remote_ver[0])
|
|
return 0;
|
|
return version_cmp(local_ver, remote_ver) < 0;
|
|
}
|
|
|
|
/* Download or offline-install cmake-clang-v1 (wizard page 0 / rebuild fallback).
|
|
* Prefer host-native curl/tar so Microsoft Store Python cannot redirect the
|
|
* unpack into Packages\\...\\LocalCache. Installs into the shared Retro
|
|
* cache: %LOCALAPPDATA%/retcomm/toolchains/cmake-clang-v1/…
|
|
* Broken latest/ stamps are healed, then GitHub /releases/latest is fetched.
|
|
*
|
|
* download: 0 = zip/cache only, 1 = download if missing, 2 = force latest. */
|
|
static int host_ensure_toolchain_with_progress(
|
|
int download, const char* zip_path, char* err_msg, size_t err_cap,
|
|
RecompLauncherCPrepareProgressFn on_progress, void* progress_ctx) {
|
|
const int force = (download == 2);
|
|
if (!g_project_root[0]) {
|
|
snprintf(err_msg, err_cap, "Project root is not available.");
|
|
return 0;
|
|
}
|
|
#if !defined(_WIN32)
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.02f, "Checking native build tools…");
|
|
if (host_system_toolchain_ready())
|
|
return 1;
|
|
snprintf(err_msg, err_cap,
|
|
"Native build tools are missing. Install CMake, Ninja, Python 3, "
|
|
"and C/C++ compilers with your package manager, then check tools again.");
|
|
return 0;
|
|
#endif
|
|
migrate_legacy_psxrecomp_toolchain();
|
|
activate_toolchain_path();
|
|
if (!force && host_portable_cmake_ready())
|
|
return 1;
|
|
|
|
#if defined(_WIN32)
|
|
if (!force) {
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.02f,
|
|
"Checking for an existing portable toolchain…");
|
|
if (harvest_store_python_toolchain(1)) {
|
|
activate_toolchain_path();
|
|
if (host_portable_cmake_ready())
|
|
return 1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.03f,
|
|
force ? "Preparing toolchain update…"
|
|
: "Repairing broken toolchain cache if needed…");
|
|
heal_broken_toolchain_pointers();
|
|
clear_project_toolchain_stamp();
|
|
g_toolchain_bin[0] = '\0';
|
|
g_cli_toolchain_bin[0] = '\0';
|
|
g_cmake[0] = '\0';
|
|
activate_toolchain_path();
|
|
if (!force && host_portable_cmake_ready())
|
|
return 1;
|
|
|
|
if (zip_path && zip_path[0]) {
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.05f, "Installing toolchain from zip…");
|
|
if (host_install_toolchain_from_zip(zip_path, on_progress, progress_ctx,
|
|
err_msg, err_cap)) {
|
|
if (host_portable_cmake_ready())
|
|
return 1;
|
|
snprintf(err_msg, err_cap,
|
|
"Toolchain zip installed but cmake will not run"
|
|
"%s. Try Unblock on the zip, or set "
|
|
"RETCOMM_TOOLCHAIN_DIR to a pack whose bin\\cmake.exe "
|
|
"passes --version.",
|
|
(toolchain_min_version()[0]
|
|
? " (or is below RETCOMM_TOOLCHAIN_MIN_VERSION)"
|
|
: ""));
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (download) {
|
|
/* Fetch GitHub /releases/latest (no per-title version pin). */
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.05f,
|
|
force ? "Downloading toolchain update…"
|
|
: "Downloading latest portable cmake/clang…");
|
|
if (host_download_and_install_toolchain(on_progress, progress_ctx,
|
|
err_msg, err_cap)) {
|
|
if (host_portable_cmake_ready())
|
|
return 1;
|
|
/* Install saw bin/cmake.exe; ready check failed (won't run / min). */
|
|
snprintf(err_msg, err_cap,
|
|
"Downloaded toolchain is present but cmake will not run"
|
|
"%s. Check "
|
|
"%%LOCALAPPDATA%%\\retcomm\\toolchains\\cmake-clang-v1\\"
|
|
"<tag>\\bin\\cmake.exe --version, or set "
|
|
"RETCOMM_TOOLCHAIN_DIR.",
|
|
(toolchain_min_version()[0]
|
|
? " (or is below RETCOMM_TOOLCHAIN_MIN_VERSION)"
|
|
: ""));
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
snprintf(err_msg, err_cap,
|
|
"No portable toolchain found. Enable automatic download, pick a "
|
|
"cmake-clang-v1 zip, or set RETCOMM_TOOLCHAIN_DIR.");
|
|
return 0;
|
|
}
|
|
|
|
/* Rebuild-time fallback if the wizard step was skipped / cache pruned. */
|
|
static int host_ensure_toolchain(RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx, char* err_msg,
|
|
size_t err_cap) {
|
|
return host_ensure_toolchain_with_progress(1, NULL, err_msg, err_cap,
|
|
on_progress, progress_ctx);
|
|
}
|
|
|
|
/* Record the located set in game.toml before Generate reads it.
|
|
*
|
|
* disc.cfg is the mounted-image cache and the runtime takes only its first
|
|
* line; the hot-swap roster is built from game.toml [game] discs. So the
|
|
* wizard's picks reach the roster only by being written there -- which is
|
|
* exactly what the Retro path does by running probe_disc.py per image and
|
|
* verify_disc_set.py over the results.
|
|
*
|
|
* update_disc_set.py performs the same probe/verify and then edits ONLY the
|
|
* disc keys, leaving the project's hand-tuned sections and comments alone. It
|
|
* is a no-op when game.toml already names these images, so a re-run of the
|
|
* wizard does not churn the file.
|
|
*
|
|
* A failure here is reported and non-fatal: Generate itself only needs the
|
|
* boot image, and refusing to build because disc 3 could not be probed would
|
|
* be a worse outcome than building with a single-disc roster. */
|
|
static void host_update_disc_set(RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx) {
|
|
char script[1200];
|
|
char cmd[4096];
|
|
const char* cut;
|
|
size_t n;
|
|
int i, off;
|
|
|
|
if (g_wizard_disc_count < 2 || !g_python[0] || !g_cli_path[0] ||
|
|
!g_game_toml[0])
|
|
return;
|
|
|
|
/* tools/ sits beside psxrecomp_cli.py, wherever the submodule lives. */
|
|
cut = strrchr(g_cli_path, '/');
|
|
#if defined(_WIN32)
|
|
{
|
|
const char* b = strrchr(g_cli_path, '\\');
|
|
if (b && (!cut || b > cut)) cut = b;
|
|
}
|
|
#endif
|
|
if (!cut) return;
|
|
n = (size_t)(cut - g_cli_path);
|
|
if (n >= sizeof(script)) return;
|
|
memcpy(script, g_cli_path, n);
|
|
script[n] = '\0';
|
|
if ((size_t)snprintf(script + n, sizeof(script) - n,
|
|
"/tools/new_project_layout/update_disc_set.py") >=
|
|
sizeof(script) - n)
|
|
return;
|
|
if (!path_is_file(script))
|
|
return; /* older SDK checkout: nothing to run, single-disc as before */
|
|
|
|
off = snprintf(cmd, sizeof(cmd), "\"%s\" \"%s\" --game-toml \"%s\"",
|
|
g_python, script, g_game_toml);
|
|
for (i = 0; i < g_wizard_disc_count; ++i) {
|
|
if (!g_wizard_discs[i][0])
|
|
return; /* a gap in the middle would renumber the set */
|
|
if (off < 0 || (size_t)off >= sizeof(cmd))
|
|
return;
|
|
off += snprintf(cmd + off, sizeof(cmd) - (size_t)off, " \"%s\"",
|
|
g_wizard_discs[i]);
|
|
}
|
|
if (off < 0 || (size_t)off >= sizeof(cmd))
|
|
return;
|
|
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.03f, "Recording the disc set…");
|
|
if (!run_cmd_exit_zero(cmd) && on_progress)
|
|
on_progress(progress_ctx, 0.03f,
|
|
"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;
|
|
const char* cause;
|
|
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));
|
|
/* 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 — %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",
|
|
cause);
|
|
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,
|
|
void* progress_ctx) {
|
|
if (!g_ready) {
|
|
snprintf(err_msg, err_cap, "Local codegen tools are not available.");
|
|
return 0;
|
|
}
|
|
if (!source_path || !source_path[0]) {
|
|
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. */
|
|
if (!host_ensure_toolchain(on_progress, progress_ctx, err_msg, err_cap))
|
|
return 0;
|
|
activate_toolchain_path();
|
|
if (!find_python(g_python, sizeof(g_python))) {
|
|
snprintf(err_msg, err_cap,
|
|
"No usable Python. Install the portable toolchain step "
|
|
"(includes CPython under python/), or set RETCOMM_PYTHON.");
|
|
return 0;
|
|
}
|
|
}
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.02f, "Starting psxrecomp generate…");
|
|
|
|
/* Must precede the CLI: generate reads game.toml. */
|
|
host_update_disc_set(on_progress, progress_ctx);
|
|
|
|
/* Hand the CLI the launcher's staged disc + BIOS. Empty g_wizard_bios means
|
|
* OpenBIOS unless setup can adopt a retail dump beside the install. */
|
|
{
|
|
char abs_bios[1100];
|
|
if (g_wizard_bios[0] &&
|
|
absolutize_existing_file(abs_bios, sizeof(abs_bios), g_wizard_bios))
|
|
snprintf(g_wizard_bios, sizeof(g_wizard_bios), "%s", abs_bios);
|
|
else if (!g_wizard_bios[0]) {
|
|
/* Intentional OpenBIOS clear stays empty; otherwise adopt SCPH1001
|
|
* from bios.cfg / discovery (resolve_bios_arg). */
|
|
if (!g_wizard_bios_explicit) {
|
|
char found[1100];
|
|
if (resolve_bios_arg(found, sizeof(found)))
|
|
snprintf(g_wizard_bios, sizeof(g_wizard_bios), "%s", found);
|
|
}
|
|
} else {
|
|
/* Staged path missing — fail clearly instead of silently OpenBIOS. */
|
|
snprintf(err_msg, err_cap, "Staged BIOS not found: %s", g_wizard_bios);
|
|
return 0;
|
|
}
|
|
host_persist_setup(NULL, source_path,
|
|
g_wizard_bios[0] ? g_wizard_bios : "");
|
|
}
|
|
|
|
char bios_path[1100];
|
|
bios_path[0] = '\0';
|
|
const int have_bios =
|
|
g_wizard_bios[0] &&
|
|
absolutize_existing_file(bios_path, sizeof(bios_path), g_wizard_bios);
|
|
fprintf(stderr, "psxrecomp-codegen: generate disc=%s bios=%s\n", source_path,
|
|
have_bios ? bios_path : "(OpenBIOS)");
|
|
|
|
/* The exact dispatch filename this host (and CMakeLists GEN_MARKER) will
|
|
* gate on later. Handing it to generate makes a boot-exe name skew fail
|
|
* there, loudly, instead of surfacing as an endless setup-wizard loop. */
|
|
const char* marker_rel =
|
|
cfg_or(g_cfg->gen_marker_relpath, "generated/SLUS_011.89_dispatch.c");
|
|
const char* marker_name = marker_rel;
|
|
for (const char* p = marker_rel; *p; ++p)
|
|
if (*p == '/' || *p == '\\')
|
|
marker_name = p + 1;
|
|
|
|
#if defined(_WIN32)
|
|
char cmdline[4096];
|
|
if (have_bios) {
|
|
snprintf(cmdline, sizeof(cmdline),
|
|
"\"%s\" \"%s\" generate --project-root \"%s\" --config \"%s\" "
|
|
"--disc \"%s\" --bios \"%s\" --gen-marker \"%s\" "
|
|
"--json-progress",
|
|
g_python, g_cli_path, g_project_root, g_game_toml, source_path,
|
|
bios_path, marker_name);
|
|
} else {
|
|
snprintf(cmdline, sizeof(cmdline),
|
|
"\"%s\" \"%s\" generate --project-root \"%s\" --config \"%s\" "
|
|
"--disc \"%s\" --gen-marker \"%s\" --json-progress",
|
|
g_python, g_cli_path, g_project_root, g_game_toml, source_path,
|
|
marker_name);
|
|
}
|
|
if (!run_cli_win(cmdline, on_progress, progress_ctx, err_msg, err_cap,
|
|
"psxrecomp generate"))
|
|
return 0;
|
|
#else
|
|
char* argv[16];
|
|
int argc = 0;
|
|
argv[argc++] = g_python;
|
|
argv[argc++] = g_cli_path;
|
|
argv[argc++] = "generate";
|
|
argv[argc++] = "--project-root";
|
|
argv[argc++] = g_project_root;
|
|
argv[argc++] = "--config";
|
|
argv[argc++] = g_game_toml;
|
|
argv[argc++] = "--disc";
|
|
argv[argc++] = (char*)source_path;
|
|
if (have_bios) {
|
|
argv[argc++] = "--bios";
|
|
argv[argc++] = bios_path;
|
|
}
|
|
argv[argc++] = "--gen-marker";
|
|
argv[argc++] = (char*)marker_name;
|
|
argv[argc++] = "--json-progress";
|
|
argv[argc] = NULL;
|
|
if (!run_cli_posix(argv, on_progress, progress_ctx, err_msg, err_cap,
|
|
"psxrecomp generate"))
|
|
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");
|
|
return 1;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
static void bat_write_set(FILE* f, const char* name, const char* value) {
|
|
fprintf(f, "set \"%s=", name);
|
|
for (const char* p = value; *p; ++p) {
|
|
if (*p == '%')
|
|
fputc('%', f);
|
|
fputc(*p, f);
|
|
}
|
|
fprintf(f, "\"\r\n");
|
|
}
|
|
|
|
/* force_pgo: Settings Optimize FMV (instrument → train → use). Else setup
|
|
* rebuild with --no-pgo. disc_path required when force_pgo. */
|
|
static int write_windows_deferred_rebuild_helper(int force_pgo,
|
|
const char* disc_path,
|
|
char* err_msg,
|
|
size_t err_cap) {
|
|
if (!join_path(g_helper_path, sizeof(g_helper_path), g_build_dir,
|
|
"recomp_deferred_rebuild.cmd")) {
|
|
snprintf(err_msg, err_cap, "Failed to form helper path.");
|
|
return 0;
|
|
}
|
|
/* Setup zips omit build-release/; create it before writing the .cmd. */
|
|
if (!mkdir_p(g_build_dir)) {
|
|
snprintf(err_msg, err_cap, "Failed to create build dir: %s",
|
|
g_build_dir);
|
|
return 0;
|
|
}
|
|
FILE* f = fopen(g_helper_path, "wb");
|
|
if (!f) {
|
|
snprintf(err_msg, err_cap, "Failed to write rebuild helper: %s",
|
|
g_helper_path);
|
|
return 0;
|
|
}
|
|
char pid_buf[32];
|
|
snprintf(pid_buf, sizeof(pid_buf), "%lu",
|
|
(unsigned long)GetCurrentProcessId());
|
|
fprintf(f, "@echo off\r\n");
|
|
fprintf(f, "setlocal EnableExtensions\r\n");
|
|
fprintf(f, "title %s - %s\r\n", g_display,
|
|
force_pgo ? "PGO optimize" : "rebuilding");
|
|
bat_write_set(f, "PARENT_PID", pid_buf);
|
|
bat_write_set(f, "PYTHON", g_python);
|
|
bat_write_set(f, "CLI", g_cli_path);
|
|
bat_write_set(f, "ROOT", g_project_root);
|
|
bat_write_set(f, "CONFIG", g_game_toml);
|
|
bat_write_set(f, "BUILD_DIR", g_build_dir);
|
|
bat_write_set(f, "TARGET", g_cmake_target);
|
|
bat_write_set(f, "EXE_BASE", g_exe_basename);
|
|
bat_write_set(f, "EXE", g_exe_path);
|
|
bat_write_set(f, "DISPLAY", g_display);
|
|
{
|
|
/* Post-build sanity: the dispatch file the launcher will gate on. */
|
|
char marker_abs[1200];
|
|
if (join_path(marker_abs, sizeof(marker_abs), g_project_root,
|
|
cfg_or(g_cfg->gen_marker_relpath,
|
|
"generated/SLUS_011.89_dispatch.c")))
|
|
bat_write_set(f, "GEN_MARKER", marker_abs);
|
|
}
|
|
if (force_pgo && disc_path && disc_path[0])
|
|
bat_write_set(f, "DISC", disc_path);
|
|
if (g_toolchain_bin[0])
|
|
bat_write_set(f, "TC_BIN", g_toolchain_bin);
|
|
fprintf(f,
|
|
"echo Waiting for %%DISPLAY%% to exit...\r\n"
|
|
":waitloop\r\n"
|
|
"tasklist /FI \"PID eq %%PARENT_PID%%\" 2>NUL | "
|
|
"findstr /I \"%%PARENT_PID%%\" >NUL\r\n"
|
|
"if not errorlevel 1 (\r\n"
|
|
" ping -n 2 127.0.0.1 >NUL\r\n"
|
|
" goto waitloop\r\n"
|
|
")\r\n"
|
|
"echo Ensuring toolchain...\r\n"
|
|
"cd /d \"%%ROOT%%\"\r\n"
|
|
"if defined TC_BIN set \"PATH=%%TC_BIN%%;%%PATH%%\"\r\n"
|
|
"\"%%PYTHON%%\" \"%%CLI%%\" ensure-toolchain --project-root \"%%ROOT%%\"\r\n"
|
|
"if errorlevel 1 (\r\n"
|
|
" echo.\r\n"
|
|
" echo Toolchain missing. Download cmake-clang-v1 or set\r\n"
|
|
" echo RETCOMM_TOOLCHAIN_DIR / pass --toolchain-zip on rebuild.\r\n"
|
|
" pause\r\n"
|
|
" exit /b 1\r\n"
|
|
")\r\n");
|
|
if (force_pgo) {
|
|
fprintf(f,
|
|
"echo PGO optimize (instrument + train + rebuild)...\r\n"
|
|
"\"%%PYTHON%%\" \"%%CLI%%\" rebuild --project-root \"%%ROOT%%\" "
|
|
"--config \"%%CONFIG%%\" --build-dir \"%%BUILD_DIR%%\" "
|
|
"--target \"%%TARGET%%\" --exe-basename \"%%EXE_BASE%%\" "
|
|
"--disc \"%%DISC%%\" --force-pgo --pgo-video\r\n");
|
|
} else {
|
|
fprintf(f,
|
|
"echo Building...\r\n"
|
|
"\"%%PYTHON%%\" \"%%CLI%%\" rebuild --project-root \"%%ROOT%%\" "
|
|
"--config \"%%CONFIG%%\" --build-dir \"%%BUILD_DIR%%\" "
|
|
"--target \"%%TARGET%%\" --exe-basename \"%%EXE_BASE%%\" "
|
|
"--no-pgo --prune-after build-intermediates\r\n");
|
|
}
|
|
fprintf(f,
|
|
"if errorlevel 1 (\r\n"
|
|
" echo.\r\n"
|
|
" echo Build failed. Fix the errors above, then rebuild manually.\r\n"
|
|
" pause\r\n"
|
|
" exit /b 1\r\n"
|
|
")\r\n"
|
|
/* %EXE% was guessed before the build; runtime.cmake publishes the
|
|
* OUTPUT_NAME it really used, which wins the moment a title is
|
|
* renamed. Then verify game code + exe exist before relaunching,
|
|
* so a setup-host build stops here with a message instead of
|
|
* reopening the wizard in a loop. */
|
|
"set \"EXE_FINAL=%%EXE%%\"\r\n"
|
|
"set \"PUBLISHED=\"\r\n"
|
|
"if exist \"%%BUILD_DIR%%\\psxrecomp_exe_name-%%TARGET%%.txt\" "
|
|
"set /p PUBLISHED=<\"%%BUILD_DIR%%\\psxrecomp_exe_name-%%TARGET%%.txt\"\r\n"
|
|
"if defined PUBLISHED if exist \"%%BUILD_DIR%%\\%%PUBLISHED%%.exe\" "
|
|
"set \"EXE_FINAL=%%BUILD_DIR%%\\%%PUBLISHED%%.exe\"\r\n"
|
|
/* Path-bearing messages sit outside ( ) blocks: cmd expands
|
|
* %VAR% when it parses a block, so a ")" in an install folder
|
|
* such as "... (1)" would close the block early. */
|
|
"if defined GEN_MARKER if not exist \"%%GEN_MARKER%%\" goto no_gen\r\n"
|
|
"if not exist \"%%EXE_FINAL%%\" goto no_exe\r\n"
|
|
"echo Starting %%DISPLAY%%...\r\n"
|
|
"start \"\" /D \"%%ROOT%%\" \"%%EXE_FINAL%%\" --launcher\r\n"
|
|
"endlocal\r\n"
|
|
"exit /b 0\r\n"
|
|
":no_gen\r\n"
|
|
"echo.\r\n"
|
|
"echo Build finished but the generated game code is missing:\r\n"
|
|
"echo %%GEN_MARKER%%\r\n"
|
|
"echo Launching now would reopen setup in a loop. Please report\r\n"
|
|
"echo this to the port maintainer: the boot-EXE name in\r\n"
|
|
"echo game.toml disagrees with GEN_MARKER in CMakeLists.txt.\r\n"
|
|
"pause\r\n"
|
|
"exit /b 1\r\n"
|
|
":no_exe\r\n"
|
|
"echo.\r\n"
|
|
"echo Build finished but the game executable is missing:\r\n"
|
|
"echo %%EXE_FINAL%%\r\n"
|
|
"pause\r\n"
|
|
"exit /b 1\r\n");
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
static int host_rebuild_game_ex(const char* disc_path, int force_pgo,
|
|
char* out_exe_path, size_t out_cap,
|
|
char* err_msg, size_t err_cap,
|
|
RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx) {
|
|
g_relaunch_is_helper = 0;
|
|
if (!g_ready || !g_build_dir[0]) {
|
|
snprintf(err_msg, err_cap, "CMake build environment is not available.");
|
|
return 0;
|
|
}
|
|
if (force_pgo) {
|
|
if (psxrecomp_codegen_host_sources_missing(g_cfg)) {
|
|
snprintf(err_msg, err_cap,
|
|
"Generated game C is missing — finish Generate & rebuild "
|
|
"in the setup wizard first.");
|
|
return 0;
|
|
}
|
|
if (!disc_path || !disc_path[0] || !path_is_file(disc_path)) {
|
|
snprintf(err_msg, err_cap,
|
|
"Select a playable disc image before optimizing FMV.");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if (!host_ensure_toolchain(on_progress, progress_ctx, err_msg, err_cap))
|
|
return 0;
|
|
|
|
activate_toolchain_path();
|
|
if (!find_python(g_python, sizeof(g_python))) {
|
|
snprintf(err_msg, err_cap,
|
|
"No usable Python. Install the portable toolchain step "
|
|
"(includes CPython under python/), or set RETCOMM_PYTHON.");
|
|
return 0;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.4f,
|
|
force_pgo ? "Scheduling Windows PGO optimize after exit…"
|
|
: "Scheduling Windows rebuild after exit…");
|
|
if (!write_windows_deferred_rebuild_helper(force_pgo, disc_path, err_msg,
|
|
err_cap))
|
|
return 0;
|
|
g_relaunch_is_helper = 1;
|
|
snprintf(out_exe_path, out_cap, "%s", g_helper_path);
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 1.0f,
|
|
force_pgo
|
|
? "Exiting so Windows can run PGO train + rebuild…"
|
|
: "Exiting so Windows can rebuild safely…");
|
|
return 1;
|
|
#else
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 0.05f,
|
|
force_pgo ? "Starting FMV PGO optimize…"
|
|
: "Starting rebuild (cmake)…");
|
|
|
|
char disc_arg_storage[1100];
|
|
char* argv[40];
|
|
int argc = 0;
|
|
argv[argc++] = g_python;
|
|
argv[argc++] = g_cli_path;
|
|
argv[argc++] = "rebuild";
|
|
argv[argc++] = "--project-root";
|
|
argv[argc++] = g_project_root;
|
|
argv[argc++] = "--config";
|
|
argv[argc++] = g_game_toml;
|
|
argv[argc++] = "--build-dir";
|
|
argv[argc++] = g_build_dir;
|
|
argv[argc++] = "--target";
|
|
argv[argc++] = g_cmake_target;
|
|
argv[argc++] = "--exe-basename";
|
|
argv[argc++] = g_exe_basename;
|
|
if (disc_path && disc_path[0]) {
|
|
snprintf(disc_arg_storage, sizeof(disc_arg_storage), "%s", disc_path);
|
|
argv[argc++] = "--disc";
|
|
argv[argc++] = disc_arg_storage;
|
|
}
|
|
if (force_pgo) {
|
|
argv[argc++] = "--force-pgo";
|
|
argv[argc++] = "--pgo-video";
|
|
} else {
|
|
argv[argc++] = "--no-pgo";
|
|
argv[argc++] = "--prune-after";
|
|
argv[argc++] = "build-intermediates";
|
|
}
|
|
argv[argc++] = "--json-progress";
|
|
argv[argc] = NULL;
|
|
|
|
if (!run_cli_posix(argv, on_progress, progress_ctx, err_msg, err_cap,
|
|
force_pgo ? "psxrecomp PGO optimize"
|
|
: "psxrecomp rebuild"))
|
|
return 0;
|
|
if (!path_is_file(g_exe_path)) {
|
|
snprintf(err_msg, err_cap, "Build succeeded but binary missing: %s",
|
|
g_exe_path);
|
|
return 0;
|
|
}
|
|
snprintf(out_exe_path, out_cap, "%s", g_exe_path);
|
|
if (on_progress)
|
|
on_progress(progress_ctx, 1.0f,
|
|
force_pgo ? "FMV optimize complete" : "Build complete");
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
static int host_rebuild_game(const char* disc_path, char* out_exe_path,
|
|
size_t out_cap, char* err_msg, size_t err_cap,
|
|
RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx) {
|
|
return host_rebuild_game_ex(disc_path, 0, out_exe_path, out_cap, err_msg,
|
|
err_cap, on_progress, progress_ctx);
|
|
}
|
|
|
|
static int host_pgo_optimize(const char* disc_path, char* out_exe_path,
|
|
size_t out_cap, char* err_msg, size_t err_cap,
|
|
RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx) {
|
|
return host_rebuild_game_ex(disc_path, 1, out_exe_path, out_cap, err_msg,
|
|
err_cap, on_progress, progress_ctx);
|
|
}
|
|
|
|
/* Settings → VIDEO → Apply FMV Timing Opt: regenerate C (picks up
|
|
* load_charge_batch) then rebuild without PGO training. */
|
|
static int host_fmv_timing_optimize(const char* disc_path, char* out_exe_path,
|
|
size_t out_cap, char* err_msg,
|
|
size_t err_cap,
|
|
RecompLauncherCPrepareProgressFn on_progress,
|
|
void* progress_ctx) {
|
|
char gen_out[512];
|
|
if (!host_prepare_generate(disc_path, gen_out, sizeof(gen_out), err_msg,
|
|
err_cap, on_progress, progress_ctx))
|
|
return 0;
|
|
return host_rebuild_game_ex(disc_path, 0, out_exe_path, out_cap, err_msg,
|
|
err_cap, on_progress, progress_ctx);
|
|
}
|
|
|
|
static int host_self_exe_path(char* out, size_t cap) {
|
|
if (!out || cap < 2)
|
|
return 0;
|
|
out[0] = '\0';
|
|
#if defined(_WIN32)
|
|
{
|
|
DWORD n = GetModuleFileNameA(NULL, out, (DWORD)cap);
|
|
return n > 0 && n < (DWORD)cap;
|
|
}
|
|
#else
|
|
{
|
|
const char* appimg = getenv("APPIMAGE");
|
|
if (appimg && appimg[0] && path_is_file(appimg)) {
|
|
snprintf(out, cap, "%s", appimg);
|
|
return 1;
|
|
}
|
|
return host_posix_exe_path(out, cap);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static int host_paths_same_file(const char* a, const char* b) {
|
|
char fa[1100], fb[1100];
|
|
if (!a || !a[0] || !b || !b[0])
|
|
return 0;
|
|
#if defined(_WIN32)
|
|
{
|
|
DWORD na = GetFullPathNameA(a, (DWORD)sizeof(fa), fa, NULL);
|
|
DWORD nb = GetFullPathNameA(b, (DWORD)sizeof(fb), fb, NULL);
|
|
if (na == 0 || na >= (DWORD)sizeof(fa) || nb == 0 ||
|
|
nb >= (DWORD)sizeof(fb))
|
|
return 0;
|
|
return _stricmp(fa, fb) == 0;
|
|
}
|
|
#else
|
|
{
|
|
char* ra = realpath(a, NULL);
|
|
char* rb = realpath(b, NULL);
|
|
int same = ra && rb && strcmp(ra, rb) == 0;
|
|
free(ra);
|
|
free(rb);
|
|
return same;
|
|
}
|
|
#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");
|
|
/* Wave-5 F4: overlay_cache is the switch that makes the runtime initialise the
|
|
* overlay loader at all; without it every streamed overlay is interpreted. CI
|
|
* asserts this field so a title cannot ship silently without it again. */
|
|
{
|
|
char toml_abs[1200];
|
|
int overlay_cache = 0;
|
|
if (join_path(toml_abs, sizeof(toml_abs), g_project_root, "game.toml")) {
|
|
FILE* tf = fopen(toml_abs, "rb");
|
|
if (tf) {
|
|
char line[512];
|
|
while (fgets(line, sizeof(line), tf)) {
|
|
const char* p = line;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (strncmp(p, "overlay_cache", 13) == 0) {
|
|
p += 13;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
if (*p == '=') {
|
|
++p;
|
|
while (*p == ' ' || *p == '\t') ++p;
|
|
overlay_cache = strncmp(p, "true", 4) == 0;
|
|
}
|
|
}
|
|
}
|
|
fclose(tf);
|
|
}
|
|
}
|
|
printf(",\n \"overlay_cache_configured\": %s", overlay_cache ? "true" : "false");
|
|
}
|
|
/* Build-tools readiness as wizard page 0 judges it, minus its cache
|
|
* healing: nothing is deleted or renamed here. toolchain_note carries the
|
|
* repair hint the wizard would show (e.g. missing Command Line Tools). */
|
|
{
|
|
int tc_ready;
|
|
g_tc_repair_note[0] = '\0';
|
|
activate_toolchain_path();
|
|
tc_ready = host_portable_cmake_ready() && host_system_compiler_ready();
|
|
printf(",\n \"toolchain_ready\": %s", tc_ready ? "true" : "false");
|
|
printf(",\n \"toolchain_note\": ");
|
|
host_json_str(g_tc_repair_note);
|
|
}
|
|
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;
|
|
(void)argc;
|
|
(void)argv;
|
|
return;
|
|
#else
|
|
char self[1100];
|
|
const char* no_fwd;
|
|
const char* force_env;
|
|
const char* force;
|
|
|
|
if (!cfg || !cfg->cmake_target || !cfg->exe_basename)
|
|
return;
|
|
|
|
no_fwd = getenv("PSXRECOMP_NO_FORWARD");
|
|
if (no_fwd && no_fwd[0] && no_fwd[0] != '0')
|
|
return;
|
|
|
|
g_cfg = cfg;
|
|
snprintf(g_display, sizeof(g_display), "%s",
|
|
cfg_or(cfg->display_name, "Game"));
|
|
snprintf(g_cmake_target, sizeof(g_cmake_target), "%s", cfg->cmake_target);
|
|
snprintf(g_exe_basename, sizeof(g_exe_basename), "%s", cfg->exe_basename);
|
|
|
|
force_env = cfg_or(cfg->force_setup_env, "PSXRECOMP_FORCE_SETUP");
|
|
force = getenv(force_env);
|
|
if (force && force[0] && force[0] != '0')
|
|
return;
|
|
|
|
/* Still need Generate — stay on the setup host. */
|
|
if (psxrecomp_codegen_host_sources_missing(cfg))
|
|
return;
|
|
|
|
if (!discover_project_root(g_project_root, sizeof(g_project_root)))
|
|
return;
|
|
if (!resolve_build_paths())
|
|
return;
|
|
if (!g_exe_path[0] || !path_is_file(g_exe_path))
|
|
return;
|
|
if (!host_self_exe_path(self, sizeof(self)))
|
|
return;
|
|
if (host_paths_same_file(self, g_exe_path))
|
|
return;
|
|
|
|
fprintf(stderr,
|
|
"psxrecomp-codegen: setup host forwarding to product build:\n %s\n",
|
|
g_exe_path);
|
|
|
|
#if defined(_WIN32)
|
|
{
|
|
STARTUPINFOA si;
|
|
PROCESS_INFORMATION pi;
|
|
char cmd[4096];
|
|
size_t pos = 0;
|
|
int i;
|
|
memset(&si, 0, sizeof(si));
|
|
memset(&pi, 0, sizeof(pi));
|
|
si.cb = sizeof(si);
|
|
pos += (size_t)snprintf(cmd + pos, sizeof(cmd) - pos, "\"%s\"",
|
|
g_exe_path);
|
|
/* Preserve caller argv (skip argv[0]); ensure --launcher so skip_launcher
|
|
* in the product tree cannot hide the UI on first handoff. */
|
|
{
|
|
int has_launcher = 0;
|
|
for (i = 1; i < argc && argv && argv[i]; ++i) {
|
|
int n;
|
|
if (strcmp(argv[i], "--launcher") == 0)
|
|
has_launcher = 1;
|
|
n = snprintf(cmd + pos, sizeof(cmd) - pos, " \"%s\"", argv[i]);
|
|
if (n <= 0 || (size_t)n >= sizeof(cmd) - pos)
|
|
break;
|
|
pos += (size_t)n;
|
|
}
|
|
if (!has_launcher && pos + 12 < sizeof(cmd))
|
|
snprintf(cmd + pos, sizeof(cmd) - pos, " --launcher");
|
|
}
|
|
if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, 0, NULL,
|
|
g_project_root[0] ? g_project_root : NULL, &si,
|
|
&pi)) {
|
|
fprintf(stderr,
|
|
"psxrecomp-codegen: failed to start product exe (error %lu)\n",
|
|
(unsigned long)GetLastError());
|
|
return;
|
|
}
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
ExitProcess(0);
|
|
}
|
|
#else
|
|
{
|
|
char** args;
|
|
int i;
|
|
int narg = (argc > 0) ? argc : 1;
|
|
int has_launcher = 0;
|
|
for (i = 1; i < argc && argv && argv[i]; ++i) {
|
|
if (strcmp(argv[i], "--launcher") == 0) {
|
|
has_launcher = 1;
|
|
break;
|
|
}
|
|
}
|
|
args = (char**)calloc((size_t)narg + 2, sizeof(char*));
|
|
if (!args)
|
|
return;
|
|
args[0] = g_exe_path;
|
|
for (i = 1; i < argc && argv && argv[i]; ++i)
|
|
args[i] = argv[i];
|
|
if (!has_launcher)
|
|
args[i++] = "--launcher";
|
|
args[i] = NULL;
|
|
if (g_project_root[0] && chdir(g_project_root) != 0) {
|
|
fprintf(stderr, "psxrecomp-codegen: chdir(%s) failed: %s\n",
|
|
g_project_root, strerror(errno));
|
|
}
|
|
execv(g_exe_path, args);
|
|
perror("psxrecomp-codegen: execv product exe failed");
|
|
free(args);
|
|
}
|
|
#endif
|
|
#endif /* !PSX_HAS_GAME_DISPATCH */
|
|
}
|
|
|
|
void psxrecomp_codegen_host_relaunch_or_exit(const char* disc_path) {
|
|
char exe[512];
|
|
const char* near_exe;
|
|
if (!recomp_launcher_relaunch_exe(exe, sizeof(exe)) || !exe[0]) {
|
|
fprintf(stderr, "psxrecomp-codegen: relaunch requested but no path\n");
|
|
exit(1);
|
|
}
|
|
/* Prefer the final game binary (build/<exe>) over a Windows helper bat. */
|
|
near_exe = g_exe_path[0] ? g_exe_path : exe;
|
|
persist_relaunch_sidecars(near_exe, disc_path);
|
|
|
|
#if defined(_WIN32)
|
|
{
|
|
STARTUPINFOA si;
|
|
PROCESS_INFORMATION pi;
|
|
char cmd[1536];
|
|
DWORD flags = 0;
|
|
memset(&si, 0, sizeof(si));
|
|
memset(&pi, 0, sizeof(pi));
|
|
si.cb = sizeof(si);
|
|
if (g_relaunch_is_helper) {
|
|
fprintf(stderr,
|
|
"psxrecomp-codegen: starting deferred rebuild helper\n");
|
|
/* cmd /C strips the first and last quote of the command line
|
|
* when it holds special characters such as the parentheses in
|
|
* "r4-1.0-windows-x64 (1)"; the doubled outer pair keeps the
|
|
* path's own quotes intact. */
|
|
snprintf(cmd, sizeof(cmd), "cmd.exe /C \"\"%s\"\"", exe);
|
|
flags = CREATE_NEW_CONSOLE;
|
|
} else {
|
|
fprintf(stderr, "psxrecomp-codegen: relaunching %s\n", exe);
|
|
snprintf(cmd, sizeof(cmd), "\"%s\" --launcher", exe);
|
|
}
|
|
if (!CreateProcessA(NULL, cmd, NULL, NULL, FALSE, flags, NULL,
|
|
g_project_root, &si, &pi)) {
|
|
fprintf(stderr, "psxrecomp-codegen: CreateProcess failed\n");
|
|
exit(1);
|
|
}
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
ExitProcess(0);
|
|
}
|
|
#else
|
|
{
|
|
if (g_project_root[0] && chdir(g_project_root) != 0) {
|
|
fprintf(stderr, "psxrecomp-codegen: chdir(%s) failed: %s\n",
|
|
g_project_root, strerror(errno));
|
|
}
|
|
fprintf(stderr, "psxrecomp-codegen: relaunching %s\n", exe);
|
|
char* args[] = {exe, "--launcher", NULL};
|
|
execv(exe, args);
|
|
perror("psxrecomp-codegen: execv failed");
|
|
exit(1);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void psxrecomp_codegen_host_apply(RecompLauncherCGameInfo* gi,
|
|
const PsxrecompCodegenHostConfig* cfg) {
|
|
if (!gi || !cfg || !cfg->cmake_target || !cfg->exe_basename)
|
|
return;
|
|
|
|
#if !defined(PSX_HAS_SETUP_WIZARD)
|
|
/* Build did not opt into the setup-wizard product surface
|
|
* (-DPSX_SETUP_WIZARD=ON / ENABLE_SETUP_WIZARD). Leave GameInfo dark so
|
|
* recomp-ui never opens first-run / Generate & rebuild. */
|
|
(void)cfg;
|
|
return;
|
|
#else
|
|
|
|
g_cfg = cfg;
|
|
g_ready = 0;
|
|
g_relaunch_is_helper = 0;
|
|
g_project_root[0] = '\0';
|
|
g_cli_path[0] = '\0';
|
|
g_game_toml[0] = '\0';
|
|
g_python[0] = '\0';
|
|
g_cmake[0] = '\0';
|
|
g_build_dir[0] = '\0';
|
|
g_exe_path[0] = '\0';
|
|
g_helper_path[0] = '\0';
|
|
g_toolchain_bin[0] = '\0';
|
|
g_cli_toolchain_bin[0] = '\0';
|
|
/* Keep g_wizard_bios across re-apply within the same process. */
|
|
|
|
snprintf(g_display, sizeof(g_display), "%s",
|
|
cfg_or(cfg->display_name, "Game"));
|
|
snprintf(g_cmake_target, sizeof(g_cmake_target), "%s", cfg->cmake_target);
|
|
snprintf(g_exe_basename, sizeof(g_exe_basename), "%s", cfg->exe_basename);
|
|
|
|
const char* force_env =
|
|
cfg_or(cfg->force_setup_env, "PSXRECOMP_FORCE_SETUP");
|
|
const char* force = getenv(force_env);
|
|
const int force_setup = force && force[0] && force[0] != '0';
|
|
|
|
/* Master switch for recomp-ui: first-run wizard + Generate & rebuild. */
|
|
gi->setup_wizard_supported = 1;
|
|
/* has_bios is NOT forced here. A setup SDK links OpenBIOS only and needs
|
|
* the retail picker so prepare can ingest a dump, but a shipped bundled
|
|
* build has no CLI, sources or toolchain to compile one in, and forcing
|
|
* the row there offered "Generate & rebuild" that could never run.
|
|
* main.cpp decides after this returns, from whether prepare_with_progress
|
|
* was actually wired below (or nothing is linked yet). */
|
|
|
|
if (!discover_project_root(g_project_root, sizeof(g_project_root))) {
|
|
/* Still force the wizard when generated/ is missing — discover may
|
|
* fail if the process cwd is unrelated to the project tree. */
|
|
if (force_setup) {
|
|
gi->needs_setup = 1;
|
|
gi->prepare_required_before_continue = 1;
|
|
}
|
|
return;
|
|
}
|
|
if (!resolve_cli_path(g_project_root, g_cli_path, sizeof(g_cli_path))) {
|
|
if (psxrecomp_codegen_host_sources_missing(cfg) || force_setup) {
|
|
gi->needs_setup = 1;
|
|
gi->prepare_required_before_continue = 1;
|
|
}
|
|
return;
|
|
}
|
|
if (!join_path(g_game_toml, sizeof(g_game_toml), g_project_root,
|
|
cfg_or(cfg->game_toml_relpath, "game.toml")))
|
|
return;
|
|
if (!path_is_file(g_game_toml))
|
|
return;
|
|
|
|
/* Wire prepare/rebuild even when Python is not on PATH yet — wizard page 0
|
|
* installs cmake-clang-v1 (1.0.6+ ships portable CPython under python/).
|
|
* generate/rebuild re-resolve after activate_toolchain_path. */
|
|
g_ready = 1;
|
|
activate_toolchain_path();
|
|
(void)find_python(g_python, sizeof(g_python));
|
|
gi->persist_setup = host_persist_setup;
|
|
gi->persist_setup_ctx = NULL;
|
|
/* Multi-disc titles only: load_game_toml_disc_roster leaves g_num_discs 0
|
|
* for a single image, so the wizard keeps its one-image layout and the
|
|
* launcher keeps taking the single-path flush. */
|
|
load_game_toml_disc_roster();
|
|
if (g_num_discs > 1) {
|
|
gi->discs = g_disc_roster;
|
|
gi->num_discs = g_num_discs;
|
|
gi->persist_setup_discs = host_persist_setup_discs;
|
|
}
|
|
gi->prepare_with_progress = host_prepare_generate;
|
|
gi->prepare_use_selected_rom = 1;
|
|
/* Number prefix is applied in the setup UI (BIOS adds a step). */
|
|
gi->prepare_section_title = "Generate BIOS + game C & rebuild";
|
|
gi->prepare_busy_status = "Generating BIOS + game sources…";
|
|
gi->prepare_success_status = "Sources ready — building…";
|
|
|
|
/* Rebuild is offered whenever the build tree can be formed; the wizard
|
|
* installs cmake-clang-v1 on page 0 before Generate & rebuild. */
|
|
const int can_rebuild = resolve_build_paths();
|
|
if (can_rebuild) {
|
|
gi->prepare_disc_label = "Generate & rebuild…";
|
|
#if defined(_WIN32)
|
|
gi->prepare_disc_note =
|
|
cfg->prepare_note_windows
|
|
? cfg->prepare_note_windows
|
|
: "Uses your disc with the local psxrecomp SDK to regenerate "
|
|
"generated/, then quits and rebuilds via a helper so the "
|
|
"running .exe is not locked.";
|
|
gi->rebuild_busy_status = "Scheduling rebuild…";
|
|
gi->rebuild_success_status =
|
|
"Exiting for Windows rebuild — a console will finish the build…";
|
|
#else
|
|
gi->prepare_disc_note =
|
|
cfg->prepare_note
|
|
? cfg->prepare_note
|
|
: "Uses your disc with the local psxrecomp SDK to regenerate "
|
|
"generated/, then runs cmake --build and restarts.";
|
|
gi->rebuild_busy_status = "Building…";
|
|
gi->rebuild_success_status = "Build complete — restarting…";
|
|
#endif
|
|
gi->rebuild_with_progress = host_rebuild_game;
|
|
gi->rebuild_after_prepare = 1;
|
|
gi->relaunch_after_rebuild = 1;
|
|
gi->setup_needs_toolchain = 1;
|
|
gi->toolchain_is_ready = host_toolchain_is_ready;
|
|
gi->ensure_toolchain_with_progress = host_ensure_toolchain_with_progress;
|
|
gi->toolchain_update_available = host_toolchain_update_available;
|
|
gi->toolchain_repair_note = host_toolchain_repair_note;
|
|
/* Settings → SYSTEM: PGO on existing generated C (no wizard). */
|
|
gi->pgo_optimize_with_progress = host_pgo_optimize;
|
|
#if defined(_WIN32)
|
|
gi->pgo_busy_status = "Scheduling FMV PGO optimize…";
|
|
gi->pgo_success_status =
|
|
"Exiting for Windows PGO train + rebuild…";
|
|
#else
|
|
gi->pgo_busy_status =
|
|
"Optimizing FMV (instrument → train → rebuild)…";
|
|
gi->pgo_success_status = "FMV optimize complete — restarting…";
|
|
#endif
|
|
/* FMV timing (emitter load_charge_batch) generate+rebuild: host kept
|
|
* but not exposed in Settings for now. */
|
|
} else {
|
|
gi->prepare_disc_label = "Generate sources…";
|
|
gi->prepare_disc_note =
|
|
cfg->prepare_note_no_cmake
|
|
? cfg->prepare_note_no_cmake
|
|
: "Regenerates generated/ with the local psxrecomp SDK. "
|
|
"Build dir could not be resolved — rebuild manually, then "
|
|
"relaunch.";
|
|
gi->prepare_success_status =
|
|
"Sources generated. Rebuild manually, then relaunch.";
|
|
}
|
|
gi->persist_setup = host_persist_setup;
|
|
gi->persist_setup_ctx = NULL;
|
|
|
|
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 */
|
|
}
|