mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
fix: name cards by files no menu rewrites
A KNULLI card renamed in its menu was read as a Batocera SHARE drive. KNULLI is now named by about.info first, Batocera by its own hostname, and a drive two systems fit is said instead of guessed. Runs without questions see mounted cards as the screen does and stop on two setups. The card marker table of the wiki is generated.
This commit is contained in:
1 parent
9c8dbcd99a
commit
a334ace439
8 files changed
+537
-66
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
|
||||
$defaultInstallSha256 = "71fa422aa38f9710c9aab81ba038cad71df66f574a527dc62e0307c69c30d1b0"
|
||||
$defaultInstallSha256 = "f4f2d041d923e32d46c828222f43f4359c5241fcce7ccc29bfc9b3e8565bba0b"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+208
-46
@@ -10262,14 +10262,28 @@ def _card_mount(path: str) -> str:
|
||||
|
||||
# Systems whose card a computer sees as their data partition, by the file
|
||||
# their own build writes there, paths below that partition. KNULLI's is
|
||||
# /userdata: S12populateshare:15,105 copies datainit there, and every board's
|
||||
# datainit/system/batocera.conf sets system.hostname=KNULLI (knulli-cfw
|
||||
# distribution ab3a6809, board/batocera/*/fsoverlay/usr/share/batocera/).
|
||||
# ROCKNIX's is /storage: userconfig-setup:12 copies /usr/config into
|
||||
# /storage/.config, and its system.cfg sets root.password=rocknix (ROCKNIX
|
||||
# distribution 8b70f693, projects/ROCKNIX/packages/rocknix/config/system/
|
||||
# configs/system.cfg:152).
|
||||
# /userdata, which S12populateshare fills from datainit (knulli-cfw
|
||||
# distribution ab3a6809, board/batocera/fsoverlay/etc/init.d/
|
||||
# S12populateshare:93). The first rule is system/configs/emulationstation/
|
||||
# about.info, which every board's datainit holds with the one line KNULLI
|
||||
# (board/batocera/*/fsoverlay/usr/share/batocera/datainit/system/configs/
|
||||
# emulationstation/about.info), and which S12populateshare copies back at
|
||||
# each start when it is missing or older. EmulationStation reads it as the
|
||||
# system's name (knulli-cfw/batocera-emulationstation f7c5ae10,
|
||||
# es-app/src/ApiSystem.cpp:190-208) and no menu writes it. The second is
|
||||
# system.hostname=KNULLI in system/batocera.conf, which datainit sets
|
||||
# (datainit/system/batocera.conf:485) and the HOSTNAME row of the menu
|
||||
# rewrites (es-app/src/guis/GuiMenu.cpp:4092): a card renamed there was
|
||||
# read as a Batocera SHARE drive. ROCKNIX's is /storage: userconfig-setup:12
|
||||
# copies /usr/config into /storage/.config, and its system.cfg sets
|
||||
# root.password=rocknix (ROCKNIX distribution 8b70f693, projects/ROCKNIX/
|
||||
# packages/rocknix/config/system/configs/system.cfg:153). Nothing else there
|
||||
# names ROCKNIX: a password changed with setrootpass
|
||||
# (packages/rocknix/sources/scripts/setrootpass:25) leaves the card named
|
||||
# by nothing, never by another system.
|
||||
_SHARE_RULES: tuple[_OsRule, ...] = (
|
||||
_OsRule(_STANDIN_MANIFESTS["KNULLI"], "KNULLI", "file_matches",
|
||||
("system/configs/emulationstation/about.info", r"\AKNULLI\s*$"), "bios"),
|
||||
_OsRule(_STANDIN_MANIFESTS["KNULLI"], "KNULLI", "file_matches",
|
||||
("system/batocera.conf", r"^system\.hostname=KNULLI\s*$"), "bios"),
|
||||
_OsRule("rocknix", "ROCKNIX", "file_matches",
|
||||
@@ -10342,22 +10356,92 @@ def card_system(root: Path) -> tuple[str, Path, str] | None:
|
||||
|
||||
|
||||
# The SHARE drive of a system a computer reaches over the network, by the
|
||||
# file the system keeps on it: Batocera's data part (/userdata) holds
|
||||
# system/batocera.conf, Recalbox's (/recalbox/share) system/recalbox.conf,
|
||||
# and both read bios/ beside it. KNULLI, built on Batocera, writes the same
|
||||
# batocera.conf with its own hostname: its rule (_SHARE_RULES) comes first.
|
||||
# Typed at the folder question, the drive was taken as the BIOS folder and
|
||||
# filled beside roms/ and system/, where neither reads.
|
||||
# file the system keeps on it, and both read bios/ beside it. Batocera's data
|
||||
# part (/userdata) holds system/batocera.conf, which S12populateshare copies
|
||||
# there (batocera.linux d1966272, board/batocera/fsoverlay/etc/init.d/
|
||||
# S12populateshare:104) and which sets system.hostname=BATOCERA
|
||||
# (package/batocera/core/batocera-system/batocera.conf:103). Recalbox's
|
||||
# (/recalbox/share) holds system/recalbox.conf (recalbox 64b0fd40,
|
||||
# board/recalbox/fsoverlay/etc/init.d/S12populateshare:93), a name no menu
|
||||
# changes. KNULLI, built on Batocera, writes the same batocera.conf: its
|
||||
# rules (_SHARE_RULES) are read first, and a batocera.conf whose hostname
|
||||
# is neither system's own, on a drive no about.info names, is one of the
|
||||
# two renamed in its menu. That drive is said to be either (share_doubt),
|
||||
# never taken for one: a renamed KNULLI card was filled with Batocera's
|
||||
# list. Typed at the folder question, a drive was taken as the BIOS folder
|
||||
# and filled beside roms/ and system/, where neither reads.
|
||||
_SHARE_DRIVES: tuple[tuple[str, str], ...] = (
|
||||
("batocera", "system/batocera.conf"),
|
||||
("recalbox", "system/recalbox.conf"),
|
||||
)
|
||||
_HOSTNAME_LINE = re.compile(r"^system\.hostname=(.*?)\s*$", re.MULTILINE)
|
||||
|
||||
|
||||
def _share_markers(root: Path) -> list[str]:
|
||||
"""The platforms whose SHARE drive file is on root's own drive."""
|
||||
return [plat for plat, marker in _SHARE_DRIVES
|
||||
if _is_file(root / marker) and _on_the_drive(root, marker)]
|
||||
|
||||
|
||||
def _share_hostname(root: Path) -> str | None:
|
||||
"""The hostname system/batocera.conf on root sets, "" when it sets none,
|
||||
None when the file cannot be read."""
|
||||
try:
|
||||
text = (root / "system" / "batocera.conf").read_text(encoding="utf-8",
|
||||
errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
found = _HOSTNAME_LINE.findall(text)
|
||||
return found[-1] if found else ""
|
||||
|
||||
|
||||
def share_drive(root: Path) -> str | None:
|
||||
"""The platform whose SHARE drive root is, by its own files, or None."""
|
||||
return next((plat for plat, marker in _SHARE_DRIVES
|
||||
if _is_file(root / marker) and _on_the_drive(root, marker)), None)
|
||||
"""The platform whose SHARE drive root is, by its own files, or None:
|
||||
for none, and for files that name two systems (share_doubt)."""
|
||||
markers = _share_markers(root)
|
||||
if len(markers) != 1:
|
||||
return None
|
||||
if markers == ["batocera"] and _share_hostname(root) not in ("", "BATOCERA"):
|
||||
return None
|
||||
return markers[0]
|
||||
|
||||
|
||||
def share_doubt(root: Path) -> tuple[tuple[str, ...], str]:
|
||||
"""(platforms, why) for a drive whose files fit more than one system,
|
||||
((), "") for any other: a batocera.conf renamed in its menu, with no
|
||||
file KNULLI alone writes, or the files of Batocera and Recalbox both."""
|
||||
if card_platform(root) or _card_matches(root):
|
||||
return (), ""
|
||||
markers = _share_markers(root)
|
||||
if len(markers) > 1:
|
||||
return tuple(markers), "it holds system/batocera.conf and system/recalbox.conf"
|
||||
if markers == ["batocera"]:
|
||||
hostname = _share_hostname(root)
|
||||
if hostname not in ("", "BATOCERA"):
|
||||
return ((_STANDIN_MANIFESTS["KNULLI"], "batocera"),
|
||||
f"its system/batocera.conf names the host {screen_text(hostname or '?')}, "
|
||||
"which the menus of KNULLI and Batocera both set")
|
||||
return (), ""
|
||||
|
||||
|
||||
def _doubt_kinds(plats: tuple[str, ...]) -> str:
|
||||
"""The systems a drive could be, in words: a KNULLI card or a Batocera
|
||||
SHARE drive."""
|
||||
return " or ".join(f"{_a(display_name(plat))} "
|
||||
f"{'SHARE drive' if plat in _SHARE_FOLDERS else 'card'}"
|
||||
for plat in plats)
|
||||
|
||||
|
||||
def doubt_words(root: Path) -> str:
|
||||
"""What is said of a mounted drive share_doubt cannot name, and how to
|
||||
name it: the same words on the first screen and in line mode."""
|
||||
plats, why = share_doubt(root)
|
||||
if not plats:
|
||||
return ""
|
||||
shown = shown_path(root)
|
||||
kinds = _doubt_kinds(plats)
|
||||
names = " or ".join(f"--platform {plat} --dest {shown}" for plat in plats)
|
||||
return f"{shown} could be {kinds}: {why}. Name it: {names}."
|
||||
|
||||
|
||||
def drive_kind(system: tuple[str, Path, str]) -> str:
|
||||
@@ -10447,8 +10531,11 @@ def card_setups() -> list[CardSetup]:
|
||||
rules = _card_rules()
|
||||
setups = [CardSetup(display_name(plat), plat, "") for plat in AVAILABLE_PLATFORMS
|
||||
if plat in CARD_PLATFORMS]
|
||||
setups += [CardSetup(rule.name, rule.manifest, rule.bios, (rule,), True)
|
||||
for rule in _SHARE_RULES]
|
||||
# KNULLI is named by two files: one setup holding both rules.
|
||||
shares = {rule.name: tuple(each for each in _SHARE_RULES if each.name == rule.name)
|
||||
for rule in _SHARE_RULES}
|
||||
setups += [CardSetup(name, rules[0].manifest, rules[0].bios, rules, True)
|
||||
for name, rules in shares.items()]
|
||||
# AmberELEC and EmuELEC mount their card's games part as /storage/roms,
|
||||
# the folder their list is written below: AmberELEC 5a3882cb fs-resize
|
||||
# formats it exFAT as GAMES (packages/sysutils/busybox/scripts/
|
||||
@@ -10529,9 +10616,12 @@ def _card_is(setup: CardSetup, card: Path, above: bool = False) -> tuple[str, st
|
||||
names = sorted({rule.name for rule in _card_matches(card, above)})
|
||||
if setup.name in names:
|
||||
return "own", setup.name
|
||||
if not names and share_drive(card):
|
||||
# A Batocera share is no KNULLI card not started yet.
|
||||
return "other", display_name(share_drive(card))
|
||||
share = share_drive(card)
|
||||
if not names and share and not (share == "batocera" and setup.name == "KNULLI"):
|
||||
# A Recalbox share is no KNULLI card not started yet. A Batocera
|
||||
# one may be: KNULLI writes the same batocera.conf, and a card
|
||||
# older than its about.info has nothing else (_unknown_card_note).
|
||||
return "other", display_name(share)
|
||||
return ("other", " and ".join(names)) if names else ("unknown", "")
|
||||
|
||||
|
||||
@@ -10576,11 +10666,17 @@ def _dest_refusal_words(choice: CardChoice, setup: CardSetup | None) -> str:
|
||||
KNULLI, and for a blank stick named for Onion, both refused the same."""
|
||||
detail = f" {choice.detail}." if choice.detail else ""
|
||||
name = setup.name if setup else display_name(choice.plat)
|
||||
if choice.other and choice.plat in _SHARE_FOLDERS and setup is None:
|
||||
return (f"{choice.refused}.{detail} Connect the {name} SHARE drive, or name that "
|
||||
f"drive's own system: --platform {_name_key(choice.other)}")
|
||||
if choice.other:
|
||||
return (f"{choice.refused}.{detail} Insert the {name} card, or name that "
|
||||
f"card's own system: --platform {_name_key(choice.other)}")
|
||||
if choice.root is not None:
|
||||
return f"{choice.refused}.{detail} Name the card's root: --dest {shown_path(choice.root)}"
|
||||
if choice.plat in _SHARE_FOLDERS and setup is None:
|
||||
return (f"{choice.refused}.{detail} Connect the {name} SHARE drive and name its "
|
||||
"root with --dest")
|
||||
return f"{choice.refused}.{detail} Name the top folder of the {name} card with --dest"
|
||||
|
||||
|
||||
@@ -10589,6 +10685,13 @@ def _unknown_card_note(setup: CardSetup, card: Path, folder: Path) -> str:
|
||||
if setup.name in _GAMES_PART_CARDS:
|
||||
# Its list writes bios/ below the games part: the plan names that.
|
||||
folder = folder / "bios"
|
||||
plats, why = share_doubt(card)
|
||||
if setup.name == "KNULLI" and share_drive(card) == "batocera":
|
||||
plats, why = ((setup.plat,), "it holds the system/batocera.conf Batocera and KNULLI "
|
||||
"both write, and no about.info naming KNULLI")
|
||||
if setup.plat in plats:
|
||||
return (f"{shown_path(card)} could be another system's: {why}. Taken as "
|
||||
f"{setup.name}'s, as named: the files go to {shown_path(folder)}")
|
||||
if setup.fresh:
|
||||
return (f"Nothing on {shown_path(card)} says it is {_a(setup.name)} card yet (not started "
|
||||
f"once?): the files go to {shown_path(folder)}")
|
||||
@@ -10682,10 +10785,24 @@ def _list_card_choice(plat: str, dest: Path) -> CardChoice:
|
||||
short=f"That is {_a(found[2])} {kind}.", other=found[2])
|
||||
if found and plat and found[0] != plat:
|
||||
shown = shown_path(dest)
|
||||
return CardChoice(plat, dest, f"Note: {shown} is {_a(found[2])} card, which reads "
|
||||
return CardChoice(plat, dest, f"Note: {shown} is {_a(found[2])} {drive_kind(found)}, "
|
||||
"which reads "
|
||||
f"the {display_name(found[0])} file list in {shown_path(found[1])}: "
|
||||
f"--platform {_name_key(found[2])} --dest {shown}. The "
|
||||
f"{display_name(plat)} list goes into it as named.")
|
||||
doubt, why = ((), "") if found else share_doubt(dest)
|
||||
if plat in doubt:
|
||||
# Named by the user, a drive its files cannot name is taken for the
|
||||
# system named, and the doubt is said.
|
||||
folder = dest / "bios"
|
||||
return CardChoice(plat, folder, f"Note: {shown_path(dest)} could be another system's: "
|
||||
f"{why}. Taken as {display_name(plat)}'s, as named: the files go "
|
||||
f"to {shown_path(folder)}")
|
||||
if doubt and plat in _SHARE_FOLDERS:
|
||||
name = display_name(plat)
|
||||
return CardChoice(plat, dest, refused=f"{shown_path(dest)} could be "
|
||||
f"{_doubt_kinds(doubt)}, not {_a(name)} SHARE drive", detail=why,
|
||||
short=f"That could be {_doubt_kinds(doubt)}.")
|
||||
matched = [] if found else _card_matches(dest)
|
||||
if len({(match.manifest, match.bios) for match in matched}) > 1:
|
||||
names = " and ".join(sorted({match.name for match in matched}))
|
||||
@@ -10963,7 +11080,8 @@ def _platform_for_dest(os_type: str, dest: Path, interactive: bool) -> tuple[str
|
||||
print(f" File list: {display_name(plat)}. Same choice without questions: "
|
||||
f"--platform {_name_key(named) if named != plat else plat}")
|
||||
return plat, choice.folder
|
||||
found = list(dict.fromkeys(name for name, _path in detect_platforms(os_type)))
|
||||
# The setups the screen and line mode find, cards mounted here included.
|
||||
found = list(dict.fromkeys(name for name, _path, _label in found_setups(os_type)[0]))
|
||||
default = found[0] if len(found) == 1 else "" if found else "retroarch"
|
||||
if not default:
|
||||
print(f"Error: {' and '.join(display_name(n) for n in found)} found here. "
|
||||
@@ -10975,6 +11093,24 @@ def _platform_for_dest(os_type: str, dest: Path, interactive: bool) -> tuple[str
|
||||
return default, dest
|
||||
|
||||
|
||||
def _several_found(platforms: list[tuple[str, Path]], setups: list) -> None:
|
||||
"""Stop a run without questions that found more than one setup, each
|
||||
named with the options that choose it."""
|
||||
labels = {(name, path): label for name, path, label in setups}
|
||||
shown = [(labels.get((name, path)) or _setup_label(name, path), name, path)
|
||||
for name, path in platforms]
|
||||
conflict = os_conflict(platforms)
|
||||
lead = (f"{' and '.join(conflict)} are both detected here, and a machine runs one"
|
||||
if conflict else f"{len(shown)} setups were found here")
|
||||
message = (f"{lead}, and a run without questions installs for one: "
|
||||
+ ", ".join(label for label, _n, _p in shown))
|
||||
EVENTS.emit("error", stage="detect", reason="conflict", platform="", message=message)
|
||||
print(f"Error: {message}. Name the one to install for:", file=sys.stderr)
|
||||
for label, name, path in shown:
|
||||
print(f" {label}: --platform {name} --dest {shown_path(path)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _unknown_target(target: str, plat: str, targets: dict) -> None:
|
||||
print(f"Error: unknown target '{target}' for {plat}.", file=sys.stderr)
|
||||
if targets:
|
||||
@@ -11068,10 +11204,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
platforms = [(args.platform, args.dest)]
|
||||
elif args.platform:
|
||||
print("Looking for setups here...")
|
||||
if interactive and not listing:
|
||||
# The cards mounted here too, as the screen finds them: a pack's
|
||||
# RetroArch list went to this computer's folder past the Onion
|
||||
# card it was downloaded for.
|
||||
if not listing:
|
||||
# The cards mounted here too, as the screen finds them, with or
|
||||
# without questions: a pack's RetroArch list went to this
|
||||
# computer's folder past the Onion card it was downloaded for,
|
||||
# and --yes did so after the screen had found the card.
|
||||
setups, mounted = found_setups(os_type)
|
||||
detected = [(name, path) for name, path, _label in setups]
|
||||
else:
|
||||
@@ -11161,12 +11298,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
print(" Found ES-DE (BIOS files go to each emulator, not to ES-DE itself)")
|
||||
if "launchbox" in frontends:
|
||||
print(" Found LaunchBox")
|
||||
if interactive and not listing:
|
||||
# A card mounted here is found as the screen finds it: line mode
|
||||
# said "No supported platform detected" beside an Onion card.
|
||||
if not listing:
|
||||
# A card mounted here is found as the screen finds it, in every
|
||||
# mode: line mode said "No supported platform detected" beside
|
||||
# an Onion card, and --yes filled this computer's folder.
|
||||
setups, mounted = found_setups(os_type)
|
||||
platforms = [(name, path) for name, path, _label in setups]
|
||||
offer = True
|
||||
offer = interactive
|
||||
else:
|
||||
platforms = detect_platforms(os_type)
|
||||
if not platforms:
|
||||
@@ -11201,18 +11339,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
for name, path in platforms:
|
||||
print(f" Found {found_label(name, path)}")
|
||||
|
||||
conflict = os_conflict(platforms)
|
||||
if conflict and not interactive and not listing:
|
||||
# A machine runs one system: installing to each folder would write
|
||||
# where nothing reads, and picking one would be a guess.
|
||||
message = (f"{' and '.join(conflict)} are both detected here, and a "
|
||||
"machine runs one")
|
||||
EVENTS.emit("error", stage="detect", reason="conflict", platform="",
|
||||
message=message)
|
||||
print(f"Error: {message}. Name the folder with --dest (and --platform for "
|
||||
"its file list).",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if len(platforms) > 1 and not interactive and not listing:
|
||||
# A run without questions installs for one setup, the one a person
|
||||
# would have chosen: --yes beside two setups filled both, 14 GB in
|
||||
# two folders, where its help says it stops on what to name.
|
||||
_several_found(platforms, setups)
|
||||
if platforms:
|
||||
EVENTS.emit("detect", os=os_type, os_name=_os_name(), platforms=[
|
||||
{"platform": name, "name": display_name(name), "bios_dir": str(path)}
|
||||
@@ -16291,15 +16422,46 @@ def found_setups(os_type: str) -> tuple[list[tuple[str, Path, str]], set]:
|
||||
# On the device itself its own card is the system found, by its own
|
||||
# path (muOS reads /run/muos/storage/bios, a bind of MUOS/bios).
|
||||
here = {detected_os(name, path) for name, path in found}
|
||||
taken = []
|
||||
for card, mount in cards:
|
||||
if card and (card[0], card[1]) not in found and card[2] not in here:
|
||||
if card is None:
|
||||
# A drive whose files fit two systems is said, never guessed.
|
||||
words = doubt_words(mount)
|
||||
if words:
|
||||
print(f" {words}")
|
||||
continue
|
||||
if (card[0], card[1]) not in found and card[2] not in here:
|
||||
_DETECTED_OS.setdefault((card[0], card[1]), card[2])
|
||||
mounted.add((card[0], card[1]))
|
||||
kind = "SHARE drive" if drive_kind(card) != "card" else "SD card"
|
||||
setups.append((card[0], card[1], f"{card[2]} {kind} ({_mount_name(mount)})"))
|
||||
taken.append((card, mount))
|
||||
systems = [card[2] for card, _mount in taken]
|
||||
counts = {mount: _bios_count(card[1]) for card, mount in taken
|
||||
if systems.count(card[2]) > 1}
|
||||
# Two cards of one system read alike: each says what its BIOS folder
|
||||
# holds, and the one with fewer files, the card being set up rather
|
||||
# than the old one, comes first (tui_audit_11 P3-15).
|
||||
taken.sort(key=lambda item: (systems.index(item[0][2]), counts.get(item[1], -1)))
|
||||
for card, mount in taken:
|
||||
kind = "SHARE drive" if drive_kind(card) != "card" else "SD card"
|
||||
held = counts.get(mount)
|
||||
what = ("" if held is None else ", no BIOS folder" if held < 0
|
||||
else f", {_plural(held, 'BIOS file')}")
|
||||
setups.append((card[0], card[1], f"{card[2]} {kind} ({_mount_name(mount)}{what})"))
|
||||
return tell_apart(setups), mounted
|
||||
|
||||
|
||||
def _bios_count(folder: Path, cap: int = 99999) -> int:
|
||||
"""How many files a card's BIOS folder holds, -1 when it has none."""
|
||||
if not _is_dir(folder):
|
||||
return -1
|
||||
count = 0
|
||||
for _top, _dirs, files in os.walk(folder):
|
||||
count += len(files)
|
||||
if count >= cap:
|
||||
break
|
||||
return count
|
||||
|
||||
|
||||
def tui_start(
|
||||
args: argparse.Namespace, os_type: str, detect: bool = True,
|
||||
) -> tuple[TuiState, list[tuple]]:
|
||||
|
||||
+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="71fa422aa38f9710c9aab81ba038cad71df66f574a527dc62e0307c69c30d1b0"
|
||||
DEFAULT_INSTALL_SHA256="f4f2d041d923e32d46c828222f43f4359c5241fcce7ccc29bfc9b3e8565bba0b"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 775811,
|
||||
"sha256": "71fa422aa38f9710c9aab81ba038cad71df66f574a527dc62e0307c69c30d1b0",
|
||||
"size": 784255,
|
||||
"sha256": "f4f2d041d923e32d46c828222f43f4359c5241fcce7ccc29bfc9b3e8565bba0b",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 828360,
|
||||
"sha256": "dc3204c1dab8db00fb10e47cb9d2cad8ad6320b9d5c7d8fff9168c238620d5ef"
|
||||
"size": 836804,
|
||||
"sha256": "2a46dfb5f18a6535fed35fc828078657fa09440abeafd3219ba099ca3ef4e580"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 775811,
|
||||
"sha256": "71fa422aa38f9710c9aab81ba038cad71df66f574a527dc62e0307c69c30d1b0",
|
||||
"size": 784255,
|
||||
"sha256": "f4f2d041d923e32d46c828222f43f4359c5241fcce7ccc29bfc9b3e8565bba0b",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 775811,
|
||||
"sha256": "71fa422aa38f9710c9aab81ba038cad71df66f574a527dc62e0307c69c30d1b0",
|
||||
"size": 784255,
|
||||
"sha256": "f4f2d041d923e32d46c828222f43f4359c5241fcce7ccc29bfc9b3e8565bba0b",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ import ast
|
||||
import importlib.util
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PAGE = ROOT / "wiki" / "installer.md"
|
||||
@@ -154,6 +154,40 @@ def route_keys() -> str:
|
||||
return _table(heading, rows)
|
||||
|
||||
|
||||
def _marker(rule) -> str:
|
||||
"""A card rule's evidence in words: the file or folder, and the line it
|
||||
holds for a rule that reads one."""
|
||||
if rule.method == "file_matches":
|
||||
path, pattern = rule.values
|
||||
line = re.sub(r"\\s\*\$$", "", re.sub(r"^(\^|\\A)", "", pattern)).replace("\\", "")
|
||||
return f"`{path}` holding the line `{line}`"
|
||||
return ", ".join(f"`{value}`" if PurePosixPath(value).suffix else f"`{value}/`"
|
||||
for value in rule.values)
|
||||
|
||||
|
||||
def _list_words(plat: str) -> str:
|
||||
name = _install.display_name(plat)
|
||||
return f"{name}'{'' if name.endswith('s') else 's'} list"
|
||||
|
||||
|
||||
def card_marker_rows() -> list[list[str]]:
|
||||
"""The files that name a card or a SHARE drive, in the order they are
|
||||
read: install.py's card rules, then its SHARE drive files."""
|
||||
rows = [[_marker(rule), rule.name, f"`{rule.bios}`, {_list_words(rule.manifest)}"]
|
||||
for rule in _install._card_rules()]
|
||||
hostname = "system.hostname=BATOCERA"
|
||||
for plat, marker in _install._SHARE_DRIVES:
|
||||
name = _install.display_name(plat)
|
||||
evidence = (f"`{marker}` holding `{hostname}` or no hostname line, and no KNULLI file "
|
||||
"above" if plat == "batocera" else f"`{marker}`")
|
||||
rows.append([evidence, f"{name} SHARE drive", f"`bios`, {_list_words(plat)}"])
|
||||
rows.append([f"`{_install._SHARE_DRIVES[0][1]}` with any other hostname, and no KNULLI "
|
||||
"file above, or the files of two SHARE drives",
|
||||
"named by neither: the run says which systems it could be",
|
||||
"the folder of the system named with `--platform`"])
|
||||
return rows
|
||||
|
||||
|
||||
def render(page: str) -> str:
|
||||
"""The page with each marked table written from install.py."""
|
||||
writers = {
|
||||
@@ -162,6 +196,8 @@ def render(page: str) -> str:
|
||||
"events": lambda body: _table(["Kind", "Keys", "When"], event_rows(body)),
|
||||
"pad-routes": lambda body: _table(["Value", "Input", "Set by"], pad_rows(body)),
|
||||
"route-keys": lambda _body: route_keys(),
|
||||
"card-markers": lambda _body: _table(["On the card", "System", "Files go to"],
|
||||
card_marker_rows()),
|
||||
}
|
||||
seen = []
|
||||
|
||||
|
||||
@@ -1174,11 +1174,17 @@ class ADetectedClosedFolderSaysWhereItCameFrom(unittest.TestCase):
|
||||
said = " ".join(proc.stderr.split())
|
||||
self.assertIn(f"than the one {self.cfg} names (name another with --dest)", said)
|
||||
|
||||
def test_nothing_installed_comes_after_the_error(self):
|
||||
def test_two_setups_stop_before_either_folder_is_read(self):
|
||||
"""code_audit_18 P2-3: a run without questions installs for one
|
||||
setup, so two found stop on what to name before either folder is
|
||||
read, the closed one included."""
|
||||
proc = self.run_detected(True)
|
||||
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
|
||||
error = proc.stderr.index("Error: ")
|
||||
self.assertGreater(proc.stderr.index("Nothing was installed."), error, proc.stderr)
|
||||
said = " ".join(proc.stderr.split())
|
||||
self.assertIn("2 setups were found here, and a run without questions installs for "
|
||||
"one", said)
|
||||
self.assertIn("--platform emudeck --dest", said)
|
||||
self.assertNotIn("Downloading", proc.stdout)
|
||||
|
||||
def test_an_assumed_default_reads_plainly(self):
|
||||
self.assertIn("used by default, since {display_name(name)} was not found",
|
||||
|
||||
+246
-1
@@ -6629,6 +6629,81 @@ class LineModeAndScreenDecideAlike(unittest.TestCase):
|
||||
self.assertEqual(screen, expected)
|
||||
self.assertNotIn("using default path", text)
|
||||
|
||||
def unasked(self, mounts: list, found: list, **given) -> tuple:
|
||||
"""(platforms or exit code, text, events) of a run without
|
||||
questions: --no-input, or --yes, which implies it."""
|
||||
def stop(_args, platforms):
|
||||
raise _Decided(platforms)
|
||||
|
||||
install.LOCAL.reset()
|
||||
self.addCleanup(install.LOCAL.reset)
|
||||
args = _args(declared_only=False, offline=False, no_input=True, **given)
|
||||
events = []
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
|
||||
mock.patch.object(install, "_mount_points", return_value=frozenset()), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=found), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
mock.patch.object(install, "app_refusal", side_effect=stop), \
|
||||
mock.patch.object(install, "detect_os", return_value="linux"), \
|
||||
mock.patch.object(install.EVENTS, "emit",
|
||||
side_effect=lambda kind, **keys: events.append((kind, keys))), \
|
||||
mock.patch.dict(os.environ, {install.OFFLINE_VARIABLE: "1"}), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out, \
|
||||
contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
try:
|
||||
install._install(args, io.StringIO())
|
||||
except _Decided as decided:
|
||||
return [tuple(each) for each in decided.args[0]], out.getvalue(), events
|
||||
except SystemExit as exit_:
|
||||
return exit_.code, out.getvalue() + err.getvalue(), events
|
||||
raise AssertionError(out.getvalue())
|
||||
|
||||
def test_a_run_without_questions_sees_the_cards_the_screen_sees(self):
|
||||
"""tui_audit_11 P1-1: --yes and --no-input looked at this machine
|
||||
alone, beside the Onion card the screen and line mode open on: the
|
||||
files went to the home folder, or the run said nothing is here."""
|
||||
onion = self.card("ONION", "Onion")
|
||||
default = install._default_dest("linux", "retroarch")
|
||||
for flag in ({"yes": True}, {}):
|
||||
for name, (mounts, given) in {
|
||||
"an Onion card, a list": ([onion], {"manifest": self.list}),
|
||||
"an Onion card, nothing named": ([onion], {}),
|
||||
}.items():
|
||||
with self.subTest(flag=flag, scenario=name):
|
||||
chosen, text, _events = self.unasked(mounts, [], **flag, **given)
|
||||
self.assertEqual(chosen, [("retroarch", onion / "BIOS")], text)
|
||||
self.assertNotIn(str(default), text)
|
||||
install._DETECTED_OS.clear()
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=mounts), \
|
||||
mock.patch.object(install, "_mount_points",
|
||||
return_value=frozenset()), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]):
|
||||
screen = screen_decision(_args(declared_only=False, **given))
|
||||
self.assertEqual(chosen, [screen])
|
||||
|
||||
def test_a_run_without_questions_stops_on_two_setups(self):
|
||||
"""code_audit_18 P2-3, tui_audit_11 P1-1: --yes beside two setups
|
||||
installed for both, where its help says it stops on what to name.
|
||||
It now stops with an error event naming each and its --dest."""
|
||||
onion, muos = self.card("ONION", "Onion"), self.card("MUOS", "muOS")
|
||||
pc = [("retroarch", self.here)]
|
||||
for flag in ({"yes": True}, {}):
|
||||
for name, (mounts, found, given) in {
|
||||
"two cards": ([onion, muos], [], {}),
|
||||
"a card and RetroArch here, a list": ([onion], pc, {"manifest": self.list}),
|
||||
"two setups here": ([], [("retroarch", self.here),
|
||||
("emudeck", self.root / "Emulation" / "bios")], {}),
|
||||
}.items():
|
||||
with self.subTest(flag=flag, scenario=name):
|
||||
code, text, events = self.unasked(mounts, found, **flag, **given)
|
||||
self.assertEqual(code, 1, text)
|
||||
errors = [keys for kind, keys in events if kind == "error"]
|
||||
self.assertEqual([(e["stage"], e["reason"]) for e in errors],
|
||||
[("detect", "conflict")])
|
||||
self.assertIn("a run without questions installs for one", text)
|
||||
self.assertRegex(text, r"--platform \w+ --dest ")
|
||||
|
||||
def test_line_mode_says_the_card_it_found_and_names_its_folder(self):
|
||||
onion = self.card("ONION", "Onion")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[onion]), \
|
||||
@@ -6759,7 +6834,8 @@ class LineModeAndScreenDecideAlike(unittest.TestCase):
|
||||
share = self.root / "media" / f"SHARE-{plat}"
|
||||
(share / "system").mkdir(parents=True)
|
||||
(share / "roms").mkdir()
|
||||
(share / marker).write_text("system.hostname=SHARE\n")
|
||||
# Batocera's own hostname: another would fit KNULLI renamed too.
|
||||
(share / marker).write_text("system.hostname=BATOCERA\n")
|
||||
shares[plat] = share
|
||||
for plat, share in shares.items():
|
||||
other = shares["recalbox" if plat == "batocera" else "batocera"]
|
||||
@@ -8350,5 +8426,174 @@ class EdgesTheMutantsFound(unittest.TestCase):
|
||||
["Odroid"])
|
||||
|
||||
|
||||
|
||||
class AuditEighteenAndEleven(unittest.TestCase):
|
||||
"""code_audit_18 and tui_audit_11: what the cards, the screens and the
|
||||
docs answer, line mode's parity being in LineModeAndScreenDecideAlike."""
|
||||
|
||||
def setUp(self):
|
||||
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
|
||||
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, self.root, True)
|
||||
install.LOCAL.reset()
|
||||
self.addCleanup(install.LOCAL.reset)
|
||||
for patcher in (mock.patch.dict(install._DETECTED_OS, {}),
|
||||
mock.patch.object(install, "_mount_points", return_value=frozenset())):
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def drive(self, name: str, files: dict) -> Path:
|
||||
root = self.root / "media" / name
|
||||
root.mkdir(parents=True)
|
||||
for path, text in files.items():
|
||||
(root / path).parent.mkdir(parents=True, exist_ok=True)
|
||||
(root / path).write_text(text, encoding="utf-8")
|
||||
return root
|
||||
|
||||
ABOUT = "system/configs/emulationstation/about.info"
|
||||
|
||||
def renamed(self) -> dict:
|
||||
"""The drives code_audit_18 P1-1 names: KNULLI and Batocera each
|
||||
renamed in its own menu, with and without the file KNULLI alone
|
||||
writes, and Recalbox's share."""
|
||||
conf = "system/batocera.conf"
|
||||
return {
|
||||
"KNULLI renamed": self.drive("K1", {self.ABOUT: "KNULLI\n",
|
||||
conf: "system.hostname=RG40XXH\n"}),
|
||||
"KNULLI as shipped": self.drive("K2", {self.ABOUT: "KNULLI\n",
|
||||
conf: "system.hostname=KNULLI\n"}),
|
||||
"renamed, no about.info": self.drive("K3", {conf: "system.hostname=RG40XXH\n"}),
|
||||
"Batocera as shipped": self.drive("B1", {conf: "system.hostname=BATOCERA\n"}),
|
||||
"Batocera, no hostname": self.drive("B2", {conf: "# system.hostname=X\n"}),
|
||||
"Recalbox": self.drive("R1", {"system/recalbox.conf": "a=1\n"}),
|
||||
"two shares": self.drive("T1", {conf: "system.hostname=BATOCERA\n",
|
||||
"system/recalbox.conf": ""}),
|
||||
}
|
||||
|
||||
def test_a_renamed_card_is_named_by_the_file_no_menu_writes(self):
|
||||
"""code_audit_18 P1-1: a KNULLI card whose hostname was changed in
|
||||
its menu was a Batocera SHARE drive, Enter filled it with Batocera's
|
||||
list and --platform knulli was refused."""
|
||||
drives = self.renamed()
|
||||
expected = {
|
||||
"KNULLI renamed": ("knulli", "bios", "KNULLI"),
|
||||
"KNULLI as shipped": ("knulli", "bios", "KNULLI"),
|
||||
"renamed, no about.info": None,
|
||||
"Batocera as shipped": ("batocera", "bios", "Batocera"),
|
||||
"Batocera, no hostname": ("batocera", "bios", "Batocera"),
|
||||
"Recalbox": ("recalbox", "bios", "Recalbox"),
|
||||
"two shares": None,
|
||||
}
|
||||
for name, drive in drives.items():
|
||||
with self.subTest(drive=name):
|
||||
found = install.card_system(drive)
|
||||
want = expected[name]
|
||||
self.assertEqual(found, want and (want[0], drive / want[1], want[2]))
|
||||
doubt, _why = install.share_doubt(drive)
|
||||
self.assertEqual(bool(doubt), want is None)
|
||||
self.assertEqual(install.share_doubt(drives["renamed, no about.info"])[0],
|
||||
("knulli", "batocera"))
|
||||
self.assertEqual(install.share_doubt(drives["two shares"])[0],
|
||||
("batocera", "recalbox"))
|
||||
|
||||
def test_a_drive_two_systems_fit_is_taken_as_named_and_said(self):
|
||||
"""code_audit_18 P1-1: --platform knulli is never refused on a
|
||||
drive whose only evidence is a batocera.conf, --platform batocera
|
||||
takes its bios folder, and the doubt is said. Recalbox refuses it
|
||||
with words for a SHARE drive (P3-4)."""
|
||||
drives = self.renamed()
|
||||
for name in ("renamed, no about.info", "Batocera as shipped"):
|
||||
drive = drives[name]
|
||||
with self.subTest(drive=name):
|
||||
choice = install.card_choice("knulli", "knulli", drive)
|
||||
self.assertEqual((choice.plat, choice.folder, choice.refused),
|
||||
("knulli", drive / "bios", ""))
|
||||
self.assertIn("Taken as KNULLI's, as named", choice.note)
|
||||
drive = drives["renamed, no about.info"]
|
||||
choice = install.card_choice("batocera", "batocera", drive)
|
||||
self.assertEqual((choice.plat, choice.folder), ("batocera", drive / "bios"))
|
||||
self.assertIn("names the host RG40XXH", choice.note)
|
||||
refused = install.card_choice("recalbox", "recalbox", drive)
|
||||
self.assertIn("could be a KNULLI card or a Batocera SHARE drive", refused.refused)
|
||||
words = install._dest_refusal_words(refused, None)
|
||||
self.assertIn("Connect the Recalbox SHARE drive", words)
|
||||
self.assertNotIn("Insert", words)
|
||||
other = install.card_choice("recalbox", "recalbox", drives["Batocera as shipped"])
|
||||
words = install._dest_refusal_words(other, None)
|
||||
self.assertIn("Connect the Recalbox SHARE drive, or name that drive's own system: "
|
||||
"--platform batocera", words)
|
||||
|
||||
def test_the_first_screen_and_line_mode_say_the_doubt(self):
|
||||
"""code_audit_18 P1-1: the screen's first row was "Batocera SHARE
|
||||
drive (KNULLI2)". A renamed card with about.info is KNULLI's, one
|
||||
without is listed by neither and said."""
|
||||
drives = self.renamed()
|
||||
knulli, doubt = drives["KNULLI renamed"], drives["renamed, no about.info"]
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[knulli, doubt]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
setups, _mounted = install.found_setups("linux")
|
||||
self.assertEqual([(plat, path) for plat, path, _label in setups],
|
||||
[("knulli", knulli / "bios")])
|
||||
self.assertIn("KNULLI SD card", setups[0][2])
|
||||
text = out.getvalue()
|
||||
self.assertIn(f"{doubt} could be a KNULLI card or a Batocera SHARE drive", text)
|
||||
self.assertIn(f"--platform knulli --dest {doubt} or --platform batocera --dest {doubt}",
|
||||
text)
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[knulli, doubt]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
mock.patch.object(install, "detect_frontends", return_value=[]), \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
state, _effects = install.tui_start(_args(), "linux")
|
||||
self.assertTrue(any("could be a KNULLI card" in note for note in state.notes))
|
||||
self.assertEqual([setup[0] for setup in state.setups], ["knulli"])
|
||||
|
||||
def test_a_share_drive_is_never_called_a_card(self):
|
||||
"""code_audit_18 P3-4, tui_audit_11 P3-14: "is a Batocera card"
|
||||
and "is a Recalbox card" for their SHARE drives."""
|
||||
drives = self.renamed()
|
||||
for name in ("Batocera as shipped", "Recalbox"):
|
||||
with self.subTest(drive=name):
|
||||
choice = install.card_choice("retroarch", "retroarch", drives[name])
|
||||
self.assertIn("SHARE drive, which reads", choice.note)
|
||||
self.assertNotIn(" card, which reads", choice.note)
|
||||
|
||||
def test_two_cards_of_one_system_say_what_they_hold(self):
|
||||
"""tui_audit_11 P3-15: an old Onion card with 6 BIOS files and a new
|
||||
empty one looked the same, and Enter took the first by name."""
|
||||
old = self.root / "media" / "OLDONION"
|
||||
make_card(old, "Onion")
|
||||
for index in range(6):
|
||||
(old / "BIOS" / f"f{index}.bin").write_bytes(b"x")
|
||||
new = self.root / "media" / "ONION"
|
||||
make_card(new, "Onion")
|
||||
shutil.rmtree(new / "BIOS")
|
||||
with mock.patch.object(install, "_mounted_cards", return_value=[old, new]), \
|
||||
mock.patch.object(install, "detect_platforms", return_value=[]), \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
setups, _mounted = install.found_setups("linux")
|
||||
self.assertEqual([label for _plat, _path, label in setups],
|
||||
["Onion SD card (ONION, no BIOS folder)",
|
||||
"Onion SD card (OLDONION, 6 BIOS files)"])
|
||||
|
||||
def test_the_card_marker_table_is_written_from_the_rules(self):
|
||||
"""code_audit_18 P3-6, tui_audit_11 P2-2: the table left out the
|
||||
SHARE drives. It is generated: every card rule and SHARE drive file
|
||||
is a row, about.info among them."""
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
self.addCleanup(sys.path.remove, str(REPO_ROOT / "scripts"))
|
||||
import generate_installer_doc # noqa: PLC0415
|
||||
|
||||
page = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
|
||||
block = page[page.index("<!-- generated: card-markers -->"):
|
||||
page.index("<!-- /generated: card-markers -->")]
|
||||
for rule in install._card_rules():
|
||||
self.assertIn(f"`{rule.values[0]}", block)
|
||||
for _plat, marker in install._SHARE_DRIVES:
|
||||
self.assertIn(f"`{marker}`", block)
|
||||
self.assertIn(self.ABOUT, block)
|
||||
self.assertIn("Recalbox SHARE drive", block)
|
||||
self.assertEqual(generate_installer_doc.render(page), page)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+27
-5
@@ -864,20 +864,42 @@ A card that holds a system's own files is read the same way, without
|
||||
`--platform` or with the one that names its list, by the rules the table above
|
||||
gives for the device, below the card's root instead of its mount point:
|
||||
|
||||
<!-- generated: card-markers -->
|
||||
|
||||
| On the card | System | Files go to |
|
||||
|-------------|--------|-------------|
|
||||
| `system/configs/emulationstation/about.info` holding the line `KNULLI` | KNULLI | `bios`, KNULLI's list |
|
||||
| `system/batocera.conf` holding the line `system.hostname=KNULLI` | KNULLI | `bios`, KNULLI's list |
|
||||
| `.config/system/configs/system.cfg` holding the line `root.password=rocknix` | ROCKNIX | `roms/bios`, ROCKNIX's list |
|
||||
| `MUOS/` | muOS | `MUOS/bios`, RetroArch's list |
|
||||
| `.tmp_update/onionVersion/version.txt` | Onion | `BIOS`, RetroArch's list |
|
||||
| `spruce/` | spruceOS | `BIOS`, RetroArch's list |
|
||||
| `System/usr/trimui/crossmix-version.txt` | CrossMix-OS | `BIOS`, RetroArch's list |
|
||||
| `.tmp_update/onionVersion/version.txt` | Onion | `BIOS`, RetroArch's list |
|
||||
| `.allium/` | Allium | `BIOS`, RetroArch's list |
|
||||
| `Koriki/version.txt` | Koriki | `BIOS`, RetroArch's list |
|
||||
| `system/batocera.conf` with `system.hostname=KNULLI`, as KNULLI's first boot writes its share | KNULLI | `bios`, KNULLI's list |
|
||||
| `.config/system/configs/system.cfg` with `root.password=rocknix`, as ROCKNIX's first boot writes its storage | ROCKNIX | `roms/bios`, ROCKNIX's list |
|
||||
| `system/batocera.conf` holding `system.hostname=BATOCERA` or no hostname line, and no KNULLI file above | Batocera SHARE drive | `bios`, Batocera's list |
|
||||
| `system/recalbox.conf` | Recalbox SHARE drive | `bios`, Recalbox's list |
|
||||
| `system/batocera.conf` with any other hostname, and no KNULLI file above, or the files of two SHARE drives | named by neither: the run says which systems it could be | the folder of the system named with `--platform` |
|
||||
|
||||
The full screen lists such a card on its first screen ("muOS SD card (E:\)").
|
||||
<!-- /generated: card-markers -->
|
||||
|
||||
Each file is one the system writes. KNULLI is read by `about.info` first,
|
||||
which no menu writes: its HOSTNAME row rewrites `system.hostname`, and a KNULLI
|
||||
card renamed there was read as a Batocera SHARE drive. ROCKNIX's line changes
|
||||
with its root password, and a card whose password was changed is named by
|
||||
nothing, never by another system. A drive whose files fit two systems is named
|
||||
by neither: the first screen and `--plain` say which systems it could be and
|
||||
the two commands that name it, and a `--dest` named with one of them takes it
|
||||
as that system's, with a note.
|
||||
|
||||
The full screen lists such a card on its first screen ("muOS SD card (E:\)"),
|
||||
and a SHARE drive the same way ("Batocera SHARE drive (SHARE)"): its root, typed
|
||||
or given with `--dest`, stands for its `bios` folder. Two cards of one system
|
||||
say what their BIOS folder holds ("Onion SD card (OLDONION, 6 BIOS files)"),
|
||||
the one holding fewer first.
|
||||
Named by its system (`--platform muos`, `--platform knulli`), a `--dest` that
|
||||
is another system's card is refused. One that holds none of the system's
|
||||
is another system's card is refused, and so is another system's SHARE drive
|
||||
for Batocera and Recalbox. One that holds none of the system's
|
||||
files is refused where the card always carries them. For KNULLI, ROCKNIX and
|
||||
the systems no file on the card names, it gets the system's folder below it
|
||||
(`bios`, `roms/bios`), with a note. Named with a file list instead
|
||||
|
||||
Reference in new issue
Block a user