Linux diagnostics updates

This commit is contained in:
vetusmagnus
2026-10-14 12:49:56 -04:00
parent 0cdad42578
commit 5fba038313
10 changed files with 211 additions and 14 deletions
+1 -1
View File
@@ -6,4 +6,4 @@ games/*/zig-cache/
games/*/build_log.txt games/*/build_log.txt
__pycache__/ __pycache__/
*.pyc *.pyc
release/rac1/ release/rac1/
+7 -1
View File
@@ -48,6 +48,8 @@ Resolution (Native, or Upscaled to a whole multiple of the PS2's 448 lines that
### Reporting problems ### Reporting problems
- **Open log** shows `rac1_log.txt` (or the build's `build_log.txt` when a build failed). - **Open log** shows `rac1_log.txt` (or the build's `build_log.txt` when a build failed).
- On Linux, `project-solana report` writes one file with everything a report needs (system details, the 32-bit library check, the launcher, build and game logs, and the previous run's game log); attach that file.
- A crash writes the function it happened in and the call chain into the game log, and the previous run's log is kept as `rac1_log.prev.txt`.
- **F9** in the game records the next few seconds of graphics input to `gsrec_NN.bin` (about 260 MB per second of capture); attach it to a report together with what looked wrong. - **F9** in the game records the next few seconds of graphics input to `gsrec_NN.bin` (about 260 MB per second of capture); attach it to a report together with what looked wrong.
## Repository layout ## Repository layout
@@ -86,4 +88,8 @@ gh release create rac1 dist/rac1/rac1.json dist/rac1/rac1-build.zip dist/rac1/to
To update an existing game release, upload the new files over the old ones (`gh release upload rac1 dist/rac1/* --clobber`). Players get them with **Rebuild the game**. To update an existing game release, upload the new files over the old ones (`gh release upload rac1 dist/rac1/* --clobber`). Players get them with **Rebuild the game**.
The launchers go in a release of their own (for example `launcher-v1`): `Project Solana.exe` and `project-solana` (Linux). The launchers go in a release of their own (for example `launcher-v1`): `Project Solana.exe` and `project-solana` (Linux).
## Licence
TODO (maintainers): choose a licence for this repository. Third-party code keeps its own (see `THIRD_PARTY_NOTICES.md`).
+1 -1
View File
@@ -241,7 +241,7 @@ if a.platform == "linux":
else: else:
# the menu and runtime as a DLL at 0x40000000, plus the small launcher that owns PS2 RAM # the menu and runtime as a DLL at 0x40000000, plus the small launcher that owns PS2 RAM
dll = os.path.join(out, "rac1_game.dll") dll = os.path.join(out, "rac1_game.dll")
link = zig + ["cc", "-shared", "-target", target, "-o", dll, objs_rsp("link")] + ["-Wl,--image-base=0x40000000", "-Wl,--no-dynamicbase", "-lshell32", "-lwinmm", "-lgdi32", "-luser32"] link = zig + ["cc", "-shared", "-target", target, "-o", dll, objs_rsp("link")] + ["-Wl,--image-base=0x40000000", "-Wl,--no-dynamicbase", "-lshell32", "-lwinmm", "-lgdi32", "-luser32", "-ladvapi32"]
r = subprocess.run(link, capture_output=True, text=True) r = subprocess.run(link, capture_output=True, text=True)
if r.returncode: if r.returncode:
print(r.stderr[-6000:]); sys.exit("link failed (dll)") print(r.stderr[-6000:]); sys.exit("link failed (dll)")
+1
View File
@@ -23,6 +23,7 @@ extern const int uya_tramp_count;
/* platform layer */ /* platform layer */
int plat_map_fixed(uint32_t addr, uint32_t size, int exec); /* 0 on success */ int plat_map_fixed(uint32_t addr, uint32_t size, int exec); /* 0 on success */
int plat_install_trap_handler(void); /* calls into unmapped PS2 code */ int plat_install_trap_handler(void); /* calls into unmapped PS2 code */
void plat_log_system(void); /* the system, for bug reports (once, at start) */
void plat_flush_icache(void *p, size_t n); void plat_flush_icache(void *p, size_t n);
const char *plat_exe_dir(void); /* folder of the running program */ const char *plat_exe_dir(void); /* folder of the running program */
void plat_at_exit(void); void plat_at_exit(void);
+1
View File
@@ -63,6 +63,7 @@ int uya_main(int argc, char **argv)
uya_log_open(); uya_log_open();
uya_log("Project Solana: starting"); uya_log("Project Solana: starting");
plat_install_trap_handler(); /* first: a crash from here on is logged */ plat_install_trap_handler(); /* first: a crash from here on is logged */
plat_log_system();
{ /* renderer=gl|soft in the cfg (UYA_RENDERER overrides it) */ { /* renderer=gl|soft in the cfg (UYA_RENDERER overrides it) */
extern const char *uya_renderer_cfg; extern const char *uya_renderer_cfg;
const char *r = cfg_value("renderer"); const char *r = cfg_value("renderer");
+22
View File
@@ -180,8 +180,30 @@ int plat_install_trap_handler(void)
sigaction(SIGBUS, &sa, 0); sigaction(SIGBUS, &sa, 0);
sigaction(SIGILL, &sa, 0); sigaction(SIGILL, &sa, 0);
sigaction(SIGFPE, &sa, 0); sigaction(SIGFPE, &sa, 0);
sigaction(SIGABRT, &sa, 0); /* (a library giving up: assert, abort) */
return 0; return 0;
} }
#include <sys/utsname.h>
void plat_log_system(void)
{
struct utsname u;
char os[160] = "unknown", line[256], cpu[160] = "?";
FILE *f = fopen("/etc/os-release", "r");
while (f && fgets(line, sizeof line, f))
if (!strncmp(line, "PRETTY_NAME=", 12)) { char *v = line + 12; if (*v == '"') v++; v[strcspn(v, "\"\n")] = 0; snprintf(os, sizeof os, "%s", v); }
if (f) fclose(f);
f = fopen("/proc/cpuinfo", "r");
while (f && fgets(line, sizeof line, f))
if (!strncmp(line, "model name", 10)) { char *v = strchr(line, ':'); if (v) { v += 2; v[strcspn(v, "\n")] = 0; snprintf(cpu, sizeof cpu, "%s", v); break; } }
if (f) fclose(f);
if (uname(&u) == 0) uya_log("system: Linux %s (%s), %s", u.release, u.machine, os);
uya_log("system: %s, %ld cores, %ld MB RAM", cpu, sysconf(_SC_NPROCESSORS_ONLN), (long)((long long)sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE) >> 20));
const char *st = getenv("XDG_SESSION_TYPE"), *de = getenv("XDG_CURRENT_DESKTOP");
uya_log("system: session %s%s%s, DISPLAY %s, WAYLAND_DISPLAY %s%s", st ? st : "?", de ? ", desktop " : "", de ? de : "",
getenv("DISPLAY") ? getenv("DISPLAY") : "-", getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "-",
getenv("SteamDeck") && !strcmp(getenv("SteamDeck"), "1") ? ", Steam Deck" : getenv("SteamAppId") ? ", started by Steam" : "");
}
#endif #endif
#ifdef __linux__ #ifdef __linux__
+10 -1
View File
@@ -83,6 +83,9 @@ static struct {
int (*QueueAudio)(SDL_AudioDeviceID, const void *, uint32_t); int (*QueueAudio)(SDL_AudioDeviceID, const void *, uint32_t);
uint32_t (*GetQueuedAudioSize)(SDL_AudioDeviceID); uint32_t (*GetQueuedAudioSize)(SDL_AudioDeviceID);
void (*ClearQueuedAudio)(SDL_AudioDeviceID); void (*ClearQueuedAudio)(SDL_AudioDeviceID);
void (*GetVersion)(uint8_t v[3]);
const char *(*GetCurrentVideoDriver)(void);
const char *(*GetCurrentAudioDriver)(void);
} sdl; } sdl;
/* 1: SDL is there and a display is (the game draws in a window); 0: headless */ /* 1: SDL is there and a display is (the game draws in a window); 0: headless */
@@ -105,7 +108,7 @@ static int sdl_on(void)
"SDL_NumJoysticks", "SDL_IsGameController", "SDL_GameControllerOpen", "SDL_GameControllerClose", "SDL_NumJoysticks", "SDL_IsGameController", "SDL_GameControllerOpen", "SDL_GameControllerClose",
"SDL_GameControllerGetAttached", "SDL_GameControllerName", "SDL_GameControllerGetButton", "SDL_GameControllerGetAxis", "SDL_GameControllerGetAttached", "SDL_GameControllerName", "SDL_GameControllerGetButton", "SDL_GameControllerGetAxis",
"SDL_OpenAudioDevice", "SDL_CloseAudioDevice", "SDL_PauseAudioDevice", "SDL_QueueAudio", "SDL_GetQueuedAudioSize", "SDL_OpenAudioDevice", "SDL_CloseAudioDevice", "SDL_PauseAudioDevice", "SDL_QueueAudio", "SDL_GetQueuedAudioSize",
"SDL_ClearQueuedAudio", "SDL_ClearQueuedAudio", "SDL_GetVersion", "SDL_GetCurrentVideoDriver", "SDL_GetCurrentAudioDriver",
}; };
void **fp = (void **)&sdl; void **fp = (void **)&sdl;
for (unsigned i = 0; i < sizeof names / sizeof names[0]; i++) for (unsigned i = 0; i < sizeof names / sizeof names[0]; i++)
@@ -116,6 +119,12 @@ static int sdl_on(void)
uya_log("SDL2: cannot start (%s)", sdl.GetError()); uya_log("SDL2: cannot start (%s)", sdl.GetError());
return 0; return 0;
} }
{
uint8_t v[3] = {0, 0, 0};
const char *vd = sdl.GetCurrentVideoDriver(), *ad = sdl.GetCurrentAudioDriver();
sdl.GetVersion(v);
uya_log("SDL %d.%d.%d: video %s, audio %s", v[0], v[1], v[2], vd ? vd : "none", ad ? ad : "none");
}
sdl_state = 1; sdl_state = 1;
return 1; return 1;
} }
+26
View File
@@ -180,11 +180,37 @@ static LONG CALLBACK on_exception(EXCEPTION_POINTERS *ep)
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
} }
#include <signal.h>
/* abort() (a library giving up) ends the process without an exception: raise one, so the
* crash handler reports it with the call chain */
static void on_abort(int sig)
{
(void)sig;
uya_log("crash: abort() was called");
RaiseException(0xE0000ABE, EXCEPTION_NONCONTINUABLE, 0, 0);
}
int plat_install_trap_handler(void) int plat_install_trap_handler(void)
{ {
signal(SIGABRT, on_abort);
return AddVectoredExceptionHandler(1, on_exception) ? 0 : -1; return AddVectoredExceptionHandler(1, on_exception) ? 0 : -1;
} }
void plat_log_system(void)
{
typedef struct { ULONG size, major, minor, build, platform; WCHAR sp[128]; } osv_t;
HMODULE nt = GetModuleHandleA("ntdll.dll");
LONG (WINAPI *rtl_ver)(osv_t *) = nt ? (void *)GetProcAddress(nt, "RtlGetVersion") : 0;
const char *(*wine_ver)(void) = nt ? (void *)GetProcAddress(nt, "wine_get_version") : 0;
osv_t v; memset(&v, 0, sizeof v); v.size = sizeof v;
if (rtl_ver) rtl_ver(&v);
SYSTEM_INFO si; GetNativeSystemInfo(&si);
MEMORYSTATUSEX ms; memset(&ms, 0, sizeof ms); ms.dwLength = sizeof ms; GlobalMemoryStatusEx(&ms);
char cpu[128] = "?"; DWORD cl = sizeof cpu;
RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, 0, cpu, &cl);
uya_log("system: Windows %lu.%lu build %lu%s%s, %s, %lu cores, %llu MB RAM", v.major, v.minor, v.build,
wine_ver ? ", Wine " : "", wine_ver ? wine_ver() : "", cpu, si.dwNumberOfProcessors, (unsigned long long)(ms.ullTotalPhys >> 20));
}
uint64_t plat_time_us(void) uint64_t plat_time_us(void)
{ {
static LARGE_INTEGER f; static LARGE_INTEGER f;
+5
View File
@@ -109,6 +109,11 @@ void uya_log_open(void)
{ {
char p[1024]; char p[1024];
snprintf(p, sizeof p, "%s/rac1_log.txt", plat_exe_dir()); snprintf(p, sizeof p, "%s/rac1_log.txt", plat_exe_dir());
{ /* the previous run's log is kept as rac1_log.prev.txt (a retry after a crash keeps the crash) */
char q[1100];
snprintf(q, sizeof q, "%s/rac1_log.prev.txt", plat_exe_dir());
remove(q); rename(p, q);
}
logfile = fopen(p, "w"); logfile = fopen(p, "w");
if (logfile) fprintf(stderr, "[uya] log file: %s\n", p); if (logfile) fprintf(stderr, "[uya] log file: %s\n", p);
} }
+137 -10
View File
@@ -6,6 +6,7 @@
project-solana status what is installed, and whether the 32-bit libraries it needs are there project-solana status what is installed, and whether the 32-bit libraries it needs are there
project-solana desktop add Project Solana to your applications menu (and so to Steam) project-solana desktop add Project Solana to your applications menu (and so to Steam)
project-solana log print the last game log (rac1_log.txt) project-solana log print the last game log (rac1_log.txt)
project-solana report write one file with everything needed for a bug report
Like the Windows launcher: nothing from the game is downloaded. The build files (the Like the Windows launcher: nothing from the game is downloaded. The build files (the
recompiler, the runtime, the decomp's source) come from the project's GitHub release, the recompiler, the runtime, the decomp's source) come from the project's GitHub release, the
@@ -25,12 +26,43 @@ OTHER = {"SCUS_971.99": "Ratchet & Clank (NTSC-U)", "SCPS_150.37": "Ratchet & Cl
"SCUS_973.53": "Ratchet & Clank: Up Your Arsenal (NTSC-U)", "SCES_524.56": "Ratchet & Clank 3 (PAL)", "SCUS_973.53": "Ratchet & Clank: Up Your Arsenal (NTSC-U)", "SCES_524.56": "Ratchet & Clank 3 (PAL)",
"SCUS_974.65": "Ratchet: Deadlocked (NTSC-U)", "SCES_532.85": "Ratchet: Gladiator (PAL)"} "SCUS_974.65": "Ratchet: Deadlocked (NTSC-U)", "SCES_532.85": "Ratchet: Gladiator (PAL)"}
VERSION = "1.1"
HOME = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "project-solana") HOME = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "project-solana")
INI = os.path.join(HOME, "solana.ini") INI = os.path.join(HOME, "solana.ini")
LAUNCHER_LOG = os.path.join(HOME, "launcher_log.txt")
def say(*a): print(*a, flush=True) def log(text):
def die(msg): sys.exit("project-solana: " + msg) """everything the launcher says also goes to launcher_log.txt (started from Steam or a menu,
there is no terminal to read it in)"""
try:
os.makedirs(HOME, exist_ok=True)
if os.path.exists(LAUNCHER_LOG) and os.path.getsize(LAUNCHER_LOG) > (1 << 20):
os.replace(LAUNCHER_LOG, LAUNCHER_LOG + ".old")
with open(LAUNCHER_LOG, "a") as f: f.write(time.strftime("%Y-%m-%d %H:%M:%S ") + text + "\n")
except OSError:
pass
def notify(text):
"""a desktop notification, when there is no terminal to print to"""
if sys.stdout.isatty(): return
for cmd in (["notify-send", "-a", "Project Solana", "Project Solana", text], ["zenity", "--error", "--title=Project Solana", "--text=" + text],
["kdialog", "--title", "Project Solana", "--error", text]):
if shutil.which(cmd[0]):
try: subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL); return
except OSError: pass
def say(*a):
t = " ".join(str(x) for x in a)
print(t, flush=True); log(t)
def die(msg):
log("error: " + msg)
notify(msg + "\n\n(project-solana report writes a file to send with a bug report)")
sys.exit("project-solana: " + msg)
def settings(): def settings():
@@ -141,15 +173,20 @@ def have_lib(name):
return False return False
def lib_paths(name):
return [os.path.join(d, name) for d in LIBDIRS + ["/lib", "/usr/lib"] if os.path.exists(os.path.join(d, name))]
def check_libs(quiet=False): def check_libs(quiet=False):
missing = [n for n in ("libSDL2-2.0.so.0", "libGL.so.1") if not have_lib(n)] missing = [n for n in ("libSDL2-2.0.so.0", "libGL.so.1") if not have_lib(n)]
if not lib_paths("ld-linux.so.2"): missing.insert(0, "the 32-bit C library (ld-linux.so.2)")
if missing and not quiet: if missing and not quiet:
say("\nThe game is a 32-bit program and needs these 32-bit libraries: %s" % ", ".join(missing)) say("\nThe game is a 32-bit program and needs these 32-bit libraries: %s" % ", ".join(missing))
say("Installing Steam brings them. Otherwise:") say("Installing Steam brings them. Otherwise:")
say(" Debian, Ubuntu, Mint, Pop!_OS: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt install libsdl2-2.0-0:i386 libgl1:i386 libgl1-mesa-dri:i386") say(" Debian, Ubuntu, Mint, Pop!_OS: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt install libc6:i386 libsdl2-2.0-0:i386 libgl1:i386 libgl1-mesa-dri:i386")
say(" Fedora: sudo dnf install SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686") say(" Fedora: sudo dnf install glibc.i686 SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686")
say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-sdl2 lib32-mesa") say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-glibc lib32-sdl2 lib32-mesa")
say(" openSUSE: sudo zypper install libSDL2-2_0-0-32bit Mesa-libGL1-32bit") say(" openSUSE: sudo zypper install glibc-32bit libSDL2-2_0-0-32bit Mesa-libGL1-32bit")
say(" (NVIDIA's own driver: also its 32-bit OpenGL library, e.g. lib32-nvidia-utils on Arch)") say(" (NVIDIA's own driver: also its 32-bit OpenGL library, e.g. lib32-nvidia-utils on Arch)")
return not missing return not missing
@@ -208,6 +245,7 @@ def build(args):
if e != "build": rmtree(os.path.join(gdir, e)) if e != "build": rmtree(os.path.join(gdir, e))
rmtree(os.path.join(gdir, "build", "linux", "gen")); rmtree(os.path.join(gdir, "build", "linux", "obj")) rmtree(os.path.join(gdir, "build", "linux", "gen")); rmtree(os.path.join(gdir, "build", "linux", "obj"))
unzip(zp, gdir, "build kit"); os.remove(zp) unzip(zp, gdir, "build kit"); os.remove(zp)
open(kv, "w").write(man["version"]) # (unpacked: an interrupted build resumes with what it compiled)
levels = c.get("build", "levels", fallback="") or man.get("levels") or ",".join(str(i) for i in range(19)) levels = c.get("build", "levels", fallback="") or man.get("levels") or ",".join(str(i) for i in range(19))
cpus = os.cpu_count() or 2 cpus = os.cpu_count() or 2
@@ -227,7 +265,6 @@ def build(args):
p.wait(); log.close(); print() p.wait(); log.close(); print()
if p.returncode: die("the build stopped (see %s)" % os.path.join(gdir, "build_log.txt")) if p.returncode: die("the build stopped (see %s)" % os.path.join(gdir, "build_log.txt"))
for j in ("build/linux/gen", "build/linux/gen.new", "build/linux/obj", "build/levels", "build/disc", "zig-cache"): rmtree(os.path.join(gdir, j)) for j in ("build/linux/gen", "build/linux/gen.new", "build/linux/obj", "build/levels", "build/disc", "zig-cache"): rmtree(os.path.join(gdir, j))
open(kv, "w").write(man["version"])
c.set("disc", kit, iso); c.set("build", "built_" + kit, man["version"]); save(c) c.set("disc", kit, iso); c.set("build", "built_" + kit, man["version"]); save(c)
say("Built. Ready to play: project-solana play") say("Built. Ready to play: project-solana play")
@@ -257,8 +294,21 @@ def play(args):
if full: env["UYA_FULLSCREEN"] = "1" if full: env["UYA_FULLSCREEN"] = "1"
if pc.get("vsync", "0") in ("1", "True", "true"): env["UYA_VSYNC"] = "1" if pc.get("vsync", "0") in ("1", "True", "true"): env["UYA_VSYNC"] = "1"
if not check_libs(quiet=True): check_libs(); die("install the 32-bit libraries above, then play again") if not check_libs(quiet=True): check_libs(); die("install the 32-bit libraries above, then play again")
os.chdir(os.path.dirname(exe)) gdir = os.path.dirname(exe)
os.execve(exe, [exe, "-", iso], env) out = os.path.join(gdir, "game_output.txt") # what SDL, the graphics driver and the game print (kept: one run back)
if os.path.exists(out): os.replace(out, out + ".prev")
log("play: %s (UYA_SCALE=%s, fullscreen %s, vsync %s)" % (iso, env.get("UYA_SCALE", "1"), full, pc.get("vsync", "0")))
with open(out, "w") as o:
try:
p = subprocess.Popen([exe, "-", iso], cwd=gdir, env=env, stdout=o, stderr=subprocess.STDOUT)
except OSError as e:
die("the game could not start (%s): run project-solana status" % e)
try: rc = p.wait()
except KeyboardInterrupt: p.terminate(); rc = p.wait()
import signal
if rc == 0: log("play: the game closed normally"); return
why = ("it crashed (%s)" % signal.Signals(-rc).name) if rc < 0 and -rc in [s.value for s in signal.Signals] else "it stopped with an error (code %d)" % rc
die("the game closed: %s. project-solana report writes a file to send with a bug report" % why)
def status(args): def status(args):
@@ -291,8 +341,74 @@ def show_log(args):
sys.stdout.write(open(p, errors="replace").read()) sys.stdout.write(open(p, errors="replace").read())
def tail(path, n):
try:
with open(path, errors="replace") as f: lines = f.readlines()
except OSError:
return "(none)\n"
return ("(... %d earlier lines left out)\n" % (len(lines) - n) if len(lines) > n else "") + "".join(lines[-n:])
def report(args):
"""one text file with what a bug report needs; nothing from the game, and no file of the user's"""
c = settings()
gdir = os.path.join(HOME, "games", "rac1")
bdir = os.path.join(gdir, "build", "linux")
out = []
w = out.append
w("Project Solana bug report (Linux launcher %s), %s\n" % (VERSION, time.strftime("%Y-%m-%d %H:%M:%S %z")))
u = os.uname()
pretty = "?"
try:
for l in open("/etc/os-release"):
if l.startswith("PRETTY_NAME="): pretty = l.split("=", 1)[1].strip().strip('"')
except OSError: pass
w("\n== system\nLinux %s %s, %s\n" % (u.release, u.machine, pretty))
try:
cpu = next((l.split(":", 1)[1].strip() for l in open("/proc/cpuinfo") if l.startswith("model name")), "?")
mem = next((l.split(":", 1)[1].strip() for l in open("/proc/meminfo") if l.startswith("MemTotal")), "?")
except OSError: cpu = mem = "?"
w("CPU %s, %d cores, RAM %s\n" % (cpu, os.cpu_count() or 0, mem))
for k in ("XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP", "DISPLAY", "WAYLAND_DISPLAY", "SteamDeck", "SteamAppId"):
w("%s=%s\n" % (k, os.environ.get(k, "")))
for cmd in (["lspci"], ["glxinfo", "-B"]):
if shutil.which(cmd[0]):
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10).stdout
if cmd[0] == "lspci": r = "".join(l + "\n" for l in r.splitlines() if "VGA" in l or "3D" in l or "Display" in l)
w("-- %s\n%s" % (" ".join(cmd), r))
except Exception: pass
w("\n== 32-bit libraries\n")
for n in ("ld-linux.so.2", "libSDL2-2.0.so.0", "libGL.so.1"):
w("%s: %s\n" % (n, ", ".join(lib_paths(n)) or ("found by ldconfig" if have_lib(n) else "MISSING")))
w("\n== Project Solana\ndata folder: %s\n" % HOME)
tv = os.path.join(HOME, "toolchain", "version.txt")
w("build tools: %s\n" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded"))
kv = os.path.join(gdir, "kit_version.txt")
w("build kit: %s\n" % (open(kv).read().strip() if os.path.exists(kv) else "none"))
w("game built: %s\n" % ("yes" if os.path.exists(os.path.join(bdir, "rac1_wrapper")) else "no"))
iso = c.get("disc", "rac1", fallback="")
w("disc image: %s\n" % ((("found, %.2f GB" % (os.path.getsize(iso) / 1e9)) if os.path.exists(iso) else "MISSING (moved?)") if iso else "none"))
w("settings:\n")
for s in c.sections():
for k, v in c.items(s):
if s == "disc" or k == "release_url": continue
w(" [%s] %s = %s\n" % (s, k, v))
for title, path, n in (("launcher log", LAUNCHER_LOG, 60), ("build log (end)", os.path.join(gdir, "build_log.txt"), 80),
("game log", os.path.join(bdir, "rac1_log.txt"), 400), ("game log, the run before", os.path.join(bdir, "rac1_log.prev.txt"), 150),
("game output (SDL, graphics driver)", os.path.join(bdir, "game_output.txt"), 120),
("game output, the run before", os.path.join(bdir, "game_output.txt.prev"), 60)):
w("\n== %s\n%s" % (title, tail(path, n)))
text = "".join(out).replace(os.path.expanduser("~"), "~") # (no user name in the report)
dest_dir = next((d for d in (os.path.expanduser("~/Desktop"), os.path.expanduser("~")) if os.path.isdir(d)), HOME)
dest = os.path.join(dest_dir, "project-solana-report-%s.txt" % time.strftime("%Y%m%d-%H%M%S"))
open(dest, "w").write(text)
say("Wrote %s: attach it to your bug report (it has your system details and the logs, nothing else)." % dest)
def main(): def main():
ap = argparse.ArgumentParser(prog="project-solana", description="Build Ratchet & Clank from your own disc, then play it.") ap = argparse.ArgumentParser(prog="project-solana", description="Build Ratchet & Clank from your own disc, then play it.")
ap.add_argument("--version", action="version", version="project-solana %s (Linux launcher)" % VERSION)
sp = ap.add_subparsers(dest="cmd") sp = ap.add_subparsers(dest="cmd")
b = sp.add_parser("build", help="build the game from your disc image") b = sp.add_parser("build", help="build the game from your disc image")
b.add_argument("disc"); b.add_argument("-j", "--jobs", type=int, default=0) b.add_argument("disc"); b.add_argument("-j", "--jobs", type=int, default=0)
@@ -310,9 +426,20 @@ def main():
v.add_argument("--no-vsync", dest="vsync", action="store_const", const=0) v.add_argument("--no-vsync", dest="vsync", action="store_const", const=0)
sp.add_parser("status", help="what is installed"); sp.add_parser("desktop", help="add it to the applications menu") sp.add_parser("status", help="what is installed"); sp.add_parser("desktop", help="add it to the applications menu")
sp.add_parser("log", help="print the last game log") sp.add_parser("log", help="print the last game log")
sp.add_parser("report", help="write one file with everything a bug report needs")
a = ap.parse_args() a = ap.parse_args()
if sys.platform != "linux": die("this is the Linux launcher: on Windows, use Project Solana.exe") if sys.platform != "linux": die("this is the Linux launcher: on Windows, use Project Solana.exe")
{"build": build, "play": play, "status": status, "desktop": desktop, "log": show_log}.get(a.cmd, lambda _: ap.print_help())(a) log("project-solana %s: %s" % (VERSION, " ".join(sys.argv[1:])))
try:
{"build": build, "play": play, "status": status, "desktop": desktop, "log": show_log, "report": report}.get(a.cmd, lambda _: ap.print_help())(a)
except SystemExit:
raise
except KeyboardInterrupt:
sys.exit(130)
except Exception as e: # (an unexpected error: in the launcher log too, with where)
import traceback
log("unexpected error:\n" + traceback.format_exc())
die("unexpected error: %s (details in %s)" % (e, LAUNCHER_LOG))
if __name__ == "__main__": if __name__ == "__main__":