mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 20:29:06 -05:00
chore: merge guides and nav as data
This commit is contained in:
commit
fd9ec5b2a7
28 files changed
+1207
-610
No files matched your search
@@ -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`).
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 [
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
@@ -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,5 +1,6 @@
|
||||
---
|
||||
level: contribute
|
||||
order: 30
|
||||
---
|
||||
|
||||
# Adding a scraper
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: reference
|
||||
order: 100
|
||||
---
|
||||
|
||||
# Build your own packs
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: reference
|
||||
order: 90
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: reference
|
||||
order: 140
|
||||
---
|
||||
|
||||
# Projects using this data
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
level: start
|
||||
order: 50
|
||||
nav_title: Common questions
|
||||
---
|
||||
|
||||
# FAQ
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
level: start
|
||||
order: 30
|
||||
nav_title: First install, step by step
|
||||
---
|
||||
|
||||
# First install
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: start
|
||||
nav: false
|
||||
---
|
||||
|
||||
# Getting started
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
level: start
|
||||
order: 60
|
||||
nav_title: Glossary
|
||||
---
|
||||
|
||||
# Words used on this site
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: reference
|
||||
order: 0
|
||||
---
|
||||
|
||||
# Reference
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: explore
|
||||
order: 30
|
||||
---
|
||||
|
||||
# All installer options
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
level: explore
|
||||
order: 40
|
||||
nav_title: "Is it legal?"
|
||||
---
|
||||
|
||||
# The project's legal position
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
level: contribute
|
||||
order: 10
|
||||
nav_title: Profiling an emulator
|
||||
---
|
||||
|
||||
# Profiling guide
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
level: reference
|
||||
order: 130
|
||||
nav_title: Project terms
|
||||
---
|
||||
|
||||
# Terms of the reference pages
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: contribute
|
||||
order: 50
|
||||
---
|
||||
|
||||
# Release process
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: reference
|
||||
order: 120
|
||||
---
|
||||
|
||||
# Technical questions
|
||||
|
||||
+31
-1
@@ -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,5 +1,6 @@
|
||||
---
|
||||
level: reference
|
||||
order: 110
|
||||
---
|
||||
|
||||
# Command-line tools
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: start
|
||||
order: 40
|
||||
---
|
||||
|
||||
# When a game does not start
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
level: reference
|
||||
order: 10
|
||||
---
|
||||
|
||||
# How each platform checks files
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
level: start
|
||||
order: 10
|
||||
nav_title: What BIOS files are
|
||||
---
|
||||
|
||||
# What is a BIOS?
|
||||
|
||||
Reference in new issue
Block a user