mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 21:43:23 -05:00
fix: follow linked dirs and ask for a platform
This commit is contained in:
1 parent
d66e891051
commit
3d9df87dd0
4 files changed
+164
-12
No files matched your search
+1
-1
@@ -8,7 +8,7 @@ param(
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "1e06e5d364d93beff2f9b98f7bf81b4eca4b6364a6a99c87e8fcc9a472c70764"
|
||||
$defaultInstallSha256 = "1cb76eb63b57e1390bfb4b6c2b24a5991819541414150aa88edc76d1d1188cd6"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
$temporary = $null
|
||||
|
||||
+75
-10
@@ -588,6 +588,20 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
|
||||
if ra_dir:
|
||||
found.append(("retroarch", ra_dir))
|
||||
|
||||
# RetroArch from Steam: a portable install under the default library
|
||||
for var in ("ProgramFiles(x86)", "ProgramFiles"):
|
||||
program_files = os.environ.get(var, "")
|
||||
if not program_files:
|
||||
continue
|
||||
steam_cfg = (
|
||||
Path(program_files) / "Steam" / "steamapps" / "common"
|
||||
/ "RetroArch" / "retroarch.cfg"
|
||||
)
|
||||
ra_dir = _parse_retroarch_system_dir(steam_cfg)
|
||||
if ra_dir and not any(name == "retroarch" for name, _ in found):
|
||||
found.append(("retroarch", ra_dir))
|
||||
break
|
||||
|
||||
# Portable RetroArch referenced by LaunchBox
|
||||
lb_root = launchbox_root(os_type)
|
||||
if lb_root and not any(name == "retroarch" for name, _ in found):
|
||||
@@ -676,15 +690,16 @@ def _safe_target_dir(value: object, field: str) -> str:
|
||||
|
||||
|
||||
def _destination_path(root: Path, value: object) -> Path:
|
||||
"""Resolve a manifest destination and prove it remains below *root*."""
|
||||
"""Place a manifest destination below *root*.
|
||||
|
||||
The manifest cannot climb: _safe_relative_path refuses absolute paths,
|
||||
drive letters and '..'. Symbolic links met under the root are the
|
||||
user's own layout and are followed. EmuDeck links bios/shadps4/sys_modules
|
||||
into shadPS4's data directory, and the file has to land where the
|
||||
emulator reads it.
|
||||
"""
|
||||
relative = _safe_relative_path(value, "dest")
|
||||
resolved_root = root.resolve()
|
||||
candidate = (resolved_root / Path(*relative.parts)).resolve()
|
||||
try:
|
||||
candidate.relative_to(resolved_root)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"destination escapes BIOS directory: {value!r}") from exc
|
||||
return candidate
|
||||
return root.resolve() / Path(*relative.parts)
|
||||
|
||||
|
||||
def _validate_manifest(data: object, plat: str) -> dict:
|
||||
@@ -1172,6 +1187,53 @@ def format_size(n: int) -> str:
|
||||
return f"{n / (1024 * 1024 * 1024):.1f} GB"
|
||||
|
||||
|
||||
def _prompt_manual_platform(os_type: str) -> list[tuple[str, Path]]:
|
||||
"""Ask for a platform and a BIOS directory when nothing was detected."""
|
||||
print("\nChoose the platform to install for:")
|
||||
for i, name in enumerate(AVAILABLE_PLATFORMS, 1):
|
||||
print(f" {i}) {name}")
|
||||
print(" q) quit")
|
||||
print()
|
||||
plat = ""
|
||||
while not plat:
|
||||
try:
|
||||
choice = input("> ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
sys.exit(0)
|
||||
if choice == "q":
|
||||
sys.exit(0)
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(AVAILABLE_PLATFORMS):
|
||||
plat = AVAILABLE_PLATFORMS[int(choice) - 1]
|
||||
elif choice in AVAILABLE_PLATFORMS:
|
||||
plat = choice
|
||||
default_dest = DEFAULT_DESTS.get(plat, Path.home() / "bios")
|
||||
try:
|
||||
raw = input(f"BIOS directory [{default_dest}]: ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
sys.exit(0)
|
||||
dest = Path(raw).expanduser() if raw else default_dest
|
||||
if os_type == "windows" and raw:
|
||||
dest = Path(os.path.expandvars(raw))
|
||||
return [(plat, dest)]
|
||||
|
||||
|
||||
def _manual_usage_hint(os_type: str) -> list[str]:
|
||||
"""How to name the platform and directory when nothing is detected."""
|
||||
if os_type == "windows":
|
||||
return [
|
||||
" Use --platform and --dest to specify manually, for example:",
|
||||
" & ([ScriptBlock]::Create((iwr -useb "
|
||||
"https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1))) "
|
||||
"--platform retroarch --dest 'C:\\RetroArch\\system'",
|
||||
]
|
||||
return [
|
||||
" Use --platform and --dest to specify manually, for example:",
|
||||
" python3 install.py --platform retroarch --dest ~/.config/retroarch/system",
|
||||
]
|
||||
|
||||
|
||||
def _prompt_platform_choice(
|
||||
platforms: list[tuple[str, Path]],
|
||||
) -> list[tuple[str, Path]]:
|
||||
@@ -1301,8 +1363,11 @@ def main() -> None:
|
||||
platforms = detect_platforms(os_type)
|
||||
if not platforms:
|
||||
print(" No supported platform detected.")
|
||||
print(" Use --platform and --dest to specify manually.")
|
||||
sys.exit(1)
|
||||
if not sys.stdin.isatty() or args.list_targets:
|
||||
for line in _manual_usage_hint(os_type):
|
||||
print(line)
|
||||
sys.exit(1)
|
||||
platforms = _prompt_manual_platform(os_type)
|
||||
for name, path in platforms:
|
||||
print(f" Found {name.capitalize()} at {path}")
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ esac
|
||||
TEMP_INSTALLER=""
|
||||
TEMP_DIRECTORY=""
|
||||
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
DEFAULT_INSTALL_SHA256="1e06e5d364d93beff2f9b98f7bf81b4eca4b6364a6a99c87e8fcc9a472c70764"
|
||||
DEFAULT_INSTALL_SHA256="1cb76eb63b57e1390bfb4b6c2b24a5991819541414150aa88edc76d1d1188cd6"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -730,6 +730,93 @@ class TestStandaloneCopyTargetsAreUntrusted(unittest.TestCase):
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("pwsh"), "pwsh not available")
|
||||
class TestDestinationFollowsUserLinks(unittest.TestCase):
|
||||
"""EmuDeck links bios/shadps4/sys_modules into shadPS4's own directory.
|
||||
|
||||
The manifest still cannot climb out of the BIOS root, but a link the
|
||||
user's frontend placed under it is where the emulator reads the file.
|
||||
"""
|
||||
|
||||
def test_symlinked_directory_under_root_is_followed(self):
|
||||
bios = Path(tempfile.mkdtemp())
|
||||
elsewhere = Path(tempfile.mkdtemp()) / "sys_modules"
|
||||
elsewhere.mkdir()
|
||||
(bios / "shadps4").mkdir()
|
||||
(bios / "shadps4" / "sys_modules").symlink_to(elsewhere)
|
||||
|
||||
dest = install._destination_path(bios, "shadps4/sys_modules/libSceLibcInternal.sprx")
|
||||
|
||||
self.assertEqual(dest, bios.resolve() / "shadps4" / "sys_modules" / "libSceLibcInternal.sprx")
|
||||
self.assertEqual(dest.resolve().parent, elsewhere.resolve())
|
||||
|
||||
def test_check_local_survives_a_linked_directory(self):
|
||||
bios = Path(tempfile.mkdtemp())
|
||||
elsewhere = Path(tempfile.mkdtemp()) / "sys_modules"
|
||||
elsewhere.mkdir()
|
||||
(bios / "shadps4").mkdir()
|
||||
(bios / "shadps4" / "sys_modules").symlink_to(elsewhere)
|
||||
entry = {"dest": "shadps4/sys_modules/x.sprx", "size": 1, "sha1": "0" * 40}
|
||||
|
||||
to_download, up_to_date, mismatched = install.check_local([entry], bios)
|
||||
|
||||
self.assertEqual([e["dest"] for e in to_download], ["shadps4/sys_modules/x.sprx"])
|
||||
self.assertEqual((up_to_date, mismatched), ([], []))
|
||||
|
||||
def test_climbing_is_still_refused(self):
|
||||
bios = Path(tempfile.mkdtemp())
|
||||
with self.assertRaises(ValueError):
|
||||
install._destination_path(bios, "../outside.bin")
|
||||
with self.assertRaises(ValueError):
|
||||
install._destination_path(bios, "/etc/passwd")
|
||||
|
||||
|
||||
class TestManualPlatformPrompt(unittest.TestCase):
|
||||
"""With nothing detected, an interactive run asks instead of exiting."""
|
||||
|
||||
def test_number_picks_the_platform_and_default_directory(self):
|
||||
answers = iter(["1", ""])
|
||||
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)):
|
||||
chosen = install._prompt_manual_platform("linux")
|
||||
self.assertEqual(chosen, [("retroarch", Path.home() / "bios")])
|
||||
|
||||
def test_name_and_explicit_directory(self):
|
||||
answers = iter(["batocera", "~/my bios"])
|
||||
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)):
|
||||
chosen = install._prompt_manual_platform("linux")
|
||||
self.assertEqual(chosen, [("batocera", Path("~/my bios").expanduser())])
|
||||
|
||||
def test_default_directory_comes_from_the_table(self):
|
||||
answers = iter(["batocera", ""])
|
||||
with unittest.mock.patch("builtins.input", lambda _prompt="": next(answers)):
|
||||
chosen = install._prompt_manual_platform("linux")
|
||||
self.assertEqual(chosen, [("batocera", install.DEFAULT_DESTS["batocera"])])
|
||||
|
||||
def test_quit_exits_cleanly(self):
|
||||
with unittest.mock.patch("builtins.input", lambda _prompt="": "q"):
|
||||
with self.assertRaises(SystemExit) as raised:
|
||||
install._prompt_manual_platform("linux")
|
||||
self.assertEqual(raised.exception.code, 0)
|
||||
|
||||
def test_windows_hint_shows_the_one_liner_with_arguments(self):
|
||||
hint = "\n".join(install._manual_usage_hint("windows"))
|
||||
self.assertIn("--platform retroarch --dest", hint)
|
||||
self.assertIn("install.ps1", hint)
|
||||
|
||||
|
||||
class TestSteamRetroarchDetection(unittest.TestCase):
|
||||
"""The Steam build is a portable install under the default library."""
|
||||
|
||||
def test_steam_library_is_found_on_windows(self):
|
||||
program_files = Path(tempfile.mkdtemp())
|
||||
ra_dir = program_files / "Steam" / "steamapps" / "common" / "RetroArch"
|
||||
ra_dir.mkdir(parents=True)
|
||||
(ra_dir / "retroarch.cfg").write_text('system_directory = ":\\system"\n')
|
||||
env = {"ProgramFiles(x86)": str(program_files)}
|
||||
with unittest.mock.patch.dict(os.environ, env, clear=True):
|
||||
found = install.detect_platforms("windows")
|
||||
self.assertIn(("retroarch", ra_dir / "system"), found)
|
||||
|
||||
|
||||
class TestLaunchboxDetectionPowershell(unittest.TestCase):
|
||||
"""install.ps1 must resolve LaunchBox's portable RetroArch on its own."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user