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
+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 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 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
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_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")
INI = os.path.join(HOME, "solana.ini")
LAUNCHER_LOG = os.path.join(HOME, "launcher_log.txt")
def say(*a): print(*a, flush=True)
def die(msg): sys.exit("project-solana: " + msg)
def log(text):
"""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():
@@ -141,15 +173,20 @@ def have_lib(name):
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):
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:
say("\nThe game is a 32-bit program and needs these 32-bit libraries: %s" % ", ".join(missing))
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(" Fedora: sudo dnf install SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686")
say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-sdl2 lib32-mesa")
say(" openSUSE: sudo zypper install libSDL2-2_0-0-32bit Mesa-libGL1-32bit")
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 glibc.i686 SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686")
say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-glibc lib32-sdl2 lib32-mesa")
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)")
return not missing
@@ -208,6 +245,7 @@ def build(args):
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"))
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))
cpus = os.cpu_count() or 2
@@ -227,7 +265,6 @@ def build(args):
p.wait(); log.close(); print()
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))
open(kv, "w").write(man["version"])
c.set("disc", kit, iso); c.set("build", "built_" + kit, man["version"]); save(c)
say("Built. Ready to play: project-solana play")
@@ -257,8 +294,21 @@ def play(args):
if full: env["UYA_FULLSCREEN"] = "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")
os.chdir(os.path.dirname(exe))
os.execve(exe, [exe, "-", iso], env)
gdir = os.path.dirname(exe)
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):
@@ -291,8 +341,74 @@ def show_log(args):
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():
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")
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)
@@ -310,9 +426,20 @@ def main():
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("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()
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__":