feat: read guides and wiki nav from data

Platform guides, device groups, terminal tabs, pack README manuals and
logo credits move from Python tables to platforms/_guides.yml, checked
against the registry on load. A folder row without a path is the
registry's extract_to, written once.

Wiki pages place themselves in the navigation with order and nav_title
in their front matter. Generated docs/ and mkdocs.yml are unchanged.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 15:06:52 +02:00
1 parent b77cc0b8fd
commit be3324f869
28 files changed
+1207 -610

No files matched your search

+541
View File
@@ -0,0 +1,541 @@
# What each platform page and pack README tell someone holding the device:
# the folder, where the command runs, what to know. Read by
# scripts/siteguides.py, which checks it on load: every platform in
# _registry.yml has a guide here, and every guide a registered platform.
#
# Values are Markdown. A folder row with no `path` is the registry's
# `extract_to` for that platform, the one place that folder is written.
# `run` names the terminals below, in the order the page shows them.
# `pack_manual` is the manual part of the README inside the pack, plain text.
# `logo_owner` and `logo_licence` credit the logo the registry links: it is
# shown only to name the platform, with the licence of the repository the
# file was taken from when that repository states one.
#
# The registry keeps what tools read (device_group, extract_to, install);
# this file keeps what people read.
install_base: https://raw.githubusercontent.com/Abdess/retrobios/main
# Is it safe: shown under every command on a guide page.
trust_items:
- It checks each file before saving it.
- It writes only in your BIOS folder.
- It never touches games or saves.
- It never moves or deletes files it does not know.
- >-
It keeps any file it replaces in a `retrobios-backup` folder, beside your BIOS folder.
The same command with `--restore` puts it back.
# Order of the groups on the picker, in the navigation and on the home page.
# `anchor` is the section id on the device picker: two keep the ids the page
# had before it was grouped by device, which links outside the site may use.
# `also_runs` are setups that are not platforms of their own: they run one,
# and use its pack.
device_groups:
computer:
label: Computers and Steam Deck
description: An emulator app on a computer or a Steam Deck.
anchor: computers-and-steam-deck
also_runs:
- setup: LaunchBox
summary: >-
A game library and launcher. It starts RetroArch or standalone emulators.
uses: retroarch
runs_on: Windows
folder: The folder of the RetroArch it starts
gaming-os:
label: Game systems for PCs and small boards
description: >-
A system that starts straight into a game menu, on a PC or a Raspberry Pi.
anchor: game-systems
handheld:
label: Handhelds and TV boxes
description: The system your handheld or TV box starts.
anchor: handhelds
also_runs:
- setup: Android (Retroid Pocket, AYN Odin and others)
summary: RetroArch for Android.
uses: retroarch
runs_on: Android
folder: '`RetroArch/system/` on the internal storage'
fpga:
label: MiSTer FPGA
description: >-
A board that recreates old consoles in hardware, not in software.
anchor: fpga
server:
label: Self-hosted game library
description: A RomM server. It plays games in your web browser.
anchor: self-hosted
# Ways to run the installer: the tab label, how to open a terminal there, and
# the shell the command is written for.
terminals:
windows:
label: Windows
how: >-
Open the Start menu. Type **PowerShell** and open it. Paste the line. Press Enter.
shell: powershell
macos:
label: macOS
how: >-
Open **Terminal**, in Applications, then Utilities. Paste the line. Press Return.
shell: sh
linux:
label: Linux
how: Open a terminal. Paste the line. Press Enter.
shell: sh
steamdeck:
label: Steam Deck
how: >-
Switch to Desktop Mode. Open **Konsole**. Paste with Ctrl+Shift+V. Press Enter.
shell: sh
android:
label: Android (Termux)
how: >-
Install **Termux**, a terminal app. Run `pkg install python` and `termux-setup-storage`
once. Then paste the line. Press Enter.
shell: sh
device:
label: On the device
how: >-
Open a terminal on the device. Or connect from a computer with SSH, a terminal over
the network. Paste the line. Press Enter.
shell: sh
server:
label: On the server
how: >-
Open a terminal on the machine that holds your RomM library. Put the path of its
`bios` folder after `--dest`.
shell: sh
guides:
retroarch:
summary: >-
An app that runs many consoles. It uses a different **core**, a small emulator,
for each one.
folders:
- where: Windows
path: '`system\` next to `retroarch.exe`'
- where: macOS
path: '`~/Documents/RetroArch/system/`'
- where: Linux
path: '`~/.config/retroarch/system/`'
- where: Steam Deck and Linux Flatpak
path: '`~/.var/app/org.libretro.RetroArch/config/retroarch/system/`'
- where: Android
path: '`RetroArch/system/` on the internal storage'
- where: Nintendo Switch
path: '`retroarch/cores/system/` on the SD card'
more_folders:
- where: Windows, Microsoft Store build
path: '`%LOCALAPPDATA%\Packages\1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_*\LocalState\system\`'
- where: macOS, Steam or portable build
path: '`~/Library/Application Support/RetroArch/system/`'
more_folders_title: 'Other builds: Microsoft Store, Mac Steam, handhelds'
# Adds a row for each named system the registry detects this platform on.
more_folders_detected: true
more_folders_note: LaunchBox and other launchers use the folder RetroArch reads.
run: [windows, macos, linux, steamdeck, android]
notes:
- >-
**Not sure?** In RetroArch, open **Settings > Directory > System/BIOS**. It shows
the folder.
reference_notes:
- >-
RetroArch reads its BIOS folder from the `system_directory` line of `retroarch.cfg`,
the value **Settings > Directory > System/BIOS** shows.
- >-
On Windows, `retroarch.cfg` is read next to `retroarch.exe` first, then from `%APPDATA%\RetroArch\`.
A `system_directory` left at its default is the `system` folder next to `retroarch.exe`
(`:\system` in RetroArch's `frontend/drivers/platform_win32.c`, where `:` stands
for the folder of the program).
pack_manual: |
Option B: Manual (PC)
---------------------
1. Find your RetroArch system directory:
- RetroArch > Settings > Directory > System/BIOS
- Default: retroarch/system/
2. Extract all files from this archive directly into your system directory
3. Overwrite if asked
Option C: Manual (handheld / SD card)
-------------------------------------
Anbernic, Retroid, Miyoo, Trimui, etc.:
1. Connect your SD card to your PC
2. Find the BIOS folder (usually BIOS/ or system/)
3. Extract all files from this archive directly into that folder
4. Eject SD card and reboot your device
Common paths by device:
Anbernic (ArkOS/JELOS): BIOS/
Retroid (RetroArch): RetroArch/system/
Miyoo Mini (Onion OS): BIOS/
Steam Deck (RetroArch): ~/.config/retroarch/system/
logo_owner: libretro
logo_licence: repository licence GPL-3.0
batocera:
summary: A system that starts straight into a game menu.
folders:
- where: On Batocera
- where: From Windows, over your home network
path: '`\\BATOCERA\share\bios\`'
- where: From a Mac or Linux, over your home network
path: '`smb://batocera/share/bios/`'
run: [device]
download_hint: From a computer, use the network folder above.
pack_manual: |
The terminal is F1 from the Batocera menu, or SSH.
Option B: Manual (network share)
--------------------------------
1. On your PC, open the Batocera network share:
- Windows: \\BATOCERA\share\bios\
- Mac/Linux: smb://batocera/share/bios/
2. Extract all files from this archive directly into the share
3. Overwrite if asked
Option C: Manual (SD card)
--------------------------
1. Put the SD card in your PC
2. Navigate to /userdata/bios/ on the SHARE partition
3. Extract all files from this archive directly into that folder
NOTE: Dreamcast flash memory is named dc_nvmem.bin
(if your setup asks for dc_flash.bin, same file).
logo_owner: Batocera
logo_licence: repository licence MIT
recalbox:
summary: >-
A system that starts straight into a game menu and sets up its emulators.
folders:
- where: On Recalbox
- where: From Windows, over your home network
path: '`\\RECALBOX\share\bios\`'
run: [device]
download_hint: From a computer, use the network folder above.
pack_manual: |
Option B: Manual (network share)
--------------------------------
1. On your PC, open the Recalbox network share:
- Windows: \\RECALBOX\share\bios\
- Mac/Linux: smb://recalbox/share/bios/
2. Extract all files from this archive directly into the share
Option C: Manual (SD card)
--------------------------
1. Put the SD card in your PC
2. Navigate to /recalbox/share/bios/
3. Extract all files from this archive directly into that folder
logo_owner: Recalbox
logo_licence: file from homarr-labs/dashboard-icons, Apache-2.0
retrobat:
summary: >-
A game menu for Windows. It comes with RetroArch and other emulators.
folders:
- where: Inside the RetroBat folder
path: '`bios\`'
- where: Default install
path: '`C:\RetroBat\bios\`'
run: [windows]
notes:
- The command finds RetroBat wherever you installed it.
reference_notes:
- >-
RetroBat records its own folder each time it starts, and the installer reads it
there.
pack_manual: |
Option B: Manual
----------------
1. Open your RetroBat installation folder
2. Navigate to the bios\ subfolder
(default: C:\RetroBat\bios\)
3. Extract all files from this archive directly into your bios\ folder
4. Overwrite if asked
logo_owner: RetroBat
logo_licence: repository states no licence
emudeck:
summary: Installs your emulators and adds your games to Steam.
folders:
- where: Steam Deck and Linux
- where: Windows, by default
path: '`C:\Emulation\bios\`'
- where: macOS
run: [steamdeck, linux, windows, macos]
notes:
- >-
The `bios` folder is inside the Emulation folder you chose in EmuDeck. If you chose
the SD card, it is on the card.
reference_notes:
- >-
The installer reads the Emulation folder from EmuDeck's settings (`emulationPath`
in `~/.config/EmuDeck/settings.sh`).
pack_manual: |
Option B: Manual
----------------
Extract all files from this archive directly into EmuDeck's bios
folder, inside the Emulation folder chosen in EmuDeck:
Steam Deck, Linux and macOS: ~/Emulation/bios/ by default
Windows: C:\Emulation\bios\ by default
logo_owner: EmuDeck
logo_licence: repository licence GPL-3.0
lakka:
summary: A system that starts straight into RetroArch.
folders:
- where: On Lakka
run: [device]
notes:
- >-
You can also reach the folder from a computer, over your home network.
reference_notes:
- The folder is reachable over SSH and over the network share.
pack_manual: |
Lakka reads its BIOS files from /storage/system/.
Option B: Via network share
--------------------------
1. On Lakka: Settings > Services > Samba Enable
2. On your PC, open the System share: \\LAKKA\System\
(macOS and Linux: smb://lakka/System)
3. Extract all files from this archive directly into that share
Option C: Via SSH
-----------------
1. On Lakka: Settings > Services > SSH Enable
2. Copy the files to /storage/system/ (scp -r * root@lakka:/storage/system/)
logo_owner: Lakka (libretro)
logo_licence: file from libretro/retroarch-assets, CC-BY-4.0
retrodeck:
summary: >-
One app with its emulators inside. Install it from Flathub or Discover.
folders:
- where: Steam Deck and Linux
path: '`~/retrodeck/bios/`'
- where: Installed on a microSD card
path: 'for example `/run/media/mmcblk0p1/retrodeck/bios/`'
run: [steamdeck, linux]
pack_manual: |
Option B: Manual
----------------
1. Open Dolphin file manager
2. Show hidden files (Ctrl+H)
3. Navigate to ~/retrodeck/
4. Extract all files from this archive directly into ~/retrodeck/
(the archive already holds the bios/ and roms/ folders)
NOTE: RetroDECK uses its own BIOS checker. After
copying, open RetroDECK > Tools > BIOS Checker to
verify everything is detected.
logo_owner: RetroDECK
logo_licence: repository licence GPL-3.0
romm:
summary: >-
A game library on your own server. It plays games in the web browser.
folders:
- where: In the RomM library
path: '`bios/`, one subfolder per platform'
more_folders:
- where: 'With `firmware: "{platform}/bios"` in `config.yml`'
path: '`<platform>/bios/` for each platform'
run: [server]
notes:
- 'You can also add BIOS files in RomM''s web page.'
dest: /path/to/library/bios
# RomM 5.4 places firmware by filesystem.structure.firmware in config.yml,
# bios/{platform} by default, and suggests {platform}/bios for a library
# laid out as {platform}/roms (rommapp/romm 68f71259,
# backend/config/config_manager.py:58,101-109,1105-1124).
pack_manual: |
Option B: Manual
----------------
The archive holds one folder per RomM platform (psx/, gba/, ...).
Where they go depends on filesystem.structure.firmware in RomM's
config.yml, read relative to the library folder:
- Not set, or "bios/{platform}" (the default): extract all files
from this archive directly into library/bios/
- "{platform}/bios" (the layout RomM suggests for a library kept as
{platform}/roms): each folder goes under its platform, psx/ into
library/psx/bios/, gba/ into library/gba/bios/, and so on.
The installer reads config.yml and does this itself. Run from
the host of a container, --romm-firmware "{platform}/bios" says it.
Then restart the RomM service to detect new files.
logo_owner: RomM
logo_licence: GitHub organisation avatar of rommapp
rocknix:
summary: A system for handhelds.
folders:
- where: On ROCKNIX
run: [device]
notes:
- >-
You can also reach the folder from a computer, over your home network.
reference_notes:
- >-
ROCKNIX runs on ARM and x86 handhelds, among them the RG35XX (H700) and the RG552
(RK3399). The folder is reachable over SSH and over the network share.
logo_owner: ROCKNIX
logo_licence: GitHub organisation avatar of ROCKNIX
amberelec:
summary: A system for Anbernic handhelds.
folders:
- where: On AmberELEC
run: [device]
reference_notes:
- >-
AmberELEC runs on the RG351P, RG351V, RG351MP, RG353, RG503 and RG552. Its list
names files under `/storage/roms`: `bios/` for most, `neogeo/` for the rest. The
pack holds those folders.
logo_owner: AmberELEC
logo_licence: GitHub organisation avatar of AmberELEC
emuelec:
summary: A system for TV boxes and some handhelds.
folders:
- where: On EmuELEC
run: [device]
reference_notes:
- >-
EmuELEC runs on Amlogic TV boxes, the Odroid Go Advance, GameForce and RK356x devices.
Its list names files under `/storage/roms`: `bios/` for most, `neogeo/` and `ports/`
for the rest. The pack holds those folders.
logo_owner: EmuELEC
logo_licence: GitHub organisation avatar of EmuELEC
knulli:
summary: A system for handhelds, based on Batocera.
folders:
- where: On KNULLI
run: [device]
reference_notes:
- >-
KNULLI runs on handhelds among them the RG35XX family (H700) and the TrimUI Smart
Pro.
logo_owner: KNULLI
logo_licence: GitHub organisation avatar of knulli-cfw
retropie:
summary: Installs RetroArch and other emulators on a Raspberry Pi.
folders:
- where: On the Raspberry Pi
run: [device]
notes:
- >-
Use the RetroPie pack, not the RetroArch one. On RetroPie 4.8, Python is too old
for the command: use the pack there.
reference_notes:
- >-
RetroPie also installs standalone emulators (openMSX, PCSX2, XRoar, Amiberry and
others) whose files only the RetroPie pack carries.
- >-
RetroPie is archived in this project: its configuration is kept and its pack is
still built, but its upstream list is no longer read again on a schedule.
pack_manual: |
Option B: Via network share
--------------------------
1. On your PC, open: \\RETROPIE\bios\
2. Extract all files from this archive directly into that folder
Option C: Via SSH
-----------------
1. SSH into your Pi: ssh pi@retropie
2. Copy files to ~/RetroPie/BIOS/
NOTE: the BIOS folder is on the card's Linux partition,
which Windows and macOS cannot open: copying through the
card works from a Linux computer only
(<rootfs>/home/pi/RetroPie/BIOS/).
logo_owner: RetroPie
logo_licence: GitHub organisation avatar of RetroPie
bizhawk:
summary: An emulator app for many consoles, from TASVideos.
folders:
- where: Inside the BizHawk folder
path: '`Firmware\`'
- where: Default place the installer looks
path: '`%USERPROFILE%\BizHawk\Firmware\`'
run: [windows, linux]
reference_notes:
- BizHawk carries rerecording and debugging tools.
pack_manual: |
Option B: Manual
----------------
1. Open the folder you extracted BizHawk to, the one holding
EmuHawk.exe (Windows) or EmuHawkMono.sh (Linux)
2. Navigate to its Firmware subfolder
3. Extract all files from this archive directly into your Firmware folder
4. In BizHawk: Config > Paths > Firmware should
point to this folder
logo_owner: TASVideos
logo_licence: GitHub organisation avatar of TASEmulators
misterfpga:
summary: >-
A board that recreates old consoles in hardware, not in software.
folders:
- where: On the MiSTer SD card
path: '`/media/fat/games/`, one subfolder per core'
run: [device]
notes:
- >-
Each file goes in its console's folder, like `/media/fat/games/3DO/boot.rom`.
reference_notes:
- >-
MiSTer runs FPGA cores on a DE10-Nano board. Only the files the MiSTer BIOS database
lists with a download address are in the pack. The others come with the MiSTer distribution
and install themselves.
logo_owner: MiSTer FPGA
logo_licence: GitHub organisation avatar of MiSTer-devel
minui:
summary: A simple game menu for handhelds. It runs from the SD card.
folders:
- where: On the SD card
path: '`Bios/<TAG>/`, one folder per console'
run: [macos, linux, windows]
notes:
- >-
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
PlayStation (PS)/` uses `Bios/PS/`.
on_card: true
reference_notes:
- >-
MinUI gives each console its own folder, named after the tag in parentheses of the
Roms folder the game sits in. A game in `Roms/Game Boy (GB)/` runs with `Bios/GB/`
as its BIOS folder (MinUI `workspace/all/minarch/minarch.c:2943`).
- >-
Without `--platform`, the installer reads the release name MinUI writes in `.system/version.txt`
on the card. A console started by a standalone program instead of a libretro core
(DraStic for NDS, native PICO-8) is not covered.
logo_owner: Shaun Inman
logo_licence: GitHub avatar of shauninman, the author of MinUI
nextui:
summary: >-
A game menu for handhelds, based on MinUI. It runs from the SD card.
folders:
- where: On the SD card
path: '`Bios/<TAG>/`, one folder per console'
run: [macos, linux, windows]
notes:
- >-
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
PlayStation (PS)/` uses `Bios/PS/`.
on_card: true
reference_notes:
- >-
NextUI gives each console its own folder, named after the tag in parentheses of
the Roms folder the game sits in (NextUI `workspace/all/minarch/ma_core.c:76`).
logo_owner: LoveRetro
logo_licence: GitHub organisation avatar of LoveRetro
myminui:
summary: >-
A game menu for handhelds, based on MinUI. It runs from the SD card.
folders:
- where: On the SD card
path: '`Bios/<TAG>/`, one folder per console'
run: [macos, linux, windows]
notes:
- >-
Each console has its own folder, named after the tag in its Roms folder. `Roms/Sony
PlayStation (PS)/` uses `Bios/PS/`.
on_card: true
reference_notes:
- >-
MyMinUI gives each console its own folder, named after the tag in parentheses of
the Roms folder the game sits in (MyMinUI `workspace/all/minarch/minarch.c:3532`).
+6 -25
View File
@@ -41,30 +41,6 @@ ICON_SIZE = 48
AVATAR_HOST = "avatars.githubusercontent.com"
USER_AGENT = "retrobios-site-images"
# Who owns each mark. Logos are shown to name the platform they belong to;
# the note says so, and gives the licence of the repository the file was
# taken from when that repository states one.
OWNERS = {
"retroarch": ("libretro", "repository licence GPL-3.0"),
"lakka": ("Lakka (libretro)", "file from libretro/retroarch-assets, CC-BY-4.0"),
"batocera": ("Batocera", "repository licence MIT"),
"recalbox": ("Recalbox", "file from homarr-labs/dashboard-icons, Apache-2.0"),
"retrobat": ("RetroBat", "repository states no licence"),
"emudeck": ("EmuDeck", "repository licence GPL-3.0"),
"retrodeck": ("RetroDECK", "repository licence GPL-3.0"),
"romm": ("RomM", "GitHub organisation avatar of rommapp"),
"rocknix": ("ROCKNIX", "GitHub organisation avatar of ROCKNIX"),
"amberelec": ("AmberELEC", "GitHub organisation avatar of AmberELEC"),
"emuelec": ("EmuELEC", "GitHub organisation avatar of EmuELEC"),
"knulli": ("KNULLI", "GitHub organisation avatar of knulli-cfw"),
"retropie": ("RetroPie", "GitHub organisation avatar of RetroPie"),
"bizhawk": ("TASVideos", "GitHub organisation avatar of TASEmulators"),
"misterfpga": ("MiSTer FPGA", "GitHub organisation avatar of MiSTer-devel"),
"minui": ("Shaun Inman", "GitHub avatar of shauninman, the author of MinUI"),
"nextui": ("LoveRetro", "GitHub organisation avatar of LoveRetro"),
}
def fetch(url: str) -> bytes:
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(request, timeout=60) as response:
@@ -107,6 +83,8 @@ def write_manifest(directory: Path, note: str, entries: list[dict]) -> None:
def fetch_logos() -> None:
from siteguides import GUIDES
with open(ROOT / "platforms" / "_registry.yml", encoding="utf-8") as handle:
platforms = yaml.safe_load(handle)["platforms"]
LOGO_DIR.mkdir(parents=True, exist_ok=True)
@@ -125,7 +103,10 @@ def fetch_logos() -> None:
if stale.name != file_name:
stale.unlink()
(LOGO_DIR / file_name).write_bytes(payload)
owner, licence = OWNERS.get(name, (name, "ownership not recorded"))
# Who owns each mark: logo_owner and logo_licence in the guides.
guide = GUIDES.get(name)
owner = (guide and guide.logo_owner) or name
licence = (guide and guide.logo_licence) or "ownership not recorded"
entries.append({
"file": file_name, "platform": name, "source": url,
"owner": owner, "licence": licence,
+112 -63
View File
@@ -75,7 +75,6 @@ from generate_readme import (
from provenance_report import build_report, distinct_missing
from siteexports import generate_exports_page, load_export_reports
from siteguides import (
Guide,
ALSO_RUNS,
DEVICE_GROUPS,
GROUP_ANCHORS,
@@ -2339,22 +2338,15 @@ def _platform_guide(name: str, display: str, cov: dict, registry: dict, no_list:
lines += [note, ""]
lines += [line for note in extract_notes("platforms") if f"The {display} pack " in note
for line in (note, "")]
if name == "retroarch":
if guide.more_folders:
detected = _detected_systems(registry, name) if guide.more_folders_detected else []
lines += [
'??? note "Other builds: Microsoft Store, Mac Steam, handhelds"',
"",
*_folder_rows(guide.more_folders, " "),
*(f" | {system} | `{folder}` |" for system, folder in _retroarch_handhelds(registry)),
"",
" LaunchBox and other launchers use the folder RetroArch reads.",
"",
]
elif guide.more_folders:
lines += [
'??? note "Other layouts"',
f'??? note "{guide.more_folders_title}"',
"",
*_folder_rows(guide.more_folders, " "),
*(f" | {system} | `{folder}` |" for system, folder in detected),
"",
*([f" {guide.more_folders_note}", ""] if guide.more_folders_note else []),
]
lines += [
*(["<!-- image: minui-card -->", ""] if guide.on_card else []),
@@ -6172,11 +6164,12 @@ def _pack_cell(name: str, record: dict, prefix: str = "") -> str:
return f"{links} ({_fmt_size(size)})" if size else links
def _retroarch_handhelds(registry: dict) -> list[tuple[str, str]]:
"""Handheld systems that run RetroArch, with the BIOS folder the installer
writes on each, read from the detection rules of the registry."""
def _detected_systems(registry: dict, platform: str) -> list[tuple[str, str]]:
"""Named systems a platform runs on, handhelds that run RetroArch for one,
with the BIOS folder the installer writes on each, read from the
detection rules of the registry."""
seen: dict[str, str] = {}
for rule in (registry.get("retroarch", {}).get("install") or {}).get("detect") or []:
for rule in (registry.get(platform, {}).get("install") or {}).get("detect") or []:
name, folder = rule.get("name"), rule.get("bios_path")
if name and folder and rule.get("installs", True) and name not in seen:
seen[name] = folder
@@ -6310,7 +6303,7 @@ def generate_which_pack() -> str:
" | System | Runs | BIOS folder |",
" |--------|------|-------------|",
*(f" | {name} | [RetroArch](platforms/retroarch.md) | `{folder}` |"
for name, folder in _retroarch_handhelds(registry)),
for name, folder in _detected_systems(registry, "retroarch")),
"",
]
lines += [
@@ -6415,11 +6408,75 @@ def _platform_nav(coverages: dict, registry: dict) -> list:
return nav
# The tabs in reading order, by the level their pages declare.
TABS = {
"start": "Start here",
"setup": "Your setup",
"explore": "Explore",
"reference": "Reference",
"contribute": "Contribute",
}
class NavError(ValueError):
"""A wiki page does not say where it goes in the navigation."""
def _h1(markdown: str) -> str:
match = re.search(r"^# (.+?)(?:\s*\{[^}]*\})?\s*$", markdown, re.M)
return match.group(1).strip() if match else ""
def wiki_nav(wiki_dir: Path | None = None) -> dict[str, list[tuple[int, object]]]:
"""Wiki pages by tab, each at the place its front matter gives.
A page states `level:` (its tab), `order:` (its place in the tab, the
smallest first) and, when the navigation names it otherwise than its
first heading, `nav_title:`. `nav: false` keeps a page out on purpose.
A page that says neither raises, naming what to add.
"""
wiki_dir = Path(wiki_dir) if wiki_dir else _REPO_ROOT / WIKI_SRC_DIR
tabs: dict[str, list[tuple[int, object]]] = {level: [] for level in TABS}
for page in sorted(wiki_dir.glob("*.md")):
where = f"{WIKI_SRC_DIR}/{page.name}"
text = page.read_text(encoding="utf-8")
meta = _front_matter(text)
level = meta.get("level")
if level not in TABS:
raise NavError(
f"{where}: front matter needs `level:`, one of {', '.join(TABS)}, "
f"to place the page in a tab (found {level!r})"
)
if meta.get("nav") is False:
continue
order = meta.get("order")
if not isinstance(order, int) or isinstance(order, bool):
raise NavError(
f"{where}: front matter needs `order:`, a whole number giving its "
f"place in the {TABS[level]} tab, smallest first (found {order!r}), "
"or `nav: false` to keep it out of the navigation"
)
path = f"{WIKI_SRC_DIR}/{page.name}"
if page.name == "index.md":
tabs[level].append((order, path))
continue
title = meta.get("nav_title") or _h1(text)
if not title:
raise NavError(f"{where}: no `# ` heading and no `nav_title:` to name it")
tabs[level].append((order, {str(title): path}))
return tabs
def _nav_target(entry: object) -> str:
return entry if isinstance(entry, str) else str(next(iter(entry.values())))
def generate_mkdocs_nav(
coverages: dict,
manufacturers: dict,
profiles: dict,
registry: dict | None = None,
wiki_dir: Path | None = None,
) -> list:
"""The navigation, five tabs in reading order.
@@ -6427,51 +6484,42 @@ def generate_mkdocs_nav(
who knows their device, Explore for one system or emulator, Reference for
checking the work, Contribute for adding to it. Each tab opens on its
index page.
The generated pages hold the places below. A wiki page takes its own
place from its front matter (wiki_nav), so a new page needs no edit here.
"""
return [
{"Start here": [
"index.md",
{"What BIOS files are": "wiki/what-are-bios-files.md"},
{"Get your files": "which-pack.md"},
{"First install, step by step": "wiki/first-install.md"},
{"When a game does not start": "wiki/troubleshooting.md"},
{"Common questions": "wiki/faq.md"},
{"Glossary": "wiki/glossary.md"},
]},
{"Your setup": _platform_nav(coverages, registry or {})},
{"Explore": [
"explore.md",
{"Systems": _system_nav(manufacturers)},
{"Emulators": _emulator_nav(profiles)},
{"All installer options": "wiki/installer.md"},
{"Is it legal?": "wiki/legal.md"},
]},
{"Reference": [
"wiki/index.md",
{"How each platform checks files": "wiki/verification-modes.md"},
{"What the packs do not hold": "gaps.md"},
{"Cores by platform": "cross-reference.md"},
{"Dump provenance": "provenance.md"},
{"Updated platform files": "exports.md"},
{"Data and API": "data.md"},
{"Find a file": "files/index.md"},
{"Data model": "wiki/data-model.md"},
{"Architecture": "wiki/architecture.md"},
{"Build your own packs": "wiki/advanced-usage.md"},
{"Command-line tools": "wiki/tools.md"},
{"Technical questions": "wiki/technical-faq.md"},
{"Project terms": "wiki/reference-terms.md"},
{"Projects using this data": "wiki/community-tools.md"},
]},
{"Contribute": [
"contributing.md",
{"Profiling an emulator": "wiki/profiling.md"},
{"Adding a platform": "wiki/adding-a-platform.md"},
{"Adding a scraper": "wiki/adding-a-scraper.md"},
{"Testing": "wiki/testing-guide.md"},
{"Release process": "wiki/release-process.md"},
]},
]
generated: dict[str, list[tuple[int, object]]] = {
"start": [(0, "index.md"), (20, {"Get your files": "which-pack.md"})],
"setup": [(10 * i, item)
for i, item in enumerate(_platform_nav(coverages, registry or {}))],
"explore": [
(0, "explore.md"),
(10, {"Systems": _system_nav(manufacturers)}),
(20, {"Emulators": _emulator_nav(profiles)}),
],
"reference": [
(20, {"What the packs do not hold": "gaps.md"}),
(30, {"Cores by platform": "cross-reference.md"}),
(40, {"Dump provenance": "provenance.md"}),
(50, {"Updated platform files": "exports.md"}),
(60, {"Data and API": "data.md"}),
(70, {"Find a file": "files/index.md"}),
(80, {"Data model": "wiki/data-model.md"}),
],
"contribute": [(0, "contributing.md")],
}
wiki = wiki_nav(wiki_dir)
nav = []
for level, label in TABS.items():
entries = sorted(generated[level] + wiki[level], key=lambda item: item[0])
for (first, a), (second, b) in zip(entries, entries[1:]):
if first == second:
raise NavError(
f"{label} tab: {_nav_target(a)} and {_nav_target(b)} both take "
f"order {first}; give one of them a free number"
)
nav.append({label: [entry for _order, entry in entries]})
return nav
# Main
@@ -6785,7 +6833,8 @@ def _generate(args: argparse.Namespace) -> None:
# Update mkdocs.yml nav section only (avoid yaml.dump round-trip mangling quotes)
print("Updating mkdocs.yml nav...")
nav = generate_mkdocs_nav(coverages, manufacturers, profiles, registry)
nav = generate_mkdocs_nav(coverages, manufacturers, profiles, registry,
Path(WIKI_SRC_DIR))
nav_yaml = yaml.dump(
{"nav": nav}, default_flow_style=False, sort_keys=False, allow_unicode=True
)
+5 -144
View File
@@ -5,8 +5,10 @@ goes, and which ones the user has to rename."""
from __future__ import annotations
INSTALL_SH = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh"
INSTALL_PS1 = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1"
from siteguides import INSTALL_BASE, pack_manual
INSTALL_SH = f"{INSTALL_BASE}/install.sh"
INSTALL_PS1 = f"{INSTALL_BASE}/install.ps1"
def _detect_systems(detect: list[dict]) -> list[str]:
"""The operating systems the registry's detection rules cover, in order."""
@@ -114,147 +116,6 @@ def _generic_manual(bios_paths: list[str]) -> str:
)
# What a manual install needs on each platform: its shares, cards and folders.
MANUAL_GUIDES: dict[str, str] = {
"retroarch": (
" Option B: Manual (PC)\n"
" ---------------------\n"
" 1. Find your RetroArch system directory:\n"
" - RetroArch > Settings > Directory > System/BIOS\n"
" - Default: retroarch/system/\n"
" 2. Extract all files from this archive directly into your system directory\n"
" 3. Overwrite if asked\n\n"
" Option C: Manual (handheld / SD card)\n"
" -------------------------------------\n"
" Anbernic, Retroid, Miyoo, Trimui, etc.:\n"
" 1. Connect your SD card to your PC\n"
" 2. Find the BIOS folder (usually BIOS/ or system/)\n"
" 3. Extract all files from this archive directly into that folder\n"
" 4. Eject SD card and reboot your device\n\n"
" Common paths by device:\n"
" Anbernic (ArkOS/JELOS): BIOS/\n"
" Retroid (RetroArch): RetroArch/system/\n"
" Miyoo Mini (Onion OS): BIOS/\n"
" Steam Deck (RetroArch): ~/.config/retroarch/system/\n\n"
),
"batocera": (
" The terminal is F1 from the Batocera menu, or SSH.\n\n"
" Option B: Manual (network share)\n"
" --------------------------------\n"
" 1. On your PC, open the Batocera network share:\n"
" - Windows: \\\\BATOCERA\\share\\bios\\\n"
" - Mac/Linux: smb://batocera/share/bios/\n"
" 2. Extract all files from this archive directly into the share\n"
" 3. Overwrite if asked\n\n"
" Option C: Manual (SD card)\n"
" --------------------------\n"
" 1. Put the SD card in your PC\n"
" 2. Navigate to /userdata/bios/ on the SHARE partition\n"
" 3. Extract all files from this archive directly into that folder\n\n"
" NOTE: Dreamcast flash memory is named dc_nvmem.bin\n"
" (if your setup asks for dc_flash.bin, same file).\n\n"
),
"recalbox": (
" Option B: Manual (network share)\n"
" --------------------------------\n"
" 1. On your PC, open the Recalbox network share:\n"
" - Windows: \\\\RECALBOX\\share\\bios\\\n"
" - Mac/Linux: smb://recalbox/share/bios/\n"
" 2. Extract all files from this archive directly into the share\n\n"
" Option C: Manual (SD card)\n"
" --------------------------\n"
" 1. Put the SD card in your PC\n"
" 2. Navigate to /recalbox/share/bios/\n"
" 3. Extract all files from this archive directly into that folder\n\n"
),
"emudeck": (
" Option B: Manual\n"
" ----------------\n"
" Extract all files from this archive directly into EmuDeck's bios\n"
" folder, inside the Emulation folder chosen in EmuDeck:\n"
" Steam Deck, Linux and macOS: ~/Emulation/bios/ by default\n"
" Windows: C:\\Emulation\\bios\\ by default\n\n"
),
"retrodeck": (
" Option B: Manual\n"
" ----------------\n"
" 1. Open Dolphin file manager\n"
" 2. Show hidden files (Ctrl+H)\n"
" 3. Navigate to ~/retrodeck/\n"
" 4. Extract all files from this archive directly into ~/retrodeck/\n"
" (the archive already holds the bios/ and roms/ folders)\n\n"
" NOTE: RetroDECK uses its own BIOS checker. After\n"
" copying, open RetroDECK > Tools > BIOS Checker to\n"
" verify everything is detected.\n\n"
),
"retrobat": (
" Option B: Manual\n"
" ----------------\n"
" 1. Open your RetroBat installation folder\n"
" 2. Navigate to the bios\\ subfolder\n"
" (default: C:\\RetroBat\\bios\\)\n"
" 3. Extract all files from this archive directly into your bios\\ folder\n"
" 4. Overwrite if asked\n\n"
),
"bizhawk": (
" Option B: Manual\n"
" ----------------\n"
" 1. Open the folder you extracted BizHawk to, the one holding\n"
" EmuHawk.exe (Windows) or EmuHawkMono.sh (Linux)\n"
" 2. Navigate to its Firmware subfolder\n"
" 3. Extract all files from this archive directly into your Firmware folder\n"
" 4. In BizHawk: Config > Paths > Firmware should\n"
" point to this folder\n\n"
),
# RomM 5.4 places firmware by filesystem.structure.firmware in config.yml,
# bios/{platform} by default, and suggests {platform}/bios for a library
# laid out as {platform}/roms (rommapp/romm 68f71259,
# backend/config/config_manager.py:58,101-109,1105-1124).
"romm": (
" Option B: Manual\n"
" ----------------\n"
" The archive holds one folder per RomM platform (psx/, gba/, ...).\n"
" Where they go depends on filesystem.structure.firmware in RomM's\n"
" config.yml, read relative to the library folder:\n\n"
" - Not set, or \"bios/{platform}\" (the default): extract all files\n"
" from this archive directly into library/bios/\n"
" - \"{platform}/bios\" (the layout RomM suggests for a library kept as\n"
" {platform}/roms): each folder goes under its platform, psx/ into\n"
" library/psx/bios/, gba/ into library/gba/bios/, and so on.\n"
" The installer reads config.yml and does this itself. Run from\n"
" the host of a container, --romm-firmware \"{platform}/bios\" says it.\n\n"
" Then restart the RomM service to detect new files.\n\n"
),
"retropie": (
" Option B: Via network share\n"
" --------------------------\n"
" 1. On your PC, open: \\\\RETROPIE\\bios\\\n"
" 2. Extract all files from this archive directly into that folder\n\n"
" Option C: Via SSH\n"
" -----------------\n"
" 1. SSH into your Pi: ssh pi@retropie\n"
" 2. Copy files to ~/RetroPie/BIOS/\n\n"
" NOTE: the BIOS folder is on the card's Linux partition,\n"
" which Windows and macOS cannot open: copying through the\n"
" card works from a Linux computer only\n"
" (<rootfs>/home/pi/RetroPie/BIOS/).\n\n"
),
"lakka": (
" Lakka reads its BIOS files from /storage/system/.\n\n"
" Option B: Via network share\n"
" --------------------------\n"
" 1. On Lakka: Settings > Services > Samba Enable\n"
" 2. On your PC, open the System share: \\\\LAKKA\\System\\\n"
" (macOS and Linux: smb://lakka/System)\n"
" 3. Extract all files from this archive directly into that share\n\n"
" Option C: Via SSH\n"
" -----------------\n"
" 1. On Lakka: Settings > Services > SSH Enable\n"
" 2. Copy the files to /storage/system/ (scp -r * root@lakka:/storage/system/)\n\n"
),
}
def _build_readme(
platform_name: str,
platform_display: str,
@@ -303,7 +164,7 @@ def _build_readme(
guide = (
"INSTALLATION GUIDE\n\n"
+ _automatic_section(platform_display, detect, standalone_copies)
+ MANUAL_GUIDES.get(platform_name, _generic_manual(bios_paths))
+ (pack_manual(platform_name) or _generic_manual(bios_paths))
)
if regions:
+208 -363
View File
@@ -1,118 +1,28 @@
"""The guide part of the site: where each setup keeps its BIOS files.
A platform page opens on what someone holding the device needs: the folder,
the command, the pack, how to check. The registry says which group of
devices a platform belongs to (`device_group`), so the device picker and the
navigation list every registered platform. The prose below is per platform,
and a test holds that every registered platform has an entry.
Every path here is one the installer detects, the registry records or the
platform documents. Menu labels of third-party software are named only where
this project already relies on them.
the command, the pack, how to check. The words live in
``platforms/_guides.yml``, one guide per registered platform, beside the
device groups, the terminal tabs and the safety notes every guide shares.
This module loads that file, checks it against the registry and renders the
parts the pages share. The registry keeps the folder the pack unzips into
(``extract_to``) and the group a platform belongs to (``device_group``).
"""
from __future__ import annotations
from dataclasses import dataclass, field
import textwrap
from dataclasses import dataclass, fields
from pathlib import Path
INSTALL_BASE = "https://raw.githubusercontent.com/Abdess/retrobios/main"
from safeparse import require_yaml
# Order of the groups on the picker, in the navigation and on the home page.
DEVICE_GROUPS: dict[str, tuple[str, str]] = {
"computer": (
"Computers and Steam Deck",
"An emulator app on a computer or a Steam Deck.",
),
"gaming-os": (
"Game systems for PCs and small boards",
"A system that starts straight into a game menu, on a PC or a Raspberry Pi.",
),
"handheld": (
"Handhelds and TV boxes",
"The system your handheld or TV box starts.",
),
"fpga": (
"MiSTer FPGA",
"A board that recreates old consoles in hardware, not in software.",
),
"server": (
"Self-hosted game library",
"A RomM server. It plays games in your web browser.",
),
}
PLATFORMS_DIR = Path(__file__).resolve().parent.parent / "platforms"
GUIDES_FILE = PLATFORMS_DIR / "_guides.yml"
# Section anchors on the device picker. Two keep the anchors the page had
# before it was grouped by device, which links outside the site may use.
GROUP_ANCHORS = {
"computer": "computers-and-steam-deck",
"gaming-os": "game-systems",
"handheld": "handhelds",
"fpga": "fpga",
"server": "self-hosted",
}
# Setups that are not platforms of their own: they run one, and use its pack.
# (setup, what it is, platform it uses, runs on, BIOS folder)
ALSO_RUNS: dict[str, tuple[tuple[str, str, str, str, str], ...]] = {
"computer": (
("LaunchBox", "A game library and launcher. It starts RetroArch or "
"standalone emulators.", "retroarch", "Windows",
"The folder of the RetroArch it starts"),
),
"handheld": (
("Android (Retroid Pocket, AYN Odin and others)", "RetroArch for Android.",
"retroarch", "Android", "`RetroArch/system/` on the internal storage"),
),
}
# Ways to run the installer. Each tab names how to open a terminal there,
# the shell the command is written for, and the options it needs.
TERMINALS: dict[str, tuple[str, str, str]] = {
"windows": (
"Windows",
"Open the Start menu. Type **PowerShell** and open it. Paste the line. "
"Press Enter.",
"powershell",
),
"macos": (
"macOS",
"Open **Terminal**, in Applications, then Utilities. Paste the line. "
"Press Return.",
"sh",
),
"linux": (
"Linux",
"Open a terminal. Paste the line. Press Enter.",
"sh",
),
"steamdeck": (
"Steam Deck",
"Switch to Desktop Mode. Open **Konsole**. Paste with Ctrl+Shift+V. "
"Press Enter.",
"sh",
),
"android": (
"Android (Termux)",
"Install **Termux**, a terminal app. Run `pkg install python` and "
"`termux-setup-storage` once. Then paste the line. Press Enter.",
"sh",
),
"device": (
"On the device",
"Open a terminal on the device. Or connect from a computer with SSH, a "
"terminal over the network. Paste the line. Press Enter.",
"sh",
),
"server": (
"On the server",
"Open a terminal on the machine that holds your RomM library. Put the "
"path of its `bios` folder after `--dest`.",
"sh",
),
}
# Windows users of a card frontend run the PowerShell form with a drive letter.
CARD_TERMINALS = ("macos", "linux", "windows")
class GuideError(ValueError):
"""The guides file does not say what a page needs, or contradicts the registry."""
@dataclass(frozen=True)
@@ -120,10 +30,14 @@ class Guide:
"""What a platform page says before its file reference.
`folders` are the common cases, `more_folders` the rarer builds shown
folded under them. `notes` stay in the guide, one plain fact each.
`reference_notes` carry the source citations and the details a reader
checking the work wants, and open the Reference part of the page.
`download_hint` is one more bullet under the pack download.
folded under them, under `more_folders_title`. `more_folders_detected`
adds a row for each named system the registry detects the platform on.
`notes` stay in the guide, one plain fact each. `reference_notes` carry
the source citations and the details a reader checking the work wants,
and open the Reference part of the page. `download_hint` is one more
bullet under the pack download. `pack_manual` is the manual part of the
README inside the pack. `logo_owner` and `logo_licence` credit the
logo the registry links.
"""
summary: str
@@ -132,257 +46,198 @@ class Guide:
notes: tuple[str, ...] = ()
dest: str = ""
on_card: bool = False
extra_args: dict[str, str] = field(default_factory=dict)
more_folders: tuple[tuple[str, str], ...] = ()
more_folders_title: str = "Other layouts"
more_folders_detected: bool = False
more_folders_note: str = ""
reference_notes: tuple[str, ...] = ()
download_hint: str = ""
pack_manual: str = ""
logo_owner: str = ""
logo_licence: str = ""
NETWORK_HINT = "From a computer, use the network folder above."
_GUIDE_KEYS = {f.name for f in fields(Guide)}
_REQUIRED = ("summary", "folders", "run")
_TOP_KEYS = {"install_base", "trust_items", "device_groups", "terminals", "guides"}
_GROUP_KEYS = {"label", "description", "anchor", "also_runs"}
_ALSO_KEYS = ("setup", "summary", "uses", "runs_on", "folder")
_TERMINAL_KEYS = ("label", "how", "shell")
GUIDES: dict[str, Guide] = {
"retroarch": Guide(
summary="An app that runs many consoles. It uses a different **core**, "
"a small emulator, for each one.",
folders=(
("Windows", "`system\\` next to `retroarch.exe`"),
("macOS", "`~/Documents/RetroArch/system/`"),
("Linux", "`~/.config/retroarch/system/`"),
("Steam Deck and Linux Flatpak", "`~/.var/app/org.libretro.RetroArch/config/retroarch/system/`"),
("Android", "`RetroArch/system/` on the internal storage"),
("Nintendo Switch", "`retroarch/cores/system/` on the SD card"),
),
more_folders=(
("Windows, Microsoft Store build",
"`%LOCALAPPDATA%\\Packages\\1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_*\\LocalState\\system\\`"),
("macOS, Steam or portable build", "`~/Library/Application Support/RetroArch/system/`"),
),
run=("windows", "macos", "linux", "steamdeck", "android"),
notes=(
"**Not sure?** In RetroArch, open **Settings > Directory > "
"System/BIOS**. It shows the folder.",
),
reference_notes=(
"RetroArch reads its BIOS folder from the `system_directory` line of "
"`retroarch.cfg`, the value **Settings > Directory > System/BIOS** "
"shows.",
"On Windows, `retroarch.cfg` is read next to `retroarch.exe` first, "
"then from `%APPDATA%\\RetroArch\\`. A `system_directory` left at its "
"default is the `system` folder next to `retroarch.exe` (`:\\system` "
"in RetroArch's `frontend/drivers/platform_win32.c`, where `:` stands "
"for the folder of the program).",
),
),
"batocera": Guide(
summary="A system that starts straight into a game menu.",
folders=(
("On Batocera", "`/userdata/bios/`"),
("From Windows, over your home network", "`\\\\BATOCERA\\share\\bios\\`"),
("From a Mac or Linux, over your home network", "`smb://batocera/share/bios/`"),
),
run=("device",),
download_hint=NETWORK_HINT,
),
"recalbox": Guide(
summary="A system that starts straight into a game menu and sets up its "
"emulators.",
folders=(
("On Recalbox", "`/recalbox/share/bios/`"),
("From Windows, over your home network", "`\\\\RECALBOX\\share\\bios\\`"),
),
run=("device",),
download_hint=NETWORK_HINT,
),
"retrobat": Guide(
summary="A game menu for Windows. It comes with RetroArch and other "
"emulators.",
folders=(
("Inside the RetroBat folder", "`bios\\`"),
("Default install", "`C:\\RetroBat\\bios\\`"),
),
run=("windows",),
notes=("The command finds RetroBat wherever you installed it.",),
reference_notes=(
"RetroBat records its own folder each time it starts, and the "
"installer reads it there.",
),
),
"emudeck": Guide(
summary="Installs your emulators and adds your games to Steam.",
folders=(
("Steam Deck and Linux", "`~/Emulation/bios/`"),
("Windows, by default", "`C:\\Emulation\\bios\\`"),
("macOS", "`~/Emulation/bios/`"),
),
run=("steamdeck", "linux", "windows", "macos"),
notes=(
"The `bios` folder is inside the Emulation folder you chose in "
"EmuDeck. If you chose the SD card, it is on the card.",
),
reference_notes=(
"The installer reads the Emulation folder from EmuDeck's settings "
"(`emulationPath` in `~/.config/EmuDeck/settings.sh`).",
),
),
"lakka": Guide(
summary="A system that starts straight into RetroArch.",
folders=(("On Lakka", "`/storage/system/`"),),
run=("device",),
notes=("You can also reach the folder from a computer, over your home network.",),
reference_notes=("The folder is reachable over SSH and over the network share.",),
),
"retrodeck": Guide(
summary="One app with its emulators inside. Install it from Flathub or "
"Discover.",
folders=(
("Steam Deck and Linux", "`~/retrodeck/bios/`"),
("Installed on a microSD card", "for example `/run/media/mmcblk0p1/retrodeck/bios/`"),
),
run=("steamdeck", "linux"),
),
"romm": Guide(
summary="A game library on your own server. It plays games in the web "
"browser.",
folders=(("In the RomM library", "`bios/`, one subfolder per platform"),),
more_folders=(
("With `firmware: \"{platform}/bios\"` in `config.yml`",
"`<platform>/bios/` for each platform"),
),
run=("server",),
dest="/path/to/library/bios",
notes=("You can also add BIOS files in RomM's web page.",),
),
"rocknix": Guide(
summary="A system for handhelds.",
folders=(("On ROCKNIX", "`/storage/roms/bios/`"),),
run=("device",),
notes=("You can also reach the folder from a computer, over your home network.",),
reference_notes=(
"ROCKNIX runs on ARM and x86 handhelds, among them the RG35XX (H700) "
"and the RG552 (RK3399). The folder is reachable over SSH and over "
"the network share.",
),
),
"amberelec": Guide(
summary="A system for Anbernic handhelds.",
folders=(("On AmberELEC", "`/storage/roms/`"),),
run=("device",),
reference_notes=(
"AmberELEC runs on the RG351P, RG351V, RG351MP, RG353, RG503 and "
"RG552. Its list names files under `/storage/roms`: `bios/` for most, "
"`neogeo/` for the rest. The pack holds those folders.",
),
),
"emuelec": Guide(
summary="A system for TV boxes and some handhelds.",
folders=(("On EmuELEC", "`/storage/roms/`"),),
run=("device",),
reference_notes=(
"EmuELEC runs on Amlogic TV boxes, the Odroid Go Advance, GameForce "
"and RK356x devices. Its list names files under `/storage/roms`: "
"`bios/` for most, `neogeo/` and `ports/` for the rest. The pack "
"holds those folders.",
),
),
"knulli": Guide(
summary="A system for handhelds, based on Batocera.",
folders=(("On KNULLI", "`/userdata/bios/`"),),
run=("device",),
reference_notes=(
"KNULLI runs on handhelds among them the RG35XX family (H700) and the "
"TrimUI Smart Pro.",
),
),
"retropie": Guide(
summary="Installs RetroArch and other emulators on a Raspberry Pi.",
folders=(("On the Raspberry Pi", "`~/RetroPie/BIOS/`"),),
run=("device",),
notes=(
"Use the RetroPie pack, not the RetroArch one. On RetroPie 4.8, "
"Python is too old for the command: use the pack there.",
),
reference_notes=(
"RetroPie also installs standalone emulators (openMSX, PCSX2, XRoar, "
"Amiberry and others) whose files only the RetroPie pack carries.",
"RetroPie is archived in this project: its configuration is kept and "
"its pack is still built, but its upstream list is no longer read "
"again on a schedule.",
),
),
"bizhawk": Guide(
summary="An emulator app for many consoles, from TASVideos.",
folders=(
("Inside the BizHawk folder", "`Firmware\\`"),
("Default place the installer looks", "`%USERPROFILE%\\BizHawk\\Firmware\\`"),
),
run=("windows", "linux"),
reference_notes=("BizHawk carries rerecording and debugging tools.",),
),
"misterfpga": Guide(
summary="A board that recreates old consoles in hardware, not in software.",
folders=(("On the MiSTer SD card", "`/media/fat/games/`, one subfolder per core"),),
run=("device",),
notes=(
"Each file goes in its console's folder, like "
"`/media/fat/games/3DO/boot.rom`.",
),
reference_notes=(
"MiSTer runs FPGA cores on a DE10-Nano board. Only the files the "
"MiSTer BIOS database lists with a download address are in the pack. "
"The others come with the MiSTer distribution and install themselves.",
),
),
"minui": Guide(
summary="A simple game menu for handhelds. It runs from the SD card.",
folders=(("On the SD card", "`Bios/<TAG>/`, one folder per console"),),
run=CARD_TERMINALS,
on_card=True,
notes=(
"Each console has its own folder, named after the tag in its Roms "
"folder. `Roms/Sony PlayStation (PS)/` uses `Bios/PS/`.",
),
reference_notes=(
"MinUI gives each console its own folder, named after the tag in "
"parentheses of the Roms folder the game sits in. A game in "
"`Roms/Game Boy (GB)/` runs with `Bios/GB/` as its BIOS folder (MinUI "
"`workspace/all/minarch/minarch.c:2943`).",
"Without `--platform`, the installer reads the release name MinUI "
"writes in `.system/version.txt` on the card. A console started by a "
"standalone program instead of a libretro core (DraStic for NDS, "
"native PICO-8) is not covered.",
),
),
"nextui": Guide(
summary="A game menu for handhelds, based on MinUI. It runs from the SD card.",
folders=(("On the SD card", "`Bios/<TAG>/`, one folder per console"),),
run=CARD_TERMINALS,
on_card=True,
notes=(
"Each console has its own folder, named after the tag in its Roms "
"folder. `Roms/Sony PlayStation (PS)/` uses `Bios/PS/`.",
),
reference_notes=(
"NextUI gives each console its own folder, named after the tag in "
"parentheses of the Roms folder the game sits in (NextUI "
"`workspace/all/minarch/ma_core.c:76`).",
),
),
"myminui": Guide(
summary="A game menu for handhelds, based on MinUI. It runs from the SD card.",
folders=(("On the SD card", "`Bios/<TAG>/`, one folder per console"),),
run=CARD_TERMINALS,
on_card=True,
notes=(
"Each console has its own folder, named after the tag in its Roms "
"folder. `Roms/Sony PlayStation (PS)/` uses `Bios/PS/`.",
),
reference_notes=(
"MyMinUI gives each console its own folder, named after the tag in "
"parentheses of the Roms folder the game sits in (MyMinUI "
"`workspace/all/minarch/minarch.c:3532`).",
),
),
}
def _text(value: object, where: str) -> str:
if not isinstance(value, str) or not value.strip():
raise GuideError(f"{where}: expected text, found {value!r}")
return value
def _texts(value: object, where: str) -> tuple[str, ...]:
if not isinstance(value, list):
raise GuideError(f"{where}: expected a list, found {value!r}")
return tuple(_text(item, f"{where}[{i}]") for i, item in enumerate(value))
def _unknown(data: dict, allowed: set[str] | tuple[str, ...], where: str) -> None:
extra = sorted(set(data) - set(allowed))
if extra:
raise GuideError(f"{where}: unknown key {', '.join(extra)} "
f"(known: {', '.join(sorted(allowed))})")
def _folder_rows(value: object, extract_to: str, where: str) -> tuple[tuple[str, str], ...]:
"""Folder rows as (where, path). A row with no path is the registry's
extract_to, so that folder is written once."""
if not isinstance(value, list) or not value:
raise GuideError(f"{where}: expected a list of folder rows")
rows = []
for i, row in enumerate(value):
at = f"{where}[{i}]"
if not isinstance(row, dict):
raise GuideError(f"{at}: expected a mapping with `where` and `path`")
_unknown(row, ("where", "path"), at)
label = _text(row.get("where"), f"{at}.where")
if "path" in row:
rows.append((label, _text(row["path"], f"{at}.path")))
elif extract_to:
rows.append((label, f"`{extract_to}`"))
else:
raise GuideError(f"{at}: no `path`, and the registry has no extract_to for it")
return tuple(rows)
def _guide(name: str, data: object, extract_to: str, terminals: dict) -> Guide:
where = f"{GUIDES_FILE.name}: guides.{name}"
if not isinstance(data, dict):
raise GuideError(f"{where}: expected a mapping")
_unknown(data, _GUIDE_KEYS, where)
missing = [key for key in _REQUIRED if key not in data]
if missing:
raise GuideError(f"{where}: missing {', '.join(missing)}")
run = _texts(data["run"], f"{where}.run")
unknown_tabs = [key for key in run if key not in terminals]
if unknown_tabs:
raise GuideError(f"{where}.run: no terminal named {', '.join(unknown_tabs)}")
values: dict = {
"summary": _text(data["summary"], f"{where}.summary"),
"folders": _folder_rows(data["folders"], extract_to, f"{where}.folders"),
"run": run,
}
if "more_folders" in data:
values["more_folders"] = _folder_rows(
data["more_folders"], extract_to, f"{where}.more_folders")
for key in ("notes", "reference_notes"):
if key in data:
values[key] = _texts(data[key], f"{where}.{key}")
for key in ("dest", "download_hint", "more_folders_title", "more_folders_note",
"logo_owner", "logo_licence"):
if key in data:
values[key] = _text(data[key], f"{where}.{key}")
for key in ("on_card", "more_folders_detected"):
if key in data:
if not isinstance(data[key], bool):
raise GuideError(f"{where}.{key}: expected true or false")
values[key] = data[key]
if "pack_manual" in data:
values["pack_manual"] = _text(data["pack_manual"], f"{where}.pack_manual")
return Guide(**values)
def load_guides(path: Path = GUIDES_FILE, registry: dict | None = None) -> dict:
"""The guides file, checked against the registry.
Every registered platform, archived ones included, has a guide and a
device group the file describes. A mistake raises GuideError naming the
key, so the site is never built from half a guide.
"""
yaml = require_yaml()
if registry is None:
with open(PLATFORMS_DIR / "_registry.yml", encoding="utf-8") as handle:
registry = (yaml.safe_load(handle) or {}).get("platforms", {})
with open(path, encoding="utf-8") as handle:
data = yaml.safe_load(handle) or {}
if not isinstance(data, dict):
raise GuideError(f"{path.name}: expected a mapping")
_unknown(data, _TOP_KEYS, path.name)
terminals: dict[str, tuple[str, str, str]] = {}
for key, entry in (data.get("terminals") or {}).items():
at = f"{path.name}: terminals.{key}"
if not isinstance(entry, dict):
raise GuideError(f"{at}: expected a mapping")
_unknown(entry, _TERMINAL_KEYS, at)
terminals[key] = tuple(_text(entry.get(k), f"{at}.{k}") for k in _TERMINAL_KEYS)
if entry["shell"] not in ("sh", "powershell"):
raise GuideError(f"{at}.shell: sh or powershell, found {entry['shell']!r}")
raw_guides = data.get("guides") or {}
groups: dict[str, tuple[str, str]] = {}
anchors: dict[str, str] = {}
also_runs: dict[str, tuple[tuple[str, str, str, str, str], ...]] = {}
for key, entry in (data.get("device_groups") or {}).items():
at = f"{path.name}: device_groups.{key}"
if not isinstance(entry, dict):
raise GuideError(f"{at}: expected a mapping")
_unknown(entry, _GROUP_KEYS, at)
groups[key] = (_text(entry.get("label"), f"{at}.label"),
_text(entry.get("description"), f"{at}.description"))
anchors[key] = _text(entry.get("anchor"), f"{at}.anchor")
rows = []
for i, row in enumerate(entry.get("also_runs") or []):
row_at = f"{at}.also_runs[{i}]"
if not isinstance(row, dict):
raise GuideError(f"{row_at}: expected a mapping")
_unknown(row, _ALSO_KEYS, row_at)
values = tuple(_text(row.get(k), f"{row_at}.{k}") for k in _ALSO_KEYS)
if values[2] not in raw_guides:
raise GuideError(f"{row_at}.uses: {values[2]!r} is not a platform with a guide")
rows.append(values)
if rows:
also_runs[key] = tuple(rows)
unregistered = sorted(set(raw_guides) - set(registry))
if unregistered:
raise GuideError(f"{path.name}: guides for unregistered platforms: "
f"{', '.join(unregistered)}")
guides = {}
for name, entry in registry.items():
if name not in raw_guides:
raise GuideError(f"{path.name}: {name} is registered and has no guide "
f"(add guides.{name})")
group = entry.get("device_group")
if group not in groups:
raise GuideError(f"_registry.yml: {name}.device_group {group!r} is not "
f"one of {', '.join(groups)} in {path.name}")
guides[name] = _guide(name, raw_guides[name], str(entry.get("extract_to") or ""),
terminals)
# The order the file is written in, which the pages do not depend on.
guides = {name: guides[name] for name in raw_guides}
return {
"install_base": _text(data.get("install_base"), f"{path.name}: install_base"),
"trust_items": _texts(data.get("trust_items"), f"{path.name}: trust_items"),
"device_groups": groups,
"group_anchors": anchors,
"also_runs": also_runs,
"terminals": terminals,
"guides": guides,
}
_LOADED = load_guides()
INSTALL_BASE: str = _LOADED["install_base"]
DEVICE_GROUPS: dict[str, tuple[str, str]] = _LOADED["device_groups"]
GROUP_ANCHORS: dict[str, str] = _LOADED["group_anchors"]
ALSO_RUNS: dict[str, tuple[tuple[str, str, str, str, str], ...]] = _LOADED["also_runs"]
TERMINALS: dict[str, tuple[str, str, str]] = _LOADED["terminals"]
TRUST_ITEMS: tuple[str, ...] = _LOADED["trust_items"]
GUIDES: dict[str, Guide] = _LOADED["guides"]
def pack_manual(name: str) -> str:
"""The manual part of a pack README, indented as the README is, or empty."""
guide = GUIDES.get(name)
if not guide or not guide.pack_manual:
return ""
return textwrap.indent(guide.pack_manual.rstrip("\n"), " ") + "\n\n"
def install_command(shell: str, args: str = "") -> str:
@@ -433,16 +288,6 @@ def platform_args(name: str, guide: Guide) -> dict[str, str]:
return args
TRUST_ITEMS = (
"It checks each file before saving it.",
"It writes only in your BIOS folder.",
"It never touches games or saves.",
"It never moves or deletes files it does not know.",
"It keeps any file it replaces in a `retrobios-backup` folder, beside your "
"BIOS folder. The same command with `--restore` puts it back.",
)
def trust_box(prefix: str, indent: str = "") -> list[str]:
"""Is it safe, under every command on a guide page: five plain facts."""
return [
+8 -2
View File
@@ -98,9 +98,15 @@ class TheOneLinerFollowsTheRegistry(unittest.TestCase):
self.assertIn("--romm-firmware", text)
def test_the_commands_are_written_once(self):
"""The address comes from install_base in platforms/_guides.yml, the
one the site's commands use."""
import packreadme
from siteguides import INSTALL_BASE
source = (REPO_ROOT / "scripts" / "packreadme.py").read_text(encoding="utf-8")
self.assertEqual(source.count("main/install.sh"), 1)
self.assertEqual(source.count("main/install.ps1"), 1)
self.assertNotIn("raw.githubusercontent.com", source)
self.assertEqual(packreadme.INSTALL_SH, f"{INSTALL_BASE}/install.sh")
self.assertEqual(packreadme.INSTALL_PS1, f"{INSTALL_BASE}/install.ps1")
if __name__ == "__main__":
+216
View File
@@ -0,0 +1,216 @@
"""The site's words live in Markdown and YAML, not in the generator.
Each platform's guide is in platforms/_guides.yml, read and checked by
siteguides.load_guides. A wiki page places itself in the navigation from its
front matter. Both are edited as data: a new platform or a new wiki page
needs no Python change, and a mistake stops the build with its name.
"""
from __future__ import annotations
import ast
import re
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_site # noqa: E402
import siteguides # noqa: E402
from siteguides import GUIDES, GuideError, load_guides # noqa: E402
GUIDES_FILE = REPO_ROOT / "platforms" / "_guides.yml"
WIKI = REPO_ROOT / "wiki"
def _registry() -> dict:
with open(REPO_ROOT / "platforms" / "_registry.yml", encoding="utf-8") as handle:
return yaml.safe_load(handle)["platforms"]
def _guides_text() -> str:
return GUIDES_FILE.read_text(encoding="utf-8")
class GuidesLiveInYaml(unittest.TestCase):
def _load(self, text: str, registry: dict | None = None) -> dict:
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
path = Path(tmp) / "_guides.yml"
path.write_text(text, encoding="utf-8")
return load_guides(path, registry if registry is not None else _registry())
def test_every_registered_platform_has_a_guide_in_the_file(self):
written = yaml.safe_load(_guides_text())["guides"]
self.assertEqual(sorted(written), sorted(_registry()))
self.assertEqual(sorted(GUIDES), sorted(_registry()))
def test_a_platform_without_a_guide_is_named(self):
data = yaml.safe_load(_guides_text())
del data["guides"]["knulli"]
with self.assertRaisesRegex(GuideError, r"knulli is registered and has no guide"):
self._load(yaml.safe_dump(data))
def test_a_guide_for_no_platform_is_named(self):
registry = _registry()
del registry["knulli"]
with self.assertRaisesRegex(GuideError, r"unregistered platforms: knulli"):
self._load(_guides_text(), registry)
def test_a_misspelt_key_is_refused(self):
data = yaml.safe_load(_guides_text())
data["guides"]["lakka"]["sumary"] = "A typo."
with self.assertRaisesRegex(GuideError, r"guides\.lakka: unknown key sumary"):
self._load(yaml.safe_dump(data))
def test_a_terminal_no_tab_describes_is_refused(self):
data = yaml.safe_load(_guides_text())
data["guides"]["lakka"]["run"] = ["telnet"]
with self.assertRaisesRegex(GuideError, r"guides\.lakka\.run: no terminal named telnet"):
self._load(yaml.safe_dump(data))
def test_a_folder_row_without_path_is_the_extract_folder(self):
"""The folder the pack unzips into is written once, in the registry."""
registry = _registry()
written = yaml.safe_load(_guides_text())["guides"]["batocera"]["folders"][0]
self.assertNotIn("path", written)
self.assertEqual(GUIDES["batocera"].folders[0],
(written["where"], f"`{registry['batocera']['extract_to']}`"))
def test_a_folder_row_without_path_needs_an_extract_folder(self):
registry = _registry()
del registry["lakka"]["extract_to"]
with self.assertRaisesRegex(GuideError, r"guides\.lakka\.folders\[0\]: no `path`"):
self._load(_guides_text(), registry)
def test_a_device_group_the_file_does_not_describe_is_refused(self):
registry = _registry()
registry["lakka"]["device_group"] = "toaster"
with self.assertRaisesRegex(GuideError, r"lakka\.device_group 'toaster'"):
self._load(_guides_text(), registry)
def test_the_pack_readme_keeps_its_indent(self):
manual = siteguides.pack_manual("retrobat")
self.assertTrue(manual.startswith(" Option B: Manual\n ----------------\n"))
self.assertTrue(manual.endswith(" 4. Overwrite if asked\n\n"))
self.assertEqual(siteguides.pack_manual("minui"), "")
# A table of prose keyed by platform: what the guides file replaced.
_PLATFORM_BRANCH = re.compile(r"""(?:==|!=)\s*["']([a-z0-9-]+)["']|["']([a-z0-9-]+)["']\s*(?:==|!=)""")
# The modules that write the site and the pack README.
_RENDERERS = ("generate_site.py", "packreadme.py", "generate_readme.py")
class NoPlatformProseInScripts(unittest.TestCase):
def test_no_table_of_prose_keyed_by_platform(self):
platforms = set(_registry())
found = []
for path in sorted((REPO_ROOT / "scripts").rglob("*.py")):
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
if not isinstance(node, ast.Dict):
continue
keyed = [(k.value, v) for k, v in zip(node.keys, node.values)
if isinstance(k, ast.Constant) and k.value in platforms]
prose = [key for key, value in keyed if any(
isinstance(c, ast.Constant) and isinstance(c.value, str)
and " " in c.value and len(c.value) >= 30
for c in ast.walk(value))]
if len(keyed) >= 2 and prose:
found.append(f"{path.relative_to(REPO_ROOT)}:{node.lineno}")
self.assertEqual(found, [], "per-platform text belongs in platforms/_guides.yml")
def test_no_page_branches_on_one_platform(self):
platforms = set(_registry())
found = []
scripts = REPO_ROOT / "scripts"
for path in [scripts / name for name in _RENDERERS] + sorted(scripts.glob("site*.py")):
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
for match in _PLATFORM_BRANCH.finditer(line):
if (match.group(1) or match.group(2)) in platforms:
found.append(f"{path.name}:{number}: {line.strip()}")
self.assertEqual(found, [], "a guide field says what one platform needs")
def _wiki_copy(tmp: str) -> Path:
target = Path(tmp) / "wiki"
shutil.copytree(WIKI, target)
return target
class NavFromFrontMatter(unittest.TestCase):
def _nav(self, wiki: Path) -> dict:
nav = generate_site.generate_mkdocs_nav({}, {}, {}, {}, wiki)
return {next(iter(tab)): next(iter(tab.values())) for tab in nav}
def test_the_repository_nav_matches_the_pages(self):
tabs = self._nav(WIKI)
self.assertEqual(tabs["Explore"][-2:], [
{"All installer options": "wiki/installer.md"},
{"Is it legal?": "wiki/legal.md"},
])
self.assertEqual(tabs["Reference"][0], "wiki/index.md")
def test_a_new_page_lands_in_its_tab_at_its_order(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
wiki = _wiki_copy(tmp)
(wiki / "save-files.md").write_text(
"---\nlevel: explore\norder: 35\n---\n\n# Save files\n\nText.\n",
encoding="utf-8")
explore = self._nav(wiki)["Explore"]
targets = [e if isinstance(e, str) else next(iter(e.values())) for e in explore]
self.assertEqual(targets.index("wiki/save-files.md"),
targets.index("wiki/installer.md") + 1)
self.assertIn({"Save files": "wiki/save-files.md"}, explore)
def test_a_page_without_a_level_fails_by_name(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
wiki = _wiki_copy(tmp)
(wiki / "orphan.md").write_text("# Orphan\n\nText.\n", encoding="utf-8")
with self.assertRaisesRegex(generate_site.NavError,
r"wiki/orphan\.md: front matter needs `level:`"):
self._nav(wiki)
def test_a_page_without_an_order_fails_by_name(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
wiki = _wiki_copy(tmp)
(wiki / "orphan.md").write_text(
"---\nlevel: reference\n---\n\n# Orphan\n", encoding="utf-8")
with self.assertRaisesRegex(
generate_site.NavError,
r"wiki/orphan\.md: front matter needs `order:`.*Reference tab",
):
self._nav(wiki)
def test_two_pages_on_one_place_fail_naming_both(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
wiki = _wiki_copy(tmp)
(wiki / "twin.md").write_text(
"---\nlevel: contribute\norder: 20\n---\n\n# Twin\n", encoding="utf-8")
with self.assertRaisesRegex(generate_site.NavError,
r"wiki/adding-a-platform\.md and wiki/twin\.md"):
self._nav(wiki)
def test_nav_false_keeps_a_page_out(self):
tabs = self._nav(WIKI)
flat = repr(tabs)
self.assertNotIn("getting-started.md", flat)
def test_every_wiki_page_states_its_place(self):
for page in sorted(WIKI.glob("*.md")):
meta = generate_site._front_matter(page.read_text(encoding="utf-8"))
with self.subTest(page=page.name):
self.assertIn(meta.get("level"), generate_site.TABS)
self.assertTrue(meta.get("nav") is False or isinstance(meta.get("order"), int))
if __name__ == "__main__":
unittest.main()
+54 -12
View File
@@ -1,5 +1,6 @@
---
level: contribute
order: 20
---
# Adding a platform
@@ -175,18 +176,8 @@ platforms:
`device_group` places the platform among the devices a reader picks from: on
the site's "Get your files" page, in the "Your setup" navigation and on the home
page. The groups and their labels are `DEVICE_GROUPS` in
`scripts/siteguides.py`, and the same file holds the platform's guide text
(what it is, where its BIOS folder is, where the install command runs). A
test fails when a registered platform has no group or no guide.
The guide text is read by someone who knows their device and little else.
`summary` is one plain sentence, with no chip or board names. `notes` hold
one fact each that the reader acts on. A source citation, a list of models or
how the installer finds the folder goes in `reference_notes`, which open the
Reference part of the page, and a rare build goes in `more_folders`, shown
folded. The guide part of every setup page is held to the word budgets of
[Pages for newcomers](testing-guide.md#pages-for-newcomers).
page. The groups, their labels and their anchors are under `device_groups` in
`platforms/_guides.yml`, beside the platform's [guide text](#guide-text).
A platform without a scraper (`scraper: null`, `schedule: null`) keeps its YAML
as a hand-maintained file. Every registered platform has a scraper today, so
@@ -235,6 +226,56 @@ platform hands it no folder. `platform_layouts` builds one view per directory,
so the builder, the manifest, verify and slots read the same placement, and the
resolver still looks the collection up by the core's own path.
### Guide text
`platforms/_guides.yml` holds each platform's guide: what it is, where its
BIOS folder is, where the install command runs. It is data, edited like the registry, and
no Python changes when a platform is added. The registry keeps what tools read
and `_guides.yml` keeps what people read, so neither grows unreadable.
```yaml
guides:
knulli:
summary: A system for handhelds, based on Batocera.
folders:
- where: On KNULLI # no path: the registry's extract_to
run: [device] # terminal tabs, in the order shown
reference_notes:
- KNULLI runs on handhelds among them the RG35XX family (H700).
logo_owner: KNULLI
logo_licence: GitHub organisation avatar of knulli-cfw
```
- `summary`, `folders` and `run` are required. Every value is Markdown.
- A folder row without `path` shows the registry's `extract_to`. That folder
is written once, where the installer and the release notes read it too.
- `run` names entries of `terminals` in the same file: `windows`, `macos`,
`linux`, `steamdeck`, `android`, `device` or `server`.
- `dest` puts `--dest` in the command, and `on_card: true` makes it the SD
card's path.
- `more_folders` lists rare builds, shown folded under `more_folders_title`.
`more_folders_detected: true` adds a row for each named system the
registry's `install.detect` rules find the platform on.
- `download_hint` adds a line under the pack download.
- `pack_manual` is the manual part of the README inside the pack, in plain
text. Without it the README names the `bios_path` of the detection rules.
- `logo_owner` and `logo_licence` credit the registry's `logo`, read by
`scripts/fetch_site_images.py`.
The guide text is read by someone who knows their device and little else.
`summary` is one plain sentence, with no chip or board names. `notes` hold
one fact each that the reader acts on. A source citation, a list of models or
how the installer finds the folder goes in `reference_notes`, which open the
Reference part of the page. The guide part of every setup page is held to the
word budgets of [Pages for newcomers](testing-guide.md#pages-for-newcomers).
`scripts/siteguides.py` loads the file and stops the build with the key at
fault: a registered platform without a guide, a guide for no registered
platform, a misspelt key, an unknown terminal, a group the file does not
describe. `tests/test_site_content_data.py` also fails when a script holds a
table of text keyed by platform or a page branches on one platform's name:
that text belongs in a guide field.
### Optional fields
```yaml
@@ -634,6 +675,7 @@ Verification is not optional. A platform that passes `pipeline.py` today may bre
- [ ] `fetch_requirements()` and `validate_format()` implemented
- [ ] System ID mapping covers all upstream systems
- [ ] Entry added to `platforms/_registry.yml`
- [ ] Guide added to `platforms/_guides.yml`
- [ ] Platform YAML generated and verified
- [ ] `python scripts/pipeline.py --offline` passes
- [ ] Exporter in `scripts/exporter/<name>_exporter.py` (if applicable)
+1
View File
@@ -1,5 +1,6 @@
---
level: contribute
order: 30
---
# Adding a scraper
+1
View File
@@ -1,5 +1,6 @@
---
level: reference
order: 100
---
# Build your own packs
+1
View File
@@ -1,5 +1,6 @@
---
level: reference
order: 90
---
# Architecture
+1
View File
@@ -1,5 +1,6 @@
---
level: reference
order: 140
---
# Projects using this data
+2
View File
@@ -1,5 +1,7 @@
---
level: start
order: 50
nav_title: Common questions
---
# FAQ
+2
View File
@@ -1,5 +1,7 @@
---
level: start
order: 30
nav_title: First install, step by step
---
# First install
+1
View File
@@ -1,5 +1,6 @@
---
level: start
nav: false
---
# Getting started
+2
View File
@@ -1,5 +1,7 @@
---
level: start
order: 60
nav_title: Glossary
---
# Words used on this site
+1
View File
@@ -1,5 +1,6 @@
---
level: reference
order: 0
---
# Reference
+1
View File
@@ -1,5 +1,6 @@
---
level: explore
order: 30
---
# All installer options
+2
View File
@@ -1,5 +1,7 @@
---
level: explore
order: 40
nav_title: "Is it legal?"
---
# The project's legal position
+2
View File
@@ -1,5 +1,7 @@
---
level: contribute
order: 10
nav_title: Profiling an emulator
---
# Profiling guide
+2
View File
@@ -1,5 +1,7 @@
---
level: reference
order: 130
nav_title: Project terms
---
# Terms of the reference pages
+1
View File
@@ -1,5 +1,6 @@
---
level: contribute
order: 50
---
# Release process
+1
View File
@@ -1,5 +1,6 @@
---
level: reference
order: 120
---
# Technical questions
+31 -1
View File
@@ -1,5 +1,7 @@
---
level: contribute
order: 40
nav_title: Testing
---
# Testing guide
@@ -54,7 +56,7 @@ Every module in `tests/`, by the area it guards.
| Handheld port | stubbed PortMaster dialogs, fake installer | `test_port` |
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_data_dir_readers`, `test_truth_diff`, `test_truth_layout` |
| Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_elec_scrapers`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_site_availability`, `test_site_copy`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_platform_hashes`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_coverage_cache`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_site_availability`, `test_site_copy`, `test_site_content_data`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_funnel`, `test_site_images`, `test_site_platform_hashes`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_weight`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
## Test architecture
@@ -362,6 +364,34 @@ and reads less on a phone. Facts a newcomer does not act on (legal analysis,
source lines, annotated runs) are kept, on the page one level down. To see
the numbers of every page, run `python -m tests.test_site_copy --report docs`.
### Adding a wiki page
A page in `wiki/` is published as soon as it exists, and its front matter
places it in the navigation. No Python changes.
```yaml
---
level: explore # its tab: start, setup, explore, reference or contribute
order: 35 # its place in the tab, the smallest first
nav_title: Save files # only when the menu names it otherwise than its heading
---
```
- `level` picks the tab and the label shown at the top of the page.
- `order` sorts the tab. Pages are spaced by ten, so a new page fits between
two without renumbering. The generated pages (systems, emulators, the gaps
and data pages) hold fixed places among them, set in `generate_mkdocs_nav`.
- `nav_title` is the menu label. Without it the menu shows the page's `# `
heading. It is not `title`, which both MkDocs and Astro Starlight read as
the page's own title.
- `nav: false` keeps a page out of the menu on purpose.
`getting-started.md` does, since it only links the pages that took its
content.
`python scripts/generate_site.py` stops with the page's name when `level` or
`order` is missing, or when two pages of a tab take the same `order`.
`tests/test_site_content_data.py` checks the same on every page in `wiki/`.
## Landing a change
A commit subject is English, 50 characters at most, `type: description` with a
+1
View File
@@ -1,5 +1,6 @@
---
level: reference
order: 110
---
# Command-line tools
+1
View File
@@ -1,5 +1,6 @@
---
level: start
order: 40
---
# When a game does not start
+1
View File
@@ -1,5 +1,6 @@
---
level: reference
order: 10
---
# How each platform checks files
+2
View File
@@ -1,5 +1,7 @@
---
level: start
order: 10
nav_title: What BIOS files are
---
# What is a BIOS?