mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
duckstation tools
This commit is contained in:
@@ -159,13 +159,23 @@ def log(msg: str) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def data_root() -> Path:
|
||||
"""The RetComM shared data root — same one the launcher and Studio use."""
|
||||
"""The RetComM shared data root — same one the launcher and Studio use.
|
||||
|
||||
The precedence must match studio_runner.cpp's retcomm_data_dir() exactly,
|
||||
XDG_DATA_HOME included. If the two disagree, the GUI reports an oracle
|
||||
that is not where this tool installed it, and neither side is obviously
|
||||
wrong — the worst kind of bug to chase.
|
||||
"""
|
||||
env = os.environ.get("RETCOMM_DATA_DIR")
|
||||
if env:
|
||||
return Path(env).expanduser()
|
||||
if sys.platform == "win32":
|
||||
base = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
|
||||
base = (os.environ.get("LOCALAPPDATA")
|
||||
or os.environ.get("USERPROFILE", str(Path.home())) + "/AppData/Local")
|
||||
return Path(base) / "retcomm"
|
||||
xdg = os.environ.get("XDG_DATA_HOME")
|
||||
if xdg:
|
||||
return Path(xdg).expanduser() / "retcomm"
|
||||
return Path.home() / ".local" / "share" / "retcomm"
|
||||
|
||||
|
||||
@@ -958,8 +968,64 @@ def state(lay: Layout) -> Dict[str, str]:
|
||||
return out
|
||||
|
||||
|
||||
def status_doc(lay: Layout) -> Dict[str, Any]:
|
||||
"""Everything a caller (or a GUI) needs to decide what button to offer."""
|
||||
pin = load_pin()
|
||||
doc: Dict[str, Any] = {}
|
||||
if lay.manifest.is_file():
|
||||
try:
|
||||
with open(lay.manifest, "r", encoding="utf-8") as f:
|
||||
doc.update(json.load(f))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass
|
||||
st = state(lay)
|
||||
running = st["running"] != "no"
|
||||
answering = st["running"].startswith("yes")
|
||||
doc.update({
|
||||
# Set AFTER the manifest merge: oracle.json carries its own `kind`
|
||||
# ("psxrecomp-duckstation-oracle") and would otherwise overwrite this
|
||||
# one, so every installed oracle would report a document Studio then
|
||||
# rejects as foreign — while an uninstalled one looked fine.
|
||||
"kind": "psxrecomp-oracle-status",
|
||||
"version": 1,
|
||||
"root": str(lay.root),
|
||||
"app": str(lay.app),
|
||||
"binary": str(lay.binary),
|
||||
"launcher": str(lay.launcher),
|
||||
"port": int(pin.get("oracle_port", 4371)),
|
||||
"source": st["source"] == "present",
|
||||
"deps": st["deps"] == "present",
|
||||
"patch_applied": st["patch"] == "applied",
|
||||
"built": st["built"] == "yes",
|
||||
"installed": st["installed"] == "yes",
|
||||
"running": running,
|
||||
"answering": answering,
|
||||
"running_detail": st["running"],
|
||||
"container_needed": bool(refused_environment()),
|
||||
"container_reason": refused_environment(),
|
||||
"container_engine": container_engine(),
|
||||
})
|
||||
# One word for a status line, so a GUI does not re-derive the ladder.
|
||||
if answering:
|
||||
doc["state"] = "answering"
|
||||
elif running:
|
||||
doc["state"] = "port-busy"
|
||||
elif doc["installed"]:
|
||||
doc["state"] = "installed"
|
||||
elif doc["built"]:
|
||||
doc["state"] = "built"
|
||||
elif doc["source"]:
|
||||
doc["state"] = "fetched"
|
||||
else:
|
||||
doc["state"] = "absent"
|
||||
return doc
|
||||
|
||||
|
||||
def cmd_status(args: argparse.Namespace) -> int:
|
||||
lay = Layout(Path(args.root) if args.root else None)
|
||||
if args.json:
|
||||
print(json.dumps(status_doc(lay), indent=1))
|
||||
return 0
|
||||
print(f"root: {lay.root}")
|
||||
if lay.manifest.is_file():
|
||||
with open(lay.manifest, "r", encoding="utf-8") as f:
|
||||
@@ -1141,6 +1207,8 @@ def main(argv=None) -> int:
|
||||
p.set_defaults(func=cmd_stop)
|
||||
|
||||
p = sub.add_parser("status", help="where it is and what state it is in")
|
||||
p.add_argument("--json", action="store_true",
|
||||
help="machine-readable, for RetComM Studio")
|
||||
p.set_defaults(func=cmd_status)
|
||||
|
||||
p = sub.add_parser("path", help="print the install root")
|
||||
@@ -1151,7 +1219,7 @@ def main(argv=None) -> int:
|
||||
for attr, default in (("cc", None), ("cxx", None), ("jobs", 0), ("lto", False),
|
||||
("reconfigure", False), ("cmake_arg", None),
|
||||
("force", False), ("bios", None), ("root", None),
|
||||
("wayland", False), ("container", None)):
|
||||
("wayland", False), ("container", None), ("json", False)):
|
||||
if not hasattr(args, attr):
|
||||
setattr(args, attr, default)
|
||||
try:
|
||||
|
||||
@@ -50,6 +50,24 @@ class TestLocations(EnvGuard):
|
||||
self.assertEqual(DSO.oracle_root(), Path("/tmp/rc-test/oracle/duckstation"))
|
||||
self.assertEqual(DSO.download_cache(), Path("/tmp/rc-test/downloads"))
|
||||
|
||||
def test_xdg_data_home_is_honoured_like_studio_does(self):
|
||||
"""studio_runner.cpp's retcomm_data_dir() checks XDG_DATA_HOME before
|
||||
~/.local/share. If this tool did not, the GUI would look for an oracle
|
||||
somewhere other than where the tool installed it, and neither side would
|
||||
look wrong."""
|
||||
os.environ.pop("RETCOMM_ORACLE_DIR", None)
|
||||
os.environ.pop("RETCOMM_DATA_DIR", None)
|
||||
os.environ["XDG_DATA_HOME"] = "/tmp/xdg-test"
|
||||
self.assertEqual(DSO.data_root(), Path("/tmp/xdg-test/retcomm"))
|
||||
self.assertEqual(DSO.oracle_root(),
|
||||
Path("/tmp/xdg-test/retcomm/oracle/duckstation"))
|
||||
|
||||
def test_retcomm_data_dir_beats_xdg(self):
|
||||
os.environ.pop("RETCOMM_ORACLE_DIR", None)
|
||||
os.environ["XDG_DATA_HOME"] = "/tmp/xdg-test"
|
||||
os.environ["RETCOMM_DATA_DIR"] = "/tmp/explicit"
|
||||
self.assertEqual(DSO.data_root(), Path("/tmp/explicit"))
|
||||
|
||||
def test_oracle_dir_override_wins(self):
|
||||
os.environ["RETCOMM_DATA_DIR"] = "/tmp/rc-test"
|
||||
os.environ["RETCOMM_ORACLE_DIR"] = "/tmp/elsewhere"
|
||||
@@ -174,6 +192,55 @@ class TestSettingsMerge(unittest.TestCase):
|
||||
self.assertIn("Renderer = Software", p.read_text())
|
||||
|
||||
|
||||
class TestStatusDoc(EnvGuard):
|
||||
"""The status document is the whole contract with RetComM Studio."""
|
||||
|
||||
def test_absent_install_reports_absent(self):
|
||||
os.environ["RETCOMM_ORACLE_DIR"] = tempfile.mkdtemp()
|
||||
doc = DSO.status_doc(DSO.Layout())
|
||||
self.assertEqual(doc["kind"], "psxrecomp-oracle-status")
|
||||
self.assertEqual(doc["state"], "absent")
|
||||
for k in ("source", "deps", "built", "installed", "running", "answering"):
|
||||
self.assertFalse(doc[k], k)
|
||||
self.assertIn("port", doc)
|
||||
self.assertIn("container_needed", doc)
|
||||
|
||||
def test_state_ladder_is_ordered(self):
|
||||
"""A GUI picks its button off `state`, so the ladder must not skip."""
|
||||
os.environ["RETCOMM_ORACLE_DIR"] = tempfile.mkdtemp()
|
||||
lay = DSO.Layout()
|
||||
lay.src.mkdir(parents=True)
|
||||
(lay.src / "CMakeLists.txt").write_text("")
|
||||
self.assertEqual(DSO.status_doc(lay)["state"], "fetched")
|
||||
(lay.build / "bin").mkdir(parents=True)
|
||||
(lay.build / "bin" / DSO.exe_name()).write_text("")
|
||||
self.assertEqual(DSO.status_doc(lay)["state"], "built")
|
||||
lay.app.mkdir(parents=True)
|
||||
(lay.app / DSO.exe_name()).write_text("")
|
||||
self.assertEqual(DSO.status_doc(lay)["state"], "installed")
|
||||
|
||||
def test_kind_survives_an_existing_manifest(self):
|
||||
"""oracle.json has its own `kind`. Merging it must not overwrite the
|
||||
status document's, or every INSTALLED oracle reports a document Studio
|
||||
rejects as foreign — while an uninstalled one parses fine, so the bug
|
||||
only appears once things are working."""
|
||||
os.environ["RETCOMM_ORACLE_DIR"] = tempfile.mkdtemp()
|
||||
lay = DSO.Layout()
|
||||
lay.root.mkdir(parents=True, exist_ok=True)
|
||||
lay.manifest.write_text(json.dumps({
|
||||
"kind": "psxrecomp-duckstation-oracle",
|
||||
"version": 1,
|
||||
"upstream_base": "deadbeef",
|
||||
}))
|
||||
doc = DSO.status_doc(lay)
|
||||
self.assertEqual(doc["kind"], "psxrecomp-oracle-status")
|
||||
self.assertEqual(doc["upstream_base"], "deadbeef") # manifest data kept
|
||||
|
||||
def test_json_is_serialisable(self):
|
||||
os.environ["RETCOMM_ORACLE_DIR"] = tempfile.mkdtemp()
|
||||
json.dumps(DSO.status_doc(DSO.Layout()))
|
||||
|
||||
|
||||
class TestPin(unittest.TestCase):
|
||||
def test_pin_is_the_single_source_of_truth(self):
|
||||
pin = DSO.load_pin()
|
||||
|
||||
Reference in New Issue
Block a user